Files
mission-control-v2/backend/services/ideen.py
T
werkstatt 9a41cce99b MC2: Live-Log-Blick - Worker-Log laufender Ideen im Auftragsbuch aufklappbar machen
Backend:
- backend/services/ideen.py: neue Funktion log_of(task_id)
  - ruft 'hermes kanban log <id>'
  - validiert task_id mit _TASK_ID_RX
  - strippt ANSI-Steuerzeichen (\x1b\[[0-9;]*m)
  - gibt letzte ~120 Zeilen zurück
  - kurzer Cache (~4s)
  - auf Windows/available=False leer zurück, nie crashen
- backend/routers/ideen.py: GET /api/ideen/{id}/log -> ideen.log_of(id)

Frontend:
- frontend/src/views/AuftragsbuchView.tsx: IdeenSection
  - Ideen mit status 'triage' oder 'running' per Klick aufklappbar
  - im aufgeklappten Zustand /api/ideen/{id}/log alle ~4s pollen
  - Log monospace/<pre> in scrollbaren, höhenbegrenztem Kasten
  - vorhandenes api()-Muster + react-query nutzen
- frontend/dist mitgebaut und committet

Branch: feature/live-log-blick
2026-07-13 17:17:16 +02:00

179 lines
6.6 KiB
Python

"""
Ideen-Queue — DIE eine Sammelstelle für Ideen des Commanders (Entscheid 10.07.2026).
Die Queue selbst ist das NATIVE Hermes-Kanban (SQLite-Board, Dispatcher im Gateway):
Idee rein (triage) → der Specifier arbeitet sie aus → ein Worker-Profil setzt sie um →
Code-Ergebnisse kommen als Vorschlags-Branch zurück und erscheinen als Karte im
Auftragsbuch. Dieser Service ist nur die MC2-Tür dazu: anlegen + anzeigen + aufräumen,
alles über die Hermes-CLI (stabile --json-Schnittstelle, kein Griff in die kanban.db).
Türen insgesamt: Telegram/Desktop (natives kanban_create-Tool), Lucy-Voice
(mcp_voice.idee_notieren → POST /api/ideen) und die Zentrale (Auftragsbuch-Tab → hier).
Lokal (Windows-Dev) ist alles harmlos: available=False, Aktionen geben Fehler statt zu crashen.
"""
import json
import logging
import os
import re
import subprocess
import time
from pathlib import Path
log = logging.getLogger(__name__)
HERMES = Path(os.environ.get("MC_HERMES_BIN", "~/.local/bin/hermes")).expanduser()
_TASK_ID_RX = re.compile(r"^t_[0-9a-f]{4,16}$")
# Die CLI kostet pro Aufruf ein paar Sekunden Python-Start — die UI pollt aber.
_list_cache: dict = {"ts": 0.0, "data": None}
_LIST_EVERY = 15.0 # s
# Cache für worker log
_log_cache: dict = {"ts": 0.0, "task_id": None, "data": None}
_LOG_EVERY = 4.0 # s
def _available() -> bool:
return os.name == "posix" and HERMES.is_file()
def _hermes(args: list[str], timeout: int = 60) -> subprocess.CompletedProcess:
return subprocess.run([str(HERMES), "kanban", *args],
capture_output=True, text=True, timeout=timeout)
def _parse_json(stdout: str):
"""CLI-Ausgabe kann Log-Zeilen vor dem JSON enthalten — ab der ersten Klammer parsen."""
text = stdout or ""
for opener in ("[", "{"):
idx = text.find(opener)
if idx >= 0:
try:
return json.loads(text[idx:])
except json.JSONDecodeError:
continue
return None
def list_queue() -> dict:
"""Alle nicht archivierten Aufgaben des Boards, jüngste zuerst — die Queue-Sicht der UI."""
if not _available():
return {"available": False, "items": [], "offen": 0}
now = time.time()
if _list_cache["data"] is not None and now - _list_cache["ts"] < _LIST_EVERY:
return _list_cache["data"]
try:
r = _hermes(["list", "--json", "--sort", "created-desc"])
raw = _parse_json(r.stdout)
except Exception:
log.warning("ideen: kanban list fehlgeschlagen", exc_info=True)
raw = None
if not isinstance(raw, list):
# CLI kaputt/Timeout → ehrlich leer melden, aber nicht cachen (nächster Poll versucht's neu)
return {"available": True, "items": [], "offen": 0, "fehler": "Queue nicht lesbar"}
items = []
for t in raw[:60]:
try:
items.append({
"id": t.get("id", ""),
"titel": (t.get("title") or "(ohne Titel)")[:200],
"body": (t.get("body") or "")[:600],
"status": t.get("status", "?"),
"assignee": t.get("assignee"),
"erstellt": t.get("created_at"),
"fertig": t.get("completed_at"),
"von": t.get("created_by"),
})
except Exception:
continue
offen = sum(1 for i in items if i["status"] not in ("done",))
data = {"available": True, "items": items, "offen": offen}
_list_cache.update(ts=now, data=data)
return data
def add_idea(titel: str, notiz: str = "", created_by: str = "mc2-ui") -> dict:
"""Idee in die Queue legen: triage — der Specifier der Box arbeitet sie selbst aus."""
if not _available():
return {"ok": False, "error": "Die Ideen-Queue lebt auf der Box."}
titel = (titel or "").strip()
if not (3 <= len(titel) <= 200):
return {"ok": False, "error": "Titel bitte zwischen 3 und 200 Zeichen."}
args = ["create", titel, "--triage", "--created-by", created_by, "--json"]
notiz = (notiz or "").strip()
if notiz:
args[2:2] = ["--body", notiz[:4000]]
try:
r = _hermes(args)
except Exception as exc:
return {"ok": False, "error": f"Queue nicht erreichbar: {exc}"}
task = _parse_json(r.stdout)
if r.returncode != 0 or not isinstance(task, dict) or not task.get("id"):
return {"ok": False, "error": (r.stderr or r.stdout or "kanban create fehlgeschlagen").strip()[:300]}
_list_cache["ts"] = 0.0 # nächster Poll zeigt die neue Idee sofort
try:
from services import announce
announce.add(f"Neue Idee in der Queue: „{titel}“ ({task['id']}) — die Box arbeitet sie aus.",
"[Ideen-Queue]", "ideen-queue", "silent")
except Exception:
pass
return {"ok": True, "id": task["id"], "status": task.get("status")}
def archive_task(task_id: str) -> dict:
"""Erledigtes/Verworfenes aus der Sicht räumen (Kanban-Archiv, nichts wird gelöscht)."""
if not _available():
return {"ok": False, "error": "Die Ideen-Queue lebt auf der Box."}
if not _TASK_ID_RX.match(task_id or ""):
return {"ok": False, "error": f"Keine gültige Aufgaben-Nummer: {task_id!r}"}
try:
r = _hermes(["archive", task_id])
except Exception as exc:
return {"ok": False, "error": f"Queue nicht erreichbar: {exc}"}
if r.returncode != 0:
return {"ok": False, "error": (r.stderr or r.stdout or "Archivieren fehlgeschlagen").strip()[:300]}
_list_cache["ts"] = 0.0
return {"ok": True}
def log_of(task_id: str) -> dict:
"""Worker-Log einer Idee (hermes kanban log <id>), ANSI entfernt, letzte ~120 Zeilen."""
if not _available():
return {"available": False, "lines": []}
if not _TASK_ID_RX.match(task_id or ""):
return {"available": True, "lines": []}
now = time.time()
if (
_log_cache["data"] is not None
and _log_cache["task_id"] == task_id
and now - _log_cache["ts"] < _LOG_EVERY
):
return _log_cache["data"]
try:
r = _hermes(["log", task_id])
except Exception as exc:
log.warning("ideen: kanban log fehlgeschlagen", exc_info=True)
return {"available": True, "lines": []}
if r.returncode != 0:
log.warning("ideen: kanban log exit=%s stderr=%s", r.returncode, r.stderr)
return {"available": True, "lines": []}
raw = r.stdout or ""
# ANSI-Steuerzeichen entfernen
ansi = re.compile(r"\x1b\[[0-9;]*m")
cleaned = ansi.sub("", raw)
# Letzte ~120 Zeilen
lines = cleaned.strip().splitlines()[-120:]
data = {"available": True, "lines": lines}
_log_cache.update(ts=now, task_id=task_id, data=data)
return data