ui: Apply everywhere — power toggles, plan preview, no start/stop/install

Rework the dashboard around convergence:
- ServiceControls / ServiceCard / JobCard: replace start/restart/stop
  with a Power toggle (set enabled → apply, i.e. activate/deactivate)
  plus Restart (the one imperative bounce).
- ToolDetail: install/uninstall becomes an Enable/Disable toggle;
  `installed` stays the live state, `enabled` the desired one.
- ConfigPanel Apply and CreateDeploymentForm now POST /apply (one
  converge that renders + restarts only what changed) instead of
  /deploy + a separate start/restart.
- New ConvergePanel on Overview: a terraform-style Preview (POST /apply
  plan=true) showing would-activate/restart/deactivate, then Apply.
- useApply + useSetEnabled hooks; `enabled` added to Service/Job/
  Deployment types; both handle the self-restart connection drop.

Backend: PUT /config/deployments/{name}/enabled sets desired on/off
(edit config; caller runs apply) — the declarative toggle the UI uses.

Dashboard builds clean (tsc + vite); 59 API tests pass; live /apply and
/services?enabled verified after an api restart.
This commit is contained in:
2026-07-02 11:49:54 -07:00
parent 5d15d18e1d
commit f422c1879d
13 changed files with 292 additions and 121 deletions

View File

@@ -323,6 +323,29 @@ def delete_deployment(name: str) -> dict:
return _delete_deployment(name)
class EnabledRequest(BaseModel):
enabled: bool
@router.put("/deployments/{name}/enabled")
def set_deployment_enabled(name: str, request: EnabledRequest) -> dict:
"""Set a deployment's declared `enabled` state (desired on/off).
Edits config only — the caller runs `POST /apply` to converge. Keeps the
declarative flow: change what you want, then apply.
"""
config = get_config()
dep = config.deployments.get(name)
if dep is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Deployment '{name}' not found",
)
dep.enabled = request.enabled
save_config(config)
return {"ok": True, "deployment": name, "enabled": request.enabled}
@router.put("/services/{name}")
def save_service(name: str, request: ServiceConfigRequest) -> dict:
"""Alias of PUT /deployments/{name} (kept for the existing dashboard)."""