feat(ollama): add Ollama local LLM (.27) card & remote SSH control to service-controller

This commit is contained in:
2026-10-12 14:12:30 +11:00
parent c9d09ac859
commit a661fb3b2e
3 changed files with 33 additions and 9 deletions

View File

@@ -84,21 +84,29 @@ def _container_state(target: str) -> str:
return out or ("unknown" if r.returncode != 0 else "unknown") return out or ("unknown" if r.returncode != 0 else "unknown")
def _systemd_user_state(target: str) -> str: def _systemd_user_state(target: str, remote: str | None = None) -> str:
r = subprocess.run(["systemctl", "--user", "is-active", target], cmd = ["systemctl", "--user", "is-active", target]
capture_output=True, text=True) if remote:
return (r.stdout.strip() or "inactive").lower() 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.""" """Run start/stop. Returns the state after a short settle."""
if kind == "container": if kind == "container":
subprocess.run(["docker", action, target], capture_output=True, text=True) subprocess.run(["docker", action, target], capture_output=True, text=True)
time.sleep(1.5) time.sleep(1.5)
return _container_state(target) 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) time.sleep(1.5)
return _systemd_user_state(target) return _systemd_user_state(target, remote=remote)
def _targets(s: dict[str, Any]) -> list[str]: 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). """Collect the state of every target. Returns (aggregate, per-target list).
Aggregate: running if ANY is running; stopped if ALL stopped; else partial.""" Aggregate: running if ANY is running; stopped if ALL stopped; else partial."""
kind = s["kind"] kind = s["kind"]
remote = s.get("remote")
states = [] states = []
for tgt in _targets(s): for tgt in _targets(s):
if kind == "container": if kind == "container":
states.append(_container_state(tgt)) states.append(_container_state(tgt))
else: 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_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 n_none = sum(1 for st in states if st in ("unknown",)) or 0
if n_ok == len(states): if n_ok == len(states):
@@ -184,9 +193,10 @@ async def service_action(sid: str, action: str,
if s is None: if s is None:
raise HTTPException(status_code=404, detail="unknown service") raise HTTPException(status_code=404, detail="unknown service")
actor = (who or "unknown-user").strip()[:40] actor = (who or "unknown-user").strip()[:40]
remote = s.get("remote")
results = {} results = {}
for tgt in _targets(s): for tgt in _targets(s):
results[tgt] = _do(s["kind"], action, tgt) results[tgt] = _do(s["kind"], action, tgt, remote=remote)
agg, _ = _probe(s) agg, _ = _probe(s)
audit(f"{actor} {action} {sid} -> {agg} ({len(_targets(s))} targets)") audit(f"{actor} {action} {sid} -> {agg} ({len(_targets(s))} targets)")
return {"ok": True, "id": sid, "state": agg, "targets": results} return {"ok": True, "id": sid, "state": agg, "targets": results}

View File

@@ -94,3 +94,14 @@ label = "World Monitor"
ram_mb = 120 ram_mb = 120
default_state = "running" default_state = "running"
who = ["sam"] 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"]

View File

@@ -85,6 +85,9 @@ def _cat() -> list[Tool]:
"Chat admin console (admins only)", "https://pb.chat.lab.audasmedia.com.au", login=True, admin=True), "Chat admin console (admins only)", "https://pb.chat.lab.audasmedia.com.au", login=True, admin=True),
Tool("ai-router", "AI Router", "ai", Tool("ai-router", "AI Router", "ai",
"OmniRoute LLM gateway dashboard", "https://omniroute.lab.audasmedia.com.au"), "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", Tool("n8n", "n8n", "ai",
"Workflow automation", "https://n8n.lab.audasmedia.com.au", login=True), "Workflow automation", "https://n8n.lab.audasmedia.com.au", login=True),
Tool("prefect", "Prefect", "ai", Tool("prefect", "Prefect", "ai",