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:
+4
-1
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
-1
@@ -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
Vendored
+1
-1
@@ -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.
|
||||
Vendored
+1
-1
@@ -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.
|
||||
Vendored
+1
-1
@@ -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.
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+3
-3
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -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};
|
||||
+3
-3
@@ -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,
|
||||
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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.
|
||||
Vendored
+1
-1
@@ -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.
|
||||
+2
-2
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+6
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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.
|
||||
+5
-10
@@ -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};
|
||||
Vendored
+2
-2
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
Reference in New Issue
Block a user