diff --git a/deploy/service-controller/service_controller.py b/deploy/service-controller/service_controller.py index 8c3317b..716906c 100644 --- a/deploy/service-controller/service_controller.py +++ b/deploy/service-controller/service_controller.py @@ -84,21 +84,29 @@ def _container_state(target: str) -> str: return out or ("unknown" if r.returncode != 0 else "unknown") -def _systemd_user_state(target: str) -> str: - r = subprocess.run(["systemctl", "--user", "is-active", target], - capture_output=True, text=True) - return (r.stdout.strip() or "inactive").lower() +def _systemd_user_state(target: str, remote: str | None = None) -> str: + cmd = ["systemctl", "--user", "is-active", target] + if remote: + cmd = ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "BatchMode=yes", remote] + cmd + r = subprocess.run(cmd, capture_output=True, text=True) + out = (r.stdout.strip() or "inactive").lower() + if out == "active": + return "running" + return out -def _do(kind: str, action: str, target: str) -> str: +def _do(kind: str, action: str, target: str, remote: str | None = None) -> str: """Run start/stop. Returns the state after a short settle.""" if kind == "container": subprocess.run(["docker", action, target], capture_output=True, text=True) time.sleep(1.5) return _container_state(target) - subprocess.run(["systemctl", "--user", action, target], capture_output=True, text=True) + cmd = ["systemctl", "--user", action, target] + if remote: + cmd = ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "BatchMode=yes", remote] + cmd + subprocess.run(cmd, capture_output=True, text=True) time.sleep(1.5) - return _systemd_user_state(target) + return _systemd_user_state(target, remote=remote) def _targets(s: dict[str, Any]) -> list[str]: @@ -114,12 +122,13 @@ 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"] + remote = s.get("remote") states = [] for tgt in _targets(s): if kind == "container": states.append(_container_state(tgt)) else: - states.append(_systemd_user_state(tgt)) + states.append(_systemd_user_state(tgt, remote=remote)) 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): @@ -184,9 +193,10 @@ 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] + remote = s.get("remote") results = {} for tgt in _targets(s): - results[tgt] = _do(s["kind"], action, tgt) + results[tgt] = _do(s["kind"], action, tgt, remote=remote) agg, _ = _probe(s) audit(f"{actor} {action} {sid} -> {agg} ({len(_targets(s))} targets)") return {"ok": True, "id": sid, "state": agg, "targets": results} diff --git a/deploy/service-controller/services.toml b/deploy/service-controller/services.toml index 23d49ea..34c4655 100644 --- a/deploy/service-controller/services.toml +++ b/deploy/service-controller/services.toml @@ -94,3 +94,14 @@ label = "World Monitor" ram_mb = 120 default_state = "running" who = ["sam"] + +[[services]] +id = "ollama" +kind = "systemd-user" +target = "ollama.service" +remote = "sam@192.168.20.27" +group = "ai" +label = "Local LLM Server (Ollama .27)" +ram_mb = 9000 +default_state = "stopped" +who = ["sam", "jo", "harry", "finn"] diff --git a/portal/tools.py b/portal/tools.py index 03991c4..cb81447 100644 --- a/portal/tools.py +++ b/portal/tools.py @@ -85,6 +85,9 @@ def _cat() -> list[Tool]: "Chat admin console (admins only)", "https://pb.chat.lab.audasmedia.com.au", login=True, admin=True), Tool("ai-router", "AI Router", "ai", "OmniRoute LLM gateway dashboard", "https://omniroute.lab.audasmedia.com.au"), + Tool("ollama", "Ollama Local LLM", "ai", + "Local AI model server (Qwen 2.5 Coder on .27)", "http://192.168.20.27:11434", + service_id="ollama", ram_mb=9000, togglable=True, lan=True), Tool("n8n", "n8n", "ai", "Workflow automation", "https://n8n.lab.audasmedia.com.au", login=True), Tool("prefect", "Prefect", "ai",