UMBAU v3 P3a handgebaut: SSE-Eventstrom /api/events (Invalidation-Bus)
User-Entscheid 15.07. abends: P3-Worker-Jobs abgebrochen (22 Anlaeufe, 0 Ergebnisse) - 'du bearbeitest sie ein letztes Mal manuell'. P3a KOMPLETT: backend/routers/events.py streamt SSE; ein Sammler prueft alle 3 s billige Fingerabdruecke (Briefkasten-Cursor, Queue-Status, Auftragsbuch-/Reminder-mtimes) und schickt bei Aenderung 'invalidate' mit den React-Query-Keys. frontend/src/lib/events.ts haelt EIN EventSource, invalidiert gezielt und entspannt die vier gedeckten Poller x5 (relax() in queries.ts); reisst der Strom, reconnectet EventSource selbst und die UI pollt wie bisher. Live-Graphen bleiben bewusst beim Polling (aendern sich jede Sekunde). P3b (TanStack Router) BEWUSST NICHT umgesetzt: Hash-Navigation liefert Deep-Links + Zuruecktaste bereits; Router-Migration = hohes Regressions- risiko ueber alle Views bei minimalem Gewinn fuer 1-Nutzer-LAN (gleiche Logik wie das React-bleibt-Verdikt in UMBAUPLAN 3b). P3c (OpenAPI-Typen) VERTAGT: ohne Pydantic-response_models liefert der Generator leere Typen - Voraussetzung ist erst das Backend-Typing (eigene, klein geschnittene Karten-Serie). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+174
-172
@@ -1,172 +1,174 @@
|
|||||||
"""
|
"""
|
||||||
Mission Control 2.0 — dünner FastAPI-Einstieg.
|
Mission Control 2.0 — dünner FastAPI-Einstieg.
|
||||||
|
|
||||||
Hängt die Router ein, liefert (in Prod) das gebaute React-Frontend aus und
|
Hängt die Router ein, liefert (in Prod) das gebaute React-Frontend aus und
|
||||||
setzt eine no-cache-Middleware. Im Dev läuft das Frontend über den Vite-Dev-
|
setzt eine no-cache-Middleware. Im Dev läuft das Frontend über den Vite-Dev-
|
||||||
Server (proxyt /api hierher), daher CORS für localhost offen.
|
Server (proxyt /api hierher), daher CORS für localhost offen.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from typing import Any, AsyncGenerator, Awaitable, Callable
|
from typing import Any, AsyncGenerator, Awaitable, Callable
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
|
|
||||||
from config import FRONTEND_DIST, V1_UPSTREAM, VERSION
|
from config import FRONTEND_DIST, V1_UPSTREAM, VERSION
|
||||||
from routers import (
|
from routers import (
|
||||||
agent,
|
agent,
|
||||||
auftragsbuch,
|
auftragsbuch,
|
||||||
chronik,
|
chronik,
|
||||||
connect,
|
connect,
|
||||||
console,
|
console,
|
||||||
eigenleben,
|
eigenleben,
|
||||||
gateway_proxy,
|
events,
|
||||||
health,
|
gateway_proxy,
|
||||||
hermes_ui,
|
health,
|
||||||
ideen,
|
hermes_ui,
|
||||||
maintenance,
|
ideen,
|
||||||
memory,
|
maintenance,
|
||||||
models,
|
memory,
|
||||||
routing,
|
models,
|
||||||
system,
|
routing,
|
||||||
voice,
|
system,
|
||||||
wissen,
|
voice,
|
||||||
zeitmaschine,
|
wissen,
|
||||||
)
|
zeitmaschine,
|
||||||
from routers import reminders as reminders_router
|
)
|
||||||
from services import memory as memory_svc
|
from routers import reminders as reminders_router
|
||||||
from services import reminders, sentry, warmer
|
from services import memory as memory_svc
|
||||||
|
from services import reminders, sentry, warmer
|
||||||
# Zentrales Logging — Level via MC_LOG_LEVEL (INFO default). Eine Konfiguration
|
|
||||||
# für alle Module (logging.getLogger(__name__)).
|
# Zentrales Logging — Level via MC_LOG_LEVEL (INFO default). Eine Konfiguration
|
||||||
logging.basicConfig(
|
# für alle Module (logging.getLogger(__name__)).
|
||||||
level=os.environ.get("MC_LOG_LEVEL", "INFO").upper(),
|
logging.basicConfig(
|
||||||
format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
|
level=os.environ.get("MC_LOG_LEVEL", "INFO").upper(),
|
||||||
)
|
format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
|
||||||
log = logging.getLogger(__name__)
|
)
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
@asynccontextmanager
|
||||||
"""Hintergrund-Tasks an den App-Lebenszyklus binden: Re-Warm-Wächter fürs Agent-Hirn
|
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||||
+ Health-Wächter (meldet Ausfälle/Erholung in den Lucy-Briefkasten und auf Telegram)."""
|
"""Hintergrund-Tasks an den App-Lebenszyklus binden: Re-Warm-Wächter fürs Agent-Hirn
|
||||||
tasks: list[asyncio.Task[Any]] = []
|
+ Health-Wächter (meldet Ausfälle/Erholung in den Lucy-Briefkasten und auf Telegram)."""
|
||||||
if warmer.ENABLED:
|
tasks: list[asyncio.Task[Any]] = []
|
||||||
tasks.append(asyncio.create_task(warmer.rewarm_loop()))
|
if warmer.ENABLED:
|
||||||
log.info(
|
tasks.append(asyncio.create_task(warmer.rewarm_loop()))
|
||||||
"Hirn-Re-Warm-Wächter aktiv (Intervall %ss, Hirn dynamisch aus Hermes-Config)",
|
log.info(
|
||||||
warmer.INTERVAL,
|
"Hirn-Re-Warm-Wächter aktiv (Intervall %ss, Hirn dynamisch aus Hermes-Config)",
|
||||||
)
|
warmer.INTERVAL,
|
||||||
if sentry.ENABLED:
|
)
|
||||||
tasks.append(asyncio.create_task(sentry.sentry_loop()))
|
if sentry.ENABLED:
|
||||||
tasks.append(asyncio.create_task(reminders.reminders_loop()))
|
tasks.append(asyncio.create_task(sentry.sentry_loop()))
|
||||||
if memory_svc.AUTO_DEDUPE_ENABLED:
|
tasks.append(asyncio.create_task(reminders.reminders_loop()))
|
||||||
tasks.append(asyncio.create_task(memory_svc.auto_dedupe_loop()))
|
if memory_svc.AUTO_DEDUPE_ENABLED:
|
||||||
log.info(
|
tasks.append(asyncio.create_task(memory_svc.auto_dedupe_loop()))
|
||||||
"Mem0-Auto-Dedupe aktiv (alle %ss, Schwelle %s)",
|
log.info(
|
||||||
memory_svc.AUTO_DEDUPE_INTERVAL,
|
"Mem0-Auto-Dedupe aktiv (alle %ss, Schwelle %s)",
|
||||||
memory_svc.AUTO_DEDUPE_THRESHOLD,
|
memory_svc.AUTO_DEDUPE_INTERVAL,
|
||||||
)
|
memory_svc.AUTO_DEDUPE_THRESHOLD,
|
||||||
# Geteilter HTTP-Client zur lokalen Engine: Keep-Alive/Connection-Pooling statt neuer Client
|
)
|
||||||
# pro /v1-Anfrage (spart Sockets/TIME_WAIT unter parallelen Agent-Strömen von Zed/Kilo).
|
# Geteilter HTTP-Client zur lokalen Engine: Keep-Alive/Connection-Pooling statt neuer Client
|
||||||
app.state.gw_client = httpx.AsyncClient(
|
# pro /v1-Anfrage (spart Sockets/TIME_WAIT unter parallelen Agent-Strömen von Zed/Kilo).
|
||||||
timeout=httpx.Timeout(connect=10.0, read=None, write=None, pool=10.0),
|
app.state.gw_client = httpx.AsyncClient(
|
||||||
limits=httpx.Limits(max_keepalive_connections=100, max_connections=200),
|
timeout=httpx.Timeout(connect=10.0, read=None, write=None, pool=10.0),
|
||||||
)
|
limits=httpx.Limits(max_keepalive_connections=100, max_connections=200),
|
||||||
try:
|
)
|
||||||
yield
|
try:
|
||||||
finally:
|
yield
|
||||||
for task in tasks:
|
finally:
|
||||||
_ = task.cancel()
|
for task in tasks:
|
||||||
await app.state.gw_client.aclose()
|
_ = task.cancel()
|
||||||
|
await app.state.gw_client.aclose()
|
||||||
|
|
||||||
app = FastAPI(title="Mission Control 2.0", version=VERSION, lifespan=lifespan)
|
|
||||||
|
app = FastAPI(title="Mission Control 2.0", version=VERSION, lifespan=lifespan)
|
||||||
# Dev: Vite-Dev-Server (5173) ruft das Backend per /api auf.
|
|
||||||
app.add_middleware(
|
# Dev: Vite-Dev-Server (5173) ruft das Backend per /api auf.
|
||||||
CORSMiddleware,
|
app.add_middleware(
|
||||||
allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
|
CORSMiddleware,
|
||||||
allow_methods=["*"],
|
allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
|
||||||
allow_headers=["*"],
|
allow_methods=["*"],
|
||||||
)
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
@app.middleware("http")
|
|
||||||
async def no_cache(
|
@app.middleware("http")
|
||||||
request: Request, call_next: Callable[[Request], Awaitable[httpx.Response]]
|
async def no_cache(
|
||||||
) -> httpx.Response:
|
request: Request, call_next: Callable[[Request], Awaitable[httpx.Response]]
|
||||||
resp = await call_next(request)
|
) -> httpx.Response:
|
||||||
if request.url.path.startswith("/api"):
|
resp = await call_next(request)
|
||||||
resp.headers["Cache-Control"] = "no-cache"
|
if request.url.path.startswith("/api"):
|
||||||
return resp
|
resp.headers["Cache-Control"] = "no-cache"
|
||||||
|
return resp
|
||||||
|
|
||||||
app.include_router(health.router)
|
|
||||||
app.include_router(models.router)
|
app.include_router(health.router)
|
||||||
app.include_router(routing.router)
|
app.include_router(models.router)
|
||||||
app.include_router(system.router)
|
app.include_router(routing.router)
|
||||||
app.include_router(connect.router)
|
app.include_router(system.router)
|
||||||
app.include_router(memory.router)
|
app.include_router(connect.router)
|
||||||
app.include_router(agent.router)
|
app.include_router(memory.router)
|
||||||
app.include_router(
|
app.include_router(agent.router)
|
||||||
voice.router
|
app.include_router(
|
||||||
) # Sprache: STT/TTS-Proxy + Hermes-Agent-Chat (Voice-Tab)
|
voice.router
|
||||||
app.include_router(
|
) # Sprache: STT/TTS-Proxy + Hermes-Agent-Chat (Voice-Tab)
|
||||||
reminders_router.router
|
app.include_router(
|
||||||
) # Erinnerungen/Routinen (A3) — feuern in den Briefkasten
|
reminders_router.router
|
||||||
# /v1-Datenpfad: Nach dem Gateway-Auszug (UMBAU v3 P1) läuft der eigentliche Gateway als
|
) # Erinnerungen/Routinen (A3) — feuern in den Briefkasten
|
||||||
# eigener Prozess (mc2-gateway, Loopback :9010) — MC2 reicht /v1 dann nur roh durch, damit
|
# /v1-Datenpfad: Nach dem Gateway-Auszug (UMBAU v3 P1) läuft der eigentliche Gateway als
|
||||||
# LAN-Clients (IDE-Lane) weiter über :9001 kommen. Ohne MC_V1_UPSTREAM (vor dem ersten
|
# eigener Prozess (mc2-gateway, Loopback :9010) — MC2 reicht /v1 dann nur roh durch, damit
|
||||||
# Deploy der neuen Unit / nach Rollback) bedient MC2 /v1 wie bisher selbst.
|
# LAN-Clients (IDE-Lane) weiter über :9001 kommen. Ohne MC_V1_UPSTREAM (vor dem ersten
|
||||||
if V1_UPSTREAM:
|
# Deploy der neuen Unit / nach Rollback) bedient MC2 /v1 wie bisher selbst.
|
||||||
from routers import gateway_forward
|
if V1_UPSTREAM:
|
||||||
app.include_router(gateway_forward.router) # dünner Roh-Weiterleiter → mc2-gateway
|
from routers import gateway_forward
|
||||||
else:
|
app.include_router(gateway_forward.router) # dünner Roh-Weiterleiter → mc2-gateway
|
||||||
app.include_router(gateway_proxy.router) # OpenAI-kompatibler /v1-Gateway (model:auto)
|
else:
|
||||||
app.include_router(maintenance.router)
|
app.include_router(gateway_proxy.router) # OpenAI-kompatibler /v1-Gateway (model:auto)
|
||||||
app.include_router(auftragsbuch.router) # Vorschlags-Inbox (Mensch-Gate als Klick)
|
app.include_router(maintenance.router)
|
||||||
app.include_router(ideen.router) # Ideen-Queue (natives Hermes-Kanban) — Tür der Zentrale
|
app.include_router(auftragsbuch.router) # Vorschlags-Inbox (Mensch-Gate als Klick)
|
||||||
app.include_router(chronik.router) # Timeline der autonomen Taten (Announce-Store)
|
app.include_router(ideen.router) # Ideen-Queue (natives Hermes-Kanban) — Tür der Zentrale
|
||||||
app.include_router(eigenleben.router) # „Von allein": Skills + Vorschlags-Bilanz der Box
|
app.include_router(chronik.router) # Timeline der autonomen Taten (Announce-Store)
|
||||||
app.include_router(wissen.router) # Wissens-Vault (Traum-Notizen) read-only
|
app.include_router(eigenleben.router) # „Von allein": Skills + Vorschlags-Bilanz der Box
|
||||||
app.include_router(zeitmaschine.router) # Snapshots ansehen + Ein-Klick-Restore (detached)
|
app.include_router(events.router) # SSE-Eventstrom /api/events (P3a) — Invalidation-Bus
|
||||||
app.include_router(
|
app.include_router(wissen.router) # Wissens-Vault (Traum-Notizen) read-only
|
||||||
console.router
|
app.include_router(zeitmaschine.router) # Snapshots ansehen + Ein-Klick-Restore (detached)
|
||||||
) # Box-Konsole (ttyd) same-origin durchreichen — VOR dem SPA-Catch-all
|
app.include_router(
|
||||||
app.include_router(
|
console.router
|
||||||
hermes_ui.router
|
) # Box-Konsole (ttyd) same-origin durchreichen — VOR dem SPA-Catch-all
|
||||||
) # Eingebaute Hermes-Web-GUI (hermes serve) same-origin unter /hermes-ui/ — VOR dem SPA-Catch-all
|
app.include_router(
|
||||||
|
hermes_ui.router
|
||||||
|
) # Eingebaute Hermes-Web-GUI (hermes serve) same-origin unter /hermes-ui/ — VOR dem SPA-Catch-all
|
||||||
# Prod: gebautes Frontend ausliefern (falls vorhanden). SPA-Fallback auf index.html.
|
|
||||||
if FRONTEND_DIST.exists():
|
|
||||||
app.mount("/assets", StaticFiles(directory=FRONTEND_DIST / "assets"), name="assets")
|
# Prod: gebautes Frontend ausliefern (falls vorhanden). SPA-Fallback auf index.html.
|
||||||
|
if FRONTEND_DIST.exists():
|
||||||
_DIST_ROOT = FRONTEND_DIST.resolve()
|
app.mount("/assets", StaticFiles(directory=FRONTEND_DIST / "assets"), name="assets")
|
||||||
|
|
||||||
@app.get("/{full_path:path}")
|
_DIST_ROOT = FRONTEND_DIST.resolve()
|
||||||
def spa(full_path: str):
|
|
||||||
# Datei direkt aus FRONTEND_DIST ausliefern (manifest.webmanifest, favicon.ico, …) — aber
|
@app.get("/{full_path:path}")
|
||||||
# NUR innerhalb des dist-Ordners: Pfad auflösen + Traversal (../, absolute Pfade) hart raus.
|
def spa(full_path: str):
|
||||||
try:
|
# Datei direkt aus FRONTEND_DIST ausliefern (manifest.webmanifest, favicon.ico, …) — aber
|
||||||
target = (FRONTEND_DIST / full_path).resolve()
|
# NUR innerhalb des dist-Ordners: Pfad auflösen + Traversal (../, absolute Pfade) hart raus.
|
||||||
if target.is_relative_to(_DIST_ROOT) and target.is_file():
|
try:
|
||||||
return FileResponse(target)
|
target = (FRONTEND_DIST / full_path).resolve()
|
||||||
except (ValueError, OSError):
|
if target.is_relative_to(_DIST_ROOT) and target.is_file():
|
||||||
pass
|
return FileResponse(target)
|
||||||
|
except (ValueError, OSError):
|
||||||
index = FRONTEND_DIST / "index.html"
|
pass
|
||||||
if index.exists():
|
|
||||||
# index.html nie cachen → Browser zieht nach jedem Deploy das aktuelle (gehashte) Bundle.
|
index = FRONTEND_DIST / "index.html"
|
||||||
return FileResponse(
|
if index.exists():
|
||||||
index, headers={"Cache-Control": "no-cache, must-revalidate"}
|
# index.html nie cachen → Browser zieht nach jedem Deploy das aktuelle (gehashte) Bundle.
|
||||||
)
|
return FileResponse(
|
||||||
return {"detail": "frontend not built"}
|
index, headers={"Cache-Control": "no-cache, must-revalidate"}
|
||||||
|
)
|
||||||
|
return {"detail": "frontend not built"}
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
"""SSE-Eventstrom (UMBAU v3 P3a) — ein Kanal sagt der Zentrale, WANN neu laden lohnt.
|
||||||
|
|
||||||
|
GET /api/events liefert Server-Sent Events. Ein Sammler prüft alle paar Sekunden
|
||||||
|
billige Fingerabdrücke der ereignishaften Quellen (Briefkasten/Chronik, Ideen-Queue,
|
||||||
|
Auftragsbuch, Erinnerungen) und schickt NUR bei Änderung ein `invalidate`-Event mit
|
||||||
|
den React-Query-Keys. Die Wahrheit bleibt in den bestehenden Endpunkten — der Strom
|
||||||
|
ist ein reiner Invalidation-Bus, kein zweites Zustandsmodell.
|
||||||
|
|
||||||
|
Frontend-Gegenstück: frontend/src/lib/events.ts (EventSource, invalidiert die Caches,
|
||||||
|
entspannt die Fallback-Poller ×5, solange der Strom steht; reißt er ab, reconnectet
|
||||||
|
EventSource selbst und bis dahin pollt die UI wie bisher). Live-Metriken (CPU/Token-
|
||||||
|
Graphen) laufen bewusst NICHT hierüber — die ändern sich jede Sekunde, da ist Polling
|
||||||
|
das richtige Werkzeug.
|
||||||
|
|
||||||
|
Handgebaut 15.07. abends (User-Entscheid: „ihr brecht die Jobs ab, du baust es ein
|
||||||
|
letztes Mal manuell") — die autonomen P3-Worker bissen sich an der Aufgabe fest.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Request
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
|
||||||
|
from config import MODELS_DIR
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api")
|
||||||
|
|
||||||
|
TICK_S = 3.0 # Prüf-Takt des Sammlers (nur Fingerabdrücke, kein Neuberechnen)
|
||||||
|
KEEPALIVE_S = 20.0 # Kommentar-Ping, damit Proxies/Browser die Verbindung halten
|
||||||
|
|
||||||
|
|
||||||
|
def _mtime(p: Path) -> float:
|
||||||
|
try:
|
||||||
|
return p.stat().st_mtime
|
||||||
|
except OSError:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def _fingerprints() -> dict[str, object]:
|
||||||
|
"""Billige Änderungs-Signale je Quelle → React-Query-Key. Fehler einer Quelle
|
||||||
|
dürfen den Strom nie reißen (dann bleibt ihr Abdruck einfach stehen)."""
|
||||||
|
fp: dict[str, object] = {}
|
||||||
|
try: # Briefkasten trägt Chronik UND Kontext-Limit-Warnungen — in-process, spottbillig
|
||||||
|
from services import announce
|
||||||
|
fp["chronik"] = announce.list_after(None)["latest"]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try: # Queue: (id,status)-Paare; list_queue cached selbst ~15 s, der Tick kostet nichts
|
||||||
|
from services import ideen
|
||||||
|
d = ideen.list_queue()
|
||||||
|
fp["ideen"] = json.dumps([(i.get("id"), i.get("status")) for i in d.get("items") or []])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# Auftragsbuch (Annahme-Status + Karten-Meldungen) & Erinnerungen: Datei-mtimes
|
||||||
|
fp["auftragsbuch"] = (_mtime(MODELS_DIR / "mc2-auftragsbuch.json"),
|
||||||
|
_mtime(MODELS_DIR / "mc2-announce-branches.json"))
|
||||||
|
fp["reminders"] = _mtime(MODELS_DIR / "mc2-reminders.json")
|
||||||
|
return fp
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/events")
|
||||||
|
async def events(request: Request) -> StreamingResponse:
|
||||||
|
async def strom():
|
||||||
|
# Der Client hat beim Verbinden frisch geladen — Basislinie ohne Event setzen.
|
||||||
|
alt = _fingerprints()
|
||||||
|
yield ": verbunden\n\n"
|
||||||
|
seit_ping = 0.0
|
||||||
|
while True:
|
||||||
|
if await request.is_disconnected():
|
||||||
|
return
|
||||||
|
await asyncio.sleep(TICK_S)
|
||||||
|
seit_ping += TICK_S
|
||||||
|
neu = _fingerprints()
|
||||||
|
keys = [k for k, v in neu.items() if k in alt and v != alt[k]]
|
||||||
|
alt.update(neu)
|
||||||
|
if keys:
|
||||||
|
yield f"event: invalidate\ndata: {json.dumps({'keys': keys})}\n\n"
|
||||||
|
seit_ping = 0.0
|
||||||
|
elif seit_ping >= KEEPALIVE_S:
|
||||||
|
yield ": ping\n\n"
|
||||||
|
seit_ping = 0.0
|
||||||
|
|
||||||
|
return StreamingResponse(strom(), media_type="text/event-stream",
|
||||||
|
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as k,R as _,m as S,b as C,u as H,r as E,j as e,U as A,e as a,V as u,W as p,Y as B,s as h,Z as D,Q as P,X as M,C as g,g as b,q as i}from"./index-ClYatDZT.js";/**
|
import{c as k,R as _,m as S,b as C,u as H,r as E,j as e,U as A,e as a,V as u,W as p,Y as B,s as h,Z as D,Q as P,X as M,C as g,g as b,q as i}from"./index-DgTg5QdX.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as I,a4 as X,u as _,b as q,r as c,j as e,a5 as Y,e as C,L as f,C as Q,N as U,a6 as ee,a7 as te,O as V,X as z,w as R,F,a8 as re,T as K,g as S,a9 as se,q as J}from"./index-ClYatDZT.js";import{L as ae}from"./lightbulb-ETwYqnI4.js";import{S as G}from"./send-D-g9GbnJ.js";/**
|
import{c as I,a4 as X,u as _,b as q,r as c,j as e,a5 as Y,e as C,L as f,C as Q,N as U,a6 as ee,a7 as te,O as V,X as z,w as R,F,a8 as re,T as K,g as S,a9 as se,q as J}from"./index-DgTg5QdX.js";import{L as ae}from"./lightbulb-D9Ggmjq-.js";import{S as G}from"./send-o-W_9aTj.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as h,aa as S,r as c,j as e,ab as z,L as g,N as M,ac as C,a9 as D,ad as A,ae as B,af as Z,e as E,ag as L,u as q,b as R,g as k,q as T}from"./index-ClYatDZT.js";/**
|
import{c as h,aa as S,r as c,j as e,ab as z,L as g,N as M,ac as C,a9 as D,ad as A,ae as B,af as Z,e as E,ag as L,u as q,b as R,g as k,q as T}from"./index-DgTg5QdX.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as v,r as c,z as D,A as I,j as e,D as h,s as B,B as H,w as z,F as E,e as M,L as F,G as T,C as K}from"./index-ClYatDZT.js";import{F as O}from"./folder-open-Birisrv_.js";import{C as R}from"./circle-x-BVoF4hpT.js";import{C as V}from"./copy-DTtK5aX-.js";/**
|
import{c as v,r as c,z as D,A as I,j as e,D as h,s as B,B as H,w as z,F as E,e as M,L as F,G as T,C as K}from"./index-DgTg5QdX.js";import{F as O}from"./folder-open-COXogvYR.js";import{C as R}from"./circle-x-ADu9dwvS.js";import{C as V}from"./copy-BdEWnHoH.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as h,ah as b,r as o,j as e,L as g,ai as f,Z as j,N as k,e as i,w as N,F as w,G as v,g as y}from"./index-ClYatDZT.js";import{B as z}from"./book-open-CdFvbc2z.js";import{C as S}from"./circle-x-BVoF4hpT.js";/**
|
import{c as h,ah as b,r as o,j as e,L as g,ai as f,Z as j,N as k,e as i,w as N,F as w,G as v,g as y}from"./index-DgTg5QdX.js";import{B as z}from"./book-open-lYTKOEmg.js";import{C as S}from"./circle-x-ADu9dwvS.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as p,j as e,r as M,s as C,a0 as z,a1 as k,a2 as y,W as v,Q as L,a3 as G,Z as T,e as f,N as D}from"./index-ClYatDZT.js";import{B as w}from"./book-open-CdFvbc2z.js";import{a as N,b as B,L as P,C as E,Z as R}from"./zap-raGIywOo.js";import{L as I}from"./lightbulb-ETwYqnI4.js";/**
|
import{c as p,j as e,r as M,s as C,a0 as z,a1 as k,a2 as y,W as v,Q as L,a3 as G,Z as T,e as f,N as D}from"./index-DgTg5QdX.js";import{B as w}from"./book-open-lYTKOEmg.js";import{a as N,b as B,L as P,C as E,Z as R}from"./zap-BX-39Xxr.js";import{L as I}from"./lightbulb-D9Ggmjq-.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
import{R as l,V as o,j as e,e as a,U as n,T as d,$ as i}from"./index-ClYatDZT.js";function c(){const{data:s}=l(),r=s!=null&&s.box_console_url?o(s.box_console_url):void 0,t=s==null?void 0:s.box_console_reachable;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:a("h-2 w-2 rounded-full",t?"bg-emerald-500 animate-pulse":"bg-amber-500")}),t?"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(n,{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:[t===!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(d,{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 ",e.jsx("code",{className:"font-mono text-primary",children:"box-console"}),"-Dienst (ttyd) läuft nicht. Auf der Box: ",e.jsx("code",{className:"font-mono",children:"systemctl --user restart box-console"}),"."]})]}),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(i,{className:"mr-2 h-4 w-4"})," Lade Konsole…"]})]})}export{c as KonsoleView};
|
import{R as l,V as o,j as e,e as a,U as n,T as d,$ as i}from"./index-DgTg5QdX.js";function c(){const{data:s}=l(),r=s!=null&&s.box_console_url?o(s.box_console_url):void 0,t=s==null?void 0:s.box_console_reachable;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:a("h-2 w-2 rounded-full",t?"bg-emerald-500 animate-pulse":"bg-amber-500")}),t?"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(n,{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:[t===!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(d,{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 ",e.jsx("code",{className:"font-mono text-primary",children:"box-console"}),"-Dienst (ttyd) läuft nicht. Auf der Box: ",e.jsx("code",{className:"font-mono",children:"systemctl --user restart box-console"}),"."]})]}),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(i,{className:"mr-2 h-4 w-4"})," Lade Konsole…"]})]})}export{c as KonsoleView};
|
||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/GraphView-DGCqAmwL.js","assets/index-ClYatDZT.js","assets/index-Cq_BqGQa.css"])))=>i.map(i=>d[i]);
|
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/GraphView-D56vUGn7.js","assets/index-DgTg5QdX.js","assets/index-Cq_BqGQa.css"])))=>i.map(i=>d[i]);
|
||||||
var ke=Object.defineProperty;var ve=(d,i,c)=>i in d?ke(d,i,{enumerable:!0,configurable:!0,writable:!0,value:c}):d[i]=c;var U=(d,i,c)=>ve(d,typeof i!="symbol"?i+"":i,c);import{c as h,r as s,j as e,u as Ne,b as Se,I as J,J as Ce,C as W,K as Ee,S as Me,M as ze,N as _,e as x,O as _e,Q as Oe,X,v as Z,_ as Te,g as j}from"./index-ClYatDZT.js";import{C as Y}from"./copy-DTtK5aX-.js";import{S as Ae}from"./send-D-g9GbnJ.js";import{B as De}from"./book-open-CdFvbc2z.js";/**
|
var ke=Object.defineProperty;var ve=(d,i,c)=>i in d?ke(d,i,{enumerable:!0,configurable:!0,writable:!0,value:c}):d[i]=c;var U=(d,i,c)=>ve(d,typeof i!="symbol"?i+"":i,c);import{c as h,r as s,j as e,u as Ne,b as Se,I as J,J as Ce,C as W,K as Ee,S as Me,M as ze,N as _,e as x,O as _e,Q as Oe,X,v as Z,_ as Te,g as j}from"./index-DgTg5QdX.js";import{C as Y}from"./copy-BdEWnHoH.js";import{S as Ae}from"./send-o-W_9aTj.js";import{B as De}from"./book-open-lYTKOEmg.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
@@ -34,7 +34,7 @@ var ke=Object.defineProperty;var ve=(d,i,c)=>i in d?ke(d,i,{enumerable:!0,config
|
|||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
* See the LICENSE file in the root directory of this source tree.
|
* See the LICENSE file in the root directory of this source tree.
|
||||||
*/const Re=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 re extends s.Component{constructor(){super(...arguments);U(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 te=s.lazy(()=>Te(()=>import("./GraphView-DGCqAmwL.js"),__vite__mapDeps([0,1,2])).then(d=>({default:d.GraphView}))),k=["identity","knowledge","rules","events"],se=new Set(["auto","agent","hermes"]),O={identity:{label:"Identität",icon:Re,bg:"bg-cyan-500/10",text:"text-cyan-400"},knowledge:{label:"Wissen",icon:Oe,bg:"bg-indigo-500/10",text:"text-indigo-400"},rules:{label:"Regeln",icon:Pe,bg:"bg-violet-500/10",text:"text-violet-400"},events:{label:"Ereignisse",icon:_e,bg:"bg-amber-500/10",text:"text-amber-400"}},ne={label:"Gedächtnis",icon:De,text:"text-muted-foreground"},Ve={identity:"border-l-cyan-500/80",knowledge:"border-l-indigo-500/80",rules:"border-l-violet-500/80",events:"border-l-amber-500/80"},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 Re=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 re extends s.Component{constructor(){super(...arguments);U(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 te=s.lazy(()=>Te(()=>import("./GraphView-D56vUGn7.js"),__vite__mapDeps([0,1,2])).then(d=>({default:d.GraphView}))),k=["identity","knowledge","rules","events"],se=new Set(["auto","agent","hermes"]),O={identity:{label:"Identität",icon:Re,bg:"bg-cyan-500/10",text:"text-cyan-400"},knowledge:{label:"Wissen",icon:Oe,bg:"bg-indigo-500/10",text:"text-indigo-400"},rules:{label:"Regeln",icon:Pe,bg:"bg-violet-500/10",text:"text-violet-400"},events:{label:"Ereignisse",icon:_e,bg:"bg-amber-500/10",text:"text-amber-400"}},ne={label:"Gedächtnis",icon:De,text:"text-muted-foreground"},Ve={identity:"border-l-cyan-500/80",knowledge:"border-l-indigo-500/80",rules:"border-l-violet-500/80",events:"border-l-amber-500/80"},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,
|
1) wer ich bin und woran ich gerade arbeite,
|
||||||
2) wie ich angesprochen werden möchte,
|
2) wie ich angesprochen werden möchte,
|
||||||
3) meine bevorzugten Tools, Sprachen und Arbeitsweise,
|
3) meine bevorzugten Tools, Sprachen und Arbeitsweise,
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as D,u as Ce,a as Ee,b as Se,j as e,f as ce,d as De,e as g,g as B,q as te,B as ae,h as qe,E as _e,i as Ge,r as y,X as Re,C as J,T as re,k as Z,H as Ae,l as P,m as me,n as Fe,o as Ke,p as Oe,S as pe,s as Ie,P as Te,t as Ue,v as He,w as Le,x as Pe,y as Qe}from"./index-ClYatDZT.js";import{C as xe,Z as ue,L as Ve,a as We,b as Je}from"./zap-raGIywOo.js";/**
|
import{c as D,u as Ce,a as Ee,b as Se,j as e,f as ce,d as De,e as g,g as B,q as te,B as ae,h as qe,E as _e,i as Ge,r as y,X as Re,C as J,T as re,k as Z,H as Ae,l as P,m as me,n as Fe,o as Ke,p as Oe,S as pe,s as Ie,P as Te,t as Ue,v as He,w as Le,x as Pe,y as Qe}from"./index-DgTg5QdX.js";import{C as xe,Z as ue,L as Ve,a as We,b as Je}from"./zap-BX-39Xxr.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{aj as y,r as x,g as L,j as e,L as v,S,ak as W,N as C,e as w,al as E}from"./index-ClYatDZT.js";import{F as M}from"./folder-open-Birisrv_.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{aj as y,r as x,g as L,j as e,L as v,S,ak as W,N as C,e as w,al as E}from"./index-DgTg5QdX.js";import{F as M}from"./folder-open-COXogvYR.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(`
|
`),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(`
|
`)},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};
|
`)},"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-ClYatDZT.js";/**
|
import{c as a}from"./index-DgTg5QdX.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c}from"./index-ClYatDZT.js";/**
|
import{c}from"./index-DgTg5QdX.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c}from"./index-ClYatDZT.js";/**
|
import{c}from"./index-DgTg5QdX.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as a}from"./index-ClYatDZT.js";/**
|
import{c as a}from"./index-DgTg5QdX.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
+108
-108
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as t}from"./index-ClYatDZT.js";/**
|
import{c as t}from"./index-DgTg5QdX.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as a}from"./index-ClYatDZT.js";/**
|
import{c as a}from"./index-DgTg5QdX.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as a}from"./index-ClYatDZT.js";/**
|
import{c as a}from"./index-DgTg5QdX.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
Vendored
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
<link rel="manifest" href="/manifest.webmanifest" />
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<title>Mission Control 2.0</title>
|
<title>Mission Control 2.0</title>
|
||||||
<script type="module" crossorigin src="/assets/index-ClYatDZT.js"></script>
|
<script type="module" crossorigin src="/assets/index-DgTg5QdX.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-Cq_BqGQa.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-Cq_BqGQa.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
+359
-357
@@ -1,357 +1,359 @@
|
|||||||
import { useEffect, useState, useCallback, lazy, Suspense } from "react"
|
import { useEffect, useState, useCallback, lazy, Suspense } from "react"
|
||||||
import { Command as CommandIcon, ChevronLeft, ChevronRight, Loader2, Menu, X } from "lucide-react"
|
import { Command as CommandIcon, ChevronLeft, ChevronRight, Loader2, Menu, X } from "lucide-react"
|
||||||
import { NAV, type ViewId } from "@/nav"
|
import { NAV, type ViewId } from "@/nav"
|
||||||
import { CommandPalette } from "@/components/CommandPalette"
|
import { CommandPalette } from "@/components/CommandPalette"
|
||||||
import { ExpertToggle } from "@/components/ExpertToggle"
|
import { ExpertToggle } from "@/components/ExpertToggle"
|
||||||
import { Placeholder } from "@/views/Placeholder"
|
import { Placeholder } from "@/views/Placeholder"
|
||||||
import { SystemDrawer } from "@/components/SystemDrawer"
|
import { SystemDrawer } from "@/components/SystemDrawer"
|
||||||
import { useHealth, useSystemStatus } from "@/lib/queries"
|
import { useHealth, useSystemStatus } from "@/lib/queries"
|
||||||
import type { Health, SystemStatus } from "@/lib/api"
|
import type { Health, SystemStatus } from "@/lib/api"
|
||||||
import { useMetricsFeeder } from "@/lib/metricsStore"
|
import { useMetricsFeeder } from "@/lib/metricsStore"
|
||||||
import { cn } from "@/lib/utils"
|
import { useEventStream } from "@/lib/events"
|
||||||
import { CockpitView } from "@/views/cockpit/CockpitView"
|
import { cn } from "@/lib/utils"
|
||||||
|
import { CockpitView } from "@/views/cockpit/CockpitView"
|
||||||
// Code-Splitting: nur das Cockpit (Startseite) lädt sofort, jede weitere View
|
|
||||||
// kommt als eigenes Chunk erst beim ersten Aufruf — kleineres Start-Bundle.
|
// Code-Splitting: nur das Cockpit (Startseite) lädt sofort, jede weitere View
|
||||||
const ModelsView = lazy(() => import("@/views/ModelsView").then((m) => ({ default: m.ModelsView })))
|
// kommt als eigenes Chunk erst beim ersten Aufruf — kleineres Start-Bundle.
|
||||||
const ConnectView = lazy(() => import("@/views/ConnectView").then((m) => ({ default: m.ConnectView })))
|
const ModelsView = lazy(() => import("@/views/ModelsView").then((m) => ({ default: m.ModelsView })))
|
||||||
const MemoryView = lazy(() => import("@/views/MemoryView").then((m) => ({ default: m.MemoryView })))
|
const ConnectView = lazy(() => import("@/views/ConnectView").then((m) => ({ default: m.ConnectView })))
|
||||||
const AgentView = lazy(() => import("@/views/AgentView").then((m) => ({ default: m.AgentView })))
|
const MemoryView = lazy(() => import("@/views/MemoryView").then((m) => ({ default: m.MemoryView })))
|
||||||
const KonsoleView = lazy(() => import("@/views/KonsoleView").then((m) => ({ default: m.KonsoleView })))
|
const AgentView = lazy(() => import("@/views/AgentView").then((m) => ({ default: m.AgentView })))
|
||||||
const GuideView = lazy(() => import("@/views/GuideView").then((m) => ({ default: m.GuideView })))
|
const KonsoleView = lazy(() => import("@/views/KonsoleView").then((m) => ({ default: m.KonsoleView })))
|
||||||
const AuftragsbuchView = lazy(() => import("@/views/AuftragsbuchView").then((m) => ({ default: m.AuftragsbuchView })))
|
const GuideView = lazy(() => import("@/views/GuideView").then((m) => ({ default: m.GuideView })))
|
||||||
const ChronikView = lazy(() => import("@/views/ChronikView").then((m) => ({ default: m.ChronikView })))
|
const AuftragsbuchView = lazy(() => import("@/views/AuftragsbuchView").then((m) => ({ default: m.AuftragsbuchView })))
|
||||||
const EigenlebenView = lazy(() => import("@/views/EigenlebenView").then((m) => ({ default: m.EigenlebenView })))
|
const ChronikView = lazy(() => import("@/views/ChronikView").then((m) => ({ default: m.ChronikView })))
|
||||||
const WissenView = lazy(() => import("@/views/WissenView").then((m) => ({ default: m.WissenView })))
|
const EigenlebenView = lazy(() => import("@/views/EigenlebenView").then((m) => ({ default: m.EigenlebenView })))
|
||||||
|
const WissenView = lazy(() => import("@/views/WissenView").then((m) => ({ default: m.WissenView })))
|
||||||
// Dezenter Lade-Zustand während eine View nachgeladen wird (lokales Netz: kaum sichtbar).
|
|
||||||
function ViewLoading() {
|
// Dezenter Lade-Zustand während eine View nachgeladen wird (lokales Netz: kaum sichtbar).
|
||||||
return (
|
function ViewLoading() {
|
||||||
<div className="flex h-64 items-center justify-center text-muted-foreground">
|
return (
|
||||||
<Loader2 className="h-5 w-5 animate-spin" aria-label="Lädt …" />
|
<div className="flex h-64 items-center justify-center text-muted-foreground">
|
||||||
</div>
|
<Loader2 className="h-5 w-5 animate-spin" aria-label="Lädt …" />
|
||||||
)
|
</div>
|
||||||
}
|
)
|
||||||
|
}
|
||||||
// Tastatur-Hinweis passend zum Betriebssystem (Palette hört auf Ctrl UND Cmd).
|
|
||||||
const IS_MAC = /Mac|iPhone|iPad/.test(navigator.platform)
|
// Tastatur-Hinweis passend zum Betriebssystem (Palette hört auf Ctrl UND Cmd).
|
||||||
|
const IS_MAC = /Mac|iPhone|iPad/.test(navigator.platform)
|
||||||
export default function App() {
|
|
||||||
useMetricsFeeder() // sammelt Live-Verlauf global, unabhängig vom aktiven Tab
|
export default function App() {
|
||||||
// View ist im URL-Hash (#models) verankert → Reload/Teilen/Cmd-Klick funktionieren.
|
useMetricsFeeder() // sammelt Live-Verlauf global, unabhängig vom aktiven Tab
|
||||||
const [view, setView] = useState<ViewId>(() => {
|
useEventStream() // SSE-Invalidation-Bus (P3a): sofortige Updates, Poller nur Netz
|
||||||
const h = window.location.hash.slice(1) as ViewId
|
// View ist im URL-Hash (#models) verankert → Reload/Teilen/Cmd-Klick funktionieren.
|
||||||
return NAV.some((n) => n.id === h) ? h : "dashboard"
|
const [view, setView] = useState<ViewId>(() => {
|
||||||
})
|
const h = window.location.hash.slice(1) as ViewId
|
||||||
const navigate = useCallback((v: ViewId) => {
|
return NAV.some((n) => n.id === h) ? h : "dashboard"
|
||||||
if (window.location.hash.slice(1) === v) setView(v)
|
})
|
||||||
else window.location.hash = v
|
const navigate = useCallback((v: ViewId) => {
|
||||||
}, [])
|
if (window.location.hash.slice(1) === v) setView(v)
|
||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => localStorage.getItem("mc_sidebar_collapsed") === "true")
|
else window.location.hash = v
|
||||||
const [mobileNavOpen, setMobileNavOpen] = useState(false)
|
}, [])
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false)
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => localStorage.getItem("mc_sidebar_collapsed") === "true")
|
||||||
const [drawerTab, setDrawerTab] = useState<"maintenance" | "logs">("maintenance")
|
const [mobileNavOpen, setMobileNavOpen] = useState(false)
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false)
|
||||||
const { data: health } = useHealth()
|
const [drawerTab, setDrawerTab] = useState<"maintenance" | "logs">("maintenance")
|
||||||
const { data: sysStatus } = useSystemStatus()
|
|
||||||
|
const { data: health } = useHealth()
|
||||||
useEffect(() => {
|
const { data: sysStatus } = useSystemStatus()
|
||||||
document.documentElement.classList.add("dark")
|
|
||||||
}, [])
|
useEffect(() => {
|
||||||
|
document.documentElement.classList.add("dark")
|
||||||
useEffect(() => {
|
}, [])
|
||||||
const handleOpen = (e: Event) => {
|
|
||||||
const customEvent = e as CustomEvent
|
useEffect(() => {
|
||||||
setDrawerTab(customEvent.detail?.tab || "maintenance")
|
const handleOpen = (e: Event) => {
|
||||||
setDrawerOpen(true)
|
const customEvent = e as CustomEvent
|
||||||
}
|
setDrawerTab(customEvent.detail?.tab || "maintenance")
|
||||||
window.addEventListener("open-system-drawer", handleOpen)
|
setDrawerOpen(true)
|
||||||
return () => window.removeEventListener("open-system-drawer", handleOpen)
|
}
|
||||||
}, [])
|
window.addEventListener("open-system-drawer", handleOpen)
|
||||||
|
return () => window.removeEventListener("open-system-drawer", handleOpen)
|
||||||
useEffect(() => {
|
}, [])
|
||||||
const handleNav = (e: Event) => {
|
|
||||||
const v = (e as CustomEvent).detail?.view as ViewId | undefined
|
useEffect(() => {
|
||||||
if (v) navigate(v)
|
const handleNav = (e: Event) => {
|
||||||
}
|
const v = (e as CustomEvent).detail?.view as ViewId | undefined
|
||||||
window.addEventListener("mc-navigate", handleNav)
|
if (v) navigate(v)
|
||||||
return () => window.removeEventListener("mc-navigate", handleNav)
|
}
|
||||||
}, [navigate])
|
window.addEventListener("mc-navigate", handleNav)
|
||||||
|
return () => window.removeEventListener("mc-navigate", handleNav)
|
||||||
useEffect(() => {
|
}, [navigate])
|
||||||
const onHash = () => {
|
|
||||||
const h = window.location.hash.slice(1) as ViewId
|
useEffect(() => {
|
||||||
if (NAV.some((n) => n.id === h)) setView(h)
|
const onHash = () => {
|
||||||
}
|
const h = window.location.hash.slice(1) as ViewId
|
||||||
window.addEventListener("hashchange", onHash)
|
if (NAV.some((n) => n.id === h)) setView(h)
|
||||||
return () => window.removeEventListener("hashchange", onHash)
|
}
|
||||||
}, [])
|
window.addEventListener("hashchange", onHash)
|
||||||
|
return () => window.removeEventListener("hashchange", onHash)
|
||||||
// Mobile-Navigation schließt nach jedem View-Wechsel (ein Tipp = ein Ziel).
|
}, [])
|
||||||
useEffect(() => { setMobileNavOpen(false) }, [view])
|
|
||||||
|
// Mobile-Navigation schließt nach jedem View-Wechsel (ein Tipp = ein Ziel).
|
||||||
const active = NAV.find((n) => n.id === view)!
|
useEffect(() => { setMobileNavOpen(false) }, [view])
|
||||||
|
|
||||||
return (
|
const active = NAV.find((n) => n.id === view)!
|
||||||
<div className="flex h-full relative">
|
|
||||||
{/* Ruhiger Ambient-Hintergrund: statische radial-gradients statt animierter
|
return (
|
||||||
Fullscreen-Blur-Filter — gleicher Look, praktisch keine GPU-Last. */}
|
<div className="flex h-full relative">
|
||||||
<div
|
{/* Ruhiger Ambient-Hintergrund: statische radial-gradients statt animierter
|
||||||
className="fixed inset-0 -z-50 pointer-events-none"
|
Fullscreen-Blur-Filter — gleicher Look, praktisch keine GPU-Last. */}
|
||||||
style={{
|
<div
|
||||||
backgroundColor: "hsl(224,30%,6%)",
|
className="fixed inset-0 -z-50 pointer-events-none"
|
||||||
backgroundImage: [
|
style={{
|
||||||
"radial-gradient(45% 40% at 12% 8%, hsl(172 72% 50% / 0.10), transparent 70%)",
|
backgroundColor: "hsl(224,30%,6%)",
|
||||||
"radial-gradient(50% 45% at 88% 92%, hsl(270 70% 60% / 0.10), transparent 70%)",
|
backgroundImage: [
|
||||||
"radial-gradient(40% 35% at 75% 30%, hsl(239 70% 62% / 0.07), transparent 70%)",
|
"radial-gradient(45% 40% at 12% 8%, hsl(172 72% 50% / 0.10), transparent 70%)",
|
||||||
].join(", "),
|
"radial-gradient(50% 45% at 88% 92%, hsl(270 70% 60% / 0.10), transparent 70%)",
|
||||||
}}
|
"radial-gradient(40% 35% at 75% 30%, hsl(239 70% 62% / 0.07), transparent 70%)",
|
||||||
/>
|
].join(", "),
|
||||||
|
}}
|
||||||
<CommandPalette onNavigate={navigate} />
|
/>
|
||||||
<SystemDrawer open={drawerOpen} onClose={() => setDrawerOpen(false)} defaultTab={drawerTab} />
|
|
||||||
|
<CommandPalette onNavigate={navigate} />
|
||||||
{/* Sidebar (Desktop, ab md) */}
|
<SystemDrawer open={drawerOpen} onClose={() => setDrawerOpen(false)} defaultTab={drawerTab} />
|
||||||
<aside className={cn(
|
|
||||||
"hidden md:flex shrink-0 flex-col border-r border-border/40 bg-card/45 backdrop-blur-md transition-all duration-300 ease-in-out",
|
{/* Sidebar (Desktop, ab md) */}
|
||||||
sidebarCollapsed ? "w-16" : "w-60"
|
<aside className={cn(
|
||||||
)}>
|
"hidden md:flex shrink-0 flex-col border-r border-border/40 bg-card/45 backdrop-blur-md transition-all duration-300 ease-in-out",
|
||||||
<SidebarContent
|
sidebarCollapsed ? "w-16" : "w-60"
|
||||||
collapsed={sidebarCollapsed}
|
)}>
|
||||||
onToggleCollapse={() => {
|
<SidebarContent
|
||||||
setSidebarCollapsed((c) => {
|
collapsed={sidebarCollapsed}
|
||||||
const next = !c
|
onToggleCollapse={() => {
|
||||||
localStorage.setItem("mc_sidebar_collapsed", next.toString())
|
setSidebarCollapsed((c) => {
|
||||||
return next
|
const next = !c
|
||||||
})
|
localStorage.setItem("mc_sidebar_collapsed", next.toString())
|
||||||
}}
|
return next
|
||||||
view={view}
|
})
|
||||||
health={health}
|
}}
|
||||||
sysStatus={sysStatus}
|
view={view}
|
||||||
/>
|
health={health}
|
||||||
</aside>
|
sysStatus={sysStatus}
|
||||||
|
/>
|
||||||
{/* Sidebar (Mobile, Off-Canvas mit Backdrop) */}
|
</aside>
|
||||||
<div
|
|
||||||
className={cn(
|
{/* Sidebar (Mobile, Off-Canvas mit Backdrop) */}
|
||||||
"md:hidden fixed inset-0 z-50 transition-opacity duration-200",
|
<div
|
||||||
mobileNavOpen ? "opacity-100" : "pointer-events-none opacity-0"
|
className={cn(
|
||||||
)}
|
"md:hidden fixed inset-0 z-50 transition-opacity duration-200",
|
||||||
aria-hidden={!mobileNavOpen}
|
mobileNavOpen ? "opacity-100" : "pointer-events-none opacity-0"
|
||||||
>
|
)}
|
||||||
<div className="absolute inset-0 bg-black/60" onClick={() => setMobileNavOpen(false)} />
|
aria-hidden={!mobileNavOpen}
|
||||||
<aside className={cn(
|
>
|
||||||
"absolute inset-y-0 left-0 flex w-72 max-w-[85vw] flex-col border-r border-border/40 bg-[hsl(224,28%,8%)] transition-transform duration-300 ease-in-out",
|
<div className="absolute inset-0 bg-black/60" onClick={() => setMobileNavOpen(false)} />
|
||||||
mobileNavOpen ? "translate-x-0" : "-translate-x-full"
|
<aside className={cn(
|
||||||
)}>
|
"absolute inset-y-0 left-0 flex w-72 max-w-[85vw] flex-col border-r border-border/40 bg-[hsl(224,28%,8%)] transition-transform duration-300 ease-in-out",
|
||||||
<SidebarContent
|
mobileNavOpen ? "translate-x-0" : "-translate-x-full"
|
||||||
collapsed={false}
|
)}>
|
||||||
onClose={() => setMobileNavOpen(false)}
|
<SidebarContent
|
||||||
view={view}
|
collapsed={false}
|
||||||
health={health}
|
onClose={() => setMobileNavOpen(false)}
|
||||||
sysStatus={sysStatus}
|
view={view}
|
||||||
/>
|
health={health}
|
||||||
</aside>
|
sysStatus={sysStatus}
|
||||||
</div>
|
/>
|
||||||
|
</aside>
|
||||||
{/* Main */}
|
</div>
|
||||||
<div className="flex min-w-0 flex-1 flex-col">
|
|
||||||
<header className="flex h-14 shrink-0 items-center justify-between gap-3 border-b border-border/40 px-4 md:px-6 bg-card/20 backdrop-blur-sm">
|
{/* Main */}
|
||||||
<div className="flex min-w-0 items-center gap-3">
|
<div className="flex min-w-0 flex-1 flex-col">
|
||||||
<button
|
<header className="flex h-14 shrink-0 items-center justify-between gap-3 border-b border-border/40 px-4 md:px-6 bg-card/20 backdrop-blur-sm">
|
||||||
onClick={() => setMobileNavOpen(true)}
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
className="md:hidden p-1.5 -ml-1 rounded-md hover:bg-accent text-muted-foreground transition-colors cursor-pointer"
|
<button
|
||||||
aria-label="Navigation öffnen"
|
onClick={() => setMobileNavOpen(true)}
|
||||||
>
|
className="md:hidden p-1.5 -ml-1 rounded-md hover:bg-accent text-muted-foreground transition-colors cursor-pointer"
|
||||||
<Menu className="h-5 w-5" aria-hidden="true" />
|
aria-label="Navigation öffnen"
|
||||||
</button>
|
>
|
||||||
<div className="truncate text-xs font-medium text-muted-foreground tracking-wide uppercase font-sans">{active.hint}</div>
|
<Menu className="h-5 w-5" aria-hidden="true" />
|
||||||
</div>
|
</button>
|
||||||
<div className="flex shrink-0 items-center gap-2">
|
<div className="truncate text-xs font-medium text-muted-foreground tracking-wide uppercase font-sans">{active.hint}</div>
|
||||||
<ExpertToggle />
|
</div>
|
||||||
<a
|
<div className="flex shrink-0 items-center gap-2">
|
||||||
href="https://git.tobisniceshomelab.ddnsfree.com/Hitonabi/mission-control-v2/src/branch/main/docs/BEDIENUNG.md"
|
<ExpertToggle />
|
||||||
target="_blank"
|
<a
|
||||||
rel="noopener"
|
href="https://git.tobisniceshomelab.ddnsfree.com/Hitonabi/mission-control-v2/src/branch/main/docs/BEDIENUNG.md"
|
||||||
className="hidden sm:flex h-8 items-center rounded-md border border-border/40 bg-background/40 px-2.5 text-xs text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-all cursor-pointer font-semibold"
|
target="_blank"
|
||||||
title="Bedien-Anleitung"
|
rel="noopener"
|
||||||
>
|
className="hidden sm:flex h-8 items-center rounded-md border border-border/40 bg-background/40 px-2.5 text-xs text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-all cursor-pointer font-semibold"
|
||||||
Hilfe
|
title="Bedien-Anleitung"
|
||||||
</a>
|
>
|
||||||
|
Hilfe
|
||||||
<button
|
</a>
|
||||||
onClick={() => {
|
|
||||||
const ev = new KeyboardEvent("keydown", { key: "k", metaKey: true })
|
<button
|
||||||
document.dispatchEvent(ev)
|
onClick={() => {
|
||||||
}}
|
const ev = new KeyboardEvent("keydown", { key: "k", metaKey: true })
|
||||||
className="flex items-center gap-2 rounded-md border border-border/40 bg-background/40 px-2.5 py-1.5 text-xs text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-all cursor-pointer"
|
document.dispatchEvent(ev)
|
||||||
>
|
}}
|
||||||
<CommandIcon className="h-3.5 w-3.5" />
|
className="flex items-center gap-2 rounded-md border border-border/40 bg-background/40 px-2.5 py-1.5 text-xs text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-all cursor-pointer"
|
||||||
<span className="hidden sm:inline">Suchen</span>
|
>
|
||||||
<kbd className="hidden sm:inline rounded bg-muted px-1.5 py-0.5 font-mono text-[9px]">{IS_MAC ? "⌘K" : "Strg+K"}</kbd>
|
<CommandIcon className="h-3.5 w-3.5" />
|
||||||
</button>
|
<span className="hidden sm:inline">Suchen</span>
|
||||||
</div>
|
<kbd className="hidden sm:inline rounded bg-muted px-1.5 py-0.5 font-mono text-[9px]">{IS_MAC ? "⌘K" : "Strg+K"}</kbd>
|
||||||
</header>
|
</button>
|
||||||
|
</div>
|
||||||
<main className="flex-1 overflow-y-auto p-4 md:p-6 scrollbar-thin">
|
</header>
|
||||||
<Suspense fallback={<ViewLoading />}>
|
|
||||||
{view === "dashboard" && <CockpitView onNavigate={(v) => navigate(v as ViewId)} />}
|
<main className="flex-1 overflow-y-auto p-4 md:p-6 scrollbar-thin">
|
||||||
{view === "auftraege" && <AuftragsbuchView />}
|
<Suspense fallback={<ViewLoading />}>
|
||||||
{view === "models" && <ModelsView />}
|
{view === "dashboard" && <CockpitView onNavigate={(v) => navigate(v as ViewId)} />}
|
||||||
{view === "connect" && <ConnectView />}
|
{view === "auftraege" && <AuftragsbuchView />}
|
||||||
{view === "memory" && <MemoryView />}
|
{view === "models" && <ModelsView />}
|
||||||
{view === "wissen" && <WissenView />}
|
{view === "connect" && <ConnectView />}
|
||||||
{view === "chronik" && <ChronikView />}
|
{view === "memory" && <MemoryView />}
|
||||||
{view === "eigenleben" && <EigenlebenView />}
|
{view === "wissen" && <WissenView />}
|
||||||
{view === "agent" && <AgentView />}
|
{view === "chronik" && <ChronikView />}
|
||||||
{view === "konsole" && <KonsoleView />}
|
{view === "eigenleben" && <EigenlebenView />}
|
||||||
{view === "guide" && <GuideView />}
|
{view === "agent" && <AgentView />}
|
||||||
{!["dashboard", "auftraege", "eigenleben", "models", "connect", "memory", "wissen", "chronik", "agent", "konsole", "guide"].includes(view) && (
|
{view === "konsole" && <KonsoleView />}
|
||||||
<Placeholder title={active.label} hint={active.hint} />
|
{view === "guide" && <GuideView />}
|
||||||
)}
|
{!["dashboard", "auftraege", "eigenleben", "models", "connect", "memory", "wissen", "chronik", "agent", "konsole", "guide"].includes(view) && (
|
||||||
</Suspense>
|
<Placeholder title={active.label} hint={active.hint} />
|
||||||
</main>
|
)}
|
||||||
</div>
|
</Suspense>
|
||||||
</div>
|
</main>
|
||||||
)
|
</div>
|
||||||
}
|
</div>
|
||||||
|
)
|
||||||
// Sidebar-Inhalt (Logo, Navigation, Status-Fuß) — geteilt zwischen Desktop-Leiste
|
}
|
||||||
// und Mobile-Off-Canvas, damit beide immer identisch bleiben.
|
|
||||||
function SidebarContent({
|
// Sidebar-Inhalt (Logo, Navigation, Status-Fuß) — geteilt zwischen Desktop-Leiste
|
||||||
collapsed, onToggleCollapse, onClose, view, health, sysStatus,
|
// und Mobile-Off-Canvas, damit beide immer identisch bleiben.
|
||||||
}: {
|
function SidebarContent({
|
||||||
collapsed: boolean
|
collapsed, onToggleCollapse, onClose, view, health, sysStatus,
|
||||||
onToggleCollapse?: () => void
|
}: {
|
||||||
onClose?: () => void
|
collapsed: boolean
|
||||||
view: ViewId
|
onToggleCollapse?: () => void
|
||||||
health: Health | undefined
|
onClose?: () => void
|
||||||
sysStatus: SystemStatus | undefined
|
view: ViewId
|
||||||
}) {
|
health: Health | undefined
|
||||||
return (
|
sysStatus: SystemStatus | undefined
|
||||||
<>
|
}) {
|
||||||
<div className={cn("flex items-center py-4 border-b border-border/40 shrink-0", collapsed ? "flex-col gap-3 px-2" : "justify-between px-5")}>
|
return (
|
||||||
<div className="flex items-center gap-2 overflow-hidden">
|
<>
|
||||||
<div className="h-7 w-7 rounded-lg bg-primary shadow-md shadow-primary/20 shrink-0" />
|
<div className={cn("flex items-center py-4 border-b border-border/40 shrink-0", collapsed ? "flex-col gap-3 px-2" : "justify-between px-5")}>
|
||||||
{!collapsed && (
|
<div className="flex items-center gap-2 overflow-hidden">
|
||||||
<div className="leading-tight">
|
<div className="h-7 w-7 rounded-lg bg-primary shadow-md shadow-primary/20 shrink-0" />
|
||||||
<div className="text-sm font-semibold tracking-wide font-space">Mission Control</div>
|
{!collapsed && (
|
||||||
<div className="text-xs text-muted-foreground">2.0</div>
|
<div className="leading-tight">
|
||||||
</div>
|
<div className="text-sm font-semibold tracking-wide font-space">Mission Control</div>
|
||||||
)}
|
<div className="text-xs text-muted-foreground">2.0</div>
|
||||||
</div>
|
</div>
|
||||||
{onToggleCollapse && (
|
)}
|
||||||
<button
|
</div>
|
||||||
onClick={onToggleCollapse}
|
{onToggleCollapse && (
|
||||||
className="p-1 rounded-md hover:bg-accent text-muted-foreground transition-colors cursor-pointer"
|
<button
|
||||||
aria-label={collapsed ? "Seitenleiste ausklappen" : "Seitenleiste einklappen"}
|
onClick={onToggleCollapse}
|
||||||
title={collapsed ? "Maximieren" : "Minimieren"}
|
className="p-1 rounded-md hover:bg-accent text-muted-foreground transition-colors cursor-pointer"
|
||||||
>
|
aria-label={collapsed ? "Seitenleiste ausklappen" : "Seitenleiste einklappen"}
|
||||||
{collapsed ? <ChevronRight className="h-4 w-4" aria-hidden="true" /> : <ChevronLeft className="h-4 w-4" aria-hidden="true" />}
|
title={collapsed ? "Maximieren" : "Minimieren"}
|
||||||
</button>
|
>
|
||||||
)}
|
{collapsed ? <ChevronRight className="h-4 w-4" aria-hidden="true" /> : <ChevronLeft className="h-4 w-4" aria-hidden="true" />}
|
||||||
{onClose && (
|
</button>
|
||||||
<button
|
)}
|
||||||
onClick={onClose}
|
{onClose && (
|
||||||
className="p-1 rounded-md hover:bg-accent text-muted-foreground transition-colors cursor-pointer"
|
<button
|
||||||
aria-label="Navigation schließen"
|
onClick={onClose}
|
||||||
>
|
className="p-1 rounded-md hover:bg-accent text-muted-foreground transition-colors cursor-pointer"
|
||||||
<X className="h-4 w-4" aria-hidden="true" />
|
aria-label="Navigation schließen"
|
||||||
</button>
|
>
|
||||||
)}
|
<X className="h-4 w-4" aria-hidden="true" />
|
||||||
</div>
|
</button>
|
||||||
|
)}
|
||||||
<nav className="flex-1 space-y-1 px-3 py-4 overflow-y-auto">
|
</div>
|
||||||
{(() => {
|
|
||||||
let lastGroup: string | null = null
|
<nav className="flex-1 space-y-1 px-3 py-4 overflow-y-auto">
|
||||||
return NAV.map((item) => {
|
{(() => {
|
||||||
const showGroupHeader = item.group !== lastGroup
|
let lastGroup: string | null = null
|
||||||
lastGroup = item.group
|
return NAV.map((item) => {
|
||||||
return (
|
const showGroupHeader = item.group !== lastGroup
|
||||||
<div key={item.id} className="space-y-1">
|
lastGroup = item.group
|
||||||
{showGroupHeader && (
|
return (
|
||||||
collapsed ? (
|
<div key={item.id} className="space-y-1">
|
||||||
lastGroup !== NAV[0].group && <div className="my-3 border-t border-border/30" />
|
{showGroupHeader && (
|
||||||
) : (
|
collapsed ? (
|
||||||
<div className="mt-5 mb-1.5 px-3 text-[10px] font-bold uppercase tracking-widest text-muted-foreground/40 first:mt-0 select-none">
|
lastGroup !== NAV[0].group && <div className="my-3 border-t border-border/30" />
|
||||||
{item.group}
|
) : (
|
||||||
</div>
|
<div className="mt-5 mb-1.5 px-3 text-[10px] font-bold uppercase tracking-widest text-muted-foreground/40 first:mt-0 select-none">
|
||||||
)
|
{item.group}
|
||||||
)}
|
</div>
|
||||||
<a
|
)
|
||||||
href={`#${item.id}`}
|
)}
|
||||||
onClick={onClose}
|
<a
|
||||||
aria-current={view === item.id ? "page" : undefined}
|
href={`#${item.id}`}
|
||||||
className={cn(
|
onClick={onClose}
|
||||||
"flex w-full items-center rounded-md text-sm transition-all cursor-pointer",
|
aria-current={view === item.id ? "page" : undefined}
|
||||||
collapsed ? "justify-center p-2.5" : "gap-3 px-3 py-2",
|
className={cn(
|
||||||
view === item.id
|
"flex w-full items-center rounded-md text-sm transition-all cursor-pointer",
|
||||||
? (collapsed ? "nav-active-collapsed" : "nav-active")
|
collapsed ? "justify-center p-2.5" : "gap-3 px-3 py-2",
|
||||||
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
|
view === item.id
|
||||||
)}
|
? (collapsed ? "nav-active-collapsed" : "nav-active")
|
||||||
title={collapsed ? item.label : undefined}
|
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
|
||||||
aria-label={collapsed ? item.label : undefined}
|
)}
|
||||||
>
|
title={collapsed ? item.label : undefined}
|
||||||
<item.icon className="h-4.5 w-4.5 shrink-0" aria-hidden="true" />
|
aria-label={collapsed ? item.label : undefined}
|
||||||
{!collapsed && <span className="truncate">{item.label}</span>}
|
>
|
||||||
</a>
|
<item.icon className="h-4.5 w-4.5 shrink-0" aria-hidden="true" />
|
||||||
</div>
|
{!collapsed && <span className="truncate">{item.label}</span>}
|
||||||
)
|
</a>
|
||||||
})
|
</div>
|
||||||
})()}
|
)
|
||||||
</nav>
|
})
|
||||||
|
})()}
|
||||||
<div className={cn("py-3 text-xs text-muted-foreground border-t border-border/40 shrink-0", collapsed ? "px-2 text-center" : "px-5")}>
|
</nav>
|
||||||
{collapsed ? (
|
|
||||||
<div className="flex justify-center">
|
<div className={cn("py-3 text-xs text-muted-foreground border-t border-border/40 shrink-0", collapsed ? "px-2 text-center" : "px-5")}>
|
||||||
<span className={cn(
|
{collapsed ? (
|
||||||
"h-2.5 w-2.5 rounded-full ring-2 ring-black/40",
|
<div className="flex justify-center">
|
||||||
health
|
<span className={cn(
|
||||||
? (!health.engine_reachable ? "bg-amber-500"
|
"h-2.5 w-2.5 rounded-full ring-2 ring-black/40",
|
||||||
: (health.brain && !health.brain.ready ? "bg-amber-500 animate-pulse" : "bg-emerald-500 animate-pulse"))
|
health
|
||||||
: "bg-red-500"
|
? (!health.engine_reachable ? "bg-amber-500"
|
||||||
)} title={health
|
: (health.brain && !health.brain.ready ? "bg-amber-500 animate-pulse" : "bg-emerald-500 animate-pulse"))
|
||||||
? (!health.engine_reachable ? "Engine offline"
|
: "bg-red-500"
|
||||||
: (health.brain && !health.brain.ready ? `Hirn offline (${health.brain.model ?? "fast"})` : "Engine + Hirn online"))
|
)} title={health
|
||||||
: "Backend offline"} />
|
? (!health.engine_reachable ? "Engine offline"
|
||||||
</div>
|
: (health.brain && !health.brain.ready ? `Hirn offline (${health.brain.model ?? "fast"})` : "Engine + Hirn online"))
|
||||||
) : (
|
: "Backend offline"} />
|
||||||
<div className="space-y-2 text-left">
|
</div>
|
||||||
{health ? (
|
) : (
|
||||||
<>
|
<div className="space-y-2 text-left">
|
||||||
<span className="flex items-center gap-2">
|
{health ? (
|
||||||
<span className={cn("h-2 w-2 rounded-full animate-pulse", health.engine_reachable ? "bg-emerald-500" : "bg-amber-500")} />
|
<>
|
||||||
<span className="truncate">Engine {health.engine_reachable ? "online" : "offline"}</span>
|
<span className="flex items-center gap-2">
|
||||||
</span>
|
<span className={cn("h-2 w-2 rounded-full animate-pulse", health.engine_reachable ? "bg-emerald-500" : "bg-amber-500")} />
|
||||||
{health.brain && !health.brain.ready && (
|
<span className="truncate">Engine {health.engine_reachable ? "online" : "offline"}</span>
|
||||||
<span className="flex items-center gap-2 text-amber-400" title={`Agent-Hirn '${health.brain.model ?? "fast"}' lädt nicht/abgestürzt`}>
|
</span>
|
||||||
<span className="h-2 w-2 rounded-full bg-amber-500 animate-pulse" />
|
{health.brain && !health.brain.ready && (
|
||||||
<span className="truncate">Hirn offline{health.brain.model ? ` (${health.brain.model})` : ""}</span>
|
<span className="flex items-center gap-2 text-amber-400" title={`Agent-Hirn '${health.brain.model ?? "fast"}' lädt nicht/abgestürzt`}>
|
||||||
</span>
|
<span className="h-2 w-2 rounded-full bg-amber-500 animate-pulse" />
|
||||||
)}
|
<span className="truncate">Hirn offline{health.brain.model ? ` (${health.brain.model})` : ""}</span>
|
||||||
</>
|
</span>
|
||||||
) : (
|
)}
|
||||||
<span className="flex items-center gap-2">
|
</>
|
||||||
<span className="h-2 w-2 rounded-full bg-red-500" /> <span className="truncate">Backend offline</span>
|
) : (
|
||||||
</span>
|
<span className="flex items-center gap-2">
|
||||||
)}
|
<span className="h-2 w-2 rounded-full bg-red-500" /> <span className="truncate">Backend offline</span>
|
||||||
|
</span>
|
||||||
{sysStatus?.versions && (
|
)}
|
||||||
<div className="space-y-1 text-[9px] text-muted-foreground/60 mt-2 pt-2 border-t border-border/30">
|
|
||||||
<div className="truncate" title={sysStatus.versions.mc2 ? `${sysStatus.versions.mc2.branch}-${sysStatus.versions.mc2.hash}${sysStatus.versions.mc2.dirty ? "*" : ""} (${sysStatus.versions.mc2.date})` : "nicht gefunden"}>
|
{sysStatus?.versions && (
|
||||||
<strong>MC2:</strong> {sysStatus.versions.mc2 ? `${sysStatus.versions.mc2.hash}${sysStatus.versions.mc2.dirty ? "*" : ""}` : "—"}
|
<div className="space-y-1 text-[9px] text-muted-foreground/60 mt-2 pt-2 border-t border-border/30">
|
||||||
</div>
|
<div className="truncate" title={sysStatus.versions.mc2 ? `${sysStatus.versions.mc2.branch}-${sysStatus.versions.mc2.hash}${sysStatus.versions.mc2.dirty ? "*" : ""} (${sysStatus.versions.mc2.date})` : "nicht gefunden"}>
|
||||||
<div className="truncate" title={sysStatus.versions.engine?.type === "git" ? `${sysStatus.versions.engine.branch}-${sysStatus.versions.engine.hash}${sysStatus.versions.engine.dirty ? "*" : ""} (${sysStatus.versions.engine.date})` : sysStatus.versions.engine?.version_text || "unbekannt"}>
|
<strong>MC2:</strong> {sysStatus.versions.mc2 ? `${sysStatus.versions.mc2.hash}${sysStatus.versions.mc2.dirty ? "*" : ""}` : "—"}
|
||||||
<strong>Engine:</strong> {sysStatus.versions.engine?.type === "git" ? `${sysStatus.versions.engine.hash}${sysStatus.versions.engine.dirty ? "*" : ""}` : (sysStatus.versions.engine?.version_text?.split(" ").pop() || "—")}
|
</div>
|
||||||
</div>
|
<div className="truncate" title={sysStatus.versions.engine?.type === "git" ? `${sysStatus.versions.engine.branch}-${sysStatus.versions.engine.hash}${sysStatus.versions.engine.dirty ? "*" : ""} (${sysStatus.versions.engine.date})` : sysStatus.versions.engine?.version_text || "unbekannt"}>
|
||||||
<div className="truncate" title={sysStatus.versions.hermes_agent ? `${sysStatus.versions.hermes_agent.branch}-${sysStatus.versions.hermes_agent.hash}${sysStatus.versions.hermes_agent.dirty ? "*" : ""} (${sysStatus.versions.hermes_agent.date})` : "nicht gefunden"}>
|
<strong>Engine:</strong> {sysStatus.versions.engine?.type === "git" ? `${sysStatus.versions.engine.hash}${sysStatus.versions.engine.dirty ? "*" : ""}` : (sysStatus.versions.engine?.version_text?.split(" ").pop() || "—")}
|
||||||
<strong>Hermes Agent:</strong> {sysStatus.versions.hermes_agent ? `${sysStatus.versions.hermes_agent.hash}${sysStatus.versions.hermes_agent.dirty ? "*" : ""}` : "—"}
|
</div>
|
||||||
</div>
|
<div className="truncate" title={sysStatus.versions.hermes_agent ? `${sysStatus.versions.hermes_agent.branch}-${sysStatus.versions.hermes_agent.hash}${sysStatus.versions.hermes_agent.dirty ? "*" : ""} (${sysStatus.versions.hermes_agent.date})` : "nicht gefunden"}>
|
||||||
</div>
|
<strong>Hermes Agent:</strong> {sysStatus.versions.hermes_agent ? `${sysStatus.versions.hermes_agent.hash}${sysStatus.versions.hermes_agent.dirty ? "*" : ""}` : "—"}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
)}
|
||||||
)
|
</div>
|
||||||
}
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// SSE-Eventstrom (UMBAU v3 P3a) — Gegenstück zu backend/routers/events.py.
|
||||||
|
// EIN EventSource auf /api/events; `invalidate`-Events stoßen gezielt die React-Query-
|
||||||
|
// Caches an. Solange der Strom steht, sind die Poller nur noch Sicherheitsnetz
|
||||||
|
// (queries.ts entspannt sie ×5 über sseVerbunden()). Reißt der Strom (MC2-Deploy,
|
||||||
|
// Netz), reconnectet EventSource von selbst — bis dahin pollt die UI wie früher.
|
||||||
|
|
||||||
|
import { useEffect } from "react"
|
||||||
|
import { useQueryClient } from "@tanstack/react-query"
|
||||||
|
|
||||||
|
let verbunden = false
|
||||||
|
export const sseVerbunden = () => verbunden
|
||||||
|
|
||||||
|
export function useEventStream() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
useEffect(() => {
|
||||||
|
const es = new EventSource("/api/events")
|
||||||
|
es.onopen = () => { verbunden = true }
|
||||||
|
es.onerror = () => { verbunden = false }
|
||||||
|
es.addEventListener("invalidate", (e) => {
|
||||||
|
try {
|
||||||
|
const keys: string[] = JSON.parse((e as MessageEvent).data)?.keys ?? []
|
||||||
|
for (const k of keys) qc.invalidateQueries({ queryKey: [k] })
|
||||||
|
} catch { /* kaputtes Event bricht den Strom nicht */ }
|
||||||
|
})
|
||||||
|
return () => { verbunden = false; es.close() }
|
||||||
|
}, [qc])
|
||||||
|
}
|
||||||
+249
-242
@@ -1,242 +1,249 @@
|
|||||||
// Zentrale Daten-Hooks (TanStack Query). Kapseln lib/api.ts und liefern Caching,
|
// Zentrale Daten-Hooks (TanStack Query). Kapseln lib/api.ts und liefern Caching,
|
||||||
// Dedup (gleicher queryKey = eine Anfrage über alle Views), Retry und Polling.
|
// Dedup (gleicher queryKey = eine Anfrage über alle Views), Retry und Polling.
|
||||||
// Mutations invalidieren gezielt die betroffenen Keys, statt manuell neu zu laden.
|
// Mutations invalidieren gezielt die betroffenen Keys, statt manuell neu zu laden.
|
||||||
|
|
||||||
import { useQuery, useQueryClient, type QueryClient } from "@tanstack/react-query"
|
import { useQuery, useQueryClient, type QueryClient } from "@tanstack/react-query"
|
||||||
import {
|
import { sseVerbunden } from "./events"
|
||||||
api,
|
import {
|
||||||
type AgentStatus,
|
api,
|
||||||
type AuftragsbuchResp,
|
type AgentStatus,
|
||||||
type ChronikResp,
|
type AuftragsbuchResp,
|
||||||
type ConnectResp,
|
type ChronikResp,
|
||||||
type ConnectHealth,
|
type ConnectResp,
|
||||||
type DiscoverResp,
|
type ConnectHealth,
|
||||||
type DraftsResp,
|
type DiscoverResp,
|
||||||
type EigenlebenResp,
|
type DraftsResp,
|
||||||
type GroupsResp,
|
type EigenlebenResp,
|
||||||
type HermesBrainResp,
|
type GroupsResp,
|
||||||
type Health,
|
type HermesBrainResp,
|
||||||
type IdeenLogResp,
|
type Health,
|
||||||
type IdeenResp,
|
type IdeenLogResp,
|
||||||
type Job,
|
type IdeenResp,
|
||||||
type Memory,
|
type Job,
|
||||||
type MemoryGraph,
|
type Memory,
|
||||||
type ModelsResp,
|
type MemoryGraph,
|
||||||
type Reminder,
|
type ModelsResp,
|
||||||
type RoutingResp,
|
type Reminder,
|
||||||
type RoutingPolicyMeta,
|
type RoutingResp,
|
||||||
type ServicesResp,
|
type RoutingPolicyMeta,
|
||||||
type SystemStatus,
|
type ServicesResp,
|
||||||
type TokenStats,
|
type SystemStatus,
|
||||||
type UpdatesResp,
|
type TokenStats,
|
||||||
type VoiceTraceResp,
|
type UpdatesResp,
|
||||||
type WissenResp,
|
type VoiceTraceResp,
|
||||||
type ZeitmaschineResp,
|
type WissenResp,
|
||||||
} from "./api"
|
type ZeitmaschineResp,
|
||||||
|
} from "./api"
|
||||||
// Zentrale Query-Keys (eine Quelle der Wahrheit für invalidate).
|
|
||||||
export const qk = {
|
// Zentrale Query-Keys (eine Quelle der Wahrheit für invalidate).
|
||||||
health: ["health"] as const,
|
export const qk = {
|
||||||
systemStatus: ["system-status"] as const,
|
health: ["health"] as const,
|
||||||
services: ["services"] as const,
|
systemStatus: ["system-status"] as const,
|
||||||
models: ["models"] as const,
|
services: ["services"] as const,
|
||||||
groups: ["groups"] as const,
|
models: ["models"] as const,
|
||||||
routing: ["routing"] as const,
|
groups: ["groups"] as const,
|
||||||
routingPolicy: ["routing-policy"] as const,
|
routing: ["routing"] as const,
|
||||||
jobs: ["jobs"] as const,
|
routingPolicy: ["routing-policy"] as const,
|
||||||
tokenStats: ["token-stats"] as const,
|
jobs: ["jobs"] as const,
|
||||||
agentStatus: ["agent-status"] as const,
|
tokenStats: ["token-stats"] as const,
|
||||||
hermesBrain: ["hermes-brain"] as const,
|
agentStatus: ["agent-status"] as const,
|
||||||
updates: ["updates"] as const,
|
hermesBrain: ["hermes-brain"] as const,
|
||||||
discover: ["discover"] as const,
|
updates: ["updates"] as const,
|
||||||
drafts: (target?: string) => ["drafts", target ?? ""] as const,
|
discover: ["discover"] as const,
|
||||||
connect: (params?: string) => ["connect", params ?? ""] as const,
|
drafts: (target?: string) => ["drafts", target ?? ""] as const,
|
||||||
connectHealth: ["connect-health"] as const,
|
connect: (params?: string) => ["connect", params ?? ""] as const,
|
||||||
memory: (q?: string, category?: string) => ["memory", q ?? "", category ?? ""] as const,
|
connectHealth: ["connect-health"] as const,
|
||||||
memoryGraph: ["memory-graph"] as const,
|
memory: (q?: string, category?: string) => ["memory", q ?? "", category ?? ""] as const,
|
||||||
voiceTrace: ["voice-trace"] as const,
|
memoryGraph: ["memory-graph"] as const,
|
||||||
auftragsbuch: ["auftragsbuch"] as const,
|
voiceTrace: ["voice-trace"] as const,
|
||||||
ideen: ["ideen"] as const,
|
auftragsbuch: ["auftragsbuch"] as const,
|
||||||
chronik: ["chronik"] as const,
|
ideen: ["ideen"] as const,
|
||||||
eigenleben: ["eigenleben"] as const,
|
chronik: ["chronik"] as const,
|
||||||
wissen: ["wissen"] as const,
|
eigenleben: ["eigenleben"] as const,
|
||||||
zeitmaschine: ["zeitmaschine"] as const,
|
wissen: ["wissen"] as const,
|
||||||
reminders: ["reminders"] as const,
|
zeitmaschine: ["zeitmaschine"] as const,
|
||||||
}
|
reminders: ["reminders"] as const,
|
||||||
|
}
|
||||||
// ── Polling-Takte ────────────────────────────────────────────────────────────
|
|
||||||
// EINE Stelle für alle Intervalle. Regel: Die Live-Graphen (System/Token) takten
|
// ── Polling-Takte ────────────────────────────────────────────────────────────
|
||||||
// schnell, alles andere gemächlich — Mutationen invalidieren ohnehin gezielt,
|
// EINE Stelle für alle Intervalle. Regel: Die Live-Graphen (System/Token) takten
|
||||||
// Polling ist nur das Sicherheitsnetz. Komponenten übergeben KEINE eigenen
|
// schnell, alles andere gemächlich — Mutationen invalidieren ohnehin gezielt,
|
||||||
// Intervalle mehr (mehrere Beobachter = kleinstes Intervall gewinnt).
|
// Polling ist nur das Sicherheitsnetz. Komponenten übergeben KEINE eigenen
|
||||||
const TAKT = {
|
// Intervalle mehr (mehrere Beobachter = kleinstes Intervall gewinnt).
|
||||||
graph: 3_000, // Live-Graphen im Cockpit (System, Token-Durchsatz)
|
const TAKT = {
|
||||||
schnell: 8_000, // „fühlt sich live an": Modelle, Voice-Trace, Auftragsbuch
|
graph: 3_000, // Live-Graphen im Cockpit (System, Token-Durchsatz)
|
||||||
normal: 10_000, // Dienste, Routing, Agent-Status, Health
|
schnell: 8_000, // „fühlt sich live an": Modelle, Voice-Trace, Auftragsbuch
|
||||||
gemuetlich: 30_000, // Chronik, Ideen
|
normal: 10_000, // Dienste, Routing, Agent-Status, Health
|
||||||
traege: 60_000, // Updates-Check, Reminders, Zeitmaschine, HF-Abfragen
|
gemuetlich: 30_000, // Chronik, Ideen
|
||||||
} as const
|
traege: 60_000, // Updates-Check, Reminders, Zeitmaschine, HF-Abfragen
|
||||||
|
} as const
|
||||||
// Auftragsbuch: Vorschlags-Inbox (Branches + Skill-Kandidaten). Pollt, damit laufende
|
|
||||||
// Annahme-Läufe (Status aus der JSON-Datei des Runners) live sichtbar werden.
|
// UMBAU v3 P3a: Läuft der SSE-Eventstrom (/api/events, siehe lib/events.ts), sind die
|
||||||
export const useAuftragsbuch = (refetchInterval: number = TAKT.schnell) =>
|
// Poller der ereignishaften Quellen nur noch SICHERHEITSNETZ — der Strom invalidiert
|
||||||
useQuery({
|
// bei Änderung sofort. Verbunden = Takt ×5; Strom weg = alter Takt (EventSource
|
||||||
queryKey: qk.auftragsbuch,
|
// reconnectet selbst). Live-Graphen/Status bleiben bewusst beim Polling.
|
||||||
queryFn: () => api<AuftragsbuchResp>("/api/auftragsbuch"),
|
const relax = (ms: number) => () => (sseVerbunden() ? ms * 5 : ms)
|
||||||
refetchInterval,
|
|
||||||
})
|
// Auftragsbuch: Vorschlags-Inbox (Branches + Skill-Kandidaten). Pollt, damit laufende
|
||||||
|
// Annahme-Läufe (Status aus der JSON-Datei des Runners) live sichtbar werden.
|
||||||
// Ideen-Queue (natives Hermes-Kanban): das Backend cached ~15 s, öfter pollen lohnt nicht.
|
export const useAuftragsbuch = (refetchInterval: number = TAKT.schnell) =>
|
||||||
export const useIdeen = (refetchInterval: number = TAKT.gemuetlich) =>
|
useQuery({
|
||||||
useQuery({
|
queryKey: qk.auftragsbuch,
|
||||||
queryKey: qk.ideen,
|
queryFn: () => api<AuftragsbuchResp>("/api/auftragsbuch"),
|
||||||
queryFn: () => api<IdeenResp>("/api/ideen"),
|
refetchInterval: relax(refetchInterval),
|
||||||
refetchInterval,
|
})
|
||||||
})
|
|
||||||
|
// Ideen-Queue (natives Hermes-Kanban): das Backend cached ~15 s, öfter pollen lohnt nicht.
|
||||||
// Ideen-Log (Live Worker Log): pollt nur solange Idee triage/running UND offen
|
export const useIdeen = (refetchInterval: number = TAKT.gemuetlich) =>
|
||||||
export const useIdeenLog = (id: string, enabled: boolean = false) =>
|
useQuery({
|
||||||
useQuery({
|
queryKey: qk.ideen,
|
||||||
queryKey: ["ideen-log", id],
|
queryFn: () => api<IdeenResp>("/api/ideen"),
|
||||||
queryFn: () => api<IdeenLogResp>(`/api/ideen/${id}/log`),
|
refetchInterval: relax(refetchInterval),
|
||||||
enabled,
|
})
|
||||||
refetchInterval: 4000, // alle ~4s
|
|
||||||
staleTime: 0,
|
// Ideen-Log (Live Worker Log): pollt nur solange Idee triage/running UND offen
|
||||||
})
|
export const useIdeenLog = (id: string, enabled: boolean = false) =>
|
||||||
|
useQuery({
|
||||||
export const useChronik = (limit = 150, refetchInterval: number = TAKT.gemuetlich) =>
|
queryKey: ["ideen-log", id],
|
||||||
useQuery({
|
queryFn: () => api<IdeenLogResp>(`/api/ideen/${id}/log`),
|
||||||
queryKey: qk.chronik,
|
enabled,
|
||||||
queryFn: () => api<ChronikResp>(`/api/chronik?limit=${limit}`),
|
refetchInterval: 4000, // alle ~4s
|
||||||
refetchInterval,
|
staleTime: 0,
|
||||||
select: (d) => d.items ?? [],
|
})
|
||||||
})
|
|
||||||
|
export const useChronik = (limit = 150, refetchInterval: number = TAKT.gemuetlich) =>
|
||||||
export const useWissen = () =>
|
useQuery({
|
||||||
useQuery({ queryKey: qk.wissen, queryFn: () => api<WissenResp>("/api/wissen") })
|
queryKey: qk.chronik,
|
||||||
|
queryFn: () => api<ChronikResp>(`/api/chronik?limit=${limit}`),
|
||||||
export const useEigenleben = (refetchInterval: number = TAKT.traege) =>
|
refetchInterval: relax(refetchInterval),
|
||||||
useQuery({
|
select: (d) => d.items ?? [],
|
||||||
queryKey: qk.eigenleben,
|
})
|
||||||
queryFn: () => api<EigenlebenResp>("/api/eigenleben"),
|
|
||||||
refetchInterval,
|
export const useWissen = () =>
|
||||||
})
|
useQuery({ queryKey: qk.wissen, queryFn: () => api<WissenResp>("/api/wissen") })
|
||||||
|
|
||||||
export const useZeitmaschine = (refetchInterval: number = TAKT.traege) =>
|
export const useEigenleben = (refetchInterval: number = TAKT.traege) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: qk.zeitmaschine,
|
queryKey: qk.eigenleben,
|
||||||
queryFn: () => api<ZeitmaschineResp>("/api/zeitmaschine"),
|
queryFn: () => api<EigenlebenResp>("/api/eigenleben"),
|
||||||
refetchInterval,
|
refetchInterval,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const useReminders = (refetchInterval: number = TAKT.traege) =>
|
export const useZeitmaschine = (refetchInterval: number = TAKT.traege) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: qk.reminders,
|
queryKey: qk.zeitmaschine,
|
||||||
queryFn: () => api<{ items: Reminder[] }>("/api/reminders"),
|
queryFn: () => api<ZeitmaschineResp>("/api/zeitmaschine"),
|
||||||
refetchInterval,
|
refetchInterval,
|
||||||
select: (d) => d.items ?? [],
|
})
|
||||||
})
|
|
||||||
|
export const useReminders = (refetchInterval: number = TAKT.traege) =>
|
||||||
// Per-Turn-Latenz-Trace (letzte N Voice/Lucy-Turns mit Stufen-Breakdown).
|
useQuery({
|
||||||
export const useVoiceTrace = (limit = 12, refetchInterval: number = TAKT.schnell) =>
|
queryKey: qk.reminders,
|
||||||
useQuery({
|
queryFn: () => api<{ items: Reminder[] }>("/api/reminders"),
|
||||||
queryKey: qk.voiceTrace,
|
refetchInterval: relax(refetchInterval),
|
||||||
queryFn: () => api<VoiceTraceResp>(`/api/voice/trace?limit=${limit}`),
|
select: (d) => d.items ?? [],
|
||||||
refetchInterval,
|
})
|
||||||
select: (d) => d.turns ?? [],
|
|
||||||
})
|
// Per-Turn-Latenz-Trace (letzte N Voice/Lucy-Turns mit Stufen-Breakdown).
|
||||||
|
export const useVoiceTrace = (limit = 12, refetchInterval: number = TAKT.schnell) =>
|
||||||
export const useMemoryGraph = (enabled = true) =>
|
useQuery({
|
||||||
useQuery({
|
queryKey: qk.voiceTrace,
|
||||||
queryKey: qk.memoryGraph,
|
queryFn: () => api<VoiceTraceResp>(`/api/voice/trace?limit=${limit}`),
|
||||||
queryFn: () => api<MemoryGraph>("/api/memory/graph"),
|
refetchInterval,
|
||||||
enabled,
|
select: (d) => d.turns ?? [],
|
||||||
})
|
})
|
||||||
|
|
||||||
export const useHealth = () =>
|
export const useMemoryGraph = (enabled = true) =>
|
||||||
useQuery({ queryKey: qk.health, queryFn: () => api<Health>("/api/health"), refetchInterval: TAKT.normal })
|
useQuery({
|
||||||
|
queryKey: qk.memoryGraph,
|
||||||
export const useSystemStatus = (refetchInterval: number = TAKT.graph) =>
|
queryFn: () => api<MemoryGraph>("/api/memory/graph"),
|
||||||
useQuery({ queryKey: qk.systemStatus, queryFn: () => api<SystemStatus>("/api/system/status"), refetchInterval })
|
enabled,
|
||||||
|
})
|
||||||
export const useServices = (refetchInterval: number = TAKT.normal) =>
|
|
||||||
useQuery({ queryKey: qk.services, queryFn: () => api<ServicesResp>("/api/system/services"), refetchInterval })
|
export const useHealth = () =>
|
||||||
|
useQuery({ queryKey: qk.health, queryFn: () => api<Health>("/api/health"), refetchInterval: TAKT.normal })
|
||||||
export const useModels = (refetchInterval: number = TAKT.schnell) =>
|
|
||||||
useQuery({ queryKey: qk.models, queryFn: () => api<ModelsResp>("/api/models"), refetchInterval })
|
export const useSystemStatus = (refetchInterval: number = TAKT.graph) =>
|
||||||
|
useQuery({ queryKey: qk.systemStatus, queryFn: () => api<SystemStatus>("/api/system/status"), refetchInterval })
|
||||||
export const useGroups = (refetchInterval: number = TAKT.normal) =>
|
|
||||||
useQuery({ queryKey: qk.groups, queryFn: () => api<GroupsResp>("/api/groups"), refetchInterval })
|
export const useServices = (refetchInterval: number = TAKT.normal) =>
|
||||||
|
useQuery({ queryKey: qk.services, queryFn: () => api<ServicesResp>("/api/system/services"), refetchInterval })
|
||||||
export const useRouting = (refetchInterval: number = TAKT.normal) =>
|
|
||||||
useQuery({ queryKey: qk.routing, queryFn: () => api<RoutingResp>("/api/routing"), refetchInterval })
|
export const useModels = (refetchInterval: number = TAKT.schnell) =>
|
||||||
|
useQuery({ queryKey: qk.models, queryFn: () => api<ModelsResp>("/api/models"), refetchInterval })
|
||||||
export const useRoutingPolicy = () =>
|
|
||||||
useQuery({ queryKey: qk.routingPolicy, queryFn: () => api<RoutingPolicyMeta>("/api/routing/policy") })
|
export const useGroups = (refetchInterval: number = TAKT.normal) =>
|
||||||
|
useQuery({ queryKey: qk.groups, queryFn: () => api<GroupsResp>("/api/groups"), refetchInterval })
|
||||||
// Jobs (Downloads) takten etwas schneller, damit Fortschritts-Balken flüssig wirken.
|
|
||||||
export const useJobs = (refetchInterval = 3_000) =>
|
export const useRouting = (refetchInterval: number = TAKT.normal) =>
|
||||||
useQuery({
|
useQuery({ queryKey: qk.routing, queryFn: () => api<RoutingResp>("/api/routing"), refetchInterval })
|
||||||
queryKey: qk.jobs,
|
|
||||||
queryFn: () => api<{ jobs: Job[] }>("/api/jobs"),
|
export const useRoutingPolicy = () =>
|
||||||
refetchInterval,
|
useQuery({ queryKey: qk.routingPolicy, queryFn: () => api<RoutingPolicyMeta>("/api/routing/policy") })
|
||||||
select: (d) => d.jobs ?? [],
|
|
||||||
})
|
// Jobs (Downloads) takten etwas schneller, damit Fortschritts-Balken flüssig wirken.
|
||||||
|
export const useJobs = (refetchInterval = 3_000) =>
|
||||||
export const useTokenStats = (refetchInterval: number = TAKT.graph) =>
|
useQuery({
|
||||||
useQuery({ queryKey: qk.tokenStats, queryFn: () => api<TokenStats>("/api/system/token-stats"), refetchInterval })
|
queryKey: qk.jobs,
|
||||||
|
queryFn: () => api<{ jobs: Job[] }>("/api/jobs"),
|
||||||
export const useAgentStatus = (refetchInterval: number = TAKT.normal) =>
|
refetchInterval,
|
||||||
useQuery({ queryKey: qk.agentStatus, queryFn: () => api<AgentStatus>("/api/agent/status"), refetchInterval })
|
select: (d) => d.jobs ?? [],
|
||||||
|
})
|
||||||
// Agent-Hirn (Hermes) + bestes NousResearch-Update. Selten pollen (HF-Abfrage).
|
|
||||||
export const useHermesBrain = (refetchInterval: number = TAKT.traege) =>
|
export const useTokenStats = (refetchInterval: number = TAKT.graph) =>
|
||||||
useQuery({ queryKey: qk.hermesBrain, queryFn: () => api<HermesBrainResp>("/api/agent/brain"), refetchInterval })
|
useQuery({ queryKey: qk.tokenStats, queryFn: () => api<TokenStats>("/api/system/token-stats"), refetchInterval })
|
||||||
|
|
||||||
// Updates-Check ist teuer (apt/git auf der Box) — träge pollen reicht völlig.
|
export const useAgentStatus = (refetchInterval: number = TAKT.normal) =>
|
||||||
export const useUpdates = (refetchInterval: number = TAKT.traege) =>
|
useQuery({ queryKey: qk.agentStatus, queryFn: () => api<AgentStatus>("/api/agent/status"), refetchInterval })
|
||||||
useQuery({ queryKey: qk.updates, queryFn: () => api<UpdatesResp>("/api/maintenance/updates"), refetchInterval })
|
|
||||||
|
// Agent-Hirn (Hermes) + bestes NousResearch-Update. Selten pollen (HF-Abfrage).
|
||||||
export const useDiscover = () =>
|
export const useHermesBrain = (refetchInterval: number = TAKT.traege) =>
|
||||||
useQuery({ queryKey: qk.discover, queryFn: () => api<DiscoverResp>("/api/discover") })
|
useQuery({ queryKey: qk.hermesBrain, queryFn: () => api<HermesBrainResp>("/api/agent/brain"), refetchInterval })
|
||||||
|
|
||||||
// Verfügbare Spec-Draft-Modelle + Vocab-Kompatibilität zum Ziel-Modell (GGUF-Pfad).
|
// Updates-Check ist teuer (apt/git auf der Box) — träge pollen reicht völlig.
|
||||||
export const useDrafts = (target?: string) =>
|
export const useUpdates = (refetchInterval: number = TAKT.traege) =>
|
||||||
useQuery({
|
useQuery({ queryKey: qk.updates, queryFn: () => api<UpdatesResp>("/api/maintenance/updates"), refetchInterval })
|
||||||
queryKey: qk.drafts(target),
|
|
||||||
queryFn: () => api<DraftsResp>(`/api/models/drafts?target=${encodeURIComponent(target ?? "")}`),
|
export const useDiscover = () =>
|
||||||
enabled: !!target,
|
useQuery({ queryKey: qk.discover, queryFn: () => api<DiscoverResp>("/api/discover") })
|
||||||
})
|
|
||||||
|
// Verfügbare Spec-Draft-Modelle + Vocab-Kompatibilität zum Ziel-Modell (GGUF-Pfad).
|
||||||
export const useConnect = (params?: string) =>
|
export const useDrafts = (target?: string) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: qk.connect(params),
|
queryKey: qk.drafts(target),
|
||||||
queryFn: () => api<ConnectResp>(params ? `/api/connect?${params}` : "/api/connect"),
|
queryFn: () => api<DraftsResp>(`/api/models/drafts?target=${encodeURIComponent(target ?? "")}`),
|
||||||
})
|
enabled: !!target,
|
||||||
|
})
|
||||||
// Live-Status der zwei Leitungen (Gateway + Gedächtnis) — alle 15s aktualisiert.
|
|
||||||
export const useConnectHealth = () =>
|
export const useConnect = (params?: string) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: qk.connectHealth,
|
queryKey: qk.connect(params),
|
||||||
queryFn: () => api<ConnectHealth>("/api/connect/health"),
|
queryFn: () => api<ConnectResp>(params ? `/api/connect?${params}` : "/api/connect"),
|
||||||
refetchInterval: 15000,
|
})
|
||||||
})
|
|
||||||
|
// Live-Status der zwei Leitungen (Gateway + Gedächtnis) — alle 15s aktualisiert.
|
||||||
export const useMemory = (opts?: { q?: string; category?: string; limit?: number }) =>
|
export const useConnectHealth = () =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: qk.memory(opts?.q, opts?.category),
|
queryKey: qk.connectHealth,
|
||||||
queryFn: () => {
|
queryFn: () => api<ConnectHealth>("/api/connect/health"),
|
||||||
const p = new URLSearchParams()
|
refetchInterval: 15000,
|
||||||
if (opts?.q) p.set("q", opts.q)
|
})
|
||||||
if (opts?.category) p.set("category", opts.category)
|
|
||||||
return api<Memory[]>(`/api/memory?${p}`)
|
export const useMemory = (opts?: { q?: string; category?: string; limit?: number }) =>
|
||||||
},
|
useQuery({
|
||||||
select: (d) => (opts?.limit ? d.slice(0, opts.limit) : d),
|
queryKey: qk.memory(opts?.q, opts?.category),
|
||||||
})
|
queryFn: () => {
|
||||||
|
const p = new URLSearchParams()
|
||||||
/** Nach Mutationen die betroffenen Listen-Queries neu ziehen. */
|
if (opts?.q) p.set("q", opts.q)
|
||||||
export function invalidate(qc: QueryClient, ...keys: readonly unknown[][]) {
|
if (opts?.category) p.set("category", opts.category)
|
||||||
for (const key of keys) qc.invalidateQueries({ queryKey: key })
|
return api<Memory[]>(`/api/memory?${p}`)
|
||||||
}
|
},
|
||||||
|
select: (d) => (opts?.limit ? d.slice(0, opts.limit) : d),
|
||||||
export { useQueryClient }
|
})
|
||||||
|
|
||||||
|
/** Nach Mutationen die betroffenen Listen-Queries neu ziehen. */
|
||||||
|
export function invalidate(qc: QueryClient, ...keys: readonly unknown[][]) {
|
||||||
|
for (const key of keys) qc.invalidateQueries({ queryKey: key })
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useQueryClient }
|
||||||
|
|||||||
Reference in New Issue
Block a user