feat: add restart gateway and update Hermes action buttons to web dashboard
All checks were successful
Lint / lint (pull_request) Successful in 29s

Implements the update/restart action buttons called out in issue #961:

- Backend (web_server.py): two new POST endpoints
  - /api/actions/restart-gateway — sends SIGUSR1 to the running gateway PID
  - /api/actions/update-hermes  — runs `hermes update --yes` in a subprocess
- Frontend (api.ts): restartGateway() / updateHermes() API helpers + ActionResponse type
- UI (StatusPage.tsx): "Actions" card with Restart Gateway and Update Hermes buttons
  - idle → running (spinner) → success/failure states
  - feedback detail text; auto-resets to idle after 8 s
- i18n: new status.actions / restartGateway / updateHermes strings in en, zh, and types

Refs #961

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Alexander Whitestone
2026-04-21 22:30:22 -04:00
parent a2a40429bd
commit a8a086548d
6 changed files with 204 additions and 1 deletions

View File

@@ -1981,6 +1981,73 @@ async def update_config_raw(body: RawConfigUpdate):
raise HTTPException(status_code=400, detail=f"Invalid YAML: {e}")
# ---------------------------------------------------------------------------
# Action endpoints — restart gateway / update Hermes
# ---------------------------------------------------------------------------
class ActionResponse(BaseModel):
ok: bool
detail: str = ""
@app.post("/api/actions/restart-gateway")
async def restart_gateway():
"""Send SIGUSR1 to the running gateway so it drains and restarts.
Falls back to a hard kill+restart if no PID is found or the signal
fails (e.g. the gateway is managed by a remote process / container).
Returns immediately with ``{"ok": true}`` if the signal was delivered;
the caller should poll ``/api/status`` to confirm the new state.
"""
from gateway.status import get_running_pid
pid = get_running_pid()
if pid is None:
raise HTTPException(status_code=409, detail="Gateway is not running")
import signal as _signal
try:
os.kill(pid, _signal.SIGUSR1)
except (ProcessLookupError, PermissionError, OSError, AttributeError) as exc:
raise HTTPException(status_code=500, detail=f"Failed to signal gateway: {exc}")
return {"ok": True, "detail": f"Restart signal sent to PID {pid}"}
@app.post("/api/actions/update-hermes")
async def update_hermes():
"""Run ``hermes update`` in a subprocess and return the output.
The update is performed synchronously (in a thread pool executor) so
the endpoint blocks until completion. Clients should treat a 200
response with ``"ok": true`` as success; ``"ok": false`` means the
subprocess exited non-zero.
"""
import subprocess
loop = asyncio.get_event_loop()
def _run_update():
try:
result = subprocess.run(
[sys.executable, "-m", "hermes_cli.main", "update", "--yes"],
capture_output=True,
text=True,
timeout=300,
)
combined = (result.stdout + result.stderr).strip()
return result.returncode == 0, combined
except subprocess.TimeoutExpired:
return False, "Update timed out after 5 minutes"
except Exception as exc:
return False, str(exc)
ok, detail = await loop.run_in_executor(None, _run_update)
return {"ok": ok, "detail": detail}
# ---------------------------------------------------------------------------
# Token / cost analytics endpoint
# ---------------------------------------------------------------------------