v0.2.0 - Always-on services panel with per-service host config
Dashboard:
- New 'Always-on services' section with cards for Parakeet and Magpie
- Each card: host:port, model loaded, status pill (Healthy/Unhealthy/Starting/Not configured)
- Start, Restart, Stop buttons. Buttons disabled when not applicable for current state
- Restart counter shown when > 1 (would have surfaced the old magpie crash loop)
Backend:
- New /api/services GET: docker container state + http health for each support service
- New POST /api/services/{name}/{action} for start | stop | restart
- services.py module: docker_state, run_action via SSH
- config.py: PARAKEET_HOST/USER/CONTAINER and MAGPIE_* env vars, default to spark2_*
- health.py: use per-service hosts (no longer hard-wired to spark2_host)
Package:
- sparkConfig.yaml.ts: add 6 new optional fields
- configureSparks action: optional 'Parakeet host', 'Parakeet container', 'Magpie host', 'Magpie container' fields; descriptions explain they default to Spark 2 when blank
- Handler normalizes nulls to empty strings before merge
- main.ts: pass new env vars to container
- bump to 0.2.0:0
This commit is contained in:
@@ -11,6 +11,7 @@ from pydantic import BaseModel
|
||||
from .config import Settings
|
||||
from .health import check_magpie, check_parakeet, check_vllm
|
||||
from .models import load_catalog
|
||||
from .services import docker_state, run_action, services_from_settings
|
||||
from .ssh import ssh_run
|
||||
from .swap import SwapManager
|
||||
|
||||
@@ -48,6 +49,64 @@ async def get_models() -> dict:
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/services")
|
||||
async def get_services() -> dict:
|
||||
"""Lifecycle state of always-on support services (Parakeet, Magpie, …).
|
||||
|
||||
Each entry includes:
|
||||
- host/port/container/user (configured)
|
||||
- state: docker container status (running | exited | restarting | missing | unconfigured)
|
||||
- http_ready: whether the service's /health endpoint responded
|
||||
- base_url
|
||||
- model (if reported by the service)
|
||||
- restart_count
|
||||
"""
|
||||
services = services_from_settings(settings)
|
||||
out: dict[str, dict] = {}
|
||||
|
||||
async def one(name: str):
|
||||
svc = services[name]
|
||||
docker = await docker_state(settings, svc)
|
||||
if name == "parakeet":
|
||||
http = await check_parakeet(settings)
|
||||
else:
|
||||
http = await check_magpie(settings)
|
||||
return name, {
|
||||
"host": svc.host,
|
||||
"user": svc.user,
|
||||
"port": svc.port,
|
||||
"container": svc.container,
|
||||
"kind": svc.kind,
|
||||
"base_url": http.get("base_url"),
|
||||
"http_ready": bool(http.get("ok")),
|
||||
"model": (http.get("detail") or {}).get("model") if isinstance(http.get("detail"), dict) else None,
|
||||
"docker_state": docker.get("state"),
|
||||
"restart_count": docker.get("restart_count"),
|
||||
"started_at": docker.get("started_at"),
|
||||
"exit_code": docker.get("exit_code"),
|
||||
"error": docker.get("error"),
|
||||
"detail": http.get("detail"),
|
||||
}
|
||||
|
||||
results = await asyncio.gather(*[one(n) for n in services.keys()])
|
||||
for name, info in results:
|
||||
out[name] = info
|
||||
return out
|
||||
|
||||
|
||||
@app.post("/api/services/{name}/{action}")
|
||||
async def service_action(name: str, action: str) -> dict:
|
||||
services = services_from_settings(settings)
|
||||
if name not in services:
|
||||
raise HTTPException(404, f"unknown service: {name}")
|
||||
if action not in ("start", "stop", "restart"):
|
||||
raise HTTPException(400, f"unknown action: {action}")
|
||||
result = await run_action(settings, services[name], action) # type: ignore[arg-type]
|
||||
if not result["ok"]:
|
||||
raise HTTPException(500, result.get("stderr") or result.get("error") or "action failed")
|
||||
return {"name": name, "action": action, **result}
|
||||
|
||||
|
||||
@app.get("/api/endpoints")
|
||||
async def get_endpoints() -> dict:
|
||||
"""Service-discovery summary. Stable shape; other apps on the LAN can poll this
|
||||
|
||||
Reference in New Issue
Block a user