ON/OFF: WorldMonitor group toggle — controller supports multi-target groups (aggregate state, one switch for 4 containers), console card wired (admin), partial-state pill

This commit is contained in:
2026-10-08 21:41:54 +11:00
parent 0ce1b6e6ba
commit 24227c527a
5 changed files with 59 additions and 14 deletions

View File

@@ -101,23 +101,49 @@ def _do(kind: str, action: str, target: str) -> str:
return _systemd_user_state(target)
def _describe(s: dict[str, Any]) -> dict[str, Any]:
state = "unknown"
if s["kind"] == "container":
state = _container_state(s["target"])
def _targets(s: dict[str, Any]) -> list[str]:
"""A service is either a single `target` or a group `targets` list."""
t = s.get("targets")
if isinstance(t, list) and t:
return [str(x) for x in t]
single = s.get("target")
return [single] if single else []
def _probe(s: dict[str, Any]) -> tuple[str, list[str]]:
"""Collect the state of every target. Returns (aggregate, per-target list).
Aggregate: running if ANY is running; stopped if ALL stopped; else partial."""
kind = s["kind"]
states = []
for tgt in _targets(s):
if kind == "container":
states.append(_container_state(tgt))
else:
states.append(_systemd_user_state(tgt))
n_ok = sum(1 for st in states if st in ("running", "active", "started"))
n_none = sum(1 for st in states if st in ("unknown",)) or 0
if n_ok == len(states):
agg = "running"
elif n_ok == 0 and len(states):
agg = "stopped"
else:
state = _systemd_user_state(s["target"])
agg = "partial" if len(states) else "unknown"
return agg, states
def _describe(s: dict[str, Any]) -> dict[str, Any]:
agg, states = _probe(s)
return {
"id": s["id"],
"kind": s["kind"],
"target": s["target"],
"target": s.get("target", ""),
"targets": s.get("targets", []),
"group": s.get("group", ""),
"label": s.get("label", s["id"]),
"ram_mb": s.get("ram_mb"),
"default_state": s.get("default_state", "stopped"),
"who": s.get("who", []),
"state": "running" if state in ("running", "active") else
("stopped" if state in ("stopped", "inactive", "exited") else "unknown"),
"state": agg,
"error": None,
}
@@ -158,9 +184,12 @@ async def service_action(sid: str, action: str,
if s is None:
raise HTTPException(status_code=404, detail="unknown service")
actor = (who or "unknown-user").strip()[:40]
state = _do(s["kind"], action, s["target"])
audit(f"{actor} {action} {sid} -> {state}")
return {"ok": True, "id": sid, "state": state}
results = {}
for tgt in _targets(s):
results[tgt] = _do(s["kind"], action, tgt)
agg, _ = _probe(s)
audit(f"{actor} {action} {sid} -> {agg} ({len(_targets(s))} targets)")
return {"ok": True, "id": sid, "state": agg, "targets": results}
if __name__ == "__main__":

View File

@@ -83,3 +83,14 @@ label = "Photo pipeline dashboard"
ram_mb = 112
default_state = "running"
who = ["sam"]
# WorldMonitor — 4-container group (UI + relay + redis + rest). One switch for all.
[[services]]
id = "worldmonitor"
kind = "container"
targets = ["worldmonitor", "worldmonitor-ais-relay", "worldmonitor-redis", "worldmonitor-redis-rest"]
group = "admin"
label = "World Monitor"
ram_mb = 120
default_state = "running"
who = ["sam"]