feat(control-plane): add Stop and Restart app controls
- POST /api/apps/:id/stop → docker stop hiy-{id}
- POST /api/apps/:id/restart → docker restart hiy-{id}
Dashboard (apps table): Stop / Restart buttons alongside Deploy and Delete.
App detail page: container status badge + Stop / Restart buttons in the nav bar.
https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
This commit is contained in:
parent
217bafc464
commit
ec0f421137
3 changed files with 68 additions and 1 deletions
|
|
@ -67,6 +67,8 @@ async fn main() -> anyhow::Result<()> {
|
||||||
.route("/api/apps/:id", get(routes::apps::get_one)
|
.route("/api/apps/:id", get(routes::apps::get_one)
|
||||||
.put(routes::apps::update)
|
.put(routes::apps::update)
|
||||||
.delete(routes::apps::delete))
|
.delete(routes::apps::delete))
|
||||||
|
.route("/api/apps/:id/stop", post(routes::apps::stop))
|
||||||
|
.route("/api/apps/:id/restart", post(routes::apps::restart))
|
||||||
// ── Deploys API ───────────────────────────────────────────
|
// ── Deploys API ───────────────────────────────────────────
|
||||||
.route("/api/apps/:id/deploy", post(routes::deploys::trigger))
|
.route("/api/apps/:id/deploy", post(routes::deploys::trigger))
|
||||||
.route("/api/apps/:id/deploys", get(routes::deploys::list))
|
.route("/api/apps/:id/deploys", get(routes::deploys::list))
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,42 @@ pub async fn delete(
|
||||||
Ok(StatusCode::NO_CONTENT)
|
Ok(StatusCode::NO_CONTENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn stop(
|
||||||
|
State(_s): State<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> Result<StatusCode, StatusCode> {
|
||||||
|
let name = format!("hiy-{}", id);
|
||||||
|
let out = tokio::process::Command::new("docker")
|
||||||
|
.args(["stop", &name])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
if out.status.success() {
|
||||||
|
Ok(StatusCode::NO_CONTENT)
|
||||||
|
} else {
|
||||||
|
tracing::warn!("docker stop {}: {}", name, String::from_utf8_lossy(&out.stderr));
|
||||||
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn restart(
|
||||||
|
State(_s): State<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> Result<StatusCode, StatusCode> {
|
||||||
|
let name = format!("hiy-{}", id);
|
||||||
|
let out = tokio::process::Command::new("docker")
|
||||||
|
.args(["restart", &name])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
if out.status.success() {
|
||||||
|
Ok(StatusCode::NO_CONTENT)
|
||||||
|
} else {
|
||||||
|
tracing::warn!("docker restart {}: {}", name, String::from_utf8_lossy(&out.stderr));
|
||||||
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn fetch_app(s: &AppState, id: &str) -> Result<App, StatusCode> {
|
async fn fetch_app(s: &AppState, id: &str) -> Result<App, StatusCode> {
|
||||||
sqlx::query_as::<_, App>("SELECT * FROM apps WHERE id = ?")
|
sqlx::query_as::<_, App>("SELECT * FROM apps WHERE id = ?")
|
||||||
.bind(id)
|
.bind(id)
|
||||||
|
|
|
||||||
|
|
@ -198,6 +198,8 @@ pub async fn index(State(s): State<AppState>) -> Result<Html<String>, StatusCode
|
||||||
<td data-deploy-badge>{d_badge}</td>
|
<td data-deploy-badge>{d_badge}</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="primary" onclick="deploy('{id}')">Deploy</button>
|
<button class="primary" onclick="deploy('{id}')">Deploy</button>
|
||||||
|
<button onclick="stopApp('{id}')">Stop</button>
|
||||||
|
<button onclick="restartApp('{id}')">Restart</button>
|
||||||
<button class="danger" onclick="del('{id}')">Delete</button>
|
<button class="danger" onclick="del('{id}')">Delete</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>"#,
|
</tr>"#,
|
||||||
|
|
@ -284,6 +286,16 @@ pub async fn index(State(s): State<AppState>) -> Result<Html<String>, StatusCode
|
||||||
await fetch('/api/apps/' + id, {{method: 'DELETE'}});
|
await fetch('/api/apps/' + id, {{method: 'DELETE'}});
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}}
|
}}
|
||||||
|
async function stopApp(id) {{
|
||||||
|
if (!confirm('Stop ' + id + '?')) return;
|
||||||
|
const r = await fetch('/api/apps/' + id + '/stop', {{method: 'POST'}});
|
||||||
|
if (!r.ok) alert('Error stopping app');
|
||||||
|
}}
|
||||||
|
async function restartApp(id) {{
|
||||||
|
if (!confirm('Restart ' + id + '?')) return;
|
||||||
|
const r = await fetch('/api/apps/' + id + '/restart', {{method: 'POST'}});
|
||||||
|
if (!r.ok) alert('Error restarting app');
|
||||||
|
}}
|
||||||
|
|
||||||
function deployBadgeHtml(s) {{
|
function deployBadgeHtml(s) {{
|
||||||
const cls = {{success:'badge-success',failed:'badge-failed',building:'badge-building',queued:'badge-building'}}[s] || 'badge-unknown';
|
const cls = {{success:'badge-success',failed:'badge-failed',building:'badge-building',queued:'badge-building'}}[s] || 'badge-unknown';
|
||||||
|
|
@ -355,6 +367,7 @@ pub async fn app_detail(
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let container_state = get_container_status(&app_id).await;
|
||||||
let latest_deploy_id = deploys.first().map(|d| d.id.as_str()).unwrap_or("");
|
let latest_deploy_id = deploys.first().map(|d| d.id.as_str()).unwrap_or("");
|
||||||
|
|
||||||
let mut deploy_rows = String::new();
|
let mut deploy_rows = String::new();
|
||||||
|
|
@ -396,7 +409,12 @@ pub async fn app_detail(
|
||||||
let body = format!(
|
let body = format!(
|
||||||
r#"<nav>
|
r#"<nav>
|
||||||
<h1><a href="/" style="color:inherit">HIY</a> / {name}</h1>
|
<h1><a href="/" style="color:inherit">HIY</a> / {name}</h1>
|
||||||
|
<div style="display:flex;gap:8px;align-items:center">
|
||||||
|
{c_badge}
|
||||||
<button class="primary" onclick="deploy()">Deploy Now</button>
|
<button class="primary" onclick="deploy()">Deploy Now</button>
|
||||||
|
<button onclick="stopApp()">Stop</button>
|
||||||
|
<button onclick="restartApp()">Restart</button>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<p class="subtitle">
|
<p class="subtitle">
|
||||||
<a href="{repo}" target="_blank">{repo}</a>
|
<a href="{repo}" target="_blank">{repo}</a>
|
||||||
|
|
@ -525,6 +543,16 @@ pub async fn app_detail(
|
||||||
await fetch('/api/apps/' + APP_ID + '/env/' + key, {{method:'DELETE'}});
|
await fetch('/api/apps/' + APP_ID + '/env/' + key, {{method:'DELETE'}});
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}}
|
}}
|
||||||
|
async function stopApp() {{
|
||||||
|
if (!confirm('Stop ' + APP_ID + '?')) return;
|
||||||
|
const r = await fetch('/api/apps/' + APP_ID + '/stop', {{method:'POST'}});
|
||||||
|
if (!r.ok) alert('Error stopping app');
|
||||||
|
}}
|
||||||
|
async function restartApp() {{
|
||||||
|
if (!confirm('Restart ' + APP_ID + '?')) return;
|
||||||
|
const r = await fetch('/api/apps/' + APP_ID + '/restart', {{method:'POST'}});
|
||||||
|
if (!r.ok) alert('Error restarting app');
|
||||||
|
}}
|
||||||
</script>"#,
|
</script>"#,
|
||||||
name = app.name,
|
name = app.name,
|
||||||
repo = app.repo_url,
|
repo = app.repo_url,
|
||||||
|
|
@ -536,6 +564,7 @@ pub async fn app_detail(
|
||||||
deploy_rows = deploy_rows,
|
deploy_rows = deploy_rows,
|
||||||
env_rows = env_rows,
|
env_rows = env_rows,
|
||||||
latest_deploy_id = latest_deploy_id,
|
latest_deploy_id = latest_deploy_id,
|
||||||
|
c_badge = container_badge(&container_state),
|
||||||
);
|
);
|
||||||
|
|
||||||
Html(page(&app.name, &body)).into_response()
|
Html(page(&app.name, &body)).into_response()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue