Auftragsbuch: Ketten-Sicht (Projekt-Gruppen) + Telegram-Ketten-Digest

Queue-UI gruppiert verkettete Karten zu Projekten: Fortschritt, laufende
Karte mit Heartbeat-Notiz, 'gleich dran', Rest eingeklappt mit 'wartet auf'.
Backend reichert die CLI-Sicht read-only aus kanban.db an (task_links +
Heartbeats). Neuer Digest-Waechter meldet per Telegram: Fragen sofort,
Projekt-Abschluss sofort, sonst 60-min-Puls (User-Wahl 20.07.).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-20 13:06:46 +02:00
parent eb1f333bba
commit a91d70d9b7
30 changed files with 519 additions and 158 deletions
+4 -1
View File
@@ -43,7 +43,7 @@ from routers import (
)
from routers import reminders as reminders_router
from services import memory as memory_svc
from services import metrics_history, reminders, sentry, warmer
from services import ketten_digest, metrics_history, reminders, sentry, warmer
# Zentrales Logging — Level via MC_LOG_LEVEL (INFO default). Eine Konfiguration
# für alle Module (logging.getLogger(__name__)).
@@ -68,6 +68,9 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
if sentry.ENABLED:
tasks.append(asyncio.create_task(sentry.sentry_loop()))
tasks.append(asyncio.create_task(reminders.reminders_loop()))
if ketten_digest.ENABLED:
# Werkstatt-Status aufs Handy: Fragen sofort, Meilensteine sofort, sonst 60-min-Puls.
tasks.append(asyncio.create_task(ketten_digest.digest_loop()))
# 24-h-Metrik-Verlauf (Cockpit-Zeitachse): 10-s-Sampler, Ringpuffer, persistiert.
tasks.append(asyncio.create_task(metrics_history.sampler_loop()))
if memory_svc.AUTO_DEDUPE_ENABLED:
+116 -1
View File
@@ -17,6 +17,7 @@ import json
import logging
import os
import re
import sqlite3
import subprocess
import time
from pathlib import Path
@@ -24,6 +25,12 @@ from pathlib import Path
log = logging.getLogger(__name__)
HERMES = Path(os.environ.get("MC_HERMES_BIN", "~/.local/bin/hermes")).expanduser()
# Ketten-Sicht (20.07.): Eltern-Links + Heartbeat-Notizen stehen NICHT im `list --json`
# der CLI — dafür (und NUR dafür) ein read-only-Blick in die kanban.db. Das bricht die
# „alles über die CLI"-Regel bewusst minimal: zwei stabile Tabellen (task_links 2 Spalten,
# task_events), Verbindung strikt mode=ro, jeder Fehler fällt lautlos auf die flache
# Sicht zurück. Schreiben läuft weiterhin ausschließlich über die CLI.
KANBAN_DB = Path(os.environ.get("MC_KANBAN_DB", "~/.hermes/kanban.db")).expanduser()
_TASK_ID_RX = re.compile(r"^t_[0-9a-f]{4,16}$")
@@ -92,6 +99,113 @@ def _blocker_frage(task_id: str) -> dict | None:
return {"frage": frage[:800], "kind": kind}
def _projekt_titel(titles: list[str]) -> str:
"""Anzeigename einer Karten-Familie: gemeinsamer Titel-Präfix (z. B. „Homelab-Dashboard 2.0"),
sonst der Titel der ältesten Karte. Reine Anzeige-Heuristik, nichts hängt davon ab."""
if not titles:
return "Projekt"
prefix = titles[0]
for t in titles[1:]:
while prefix and not t.startswith(prefix):
prefix = prefix[:-1]
prefix = prefix.strip(" -:·")
return prefix if len(prefix) >= 10 else titles[0][:80]
def _ketten_anreichern(items: list[dict]) -> list[dict]:
"""Ketten-Sicht über die Queue legen (read-only, best-effort).
Ergänzt pro Karte: `eltern` (offene Vorgänger-IDs), `wartet_auf` (deren Titel),
`notiz` (jüngste Heartbeat-Notiz des Workers bei running) und `projekt`
(Familien-Schlüssel). Liefert die Projekt-Gruppen für die UI: zusammenhängende
Karten (task_links) = ein Projekt; Einzelkarten bleiben ohne `projekt`.
Jeder Fehler (Windows-Dev, Schema-Drift, Lock) → unveränderte flache Sicht.
"""
for it in items:
it.setdefault("eltern", [])
it.setdefault("wartet_auf", [])
it.setdefault("notiz", None)
it.setdefault("projekt", None)
if not items or not KANBAN_DB.is_file():
return []
by_id = {it["id"]: it for it in items if it.get("id")}
try:
con = sqlite3.connect(f"file:{KANBAN_DB}?mode=ro", uri=True, timeout=3)
try:
con.execute("PRAGMA busy_timeout=2000")
links = [(p, c) for p, c in con.execute(
"SELECT parent_id, child_id FROM task_links")
if p in by_id or c in by_id]
for it in items:
if it.get("status") != "running":
continue
for (payload,) in con.execute(
"SELECT payload FROM task_events WHERE task_id=? AND kind='heartbeat' "
"AND payload IS NOT NULL ORDER BY created_at DESC LIMIT 5", (it["id"],)):
try:
note = (json.loads(payload) or {}).get("note")
except Exception:
note = None
if note:
it["notiz"] = str(note)[:300]
break
finally:
con.close()
except Exception:
log.debug("ideen: Ketten-Anreicherung fehlgeschlagen", exc_info=True)
return []
# Eltern/wartet_auf: nur Vorgänger zählen, die selbst noch offen in der Liste stehen —
# archivierte/fremde Parents gelten als erledigt.
ERLEDIGT = ("done",)
for parent, child in links:
it = by_id.get(child)
p = by_id.get(parent)
if it and p and p.get("status") not in ERLEDIGT:
it["eltern"].append(parent)
if len(it["wartet_auf"]) < 3:
it["wartet_auf"].append((p.get("titel") or parent)[:90])
# Familien = zusammenhängende Komponenten über die Links (Union-Find, klein genug).
chef: dict[str, str] = {}
def boss(x: str) -> str:
while chef.get(x, x) != x:
chef[x] = chef.get(chef[x], chef[x])
x = chef[x]
return x
for parent, child in links:
if parent in by_id and child in by_id:
a, b = boss(parent), boss(child)
if a != b:
chef[a] = b
familien: dict[str, list[dict]] = {}
for it in items:
familien.setdefault(boss(it["id"]), []).append(it)
projekte = []
for wurzel, mitglieder in familien.items():
if len(mitglieder) < 2:
continue
mitglieder.sort(key=lambda i: i.get("erstellt") or 0)
key = mitglieder[0]["id"]
for it in mitglieder:
it["projekt"] = key
fertig = sum(1 for i in mitglieder if i["status"] in ERLEDIGT)
projekte.append({
"key": key,
"titel": _projekt_titel([i["titel"] for i in mitglieder]),
"gesamt": len(mitglieder),
"fertig": fertig,
"laeuft": sum(1 for i in mitglieder if i["status"] == "running"),
"haengt": sum(1 for i in mitglieder if i["status"] == "blocked"),
})
# Aktive Projekte zuerst (hängend > laufend > Rest), dann nach Größe.
projekte.sort(key=lambda p: (-p["haengt"], -p["laeuft"], -p["gesamt"]))
return projekte
def list_queue() -> dict:
"""Alle nicht archivierten Aufgaben des Boards, jüngste zuerst — die Queue-Sicht der UI."""
if not _available():
@@ -137,8 +251,9 @@ def list_queue() -> dict:
it["frage"] = info["frage"]
it["frage_kind"] = info["kind"]
projekte = _ketten_anreichern(items)
offen = sum(1 for i in items if i["status"] not in ("done",))
data = {"available": True, "items": items, "offen": offen}
data = {"available": True, "items": items, "offen": offen, "projekte": projekte}
_list_cache.update(ts=now, data=data)
return data
+146
View File
@@ -0,0 +1,146 @@
"""
Ketten-Digest — Telegram-Status für lange Aufgaben-Ketten (User-Wunsch 20.07.2026).
Wenn die Werkstatt eine Karten-Familie abarbeitet (z. B. „Homelab-Dashboard 2.0" mit
19 verketteten Karten), soll der Commander nicht raten müssen: dieser Wächter liest
die Queue-Sicht (services/ideen.py, inkl. Ketten-Anreicherung) und meldet per Telegram
• SOFORT, wenn eine Karte hängt und eine Frage an den Commander hat (blocked) —
einmal pro Karte, mit der Frage im Wortlaut,
• bei MEILENSTEINEN (Projekt komplett fertig) sofort,
• sonst höchstens einmal pro PULS_SEKUNDEN (Default 60 min) einen Zwischenstand
je aktivem Projekt („5/19 fertig · läuft: … · als Nächstes: …").
Taktung war explizite User-Wahl („Meilensteine + 60-min-Puls, Fragen immer sofort").
Zustand (was wurde wann gemeldet) liegt als JSON neben den Modellen und übersteht
Neustarts — sonst käme nach jedem Deploy ein Duplikat-Schwall. Auf Windows (Dev) No-op.
"""
import asyncio
import json
import logging
import os
import time
import zlib
from pathlib import Path
from config import MODELS_DIR
log = logging.getLogger(__name__)
ENABLED = os.name == "posix" and os.environ.get("MC_KETTEN_DIGEST", "1") != "0"
INTERVAL = int(os.environ.get("MC_KETTEN_DIGEST_INTERVAL", "300")) # Prüf-Tick: 5 min
PULS_SEKUNDEN = int(os.environ.get("MC_KETTEN_DIGEST_PULS", "3600")) # Zwischenstand: 60 min
STATE_PATH = Path(os.environ.get("MC_KETTEN_DIGEST_STATE", str(MODELS_DIR / "mc2-ketten-digest.json")))
def _load_state() -> dict:
try:
d = json.loads(STATE_PATH.read_text(encoding="utf-8"))
return d if isinstance(d, dict) else {}
except Exception:
return {}
def _save_state(state: dict) -> None:
try:
tmp = STATE_PATH.with_suffix(".tmp")
tmp.write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8")
tmp.replace(STATE_PATH)
except OSError:
log.warning("ketten-digest: Zustand %s nicht schreibbar", STATE_PATH, exc_info=True)
def _melden(subject: str, text: str) -> None:
"""Aufs Handy UND in den Briefkasten (silent — Lucy muss den Status nicht sprechen)."""
from services import announce
announce.notify_telegram(subject, text)
try:
announce.add(text, subject, "ketten-digest", "silent")
except Exception:
pass
def _tick() -> None:
from services import ideen
data = ideen.list_queue()
if not data.get("available"):
return
items = data.get("items") or []
projekte = data.get("projekte") or []
by_id = {i["id"]: i for i in items if i.get("id")}
state = _load_state()
gemeldete_fragen: dict = state.setdefault("fragen", {})
projekt_state: dict = state.setdefault("projekte", {})
dirty = False
now = int(time.time())
# 1) Hängende Karten mit Frage → sofort, einmal pro Karte. Der Schlüssel enthält den
# Fragen-Text-Hash: hängt dieselbe Karte später mit NEUER Frage, wird wieder gemeldet.
for it in items:
if it.get("status") != "blocked":
continue
frage = (it.get("frage") or "").strip()
key = f"{it['id']}:{zlib.crc32(frage.encode('utf-8')):x}"
if key in gemeldete_fragen:
continue
text = (f"Aufgabe hängt und wartet auf dich: „{(it.get('titel') or '')[:120]}\n"
+ (f"Frage: {frage[:500]}\n" if frage else "")
+ "Antworten geht im Auftragsbuch (Zentrale) — die Box macht dann weiter.")
_melden("[Werkstatt]", text)
gemeldete_fragen[key] = now
dirty = True
# Fragen-Gedächtnis klein halten (Karten verschwinden irgendwann ins Archiv).
if len(gemeldete_fragen) > 200:
for k in sorted(gemeldete_fragen, key=gemeldete_fragen.get)[:100]:
gemeldete_fragen.pop(k, None)
dirty = True
# 2) Projekt-Status: Abschluss sofort, sonst gedrosselter Puls solange gearbeitet wird.
for p in projekte:
ps = projekt_state.setdefault(p["key"], {})
fertig, gesamt = p.get("fertig", 0), p.get("gesamt", 0)
if gesamt > 0 and fertig >= gesamt and not ps.get("abschluss_gemeldet"):
_melden("[Werkstatt]", f"Projekt fertig: „{p['titel']}“ — alle {gesamt} Karten erledigt. "
"Ergebnisse liegen im Auftragsbuch.")
ps.update(abschluss_gemeldet=True, letzter_puls=now, letzter_stand=fertig)
dirty = True
continue
aktiv = p.get("laeuft", 0) > 0
if not aktiv:
continue
if now - int(ps.get("letzter_puls") or 0) < PULS_SEKUNDEN:
continue
laufende = [i for i in items if i.get("projekt") == p["key"] and i["status"] == "running"]
naechste = [i for i in items if i.get("projekt") == p["key"]
and i["status"] in ("ready", "todo") and not i.get("wartet_auf")]
zeilen = [f"Werkstatt-Status „{p['titel']}“: {fertig}/{gesamt} fertig."]
for l in laufende[:2]:
note = f"{l['notiz']}" if l.get("notiz") else ""
zeilen.append(f"Läuft: {l['titel'][:90]}{note}")
if naechste:
zeilen.append(f"Als Nächstes: {naechste[0]['titel'][:90]}")
haengt = p.get("haengt", 0)
if haengt:
zeilen.append(f"{haengt} Karte(n) warten auf deine Antwort im Auftragsbuch.")
_melden("[Werkstatt]", "\n".join(zeilen))
ps.update(letzter_puls=now, letzter_stand=fertig)
dirty = True
if dirty:
_save_state(state)
async def digest_loop() -> None:
"""Hintergrund-Task im MC2-Lifespan (Muster: reminders_loop)."""
log.info("ketten-digest: aktiv (Tick %ss, Puls %ss)", INTERVAL, PULS_SEKUNDEN)
while True:
try:
await asyncio.to_thread(_tick)
except Exception:
log.debug("ketten-digest: Tick fehlgeschlagen", exc_info=True)
await asyncio.sleep(INTERVAL)
@@ -1,4 +1,4 @@
import{c as A,Y as P,l as B,b as M,u as D,r as z,j as e,Z as T,e as a,$ as g,a0 as b,a1 as W,x as f,a2 as j,W as G,X as I,C as N,g as y,q as i}from"./index-4TXIIaNW.js";/**
import{c as A,Y as P,l as B,b as M,u as D,r as z,j as e,Z as T,e as a,$ as g,a0 as b,a1 as W,x as f,a2 as j,W as G,X as I,C as N,g as y,q as i}from"./index-BntQHmEa.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
import{c as m,af as S,r as c,j as e,ag as z,L as g,U as M,ah as C,ae as D,ai as A,aj as B,ak as Z,e as E,al as L,u as R,b as q,g as k,q as T}from"./index-4TXIIaNW.js";import{R as j}from"./rotate-ccw-CYpBo2o9.js";/**
import{c as m,af as S,r as c,j as e,ag as z,L as g,U as M,ah as C,ad as D,ai as A,aj as B,ak as Z,e as E,al as L,u as R,b as q,g as k,q as T}from"./index-BntQHmEa.js";import{R as j}from"./rotate-ccw-BFhp_4da.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as v,r as c,I,J as A,j as e,K as h,x as B,B as H,D as E,M as z,e as M,L as T,N as F,C as K}from"./index-4TXIIaNW.js";import{F as O}from"./folder-open-DYgcn-QY.js";import{C as R}from"./circle-x-1dEEOF16.js";import{C as W}from"./copy-R_b2-9j5.js";/**
import{c as v,r as c,I,J as A,j as e,K as h,x as B,B as H,D as E,M as z,e as M,L as T,N as F,C as K}from"./index-BntQHmEa.js";import{F as O}from"./folder-open-BUTrMI6F.js";import{C as R}from"./circle-x-DeBWMUGH.js";import{C as W}from"./copy-D-sgiUjj.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as h,am as b,r as i,j as e,L as g,an as f,a2 as j,U as k,e as o,D as N,M as v,N as w,g as y}from"./index-4TXIIaNW.js";import{B as S}from"./book-open-DJBmaAtc.js";import{C as z}from"./circle-x-1dEEOF16.js";/**
import{c as h,am as b,r as i,j as e,L as g,an as f,a2 as j,U as k,e as o,D as N,M as v,N as w,g as y}from"./index-BntQHmEa.js";import{B as S}from"./book-open-Cz-PKFIW.js";import{C as z}from"./circle-x-DeBWMUGH.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{Y as b,u as f,$ as h,r as d,j as e,e as c,Z as g,T as p,a3 as j,a4 as N,g as v,a5 as w,q as x}from"./index-4TXIIaNW.js";function y(){const{data:s}=b(),m=f(),r=s!=null&&s.box_console_url?h(s.box_console_url):void 0,a=s==null?void 0:s.box_console_reachable,[l,o]=d.useState(!1),[i,n]=d.useState("");async function u(){o(!0),n("");try{const t=await v("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})});n(t.ok?"Neu gestartet — einen Moment, dann lädt das Terminal.":`Fehlgeschlagen: ${t.err||"Unbekannter Fehler"}`),w(m,x.agentStatus,x.services)}catch(t){n(`Fehlgeschlagen: ${(t==null?void 0:t.message)||t}`)}finally{o(!1)}}return e.jsxs("div",{className:"flex h-full flex-col gap-4",children:[e.jsxs("div",{className:"flex flex-wrap items-end justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Konsole"}),e.jsx("p",{className:"flex items-center gap-2 text-sm text-muted-foreground",children:"Direkte Shell auf der Box — wie ein SSH-Fenster, mitten im Browser."})]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground",children:[e.jsx("span",{className:c("h-2 w-2 rounded-full",a?"bg-emerald-500 animate-pulse":"bg-amber-500")}),a?"online":"offline"]}),r&&e.jsxs("a",{href:r,target:"_blank",rel:"noopener",className:"flex h-8 items-center gap-1.5 rounded-lg border border-border/60 bg-background/20 px-3 text-xs font-semibold text-muted-foreground transition-all hover:border-primary/50 hover:text-foreground",children:[e.jsx(g,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),r?e.jsxs("div",{className:"relative min-h-[58vh] flex-1 overflow-hidden rounded-2xl border border-border/60 bg-black/50 shadow-lg shadow-black/25",children:[a===!1&&e.jsxs("div",{className:"absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/70 text-center",children:[e.jsx(p,{className:"h-8 w-8 text-amber-400"}),e.jsx("div",{className:"text-sm font-semibold text-amber-300",children:"Konsole nicht erreichbar"}),e.jsxs("p",{className:"max-w-sm text-[11px] leading-normal text-muted-foreground",children:["Der Terminal-Dienst (",e.jsx("code",{className:"font-mono text-primary",children:"box-console"}),") läuft gerade nicht."]}),e.jsxs("button",{onClick:u,disabled:l,className:"flex h-9 items-center gap-1.5 rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 text-[11px] font-bold uppercase tracking-wide text-amber-300 transition-all hover:bg-amber-500/20 cursor-pointer disabled:opacity-50",children:[e.jsx(j,{className:c("h-3.5 w-3.5",l&&"animate-spin")})," Dienst neu starten"]}),i&&e.jsx("p",{className:"max-w-sm text-[11px] text-muted-foreground",children:i})]}),e.jsx("iframe",{src:r,title:"Box-Konsole",className:"h-full w-full border-0",style:{minHeight:"58vh"}})]}):e.jsxs("div",{className:"flex min-h-[58vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground",children:[e.jsx(N,{className:"mr-2 h-4 w-4"})," Lade Konsole…"]})]})}export{y as KonsoleView};
import{Y as b,u as f,$ as h,r as d,j as e,e as c,Z as g,T as p,a3 as j,a4 as N,g as v,a5 as w,q as x}from"./index-BntQHmEa.js";function y(){const{data:s}=b(),m=f(),r=s!=null&&s.box_console_url?h(s.box_console_url):void 0,a=s==null?void 0:s.box_console_reachable,[l,o]=d.useState(!1),[i,n]=d.useState("");async function u(){o(!0),n("");try{const t=await v("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})});n(t.ok?"Neu gestartet — einen Moment, dann lädt das Terminal.":`Fehlgeschlagen: ${t.err||"Unbekannter Fehler"}`),w(m,x.agentStatus,x.services)}catch(t){n(`Fehlgeschlagen: ${(t==null?void 0:t.message)||t}`)}finally{o(!1)}}return e.jsxs("div",{className:"flex h-full flex-col gap-4",children:[e.jsxs("div",{className:"flex flex-wrap items-end justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Konsole"}),e.jsx("p",{className:"flex items-center gap-2 text-sm text-muted-foreground",children:"Direkte Shell auf der Box — wie ein SSH-Fenster, mitten im Browser."})]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground",children:[e.jsx("span",{className:c("h-2 w-2 rounded-full",a?"bg-emerald-500 animate-pulse":"bg-amber-500")}),a?"online":"offline"]}),r&&e.jsxs("a",{href:r,target:"_blank",rel:"noopener",className:"flex h-8 items-center gap-1.5 rounded-lg border border-border/60 bg-background/20 px-3 text-xs font-semibold text-muted-foreground transition-all hover:border-primary/50 hover:text-foreground",children:[e.jsx(g,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),r?e.jsxs("div",{className:"relative min-h-[58vh] flex-1 overflow-hidden rounded-2xl border border-border/60 bg-black/50 shadow-lg shadow-black/25",children:[a===!1&&e.jsxs("div",{className:"absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/70 text-center",children:[e.jsx(p,{className:"h-8 w-8 text-amber-400"}),e.jsx("div",{className:"text-sm font-semibold text-amber-300",children:"Konsole nicht erreichbar"}),e.jsxs("p",{className:"max-w-sm text-[11px] leading-normal text-muted-foreground",children:["Der Terminal-Dienst (",e.jsx("code",{className:"font-mono text-primary",children:"box-console"}),") läuft gerade nicht."]}),e.jsxs("button",{onClick:u,disabled:l,className:"flex h-9 items-center gap-1.5 rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 text-[11px] font-bold uppercase tracking-wide text-amber-300 transition-all hover:bg-amber-500/20 cursor-pointer disabled:opacity-50",children:[e.jsx(j,{className:c("h-3.5 w-3.5",l&&"animate-spin")})," Dienst neu starten"]}),i&&e.jsx("p",{className:"max-w-sm text-[11px] text-muted-foreground",children:i})]}),e.jsx("iframe",{src:r,title:"Box-Konsole",className:"h-full w-full border-0",style:{minHeight:"58vh"}})]}):e.jsxs("div",{className:"flex min-h-[58vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground",children:[e.jsx(N,{className:"mr-2 h-4 w-4"})," Lade Konsole…"]})]})}export{y as KonsoleView};
@@ -1,5 +1,5 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/GraphView-Btsdiiax.js","assets/index-4TXIIaNW.js","assets/index-Bsg6hDDU.css"])))=>i.map(i=>d[i]);
var je=Object.defineProperty;var ke=(d,i,c)=>i in d?je(d,i,{enumerable:!0,configurable:!0,writable:!0,value:c}):d[i]=c;var J=(d,i,c)=>ke(d,typeof i!="symbol"?i+"":i,c);import{c as h,r as s,j as e,u as ve,b as Ne,O as W,Q as Se,C as X,R as Ce,S as Ee,P as Me,U as O,e as x,V as ze,W as De,X as Z,A as Y,_ as Oe,g as j}from"./index-4TXIIaNW.js";import{C as ee}from"./copy-R_b2-9j5.js";import{S as _e}from"./send-Cbry629d.js";import{B as Ge}from"./book-open-DJBmaAtc.js";/**
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/GraphView-Btm2f4YT.js","assets/index-BntQHmEa.js","assets/index-CeczSNp8.css"])))=>i.map(i=>d[i]);
var je=Object.defineProperty;var ke=(d,i,c)=>i in d?je(d,i,{enumerable:!0,configurable:!0,writable:!0,value:c}):d[i]=c;var J=(d,i,c)=>ke(d,typeof i!="symbol"?i+"":i,c);import{c as h,r as s,j as e,u as ve,b as Ne,O as W,Q as Se,C as X,R as Ce,S as Ee,P as Me,U as O,e as x,V as ze,W as De,X as Z,A as Y,_ as Oe,g as j}from"./index-BntQHmEa.js";import{C as ee}from"./copy-D-sgiUjj.js";import{S as _e}from"./send-DDnzwf-a.js";import{B as Ge}from"./book-open-Cz-PKFIW.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -34,7 +34,7 @@ var je=Object.defineProperty;var ke=(d,i,c)=>i in d?je(d,i,{enumerable:!0,config
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Pe=h("User",[["path",{d:"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2",key:"975kel"}],["circle",{cx:"12",cy:"7",r:"4",key:"17ys0d"}]]);class te extends s.Component{constructor(){super(...arguments);J(this,"state",{error:null})}static getDerivedStateFromError(c){return{error:c}}render(){return this.state.error?e.jsxs("div",{className:"h-[480px] rounded-2xl border border-red-500/20 bg-red-500/5 p-6 text-xs text-red-400 overflow-auto",children:[e.jsx("div",{className:"font-semibold mb-2",children:"Graph konnte nicht gerendert werden"}),e.jsx("pre",{className:"whitespace-pre-wrap break-words text-[11px] leading-relaxed",children:this.state.error.message})]}):this.props.children}}const se=s.lazy(()=>Oe(()=>import("./GraphView-Btsdiiax.js"),__vite__mapDeps([0,1,2])).then(d=>({default:d.GraphView}))),k=["identity","knowledge","rules","events"],v=new Set(["auto","agent","hermes"]),N={identity:{label:"Identität",icon:Pe,bg:"bg-cyan-500/10",text:"text-cyan-400"},knowledge:{label:"Wissen",icon:De,bg:"bg-indigo-500/10",text:"text-indigo-400"},rules:{label:"Regeln",icon:Le,bg:"bg-violet-500/10",text:"text-violet-400"},events:{label:"Ereignisse",icon:ze,bg:"bg-amber-500/10",text:"text-amber-400"}},ne={label:"Gedächtnis",icon:Ge,text:"text-muted-foreground"},Re={identity:"border-l-cyan-500/80",knowledge:"border-l-indigo-500/80",rules:"border-l-violet-500/80",events:"border-l-amber-500/80"},_={identity:"#00f5ff",knowledge:"#3b82f6",rules:"#d946ef",events:"#fbbf24"},ae=`Hi Hermes! Lass uns ein kurzes Onboarding machen, damit du dich künftig an mich erinnerst. Stell mir nacheinander ein paar kurze Fragen zu:
*/const Pe=h("User",[["path",{d:"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2",key:"975kel"}],["circle",{cx:"12",cy:"7",r:"4",key:"17ys0d"}]]);class te extends s.Component{constructor(){super(...arguments);J(this,"state",{error:null})}static getDerivedStateFromError(c){return{error:c}}render(){return this.state.error?e.jsxs("div",{className:"h-[480px] rounded-2xl border border-red-500/20 bg-red-500/5 p-6 text-xs text-red-400 overflow-auto",children:[e.jsx("div",{className:"font-semibold mb-2",children:"Graph konnte nicht gerendert werden"}),e.jsx("pre",{className:"whitespace-pre-wrap break-words text-[11px] leading-relaxed",children:this.state.error.message})]}):this.props.children}}const se=s.lazy(()=>Oe(()=>import("./GraphView-Btm2f4YT.js"),__vite__mapDeps([0,1,2])).then(d=>({default:d.GraphView}))),k=["identity","knowledge","rules","events"],v=new Set(["auto","agent","hermes"]),N={identity:{label:"Identität",icon:Pe,bg:"bg-cyan-500/10",text:"text-cyan-400"},knowledge:{label:"Wissen",icon:De,bg:"bg-indigo-500/10",text:"text-indigo-400"},rules:{label:"Regeln",icon:Le,bg:"bg-violet-500/10",text:"text-violet-400"},events:{label:"Ereignisse",icon:ze,bg:"bg-amber-500/10",text:"text-amber-400"}},ne={label:"Gedächtnis",icon:Ge,text:"text-muted-foreground"},Re={identity:"border-l-cyan-500/80",knowledge:"border-l-indigo-500/80",rules:"border-l-violet-500/80",events:"border-l-amber-500/80"},_={identity:"#00f5ff",knowledge:"#3b82f6",rules:"#d946ef",events:"#fbbf24"},ae=`Hi Hermes! Lass uns ein kurzes Onboarding machen, damit du dich künftig an mich erinnerst. Stell mir nacheinander ein paar kurze Fragen zu:
1) wer ich bin und woran ich gerade arbeite,
2) wie ich angesprochen werden möchte,
3) meine bevorzugten Tools, Sprachen und Arbeitsweise,
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
import{ao as y,r as x,g as L,j as e,L as v,S,ap as W,U as C,e as w,aq as E}from"./index-4TXIIaNW.js";import{F as M}from"./folder-open-DYgcn-QY.js";const D={"":"Allgemein",traeume:"Träume",muster:"Muster","skill-kandidaten":"Skill-Kandidaten","skill-kandidaten/beauftragt":"Skill-Kandidaten · beauftragt","skill-kandidaten/verworfen":"Skill-Kandidaten · verworfen"};function T(){const{data:o,isLoading:h}=y(),[l,f]=x.useState(null),[d,r]=x.useState(null),[u,i]=x.useState(!1),[a,p]=x.useState(""),t=x.useMemo(()=>(o==null?void 0:o.files)??[],[o]),m=x.useMemo(()=>{const s=new Map;for(const n of t)s.set(n.name.toLowerCase(),n.path);return s},[t]),g=x.useMemo(()=>{if(!a.trim())return t;const s=a.toLowerCase();return t.filter(n=>n.title.toLowerCase().includes(s)||n.path.toLowerCase().includes(s))},[t,a]),b=x.useMemo(()=>{const s=[];for(const n of g){const c=s.find(k=>k.dir===n.dir);c?c.files.push(n):s.push({dir:n.dir,files:[n]})}return s.sort((n,c)=>n.dir===""?-1:c.dir===""?1:n.dir.localeCompare(c.dir))},[g]);x.useEffect(()=>{if(!l&&t.length){const s=t.find(n=>n.path.toLowerCase()==="index.md");f((s==null?void 0:s.path)??t[0].path)}},[t,l]),x.useEffect(()=>{l&&(i(!0),L(`/api/wissen/datei?pfad=${encodeURIComponent(l)}`).then(r).catch(()=>r({path:l,content:"*(Notiz nicht ladbar)*",mtime:0})).finally(()=>i(!1)))},[l]);const j=s=>{const n=m.get(s.toLowerCase());n&&f(n)};return e.jsxs("div",{className:"space-y-5",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Wissen"}),e.jsx("p",{className:"text-sm text-muted-foreground",children:"Was sich die Box nachts erschließt — Traum-Notizen, Muster und Skill-Ideen aus dem Wissens-Vault, verlinkt wie ein Wiki."})]}),o&&!o.available&&e.jsx("div",{className:"rounded-2xl border border-amber-500/30 bg-amber-500/5 p-4 text-xs text-amber-300",children:"Der Wissens-Vault liegt auf der Box (~/wissens-vault) — im lokalen Dev-Modus nicht verfügbar."}),h&&e.jsxs("div",{className:"flex items-center gap-2 rounded-2xl border border-border/60 bg-card/30 p-6 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Vault wird geladen …"]}),(o==null?void 0:o.available)&&t.length===0&&e.jsx("div",{className:"rounded-2xl border border-dashed border-border/60 bg-card/20 p-8 text-center text-xs text-muted-foreground",children:"Noch keine Notizen. Der nächtliche Traum (03:15) legt sie an — morgen früh steht hier die erste."}),t.length>0&&e.jsxs("div",{className:"flex flex-col gap-4 lg:flex-row",children:[e.jsxs("aside",{className:"w-full shrink-0 space-y-3 lg:w-72",children:[e.jsxs("div",{className:"relative",children:[e.jsx("input",{value:a,onChange:s=>p(s.target.value),type:"search",placeholder:"Notizen durchsuchen…","aria-label":"Notizen durchsuchen",className:"h-9 w-full rounded-lg border border-border/60 bg-card/45 pl-8 pr-3 text-xs text-foreground outline-none focus:ring-1 focus:ring-primary/50"}),e.jsx(S,{className:"absolute left-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground"})]}),e.jsx("div",{className:"max-h-[70vh] space-y-3 overflow-y-auto pr-1 scrollbar-thin",children:b.map(({dir:s,files:n})=>e.jsxs("div",{children:[e.jsxs("p",{className:"mb-1 flex items-center gap-1 text-[10px] font-bold uppercase tracking-wider text-muted-foreground/60",children:[e.jsx(M,{className:"h-3 w-3"})," ",D[s]??s]}),e.jsx("div",{className:"space-y-0.5",children:n.map(c=>e.jsxs("button",{onClick:()=>f(c.path),className:w("flex w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left text-xs transition-all cursor-pointer",l===c.path?"bg-primary/15 text-primary":"text-muted-foreground hover:bg-accent hover:text-foreground"),children:[e.jsx(W,{className:"h-3.5 w-3.5 shrink-0"}),e.jsx("span",{className:"truncate",title:c.title,children:c.name}),c.neu&&e.jsxs("span",{className:"ml-auto flex items-center gap-0.5 rounded bg-violet-500/15 px-1 py-0.5 text-[8px] font-bold uppercase text-violet-300",children:[e.jsx(C,{className:"h-2.5 w-2.5"}),"neu"]})]},c.path))})]},s||"__root"))})]}),e.jsx("div",{className:"min-w-0 flex-1 mc-card p-5",children:u?e.jsxs("div",{className:"flex items-center gap-2 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Notiz wird geladen …"]}):d?e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"mb-3 flex items-center justify-between border-b border-border/40 pb-2",children:[e.jsxs("span",{className:"flex items-center gap-1.5 font-mono text-[11px] text-muted-foreground",children:[e.jsx(E,{className:"h-3.5 w-3.5"})," ",d.path]}),d.mtime>0&&e.jsxs("span",{className:"text-[10px] text-muted-foreground/60",children:["Stand ",new Date(d.mtime*1e3).toLocaleString("de-DE",{day:"2-digit",month:"2-digit",hour:"2-digit",minute:"2-digit"})]})]}),e.jsx(z,{text:d.content,onWikiLink:j,known:m})]}):null})]})]})}function z({text:o,onWikiLink:h,known:l}){const f=o.split(`
import{ao as y,r as x,g as L,j as e,L as v,S,ap as W,U as C,e as w,aq as E}from"./index-BntQHmEa.js";import{F as M}from"./folder-open-BUTrMI6F.js";const D={"":"Allgemein",traeume:"Träume",muster:"Muster","skill-kandidaten":"Skill-Kandidaten","skill-kandidaten/beauftragt":"Skill-Kandidaten · beauftragt","skill-kandidaten/verworfen":"Skill-Kandidaten · verworfen"};function T(){const{data:o,isLoading:h}=y(),[l,f]=x.useState(null),[d,r]=x.useState(null),[u,i]=x.useState(!1),[a,p]=x.useState(""),t=x.useMemo(()=>(o==null?void 0:o.files)??[],[o]),m=x.useMemo(()=>{const s=new Map;for(const n of t)s.set(n.name.toLowerCase(),n.path);return s},[t]),g=x.useMemo(()=>{if(!a.trim())return t;const s=a.toLowerCase();return t.filter(n=>n.title.toLowerCase().includes(s)||n.path.toLowerCase().includes(s))},[t,a]),b=x.useMemo(()=>{const s=[];for(const n of g){const c=s.find(k=>k.dir===n.dir);c?c.files.push(n):s.push({dir:n.dir,files:[n]})}return s.sort((n,c)=>n.dir===""?-1:c.dir===""?1:n.dir.localeCompare(c.dir))},[g]);x.useEffect(()=>{if(!l&&t.length){const s=t.find(n=>n.path.toLowerCase()==="index.md");f((s==null?void 0:s.path)??t[0].path)}},[t,l]),x.useEffect(()=>{l&&(i(!0),L(`/api/wissen/datei?pfad=${encodeURIComponent(l)}`).then(r).catch(()=>r({path:l,content:"*(Notiz nicht ladbar)*",mtime:0})).finally(()=>i(!1)))},[l]);const j=s=>{const n=m.get(s.toLowerCase());n&&f(n)};return e.jsxs("div",{className:"space-y-5",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Wissen"}),e.jsx("p",{className:"text-sm text-muted-foreground",children:"Was sich die Box nachts erschließt — Traum-Notizen, Muster und Skill-Ideen aus dem Wissens-Vault, verlinkt wie ein Wiki."})]}),o&&!o.available&&e.jsx("div",{className:"rounded-2xl border border-amber-500/30 bg-amber-500/5 p-4 text-xs text-amber-300",children:"Der Wissens-Vault liegt auf der Box (~/wissens-vault) — im lokalen Dev-Modus nicht verfügbar."}),h&&e.jsxs("div",{className:"flex items-center gap-2 rounded-2xl border border-border/60 bg-card/30 p-6 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Vault wird geladen …"]}),(o==null?void 0:o.available)&&t.length===0&&e.jsx("div",{className:"rounded-2xl border border-dashed border-border/60 bg-card/20 p-8 text-center text-xs text-muted-foreground",children:"Noch keine Notizen. Der nächtliche Traum (03:15) legt sie an — morgen früh steht hier die erste."}),t.length>0&&e.jsxs("div",{className:"flex flex-col gap-4 lg:flex-row",children:[e.jsxs("aside",{className:"w-full shrink-0 space-y-3 lg:w-72",children:[e.jsxs("div",{className:"relative",children:[e.jsx("input",{value:a,onChange:s=>p(s.target.value),type:"search",placeholder:"Notizen durchsuchen…","aria-label":"Notizen durchsuchen",className:"h-9 w-full rounded-lg border border-border/60 bg-card/45 pl-8 pr-3 text-xs text-foreground outline-none focus:ring-1 focus:ring-primary/50"}),e.jsx(S,{className:"absolute left-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground"})]}),e.jsx("div",{className:"max-h-[70vh] space-y-3 overflow-y-auto pr-1 scrollbar-thin",children:b.map(({dir:s,files:n})=>e.jsxs("div",{children:[e.jsxs("p",{className:"mb-1 flex items-center gap-1 text-[10px] font-bold uppercase tracking-wider text-muted-foreground/60",children:[e.jsx(M,{className:"h-3 w-3"})," ",D[s]??s]}),e.jsx("div",{className:"space-y-0.5",children:n.map(c=>e.jsxs("button",{onClick:()=>f(c.path),className:w("flex w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left text-xs transition-all cursor-pointer",l===c.path?"bg-primary/15 text-primary":"text-muted-foreground hover:bg-accent hover:text-foreground"),children:[e.jsx(W,{className:"h-3.5 w-3.5 shrink-0"}),e.jsx("span",{className:"truncate",title:c.title,children:c.name}),c.neu&&e.jsxs("span",{className:"ml-auto flex items-center gap-0.5 rounded bg-violet-500/15 px-1 py-0.5 text-[8px] font-bold uppercase text-violet-300",children:[e.jsx(C,{className:"h-2.5 w-2.5"}),"neu"]})]},c.path))})]},s||"__root"))})]}),e.jsx("div",{className:"min-w-0 flex-1 mc-card p-5",children:u?e.jsxs("div",{className:"flex items-center gap-2 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Notiz wird geladen …"]}):d?e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"mb-3 flex items-center justify-between border-b border-border/40 pb-2",children:[e.jsxs("span",{className:"flex items-center gap-1.5 font-mono text-[11px] text-muted-foreground",children:[e.jsx(E,{className:"h-3.5 w-3.5"})," ",d.path]}),d.mtime>0&&e.jsxs("span",{className:"text-[10px] text-muted-foreground/60",children:["Stand ",new Date(d.mtime*1e3).toLocaleString("de-DE",{day:"2-digit",month:"2-digit",hour:"2-digit",minute:"2-digit"})]})]}),e.jsx(z,{text:d.content,onWikiLink:j,known:m})]}):null})]})]})}function z({text:o,onWikiLink:h,known:l}){const f=o.split(`
`),d=[];let r=[],u=null;const i=a=>{r.length&&(d.push(e.jsx("ul",{className:"mb-3 ml-4 list-disc space-y-1",children:r},a)),r=[])};return f.forEach((a,p)=>{const t=`l${p}`;if(u!==null){a.trimEnd()==="```"?(d.push(e.jsx("pre",{className:"mb-3 overflow-x-auto rounded-xl border border-border/50 bg-background/50 p-3 font-mono text-[11px] leading-relaxed text-foreground/85",children:u.join(`
`)},t)),u=null):u.push(a);return}if(a.trimStart().startsWith("```")){i(t),u=[];return}const m=a.trimEnd();if(!m.trim()){i(t);return}const g=m.match(/^(#{1,4})\s+(.*)$/);if(g){i(t);const j=g[1].length,s=j===1?"text-lg font-bold mt-1 mb-3":j===2?"text-base font-bold mt-4 mb-2":"text-sm font-bold mt-3 mb-1.5";d.push(e.jsx("p",{className:w(s,"font-space text-foreground"),children:N(g[2],h,l,t)},t));return}const b=m.match(/^\s*[-*•]\s+(.*)$/);if(b){r.push(e.jsx("li",{className:"text-xs leading-relaxed text-foreground/85",children:N(b[1],h,l,t)},t));return}i(t),d.push(e.jsx("p",{className:"mb-2 text-xs leading-relaxed text-foreground/85",children:N(m,h,l,t)},t))}),i("end"),u!==null&&d.push(e.jsx("pre",{className:"mb-3 overflow-x-auto rounded-xl border border-border/50 bg-background/50 p-3 font-mono text-[11px]",children:u.join(`
`)},"code-end")),e.jsx("div",{className:"max-w-3xl",children:d})}function N(o,h,l,f){return o.split(/(\[\[[^\]]+\]\]|\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g).map((r,u)=>{const i=`${f}-${u}`,a=r.match(/^\[\[([^\]|]+)(?:\|([^\]]+))?\]\]$/);if(a){const p=a[1].trim(),t=(a[2]??a[1]).trim();return l.has(p.toLowerCase())?e.jsx("button",{onClick:()=>h(p),className:"rounded bg-primary/10 px-1 text-primary underline decoration-primary/40 underline-offset-2 hover:bg-primary/20 cursor-pointer",children:t},i):e.jsx("span",{className:"rounded bg-background/40 px-1 text-muted-foreground",title:"Notiz existiert (noch) nicht",children:t},i)}return r.startsWith("**")&&r.endsWith("**")?e.jsx("b",{className:"font-semibold text-foreground",children:r.slice(2,-2)},i):r.startsWith("*")&&r.endsWith("*")&&r.length>2?e.jsx("i",{children:r.slice(1,-1)},i):r.startsWith("`")&&r.endsWith("`")?e.jsx("code",{className:"rounded bg-background/50 px-1 font-mono text-[11px] text-teal-300",children:r.slice(1,-1)},i):e.jsx("span",{children:r},i)})}export{T as WissenView};
@@ -1,4 +1,4 @@
import{c as a}from"./index-4TXIIaNW.js";/**
import{c as a}from"./index-BntQHmEa.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c}from"./index-4TXIIaNW.js";/**
import{c}from"./index-BntQHmEa.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c}from"./index-4TXIIaNW.js";/**
import{c}from"./index-BntQHmEa.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as a}from"./index-4TXIIaNW.js";/**
import{c as a}from"./index-BntQHmEa.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+6
View File
@@ -0,0 +1,6 @@
import{c as a}from"./index-BntQHmEa.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const r=a("Layers",[["path",{d:"m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z",key:"8b97xw"}],["path",{d:"m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65",key:"dd6zsq"}],["path",{d:"m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65",key:"ep9fru"}]]);export{r as L};
@@ -1,4 +1,4 @@
import{c as t}from"./index-4TXIIaNW.js";/**
import{c as t}from"./index-BntQHmEa.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as t}from"./index-4TXIIaNW.js";/**
import{c as t}from"./index-BntQHmEa.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as a}from"./index-4TXIIaNW.js";/**
import{c as a}from"./index-BntQHmEa.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,26 +1,21 @@
import{c as a}from"./index-4TXIIaNW.js";/**
import{c as a}from"./index-BntQHmEa.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const t=a("Code",[["polyline",{points:"16 18 22 12 16 6",key:"z7tu5w"}],["polyline",{points:"8 6 2 12 8 18",key:"1eg1df"}]]);/**
*/const o=a("Code",[["polyline",{points:"16 18 22 12 16 6",key:"z7tu5w"}],["polyline",{points:"8 6 2 12 8 18",key:"1eg1df"}]]);/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const s=a("Compass",[["path",{d:"m16.24 7.76-1.804 5.411a2 2 0 0 1-1.265 1.265L7.76 16.24l1.804-5.411a2 2 0 0 1 1.265-1.265z",key:"9ktpf1"}],["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]]);/**
*/const t=a("Compass",[["path",{d:"m16.24 7.76-1.804 5.411a2 2 0 0 1-1.265 1.265L7.76 16.24l1.804-5.411a2 2 0 0 1 1.265-1.265z",key:"9ktpf1"}],["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]]);/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const o=a("Layers",[["path",{d:"m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z",key:"8b97xw"}],["path",{d:"m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65",key:"dd6zsq"}],["path",{d:"m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65",key:"ep9fru"}]]);/**
*/const c=a("Lock",[["rect",{width:"18",height:"11",x:"3",y:"11",rx:"2",ry:"2",key:"1w4ew1"}],["path",{d:"M7 11V7a5 5 0 0 1 10 0v4",key:"fwvmzm"}]]);/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const p=a("Lock",[["rect",{width:"18",height:"11",x:"3",y:"11",rx:"2",ry:"2",key:"1w4ew1"}],["path",{d:"M7 11V7a5 5 0 0 1 10 0v4",key:"fwvmzm"}]]);/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const y=a("Zap",[["path",{d:"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z",key:"1xq2db"}]]);export{t as C,p as L,y as Z,s as a,o as b};
*/const s=a("Zap",[["path",{d:"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z",key:"1xq2db"}]]);export{o as C,c as L,s as Z,t as a};
+2 -2
View File
@@ -7,8 +7,8 @@
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-4TXIIaNW.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Bsg6hDDU.css">
<script type="module" crossorigin src="/assets/index-BntQHmEa.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CeczSNp8.css">
</head>
<body>
<div id="root"></div>
+14
View File
@@ -519,6 +519,19 @@ export interface IdeenItem {
von: string | null
frage?: string | null // bei status=blocked: die Frage/der Grund des Workers
frage_kind?: string | null // needs_input | capability | transient | ""
eltern?: string[] // offene Vorgänger-Karten (task_links) — leer = startbereit
wartet_auf?: string[] // Titel der offenen Vorgänger (max. 3, für die Anzeige)
notiz?: string | null // bei running: jüngste Heartbeat-Notiz des Workers
projekt?: string | null // Familien-Schlüssel (verkettete Karten) — null = Einzelkarte
}
// Karten-Familie (verkettete Karten = ein Projekt) für die gruppierte Queue-Sicht
export interface IdeenProjekt {
key: string
titel: string
gesamt: number
fertig: number
laeuft: number
haengt: number
}
export interface IdeenLogResp {
available: boolean
@@ -529,6 +542,7 @@ export interface IdeenResp {
items: IdeenItem[]
offen: number
fehler?: string
projekte?: IdeenProjekt[]
}
// Ergebnis einer fertigen Queue-Aufgabe (Abschluss-Zusammenfassung des Workers)
+127 -45
View File
@@ -1,9 +1,9 @@
import { useEffect, useState } from "react"
import {
Inbox, Check, X, GitBranch, FileDiff, Loader2, AlertTriangle, Sparkles,
ChevronDown, ChevronRight, Clock, ClipboardCheck, Hammer, RefreshCw, Lightbulb, Send, HelpCircle,
ChevronDown, ChevronRight, Clock, ClipboardCheck, Hammer, RefreshCw, Layers, Lightbulb, Send, HelpCircle,
} from "lucide-react"
import { api, type AuftragItem, type IdeenErgebnis, type IdeenItem, type SkillKandidat } from "@/lib/api"
import { api, type AuftragItem, type IdeenErgebnis, type IdeenItem, type IdeenProjekt, type SkillKandidat } from "@/lib/api"
import { useAuftragsbuch, useIdeen, useIdeenLog, useQueryClient, qk } from "@/lib/queries"
import { useDialog } from "@/lib/useDialog"
import { cn } from "@/lib/utils"
@@ -143,7 +143,7 @@ const IDEEN_STATE: Record<string, { label: string; cls: string; spin?: boolean }
triage: { label: "wird ausgearbeitet …", cls: "text-violet-300 border-violet-500/40 bg-violet-500/10", spin: true },
todo: { label: "wartet", cls: "text-muted-foreground border-border/60 bg-background/30" },
scheduled: { label: "geplant", cls: "text-muted-foreground border-border/60 bg-background/30" },
ready: { label: "wartet", cls: "text-muted-foreground border-border/60 bg-background/30" },
ready: { label: "gleich dran", cls: "text-sky-300/80 border-sky-500/30 bg-sky-500/5" },
running: { label: "in Arbeit …", cls: "text-sky-300 border-sky-500/40 bg-sky-500/10", spin: true },
review: { label: "im Review", cls: "text-amber-300 border-amber-500/40 bg-amber-500/10" },
blocked: { label: "hängt — braucht dich", cls: "text-red-300 border-red-500/40 bg-red-500/10" },
@@ -221,46 +221,9 @@ function IdeenSection() {
}
}
// Windows-Dev / Queue nicht verfügbar → Sektion still weglassen (wie der Rest der Seite).
if (data && !data.available) return null
return (
<section className="space-y-3">
<SectionLabel icon={Lightbulb}>
Ideen-Queue{data ? `${data.offen} offen` : ""}
</SectionLabel>
<div className="mc-card p-4 space-y-3">
<div className="flex gap-2">
<input
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") submit() }}
placeholder="Neue Idee — einfach hinschreiben, die Box macht den Rest"
className="h-9 min-w-0 flex-1 rounded-lg border border-border/60 bg-background/50 px-3 text-sm text-foreground placeholder:text-muted-foreground/50 focus:border-primary/50 focus:outline-none"
/>
<button
onClick={submit}
disabled={sending || text.trim().length < 3}
className="flex h-9 shrink-0 items-center gap-1.5 rounded-lg border border-primary/40 bg-primary/10 px-3 text-[11px] font-bold uppercase tracking-wide text-primary transition-all hover:bg-primary/20 cursor-pointer disabled:opacity-50">
{sending ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Send className="h-3.5 w-3.5" />} In die Queue
</button>
</div>
<p className="text-[11px] text-muted-foreground">
Eine Queue, alle Türen: hier, per Telegram oder im Gespräch mit Lucy. Die Box arbeitet Ideen selbst
aus und baut Code-Ergebnisse erscheinen als Karte unter Patch-Vorschläge, nichts davon geht ohne
deinen Klick live. Nur reine Doku-Bagatellen zieht die Box nachts selbst ein (mit Fangnetz und Meldung).
</p>
{isLoading && (
<p className="flex items-center gap-2 text-xs text-muted-foreground"><Loader2 className="h-3.5 w-3.5 animate-spin" /> Queue wird geladen </p>
)}
{data?.fehler && (
<p className="text-xs text-amber-300">Queue gerade nicht lesbar neuer Versuch läuft.</p>
)}
{items.length > 0 && (
<ul className="space-y-1.5">
{items.map((it: IdeenItem) => {
// EINE Zeilen-Darstellung für alle Sichten (Projekt-Gruppen + Einzelkarten) — damit
// Antwortfeld, Live-Log und Ergebnis-Panel überall identisch funktionieren.
const zeile = (it: IdeenItem) => {
const ui = IDEEN_STATE[it.status] ?? { label: it.status, cls: "text-muted-foreground border-border/60 bg-background/30" }
const isOpen = openLog === it.id
// Aufklappbar: laufende Karten zeigen das Live-Log, FERTIGE das Ergebnis
@@ -296,6 +259,17 @@ function IdeenSection() {
)}
</div>
{it.status === "running" && it.notiz && (
<p className="-mt-1 ml-1 text-[11px] italic leading-relaxed text-sky-200/80" title="Jüngste Zwischenmeldung des Workers (Heartbeat)">
{it.notiz}
</p>
)}
{(it.status === "todo" || it.status === "scheduled" || it.status === "ready") && (it.wartet_auf?.length ?? 0) > 0 && (
<p className="-mt-1 ml-1 text-[10px] text-muted-foreground/70">
wartet auf: {it.wartet_auf!.join(" · ")}
</p>
)}
{blocked && (
<div className="space-y-2 border-t border-red-500/20 pt-2">
<p className="flex items-start gap-1.5 text-[11px] leading-relaxed text-red-200/90">
@@ -347,8 +321,57 @@ function IdeenSection() {
{isOpen && it.status === "done" && <ErgebnisPanel id={it.id} />}
</li>
)
})}
</ul>
}
// Windows-Dev / Queue nicht verfügbar → Sektion still weglassen (wie der Rest der Seite).
if (data && !data.available) return null
return (
<section className="space-y-3">
<SectionLabel icon={Lightbulb}>
Ideen-Queue{data ? `${data.offen} offen` : ""}
</SectionLabel>
<div className="mc-card p-4 space-y-3">
<div className="flex gap-2">
<input
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") submit() }}
placeholder="Neue Idee — einfach hinschreiben, die Box macht den Rest"
className="h-9 min-w-0 flex-1 rounded-lg border border-border/60 bg-background/50 px-3 text-sm text-foreground placeholder:text-muted-foreground/50 focus:border-primary/50 focus:outline-none"
/>
<button
onClick={submit}
disabled={sending || text.trim().length < 3}
className="flex h-9 shrink-0 items-center gap-1.5 rounded-lg border border-primary/40 bg-primary/10 px-3 text-[11px] font-bold uppercase tracking-wide text-primary transition-all hover:bg-primary/20 cursor-pointer disabled:opacity-50">
{sending ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Send className="h-3.5 w-3.5" />} In die Queue
</button>
</div>
<p className="text-[11px] text-muted-foreground">
Eine Queue, alle Türen: hier, per Telegram oder im Gespräch mit Lucy. Die Box arbeitet Ideen selbst
aus und baut Code-Ergebnisse erscheinen als Karte unter Patch-Vorschläge, nichts davon geht ohne
deinen Klick live. Nur reine Doku-Bagatellen zieht die Box nachts selbst ein (mit Fangnetz und Meldung).
</p>
{isLoading && (
<p className="flex items-center gap-2 text-xs text-muted-foreground"><Loader2 className="h-3.5 w-3.5 animate-spin" /> Queue wird geladen </p>
)}
{data?.fehler && (
<p className="text-xs text-amber-300">Queue gerade nicht lesbar neuer Versuch läuft.</p>
)}
{items.length > 0 && (
<div className="space-y-3">
{(data?.projekte ?? []).map((p) => (
<ProjektGruppe key={p.key} projekt={p} karten={items.filter((i) => i.projekt === p.key)} zeile={zeile} />
))}
{(() => {
// Einzelkarten (keine Kette) — die klassische flache Liste wie bisher.
const keys = new Set((data?.projekte ?? []).map((p) => p.key))
const einzeln = items.filter((i) => !i.projekt || !keys.has(i.projekt))
return einzeln.length > 0 ? <ul className="space-y-1.5">{einzeln.map((it) => zeile(it))}</ul> : null
})()}
</div>
)}
{data?.available && !isLoading && items.length === 0 && (
<p className="text-xs text-muted-foreground/70">Queue ist leer wirf der Box eine Idee zu.</p>
@@ -359,6 +382,65 @@ function IdeenSection() {
)
}
// Karten-Familie (verkettete Karten = ein Projekt) als Block: Fortschritt oben, dann die
// AKTIVEN Karten in Ausführungs-Reihenfolge, Wartende und Fertige zusammengeklappt.
// Ersetzt für Ketten die flache Erstellzeit-Liste (User: „Reihenfolge komplett durcheinander").
const AKTIV_RANG: Record<string, number> = { blocked: 0, running: 1, review: 2, ready: 3, triage: 4 }
function ProjektGruppe({ projekt, karten, zeile }: {
projekt: IdeenProjekt
karten: IdeenItem[]
zeile: (it: IdeenItem) => React.ReactNode
}) {
const [zeigWartende, setZeigWartende] = useState(false)
const [zeigFertige, setZeigFertige] = useState(false)
const aktiv = karten
.filter((i) => AKTIV_RANG[i.status] !== undefined)
.sort((a, b) => (AKTIV_RANG[a.status] - AKTIV_RANG[b.status]) || ((a.erstellt ?? 0) - (b.erstellt ?? 0)))
const wartend = karten
.filter((i) => i.status === "todo" || i.status === "scheduled")
.sort((a, b) => ((a.wartet_auf?.length ? 1 : 0) - (b.wartet_auf?.length ? 1 : 0)) || ((a.erstellt ?? 0) - (b.erstellt ?? 0)))
const fertig = karten
.filter((i) => i.status === "done")
.sort((a, b) => (b.fertig ?? 0) - (a.fertig ?? 0))
const anteil = projekt.gesamt > 0 ? Math.round((projekt.fertig / projekt.gesamt) * 100) : 0
return (
<div className="space-y-2 rounded-xl border border-border/50 bg-background/20 p-3">
<div className="flex items-center gap-2">
<Layers className="h-4 w-4 shrink-0 text-primary/70" />
<p className="min-w-0 flex-1 truncate text-xs font-semibold text-foreground" title={projekt.titel}>{projekt.titel}</p>
<span className="shrink-0 text-[10px] text-muted-foreground">{projekt.fertig} von {projekt.gesamt} fertig</span>
</div>
<div className="h-1 overflow-hidden rounded-full bg-background/60" title={`${anteil}%`}>
<div className="h-1 rounded-full bg-primary/70 transition-all" style={{ width: `${anteil}%` }} />
</div>
{aktiv.length > 0 && <ul className="space-y-1.5">{aktiv.map((it) => zeile(it))}</ul>}
{wartend.length > 0 && (
<div>
<button
onClick={() => setZeigWartende((v) => !v)}
className="flex items-center gap-1.5 text-[11px] font-semibold text-muted-foreground hover:text-foreground transition-colors cursor-pointer">
{zeigWartende ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
{wartend.length} Karte{wartend.length === 1 ? "" : "n"} in der Kette laufen automatisch der Reihe nach
</button>
{zeigWartende && <ul className="mt-1.5 space-y-1.5">{wartend.map((it) => zeile(it))}</ul>}
</div>
)}
{fertig.length > 0 && (
<div>
<button
onClick={() => setZeigFertige((v) => !v)}
className="flex items-center gap-1.5 text-[11px] font-semibold text-emerald-300/70 hover:text-emerald-300 transition-colors cursor-pointer">
{zeigFertige ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
{fertig.length} fertig
</button>
{zeigFertige && <ul className="mt-1.5 space-y-1.5">{fertig.map((it) => zeile(it))}</ul>}
</div>
)}
</div>
)
}
// Ergebnis einer FERTIGEN Queue-Aufgabe: die Abschluss-Zusammenfassung des Workers
// (kanban_complete). Lazy — wird erst beim Aufklappen geladen, ändert sich nie mehr.
function ErgebnisPanel({ id }: { id: string }) {