Zeitachse (24h-Historie), Verbinden-Autofill, Modelltausch abgesichert
Paket 1 - Metrik-Historie mit Zeitachse (User: 'WANN war Last?'): - services/metrics_history.py: 10s-Sampler (CPU/RAM/GPU/Disk + Token-Totale) in 24h-Ringpuffer (aelteres faellt automatisch raus, nichts waechst unbegrenzt), persistiert alle 5 min -> uebersteht Deploys; GET /api/system/history?minutes mit Downsampling auf ~300 Punkte; Lifespan-Task in app.py - Cockpit-Karten System-Status + Token-Durchsatz: Bereichs-Schalter Live/1h/24h, sichtbare Zeitachse (HH:MM:SS bei kurzen, HH:MM bei langen Fenstern), Tooltip zeigt Uhrzeit; Token-Raten in 1h/24h aus Totale-Deltas Paket 2 - Verbinden selbsterklaerend: - Box-IP vorbefuellt aus window.location.hostname (Dev-Fallback bleibt) - MCP-Scriptpfad-Feld aus der Kopfzeile in die 'Gedaechtnis anbinden'-Karte verschoben, mit Erklaerung WANN man es braucht Paket 3 - Modelltausch-Loecher gestopft (Review 16.07.): - install: Registrierung OHNE Alias-Umzug; Rolle wird erst NACH erfolgreichem Download uebernommen (on_done) - hermes dabei durch den warm-bewussten set_agent_brain-Flow statt rohem Alias-Move (Lucy waere sonst bis Download- Ende tot gewesen); Re-Install erhaelt bestehende Aliase - set_role_alias: lebenswichtige Aliase (hermes/embed) des Ziel-Modells ueberleben jeden Rollen-Klick (Qwen3.6 haelt live hermes+fast!) - Rollen-Endpoint verweigert Rollen-Wechsel am Halter geschuetzter Aliase mit klarer Anleitung; Werkbank sperrt die Rollen-Chips sichtbar Verifiziert: py_compile + Sampler-Smoke (2 Punkte, Persist ok) + Alias-Logik- Unittest (3 Faelle) + Browser (Zeitachse tickt, Schalter, Leerzustand, Verbinden). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+3
-1
@@ -43,7 +43,7 @@ from routers import (
|
||||
)
|
||||
from routers import reminders as reminders_router
|
||||
from services import memory as memory_svc
|
||||
from services import reminders, sentry, warmer
|
||||
from services import metrics_history, reminders, sentry, warmer
|
||||
|
||||
# Zentrales Logging — Level via MC_LOG_LEVEL (INFO default). Eine Konfiguration
|
||||
# für alle Module (logging.getLogger(__name__)).
|
||||
@@ -68,6 +68,8 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
if sentry.ENABLED:
|
||||
tasks.append(asyncio.create_task(sentry.sentry_loop()))
|
||||
tasks.append(asyncio.create_task(reminders.reminders_loop()))
|
||||
# 24-h-Metrik-Verlauf (Cockpit-Zeitachse): 10-s-Sampler, Ringpuffer, persistiert.
|
||||
tasks.append(asyncio.create_task(metrics_history.sampler_loop()))
|
||||
if memory_svc.AUTO_DEDUPE_ENABLED:
|
||||
tasks.append(asyncio.create_task(memory_svc.auto_dedupe_loop()))
|
||||
log.info(
|
||||
|
||||
@@ -114,13 +114,30 @@ def install(req: InstallReq) -> dict:
|
||||
# SETUP-BEWUSST: größter ctx, der neben Hirn/warmem Set passt (nicht nur Modell allein).
|
||||
ctx = budget.setup_aware_ctx(budget.params_b_for(repo), req.quant, role=req.role)["ctx"]
|
||||
|
||||
# Sofort registrieren (Datei kommt gleich) — robust gegen -watch-config.
|
||||
# Sofort registrieren (robust gegen -watch-config) — aber OHNE den Rollen-Alias
|
||||
# umzuhängen: der zeigte sonst minutenlang auf eine noch ladende Datei (Lane kalt;
|
||||
# bei role=hermes wäre Lucy bis Download-Ende tot gewesen — Review 16.07.).
|
||||
try:
|
||||
model_id = llamaswap.register_model(
|
||||
model_path, role=req.role, ctx=ctx, mmproj_path=mmproj_path, jinja=req.jinja)
|
||||
model_path, role=req.role, ctx=ctx, mmproj_path=mmproj_path, jinja=req.jinja,
|
||||
set_alias=False)
|
||||
except PermissionError as exc:
|
||||
raise HTTPException(500, str(exc))
|
||||
|
||||
# Rolle erst NACH erfolgreichem Download übernehmen. hermes geht dabei durch den
|
||||
# warm-bewussten Hirn-Flow (brains-Gruppe, ttl 0, Hermes-Config, Gateway-Restart) —
|
||||
# der rohe Alias-Move hatte diesen Flow bisher umgangen.
|
||||
role = (req.role or "").strip().lower()
|
||||
|
||||
def _apply_role() -> None:
|
||||
if role == "hermes":
|
||||
from services.agent import set_agent_brain
|
||||
res = set_agent_brain(model_id)
|
||||
if not res.get("ok"):
|
||||
raise RuntimeError(res.get("reason", "Hirn-Wechsel fehlgeschlagen"))
|
||||
else:
|
||||
llamaswap.set_role(model_id, role)
|
||||
|
||||
# Download-Job: alle GGUF-Teile (+ mmproj) per --include holen.
|
||||
args = [hf.hf_bin(), "download", repo]
|
||||
for f in info["files"]:
|
||||
@@ -131,7 +148,8 @@ def install(req: InstallReq) -> dict:
|
||||
env = dict(HF_DOWNLOAD_ENV)
|
||||
if req.hf_token:
|
||||
env["HF_TOKEN"] = req.hf_token
|
||||
job_id = jobengine.start_job(args, f"download {req.repo}", env=env)
|
||||
job_id = jobengine.start_job(args, f"download {req.repo}", env=env,
|
||||
on_done=_apply_role if role else None)
|
||||
jobengine.attach_download_progress(job_id, str(target), info["total_bytes"])
|
||||
return {"ok": True, "job_id": job_id, "model_id": model_id, "model_path": model_path,
|
||||
"total_bytes": info["total_bytes"], "files": len(info["files"])}
|
||||
@@ -161,9 +179,18 @@ def recommend_role(role: str) -> dict:
|
||||
|
||||
@router.post("/models/{model_id}/role")
|
||||
def set_model_role(model_id: str, body: RoleReq) -> dict:
|
||||
new_role = (body.role or "").strip().lower()
|
||||
# Guard: Hält das Modell einen LEBENSWICHTIGEN Alias (hermes/embed), darf die Rolle
|
||||
# hier nicht weggeklickt werden — sonst verliert Lucy Hirn/Gedächtnis mit einem Klick.
|
||||
# Weg: die geschützte Rolle zuerst einem ANDEREN Modell zuweisen (Alias zieht um).
|
||||
prot = llamaswap.protected_alias_of(model_id)
|
||||
if prot and new_role != prot:
|
||||
raise HTTPException(400,
|
||||
f"Dieses Modell hält die lebenswichtige Rolle '{prot}'. Weise '{prot}' zuerst "
|
||||
f"einem anderen Modell zu — danach lässt sich die Rolle hier ändern.")
|
||||
# Das Agent-Hirn (Rolle 'hermes') braucht den warm-bewussten Flow (Alias + brains-Gruppe +
|
||||
# ttl 0 + Hermes config.default + Gateway-Restart) — Single Source of Truth UI ↔ Hermes.
|
||||
if (body.role or "").strip().lower() == "hermes":
|
||||
if new_role == "hermes":
|
||||
from services.agent import set_agent_brain
|
||||
res = set_agent_brain(model_id)
|
||||
if not res.get("ok"):
|
||||
|
||||
@@ -37,6 +37,13 @@ def status() -> dict:
|
||||
return system_status()
|
||||
|
||||
|
||||
@router.get("/system/history")
|
||||
def history(minutes: int = 60) -> dict:
|
||||
"""Metrik-Verlauf (max. 24 h) für die Cockpit-Zeitachse — s. services/metrics_history."""
|
||||
from services import metrics_history
|
||||
return metrics_history.history(minutes)
|
||||
|
||||
|
||||
def _mem0_reachable() -> bool:
|
||||
try:
|
||||
return httpx.get(f"{MEM0_SERVICE_URL}/health", timeout=2).status_code == 200
|
||||
|
||||
@@ -153,9 +153,26 @@ def model_id_from_path(model_path: str) -> str:
|
||||
return name or "modell"
|
||||
|
||||
|
||||
# Lebenswichtige Aliase: hängen direkt an Lucys Denk- und Gedächtnis-Pfad. Sie dürfen
|
||||
# beim Rollen-Umhängen NIE stillschweigend verloren gehen (Review 16.07.: Qwen3.6 hält
|
||||
# live `hermes` UND `fast` — ein Rollen-Klick hätte beide gelöscht → Lucy tot).
|
||||
PROTECTED_ALIASES = {"hermes", "embed"}
|
||||
|
||||
|
||||
def protected_alias_of(model_id: str) -> str | None:
|
||||
"""Hält dieses Modell gerade einen lebenswichtigen Alias? (für Guards in der API)"""
|
||||
spec = (read_config().get("models") or {}).get(model_id)
|
||||
if isinstance(spec, dict):
|
||||
for a in spec.get("aliases") or []:
|
||||
if str(a).lower() in PROTECTED_ALIASES:
|
||||
return str(a).lower()
|
||||
return None
|
||||
|
||||
|
||||
def set_role_alias(cfg: dict, model_id: str, role: str | None) -> None:
|
||||
"""Rolle als eindeutigen llama-swap-`aliases`-Eintrag setzen (vorher bei allen
|
||||
anderen Modellen entfernen). role=None/leer entfernt den Alias."""
|
||||
"""Rolle als llama-swap-`aliases`-Eintrag setzen (vorher bei allen anderen Modellen
|
||||
entfernen — deren übrige Aliase bleiben). role=None/leer entfernt die Rolle.
|
||||
Lebenswichtige Aliase (PROTECTED_ALIASES) des Ziel-Modells bleiben IMMER erhalten."""
|
||||
models = cfg.get("models") or {}
|
||||
role = (role or "").strip().lower()
|
||||
if role:
|
||||
@@ -169,8 +186,11 @@ def set_role_alias(cfg: dict, model_id: str, role: str | None) -> None:
|
||||
spec.pop("aliases", None)
|
||||
spec = models.get(model_id)
|
||||
if isinstance(spec, dict):
|
||||
if role and role != model_id.lower():
|
||||
spec["aliases"] = [role]
|
||||
keep = [a for a in (spec.get("aliases") or [])
|
||||
if str(a).lower() in PROTECTED_ALIASES and str(a).lower() != role]
|
||||
new = keep + ([role] if role and role != model_id.lower() else [])
|
||||
if new:
|
||||
spec["aliases"] = new
|
||||
else:
|
||||
spec.pop("aliases", None)
|
||||
|
||||
@@ -214,9 +234,12 @@ def write_config(cfg: dict) -> None:
|
||||
|
||||
def register_model(model_path: str, role: str | None = None, ctx: int = 8192,
|
||||
ttl: int | None = None, mmproj_path: str | None = None,
|
||||
jinja: bool = False) -> str:
|
||||
jinja: bool = False, set_alias: bool = True) -> str:
|
||||
"""Ein GGUF als llama-swap-Modell eintragen (cmd + Rolle-Alias). Gibt die
|
||||
Modell-ID zurück. jinja=True erzwingt --jinja (Tool-Calling, z.B. fürs Agent-Hirn)."""
|
||||
Modell-ID zurück. jinja=True erzwingt --jinja (Tool-Calling, z.B. fürs Agent-Hirn).
|
||||
set_alias=False: Rolle nur für die cmd-Flags nutzen, den Alias aber NICHT umhängen —
|
||||
der Install-Flow setzt ihn erst NACH fertigem Download (sonst zeigt die Rolle
|
||||
minutenlang auf eine Datei, die noch gar nicht existiert)."""
|
||||
cfg = read_config()
|
||||
model_id = model_id_from_path(model_path)
|
||||
cmd = CMD_TEMPLATE.replace("{model}", model_path).replace("{ctx}", str(ctx))
|
||||
@@ -242,10 +265,18 @@ def register_model(model_path: str, role: str | None = None, ctx: int = 8192,
|
||||
if "--spec-draft-model" not in cmd and "--model-draft" not in cmd:
|
||||
cmd += spec_draft_flags(model_path)
|
||||
|
||||
cfg.setdefault("models", {})[model_id] = {
|
||||
# Bestehende Aliase des Eintrags erhalten (Re-Install/Upgrade desselben Repos):
|
||||
# die Rolle soll während des Downloads beim ALTEN Stand bleiben.
|
||||
old_aliases = (cfg.get("models") or {}).get(model_id, {})
|
||||
old_aliases = old_aliases.get("aliases") if isinstance(old_aliases, dict) else None
|
||||
entry: dict = {
|
||||
"cmd": LiteralScalarString(cmd + "\n"),
|
||||
"ttl": ttl if ttl is not None else DEFAULT_TTL,
|
||||
}
|
||||
if old_aliases:
|
||||
entry["aliases"] = old_aliases
|
||||
cfg.setdefault("models", {})[model_id] = entry
|
||||
if set_alias:
|
||||
set_role_alias(cfg, model_id, role)
|
||||
write_config(cfg)
|
||||
return model_id
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
"""24-h-Metrik-Verlauf für die Cockpit-Zeitachse (User-Wunsch 16.07.: „WANN war Last?").
|
||||
|
||||
Ein leichter Sammler (Lifespan-Task in app.py) legt alle SAMPLE_S Sekunden einen
|
||||
kompakten Punkt in einen Ringpuffer: CPU/RAM/GPU/Disk in Prozent + die Token-
|
||||
GESAMTZÄHLER (das Frontend rechnet Raten aus den Deltas). Der Puffer hält exakt
|
||||
24 h — Älteres fällt automatisch raus (deque maxlen), nichts wächst unbegrenzt.
|
||||
Periodisch wird auf Platte persistiert (übersteht MC2-Restarts/Deploys); beim
|
||||
Laden fliegt alles raus, was älter als 24 h ist.
|
||||
|
||||
Bewusst NICHT im Steward: die Historie ist reine UI-Ware, und ein paar Sekunden
|
||||
Lücke pro Deploy sind egal.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from collections import deque
|
||||
|
||||
from config import MODELS_DIR
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
SAMPLE_S = 10 # Abtast-Takt
|
||||
RETENTION_S = 24 * 3600 # 24 h — danach löschen und neu bauen (Ringpuffer)
|
||||
MAXLEN = RETENTION_S // SAMPLE_S # 8640 Punkte
|
||||
FLUSH_S = 300 # höchstens alle 5 min auf Platte
|
||||
MAX_RETURN_POINTS = 300 # Endpoint downsampled auf ~diese Punktzahl
|
||||
|
||||
HISTORY_PATH = MODELS_DIR / "mc2-metrics-history.json"
|
||||
|
||||
# Punkt = [t, cpu, ram, gpu, disk, tp, tc] (t = Epoch-Sekunden; tp/tc = Token-Totale)
|
||||
_points: deque = deque(maxlen=MAXLEN)
|
||||
_loaded = False
|
||||
_last_flush = 0.0
|
||||
|
||||
|
||||
def _load() -> None:
|
||||
global _loaded
|
||||
if _loaded:
|
||||
return
|
||||
_loaded = True
|
||||
try:
|
||||
raw = json.loads(HISTORY_PATH.read_text(encoding="utf-8"))
|
||||
cutoff = time.time() - RETENTION_S
|
||||
for p in raw if isinstance(raw, list) else []:
|
||||
if isinstance(p, list) and len(p) == 7 and isinstance(p[0], (int, float)) and p[0] >= cutoff:
|
||||
_points.append(p)
|
||||
if _points:
|
||||
log.info("metrics_history: %d Punkte aus %s geladen", len(_points), HISTORY_PATH)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except Exception: # noqa: BLE001 — kaputte Datei = leer starten, nie crashen
|
||||
log.warning("metrics_history: %s nicht lesbar — starte leer", HISTORY_PATH, exc_info=True)
|
||||
|
||||
|
||||
def _flush() -> None:
|
||||
global _last_flush
|
||||
_last_flush = time.time()
|
||||
try:
|
||||
tmp = HISTORY_PATH.with_suffix(".tmp")
|
||||
tmp.write_text(json.dumps(list(_points), separators=(",", ":")), encoding="utf-8")
|
||||
tmp.replace(HISTORY_PATH)
|
||||
except OSError:
|
||||
log.warning("metrics_history: %s nicht schreibbar", HISTORY_PATH, exc_info=True)
|
||||
|
||||
|
||||
def _sample() -> None:
|
||||
import psutil
|
||||
|
||||
from services.system import _gpu_sysfs
|
||||
from services.token_stats import get_stats
|
||||
|
||||
vm = psutil.virtual_memory()
|
||||
disk = None
|
||||
try:
|
||||
du = psutil.disk_usage(str(MODELS_DIR) if MODELS_DIR.exists() else os.getcwd())
|
||||
disk = du.percent
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
gpu = None
|
||||
try:
|
||||
g = _gpu_sysfs()
|
||||
gpu = g.get("busy_percent") if g else None
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
tp = tc = None
|
||||
try:
|
||||
ts = get_stats()
|
||||
tp, tc = ts.get("prompt_tokens"), ts.get("completion_tokens")
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
# interval=None: nicht-blockierende CPU-Messung seit dem letzten Aufruf (10 s her — ideal).
|
||||
_points.append([int(time.time()), psutil.cpu_percent(interval=None), vm.percent, gpu, disk, tp, tc])
|
||||
|
||||
|
||||
async def sampler_loop() -> None:
|
||||
"""Dauer-Sammler fürs App-Lifespan. Fehler eines Ticks reißen die Schleife nie."""
|
||||
_load()
|
||||
while True:
|
||||
try:
|
||||
await asyncio.to_thread(_sample)
|
||||
except Exception: # noqa: BLE001
|
||||
log.debug("metrics_history: Sample fehlgeschlagen", exc_info=True)
|
||||
if time.time() - _last_flush >= FLUSH_S:
|
||||
try:
|
||||
await asyncio.to_thread(_flush)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
await asyncio.sleep(SAMPLE_S)
|
||||
|
||||
|
||||
def history(minutes: int = 60) -> dict:
|
||||
"""Punkte der letzten N Minuten, auf ≤ MAX_RETURN_POINTS ausgedünnt (Stride)."""
|
||||
_load()
|
||||
minutes = max(1, min(int(minutes), RETENTION_S // 60))
|
||||
cutoff = time.time() - minutes * 60
|
||||
pts = [p for p in _points if p[0] >= cutoff]
|
||||
stride = max(1, len(pts) // MAX_RETURN_POINTS)
|
||||
pts = pts[::stride]
|
||||
return {
|
||||
"sample_s": SAMPLE_S * stride,
|
||||
"points": [
|
||||
{"t": p[0], "cpu": p[1], "ram": p[2], "gpu": p[3], "disk": p[4], "tp": p[5], "tc": p[6]}
|
||||
for p in pts
|
||||
],
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as A,Y as P,l as B,b as M,u as D,r as z,j as e,Z as T,e as a,$ as g,a0 as b,a1 as W,x as f,a2 as j,W as G,X as I,C as N,g as y,q as i}from"./index-CQgUnd1p.js";/**
|
||||
import{c as A,Y as P,l as B,b as M,u as D,r as z,j as e,Z as T,e as a,$ as g,a0 as b,a1 as W,x as f,a2 as j,W as G,X as I,C as N,g as y,q as i}from"./index-BJUCwF6N.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as I,aa as Y,ab as _,u as q,b as Q,r as u,j as e,a3 as ee,e as C,L as N,C as U,U as J,ac as te,V as O,X as z,D as M,M as R,ad as re,T as K,g as E,ae as se,q as Z}from"./index-CQgUnd1p.js";import{L as ae}from"./lightbulb-Btbex65P.js";import{S as G}from"./send-COuI40dq.js";/**
|
||||
import{c as I,aa as Y,ab as _,u as q,b as Q,r as u,j as e,a3 as ee,e as C,L as N,C as U,U as J,ac as te,V as O,X as z,D as M,M as R,ad as re,T as K,g as E,ae as se,q as Z}from"./index-BJUCwF6N.js";import{L as ae}from"./lightbulb-Bl7otbNF.js";import{S as G}from"./send-CLnv-s74.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as m,af as S,r as c,j as e,ag as z,L as g,U as M,ah as C,ae as D,ai as A,aj as B,ak as Z,e as E,al as L,u as R,b as q,g as k,q as T}from"./index-CQgUnd1p.js";import{R as j}from"./rotate-ccw-ChVZWN8g.js";/**
|
||||
import{c as m,af as S,r as c,j as e,ag as z,L as g,U as M,ah as C,ae as D,ai as A,aj as B,ak as Z,e as E,al as L,u as R,b as q,g as k,q as T}from"./index-BJUCwF6N.js";import{R as j}from"./rotate-ccw-sjUuOvXz.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
-16
File diff suppressed because one or more lines are too long
+16
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as h,am as b,r as i,j as e,L as g,an as f,a2 as j,U as k,e as o,D as N,M as v,N as w,g as y}from"./index-CQgUnd1p.js";import{B as S}from"./book-open-BFFOe9re.js";import{C as z}from"./circle-x-DKuGYlQP.js";/**
|
||||
import{c as h,am as b,r as i,j as e,L as g,an as f,a2 as j,U as k,e as o,D as N,M as v,N as w,g as y}from"./index-BJUCwF6N.js";import{B as S}from"./book-open-DSKbx2re.js";import{C as z}from"./circle-x-CXUY3WgN.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as p,j as e,r as M,x as C,a6 as z,a7 as k,a8 as y,a0 as v,W as L,a9 as G,a2 as T,e as f,U as D}from"./index-CQgUnd1p.js";import{B as w}from"./book-open-BFFOe9re.js";import{a as N,b as B,L as P,C as E,Z as R}from"./zap-Df-ORHkh.js";import{L as I}from"./lightbulb-Btbex65P.js";/**
|
||||
import{c as p,j as e,r as M,x as C,a6 as z,a7 as k,a8 as y,a0 as v,W as L,a9 as G,a2 as T,e as f,U as D}from"./index-BJUCwF6N.js";import{B as w}from"./book-open-DSKbx2re.js";import{a as N,b as B,L as P,C as E,Z as R}from"./zap-C9k94tAM.js";import{L as I}from"./lightbulb-Bl7otbNF.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
import{Y as b,u as f,$ as h,r as d,j as e,e as c,Z as g,T as p,a3 as j,a4 as N,g as v,a5 as w,q as x}from"./index-CQgUnd1p.js";function y(){const{data:s}=b(),m=f(),r=s!=null&&s.box_console_url?h(s.box_console_url):void 0,a=s==null?void 0:s.box_console_reachable,[l,o]=d.useState(!1),[i,n]=d.useState("");async function u(){o(!0),n("");try{const t=await v("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})});n(t.ok?"Neu gestartet — einen Moment, dann lädt das Terminal.":`Fehlgeschlagen: ${t.err||"Unbekannter Fehler"}`),w(m,x.agentStatus,x.services)}catch(t){n(`Fehlgeschlagen: ${(t==null?void 0:t.message)||t}`)}finally{o(!1)}}return e.jsxs("div",{className:"flex h-full flex-col gap-4",children:[e.jsxs("div",{className:"flex flex-wrap items-end justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Konsole"}),e.jsx("p",{className:"flex items-center gap-2 text-sm text-muted-foreground",children:"Direkte Shell auf der Box — wie ein SSH-Fenster, mitten im Browser."})]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground",children:[e.jsx("span",{className:c("h-2 w-2 rounded-full",a?"bg-emerald-500 animate-pulse":"bg-amber-500")}),a?"online":"offline"]}),r&&e.jsxs("a",{href:r,target:"_blank",rel:"noopener",className:"flex h-8 items-center gap-1.5 rounded-lg border border-border/60 bg-background/20 px-3 text-xs font-semibold text-muted-foreground transition-all hover:border-primary/50 hover:text-foreground",children:[e.jsx(g,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),r?e.jsxs("div",{className:"relative min-h-[58vh] flex-1 overflow-hidden rounded-2xl border border-border/60 bg-black/50 shadow-lg shadow-black/25",children:[a===!1&&e.jsxs("div",{className:"absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/70 text-center",children:[e.jsx(p,{className:"h-8 w-8 text-amber-400"}),e.jsx("div",{className:"text-sm font-semibold text-amber-300",children:"Konsole nicht erreichbar"}),e.jsxs("p",{className:"max-w-sm text-[11px] leading-normal text-muted-foreground",children:["Der Terminal-Dienst (",e.jsx("code",{className:"font-mono text-primary",children:"box-console"}),") läuft gerade nicht."]}),e.jsxs("button",{onClick:u,disabled:l,className:"flex h-9 items-center gap-1.5 rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 text-[11px] font-bold uppercase tracking-wide text-amber-300 transition-all hover:bg-amber-500/20 cursor-pointer disabled:opacity-50",children:[e.jsx(j,{className:c("h-3.5 w-3.5",l&&"animate-spin")})," Dienst neu starten"]}),i&&e.jsx("p",{className:"max-w-sm text-[11px] text-muted-foreground",children:i})]}),e.jsx("iframe",{src:r,title:"Box-Konsole",className:"h-full w-full border-0",style:{minHeight:"58vh"}})]}):e.jsxs("div",{className:"flex min-h-[58vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground",children:[e.jsx(N,{className:"mr-2 h-4 w-4"})," Lade Konsole…"]})]})}export{y as KonsoleView};
|
||||
import{Y as b,u as f,$ as h,r as d,j as e,e as c,Z as g,T as p,a3 as j,a4 as N,g as v,a5 as w,q as x}from"./index-BJUCwF6N.js";function y(){const{data:s}=b(),m=f(),r=s!=null&&s.box_console_url?h(s.box_console_url):void 0,a=s==null?void 0:s.box_console_reachable,[l,o]=d.useState(!1),[i,n]=d.useState("");async function u(){o(!0),n("");try{const t=await v("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})});n(t.ok?"Neu gestartet — einen Moment, dann lädt das Terminal.":`Fehlgeschlagen: ${t.err||"Unbekannter Fehler"}`),w(m,x.agentStatus,x.services)}catch(t){n(`Fehlgeschlagen: ${(t==null?void 0:t.message)||t}`)}finally{o(!1)}}return e.jsxs("div",{className:"flex h-full flex-col gap-4",children:[e.jsxs("div",{className:"flex flex-wrap items-end justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Konsole"}),e.jsx("p",{className:"flex items-center gap-2 text-sm text-muted-foreground",children:"Direkte Shell auf der Box — wie ein SSH-Fenster, mitten im Browser."})]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground",children:[e.jsx("span",{className:c("h-2 w-2 rounded-full",a?"bg-emerald-500 animate-pulse":"bg-amber-500")}),a?"online":"offline"]}),r&&e.jsxs("a",{href:r,target:"_blank",rel:"noopener",className:"flex h-8 items-center gap-1.5 rounded-lg border border-border/60 bg-background/20 px-3 text-xs font-semibold text-muted-foreground transition-all hover:border-primary/50 hover:text-foreground",children:[e.jsx(g,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),r?e.jsxs("div",{className:"relative min-h-[58vh] flex-1 overflow-hidden rounded-2xl border border-border/60 bg-black/50 shadow-lg shadow-black/25",children:[a===!1&&e.jsxs("div",{className:"absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/70 text-center",children:[e.jsx(p,{className:"h-8 w-8 text-amber-400"}),e.jsx("div",{className:"text-sm font-semibold text-amber-300",children:"Konsole nicht erreichbar"}),e.jsxs("p",{className:"max-w-sm text-[11px] leading-normal text-muted-foreground",children:["Der Terminal-Dienst (",e.jsx("code",{className:"font-mono text-primary",children:"box-console"}),") läuft gerade nicht."]}),e.jsxs("button",{onClick:u,disabled:l,className:"flex h-9 items-center gap-1.5 rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 text-[11px] font-bold uppercase tracking-wide text-amber-300 transition-all hover:bg-amber-500/20 cursor-pointer disabled:opacity-50",children:[e.jsx(j,{className:c("h-3.5 w-3.5",l&&"animate-spin")})," Dienst neu starten"]}),i&&e.jsx("p",{className:"max-w-sm text-[11px] text-muted-foreground",children:i})]}),e.jsx("iframe",{src:r,title:"Box-Konsole",className:"h-full w-full border-0",style:{minHeight:"58vh"}})]}):e.jsxs("div",{className:"flex min-h-[58vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground",children:[e.jsx(N,{className:"mr-2 h-4 w-4"})," Lade Konsole…"]})]})}export{y as KonsoleView};
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/GraphView-BOVrShvN.js","assets/index-CQgUnd1p.js","assets/index-BH760w1h.css"])))=>i.map(i=>d[i]);
|
||||
var je=Object.defineProperty;var ke=(d,i,c)=>i in d?je(d,i,{enumerable:!0,configurable:!0,writable:!0,value:c}):d[i]=c;var J=(d,i,c)=>ke(d,typeof i!="symbol"?i+"":i,c);import{c as h,r as s,j as e,u as ve,b as Ne,O as W,Q as Se,C as X,R as Ce,S as Ee,P as Me,U as O,e as x,V as ze,W as De,X as Z,A as Y,_ as Oe,g as j}from"./index-CQgUnd1p.js";import{C as ee}from"./copy-hJYMa3at.js";import{S as _e}from"./send-COuI40dq.js";import{B as Ge}from"./book-open-BFFOe9re.js";/**
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/GraphView-BQzGpC9Q.js","assets/index-BJUCwF6N.js","assets/index-DEL5L7Ku.css"])))=>i.map(i=>d[i]);
|
||||
var je=Object.defineProperty;var ke=(d,i,c)=>i in d?je(d,i,{enumerable:!0,configurable:!0,writable:!0,value:c}):d[i]=c;var J=(d,i,c)=>ke(d,typeof i!="symbol"?i+"":i,c);import{c as h,r as s,j as e,u as ve,b as Ne,O as W,Q as Se,C as X,R as Ce,S as Ee,P as Me,U as O,e as x,V as ze,W as De,X as Z,A as Y,_ as Oe,g as j}from"./index-BJUCwF6N.js";import{C as ee}from"./copy-s7zFTglG.js";import{S as _e}from"./send-CLnv-s74.js";import{B as Ge}from"./book-open-DSKbx2re.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -34,7 +34,7 @@ var je=Object.defineProperty;var ke=(d,i,c)=>i in d?je(d,i,{enumerable:!0,config
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const Pe=h("User",[["path",{d:"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2",key:"975kel"}],["circle",{cx:"12",cy:"7",r:"4",key:"17ys0d"}]]);class te extends s.Component{constructor(){super(...arguments);J(this,"state",{error:null})}static getDerivedStateFromError(c){return{error:c}}render(){return this.state.error?e.jsxs("div",{className:"h-[480px] rounded-2xl border border-red-500/20 bg-red-500/5 p-6 text-xs text-red-400 overflow-auto",children:[e.jsx("div",{className:"font-semibold mb-2",children:"Graph konnte nicht gerendert werden"}),e.jsx("pre",{className:"whitespace-pre-wrap break-words text-[11px] leading-relaxed",children:this.state.error.message})]}):this.props.children}}const se=s.lazy(()=>Oe(()=>import("./GraphView-BOVrShvN.js"),__vite__mapDeps([0,1,2])).then(d=>({default:d.GraphView}))),k=["identity","knowledge","rules","events"],v=new Set(["auto","agent","hermes"]),N={identity:{label:"Identität",icon:Pe,bg:"bg-cyan-500/10",text:"text-cyan-400"},knowledge:{label:"Wissen",icon:De,bg:"bg-indigo-500/10",text:"text-indigo-400"},rules:{label:"Regeln",icon:Le,bg:"bg-violet-500/10",text:"text-violet-400"},events:{label:"Ereignisse",icon:ze,bg:"bg-amber-500/10",text:"text-amber-400"}},ne={label:"Gedächtnis",icon:Ge,text:"text-muted-foreground"},Re={identity:"border-l-cyan-500/80",knowledge:"border-l-indigo-500/80",rules:"border-l-violet-500/80",events:"border-l-amber-500/80"},_={identity:"#00f5ff",knowledge:"#3b82f6",rules:"#d946ef",events:"#fbbf24"},ae=`Hi Hermes! Lass uns ein kurzes Onboarding machen, damit du dich künftig an mich erinnerst. Stell mir nacheinander ein paar kurze Fragen zu:
|
||||
*/const Pe=h("User",[["path",{d:"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2",key:"975kel"}],["circle",{cx:"12",cy:"7",r:"4",key:"17ys0d"}]]);class te extends s.Component{constructor(){super(...arguments);J(this,"state",{error:null})}static getDerivedStateFromError(c){return{error:c}}render(){return this.state.error?e.jsxs("div",{className:"h-[480px] rounded-2xl border border-red-500/20 bg-red-500/5 p-6 text-xs text-red-400 overflow-auto",children:[e.jsx("div",{className:"font-semibold mb-2",children:"Graph konnte nicht gerendert werden"}),e.jsx("pre",{className:"whitespace-pre-wrap break-words text-[11px] leading-relaxed",children:this.state.error.message})]}):this.props.children}}const se=s.lazy(()=>Oe(()=>import("./GraphView-BQzGpC9Q.js"),__vite__mapDeps([0,1,2])).then(d=>({default:d.GraphView}))),k=["identity","knowledge","rules","events"],v=new Set(["auto","agent","hermes"]),N={identity:{label:"Identität",icon:Pe,bg:"bg-cyan-500/10",text:"text-cyan-400"},knowledge:{label:"Wissen",icon:De,bg:"bg-indigo-500/10",text:"text-indigo-400"},rules:{label:"Regeln",icon:Le,bg:"bg-violet-500/10",text:"text-violet-400"},events:{label:"Ereignisse",icon:ze,bg:"bg-amber-500/10",text:"text-amber-400"}},ne={label:"Gedächtnis",icon:Ge,text:"text-muted-foreground"},Re={identity:"border-l-cyan-500/80",knowledge:"border-l-indigo-500/80",rules:"border-l-violet-500/80",events:"border-l-amber-500/80"},_={identity:"#00f5ff",knowledge:"#3b82f6",rules:"#d946ef",events:"#fbbf24"},ae=`Hi Hermes! Lass uns ein kurzes Onboarding machen, damit du dich künftig an mich erinnerst. Stell mir nacheinander ein paar kurze Fragen zu:
|
||||
1) wer ich bin und woran ich gerade arbeite,
|
||||
2) wie ich angesprochen werden möchte,
|
||||
3) meine bevorzugten Tools, Sprachen und Arbeitsweise,
|
||||
+3
-3
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{ao as y,r as x,g as L,j as e,L as v,S,ap as W,U as C,e as w,aq as E}from"./index-CQgUnd1p.js";import{F as M}from"./folder-open-vcdogQ3l.js";const D={"":"Allgemein",traeume:"Träume",muster:"Muster","skill-kandidaten":"Skill-Kandidaten","skill-kandidaten/beauftragt":"Skill-Kandidaten · beauftragt","skill-kandidaten/verworfen":"Skill-Kandidaten · verworfen"};function T(){const{data:o,isLoading:h}=y(),[l,f]=x.useState(null),[d,r]=x.useState(null),[u,i]=x.useState(!1),[a,p]=x.useState(""),t=x.useMemo(()=>(o==null?void 0:o.files)??[],[o]),m=x.useMemo(()=>{const s=new Map;for(const n of t)s.set(n.name.toLowerCase(),n.path);return s},[t]),g=x.useMemo(()=>{if(!a.trim())return t;const s=a.toLowerCase();return t.filter(n=>n.title.toLowerCase().includes(s)||n.path.toLowerCase().includes(s))},[t,a]),b=x.useMemo(()=>{const s=[];for(const n of g){const c=s.find(k=>k.dir===n.dir);c?c.files.push(n):s.push({dir:n.dir,files:[n]})}return s.sort((n,c)=>n.dir===""?-1:c.dir===""?1:n.dir.localeCompare(c.dir))},[g]);x.useEffect(()=>{if(!l&&t.length){const s=t.find(n=>n.path.toLowerCase()==="index.md");f((s==null?void 0:s.path)??t[0].path)}},[t,l]),x.useEffect(()=>{l&&(i(!0),L(`/api/wissen/datei?pfad=${encodeURIComponent(l)}`).then(r).catch(()=>r({path:l,content:"*(Notiz nicht ladbar)*",mtime:0})).finally(()=>i(!1)))},[l]);const j=s=>{const n=m.get(s.toLowerCase());n&&f(n)};return e.jsxs("div",{className:"space-y-5",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Wissen"}),e.jsx("p",{className:"text-sm text-muted-foreground",children:"Was sich die Box nachts erschließt — Traum-Notizen, Muster und Skill-Ideen aus dem Wissens-Vault, verlinkt wie ein Wiki."})]}),o&&!o.available&&e.jsx("div",{className:"rounded-2xl border border-amber-500/30 bg-amber-500/5 p-4 text-xs text-amber-300",children:"Der Wissens-Vault liegt auf der Box (~/wissens-vault) — im lokalen Dev-Modus nicht verfügbar."}),h&&e.jsxs("div",{className:"flex items-center gap-2 rounded-2xl border border-border/60 bg-card/30 p-6 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Vault wird geladen …"]}),(o==null?void 0:o.available)&&t.length===0&&e.jsx("div",{className:"rounded-2xl border border-dashed border-border/60 bg-card/20 p-8 text-center text-xs text-muted-foreground",children:"Noch keine Notizen. Der nächtliche Traum (03:15) legt sie an — morgen früh steht hier die erste."}),t.length>0&&e.jsxs("div",{className:"flex flex-col gap-4 lg:flex-row",children:[e.jsxs("aside",{className:"w-full shrink-0 space-y-3 lg:w-72",children:[e.jsxs("div",{className:"relative",children:[e.jsx("input",{value:a,onChange:s=>p(s.target.value),type:"search",placeholder:"Notizen durchsuchen…","aria-label":"Notizen durchsuchen",className:"h-9 w-full rounded-lg border border-border/60 bg-card/45 pl-8 pr-3 text-xs text-foreground outline-none focus:ring-1 focus:ring-primary/50"}),e.jsx(S,{className:"absolute left-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground"})]}),e.jsx("div",{className:"max-h-[70vh] space-y-3 overflow-y-auto pr-1 scrollbar-thin",children:b.map(({dir:s,files:n})=>e.jsxs("div",{children:[e.jsxs("p",{className:"mb-1 flex items-center gap-1 text-[10px] font-bold uppercase tracking-wider text-muted-foreground/60",children:[e.jsx(M,{className:"h-3 w-3"})," ",D[s]??s]}),e.jsx("div",{className:"space-y-0.5",children:n.map(c=>e.jsxs("button",{onClick:()=>f(c.path),className:w("flex w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left text-xs transition-all cursor-pointer",l===c.path?"bg-primary/15 text-primary":"text-muted-foreground hover:bg-accent hover:text-foreground"),children:[e.jsx(W,{className:"h-3.5 w-3.5 shrink-0"}),e.jsx("span",{className:"truncate",title:c.title,children:c.name}),c.neu&&e.jsxs("span",{className:"ml-auto flex items-center gap-0.5 rounded bg-violet-500/15 px-1 py-0.5 text-[8px] font-bold uppercase text-violet-300",children:[e.jsx(C,{className:"h-2.5 w-2.5"}),"neu"]})]},c.path))})]},s||"__root"))})]}),e.jsx("div",{className:"min-w-0 flex-1 mc-card p-5",children:u?e.jsxs("div",{className:"flex items-center gap-2 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Notiz wird geladen …"]}):d?e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"mb-3 flex items-center justify-between border-b border-border/40 pb-2",children:[e.jsxs("span",{className:"flex items-center gap-1.5 font-mono text-[11px] text-muted-foreground",children:[e.jsx(E,{className:"h-3.5 w-3.5"})," ",d.path]}),d.mtime>0&&e.jsxs("span",{className:"text-[10px] text-muted-foreground/60",children:["Stand ",new Date(d.mtime*1e3).toLocaleString("de-DE",{day:"2-digit",month:"2-digit",hour:"2-digit",minute:"2-digit"})]})]}),e.jsx(z,{text:d.content,onWikiLink:j,known:m})]}):null})]})]})}function z({text:o,onWikiLink:h,known:l}){const f=o.split(`
|
||||
import{ao as y,r as x,g as L,j as e,L as v,S,ap as W,U as C,e as w,aq as E}from"./index-BJUCwF6N.js";import{F as M}from"./folder-open-oIQVVo05.js";const D={"":"Allgemein",traeume:"Träume",muster:"Muster","skill-kandidaten":"Skill-Kandidaten","skill-kandidaten/beauftragt":"Skill-Kandidaten · beauftragt","skill-kandidaten/verworfen":"Skill-Kandidaten · verworfen"};function T(){const{data:o,isLoading:h}=y(),[l,f]=x.useState(null),[d,r]=x.useState(null),[u,i]=x.useState(!1),[a,p]=x.useState(""),t=x.useMemo(()=>(o==null?void 0:o.files)??[],[o]),m=x.useMemo(()=>{const s=new Map;for(const n of t)s.set(n.name.toLowerCase(),n.path);return s},[t]),g=x.useMemo(()=>{if(!a.trim())return t;const s=a.toLowerCase();return t.filter(n=>n.title.toLowerCase().includes(s)||n.path.toLowerCase().includes(s))},[t,a]),b=x.useMemo(()=>{const s=[];for(const n of g){const c=s.find(k=>k.dir===n.dir);c?c.files.push(n):s.push({dir:n.dir,files:[n]})}return s.sort((n,c)=>n.dir===""?-1:c.dir===""?1:n.dir.localeCompare(c.dir))},[g]);x.useEffect(()=>{if(!l&&t.length){const s=t.find(n=>n.path.toLowerCase()==="index.md");f((s==null?void 0:s.path)??t[0].path)}},[t,l]),x.useEffect(()=>{l&&(i(!0),L(`/api/wissen/datei?pfad=${encodeURIComponent(l)}`).then(r).catch(()=>r({path:l,content:"*(Notiz nicht ladbar)*",mtime:0})).finally(()=>i(!1)))},[l]);const j=s=>{const n=m.get(s.toLowerCase());n&&f(n)};return e.jsxs("div",{className:"space-y-5",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Wissen"}),e.jsx("p",{className:"text-sm text-muted-foreground",children:"Was sich die Box nachts erschließt — Traum-Notizen, Muster und Skill-Ideen aus dem Wissens-Vault, verlinkt wie ein Wiki."})]}),o&&!o.available&&e.jsx("div",{className:"rounded-2xl border border-amber-500/30 bg-amber-500/5 p-4 text-xs text-amber-300",children:"Der Wissens-Vault liegt auf der Box (~/wissens-vault) — im lokalen Dev-Modus nicht verfügbar."}),h&&e.jsxs("div",{className:"flex items-center gap-2 rounded-2xl border border-border/60 bg-card/30 p-6 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Vault wird geladen …"]}),(o==null?void 0:o.available)&&t.length===0&&e.jsx("div",{className:"rounded-2xl border border-dashed border-border/60 bg-card/20 p-8 text-center text-xs text-muted-foreground",children:"Noch keine Notizen. Der nächtliche Traum (03:15) legt sie an — morgen früh steht hier die erste."}),t.length>0&&e.jsxs("div",{className:"flex flex-col gap-4 lg:flex-row",children:[e.jsxs("aside",{className:"w-full shrink-0 space-y-3 lg:w-72",children:[e.jsxs("div",{className:"relative",children:[e.jsx("input",{value:a,onChange:s=>p(s.target.value),type:"search",placeholder:"Notizen durchsuchen…","aria-label":"Notizen durchsuchen",className:"h-9 w-full rounded-lg border border-border/60 bg-card/45 pl-8 pr-3 text-xs text-foreground outline-none focus:ring-1 focus:ring-primary/50"}),e.jsx(S,{className:"absolute left-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground"})]}),e.jsx("div",{className:"max-h-[70vh] space-y-3 overflow-y-auto pr-1 scrollbar-thin",children:b.map(({dir:s,files:n})=>e.jsxs("div",{children:[e.jsxs("p",{className:"mb-1 flex items-center gap-1 text-[10px] font-bold uppercase tracking-wider text-muted-foreground/60",children:[e.jsx(M,{className:"h-3 w-3"})," ",D[s]??s]}),e.jsx("div",{className:"space-y-0.5",children:n.map(c=>e.jsxs("button",{onClick:()=>f(c.path),className:w("flex w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left text-xs transition-all cursor-pointer",l===c.path?"bg-primary/15 text-primary":"text-muted-foreground hover:bg-accent hover:text-foreground"),children:[e.jsx(W,{className:"h-3.5 w-3.5 shrink-0"}),e.jsx("span",{className:"truncate",title:c.title,children:c.name}),c.neu&&e.jsxs("span",{className:"ml-auto flex items-center gap-0.5 rounded bg-violet-500/15 px-1 py-0.5 text-[8px] font-bold uppercase text-violet-300",children:[e.jsx(C,{className:"h-2.5 w-2.5"}),"neu"]})]},c.path))})]},s||"__root"))})]}),e.jsx("div",{className:"min-w-0 flex-1 mc-card p-5",children:u?e.jsxs("div",{className:"flex items-center gap-2 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Notiz wird geladen …"]}):d?e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"mb-3 flex items-center justify-between border-b border-border/40 pb-2",children:[e.jsxs("span",{className:"flex items-center gap-1.5 font-mono text-[11px] text-muted-foreground",children:[e.jsx(E,{className:"h-3.5 w-3.5"})," ",d.path]}),d.mtime>0&&e.jsxs("span",{className:"text-[10px] text-muted-foreground/60",children:["Stand ",new Date(d.mtime*1e3).toLocaleString("de-DE",{day:"2-digit",month:"2-digit",hour:"2-digit",minute:"2-digit"})]})]}),e.jsx(z,{text:d.content,onWikiLink:j,known:m})]}):null})]})]})}function z({text:o,onWikiLink:h,known:l}){const f=o.split(`
|
||||
`),d=[];let r=[],u=null;const i=a=>{r.length&&(d.push(e.jsx("ul",{className:"mb-3 ml-4 list-disc space-y-1",children:r},a)),r=[])};return f.forEach((a,p)=>{const t=`l${p}`;if(u!==null){a.trimEnd()==="```"?(d.push(e.jsx("pre",{className:"mb-3 overflow-x-auto rounded-xl border border-border/50 bg-background/50 p-3 font-mono text-[11px] leading-relaxed text-foreground/85",children:u.join(`
|
||||
`)},t)),u=null):u.push(a);return}if(a.trimStart().startsWith("```")){i(t),u=[];return}const m=a.trimEnd();if(!m.trim()){i(t);return}const g=m.match(/^(#{1,4})\s+(.*)$/);if(g){i(t);const j=g[1].length,s=j===1?"text-lg font-bold mt-1 mb-3":j===2?"text-base font-bold mt-4 mb-2":"text-sm font-bold mt-3 mb-1.5";d.push(e.jsx("p",{className:w(s,"font-space text-foreground"),children:N(g[2],h,l,t)},t));return}const b=m.match(/^\s*[-*•]\s+(.*)$/);if(b){r.push(e.jsx("li",{className:"text-xs leading-relaxed text-foreground/85",children:N(b[1],h,l,t)},t));return}i(t),d.push(e.jsx("p",{className:"mb-2 text-xs leading-relaxed text-foreground/85",children:N(m,h,l,t)},t))}),i("end"),u!==null&&d.push(e.jsx("pre",{className:"mb-3 overflow-x-auto rounded-xl border border-border/50 bg-background/50 p-3 font-mono text-[11px]",children:u.join(`
|
||||
`)},"code-end")),e.jsx("div",{className:"max-w-3xl",children:d})}function N(o,h,l,f){return o.split(/(\[\[[^\]]+\]\]|\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g).map((r,u)=>{const i=`${f}-${u}`,a=r.match(/^\[\[([^\]|]+)(?:\|([^\]]+))?\]\]$/);if(a){const p=a[1].trim(),t=(a[2]??a[1]).trim();return l.has(p.toLowerCase())?e.jsx("button",{onClick:()=>h(p),className:"rounded bg-primary/10 px-1 text-primary underline decoration-primary/40 underline-offset-2 hover:bg-primary/20 cursor-pointer",children:t},i):e.jsx("span",{className:"rounded bg-background/40 px-1 text-muted-foreground",title:"Notiz existiert (noch) nicht",children:t},i)}return r.startsWith("**")&&r.endsWith("**")?e.jsx("b",{className:"font-semibold text-foreground",children:r.slice(2,-2)},i):r.startsWith("*")&&r.endsWith("*")&&r.length>2?e.jsx("i",{children:r.slice(1,-1)},i):r.startsWith("`")&&r.endsWith("`")?e.jsx("code",{className:"rounded bg-background/50 px-1 font-mono text-[11px] text-teal-300",children:r.slice(1,-1)},i):e.jsx("span",{children:r},i)})}export{T as WissenView};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as a}from"./index-CQgUnd1p.js";/**
|
||||
import{c as a}from"./index-BJUCwF6N.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c}from"./index-CQgUnd1p.js";/**
|
||||
import{c}from"./index-BJUCwF6N.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c}from"./index-CQgUnd1p.js";/**
|
||||
import{c}from"./index-BJUCwF6N.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as a}from"./index-CQgUnd1p.js";/**
|
||||
import{c as a}from"./index-BJUCwF6N.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+107
-107
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as t}from"./index-CQgUnd1p.js";/**
|
||||
import{c as t}from"./index-BJUCwF6N.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as t}from"./index-CQgUnd1p.js";/**
|
||||
import{c as t}from"./index-BJUCwF6N.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as a}from"./index-CQgUnd1p.js";/**
|
||||
import{c as a}from"./index-BJUCwF6N.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as a}from"./index-CQgUnd1p.js";/**
|
||||
import{c as a}from"./index-BJUCwF6N.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
Vendored
+2
-2
@@ -7,8 +7,8 @@
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>Mission Control 2.0</title>
|
||||
<script type="module" crossorigin src="/assets/index-CQgUnd1p.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BH760w1h.css">
|
||||
<script type="module" crossorigin src="/assets/index-BJUCwF6N.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DEL5L7Ku.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -18,10 +18,15 @@ function fmt(v: number, unit: string): string {
|
||||
return `${n}${unit}`
|
||||
}
|
||||
|
||||
function ChartTooltip({ active, payload, unit }: any) {
|
||||
function ChartTooltip({ active, payload, unit, label }: any) {
|
||||
if (!active || !payload?.length) return null
|
||||
return (
|
||||
<div className="rounded-lg border border-border/70 bg-popover/95 px-3 py-2 shadow-xl backdrop-blur">
|
||||
{Number.isFinite(label) && (
|
||||
<div className="mb-1 border-b border-border/40 pb-1 font-mono text-[10px] text-muted-foreground/80">
|
||||
{new Date(label).toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit", second: "2-digit" })}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-1">
|
||||
{payload.map((p: any) => (
|
||||
<div key={p.dataKey} className="flex items-center gap-2 text-[11px] font-mono">
|
||||
@@ -36,15 +41,17 @@ function ChartTooltip({ active, payload, unit }: any) {
|
||||
}
|
||||
|
||||
/** Wiederverwendbarer Live-Verlaufsgraph (Recharts Area, glatte Splines, Gradient-Fill,
|
||||
* Hover-Tooltip). yMode='percent' → 0..100 in 25er-Schritten; 'auto' → dynamisch (nice). */
|
||||
* Hover-Tooltip). yMode='percent' → 0..100 in 25er-Schritten; 'auto' → dynamisch (nice).
|
||||
* showTime=true blendet die Zeitachse ein (t in ms; Label-Format folgt der Spannweite). */
|
||||
export function LiveAreaChart({
|
||||
data, series, unit = "%", yMode = "percent", height = 176,
|
||||
data, series, unit = "%", yMode = "percent", height = 176, showTime = false,
|
||||
}: {
|
||||
data: any[]
|
||||
series: ChartSeries[]
|
||||
unit?: string
|
||||
yMode?: "percent" | "auto"
|
||||
height?: number
|
||||
showTime?: boolean
|
||||
}) {
|
||||
const peak = data.reduce(
|
||||
(m, row) => series.reduce((mm, s) => Math.max(mm, Number(row[s.key]) || 0), m), 0
|
||||
@@ -53,6 +60,13 @@ export function LiveAreaChart({
|
||||
? Math.min(100, Math.max(25, Math.ceil((peak * 1.2) / 25) * 25))
|
||||
: Math.max(niceCeil(peak * 1.15), 10)
|
||||
|
||||
// Kurze Fenster (Live ≈ 2 min) brauchen Sekunden, lange (1 h / 24 h) nur HH:MM.
|
||||
const spanMs = data.length > 1 ? Number(data[data.length - 1]?.t) - Number(data[0]?.t) : 0
|
||||
const fmtTime = (v: number) =>
|
||||
new Date(v).toLocaleTimeString("de-DE",
|
||||
spanMs <= 10 * 60_000 ? { hour: "2-digit", minute: "2-digit", second: "2-digit" }
|
||||
: { hour: "2-digit", minute: "2-digit" })
|
||||
|
||||
return (
|
||||
<div style={{ height }} className="w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
@@ -66,7 +80,15 @@ export function LiveAreaChart({
|
||||
))}
|
||||
</defs>
|
||||
<CartesianGrid vertical={false} stroke={GRID} />
|
||||
{showTime ? (
|
||||
<XAxis
|
||||
dataKey="t" type="number" domain={["dataMin", "dataMax"]}
|
||||
tickFormatter={fmtTime} tickCount={5} minTickGap={40} interval="preserveStartEnd"
|
||||
axisLine={false} tickLine={false} tick={{ fontSize: 10, fill: AXIS }} height={18}
|
||||
/>
|
||||
) : (
|
||||
<XAxis dataKey="t" hide />
|
||||
)}
|
||||
<YAxis
|
||||
domain={[0, yMax]} ticks={[0, yMax / 2, yMax]} tickFormatter={(v) => fmt(v, unit)}
|
||||
width={42} axisLine={false} tickLine={false} tick={{ fontSize: 10, fill: AXIS }}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
// Bereichs-Schalter der Leistungs-Karten: Live (2-min-Browser-Store) vs. 1h/24h
|
||||
// (Backend-Ringpuffer, /api/system/history). Bewusst winzig — kein Chart-Gebirge.
|
||||
export type ChartRange = "live" | "1h" | "24h"
|
||||
export const RANGE_MINUTES: Record<Exclude<ChartRange, "live">, number> = { "1h": 60, "24h": 1440 }
|
||||
|
||||
const LABELS: Record<ChartRange, string> = { live: "Live", "1h": "1 h", "24h": "24 h" }
|
||||
|
||||
export function RangeSwitch({ value, onChange }: { value: ChartRange; onChange: (r: ChartRange) => void }) {
|
||||
return (
|
||||
<div className="flex rounded-lg border border-border/40 bg-background/30 p-0.5">
|
||||
{(Object.keys(LABELS) as ChartRange[]).map((r) => (
|
||||
<button
|
||||
key={r}
|
||||
onClick={() => onChange(r)}
|
||||
className={cn(
|
||||
"rounded-md px-2 py-0.5 text-[9px] font-bold uppercase tracking-wide transition-all cursor-pointer",
|
||||
value === r ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{LABELS[r]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
import { useMemo, useState } from "react"
|
||||
import { Cpu } from "lucide-react"
|
||||
import { useSystemHistory } from "@/lib/useSystemHistory"
|
||||
import { useMetricHistory } from "@/lib/queries"
|
||||
import { gb } from "@/lib/format"
|
||||
import { LiveAreaChart, type ChartSeries } from "./LiveAreaChart"
|
||||
import { RangeSwitch, RANGE_MINUTES, type ChartRange } from "./RangeSwitch"
|
||||
|
||||
const SERIES: ChartSeries[] = [
|
||||
{ key: "cpu", label: "CPU", color: "#2dd4bf" },
|
||||
@@ -12,6 +15,14 @@ const SERIES: ChartSeries[] = [
|
||||
|
||||
export function SystemStatusCard() {
|
||||
const { sys, hist } = useSystemHistory()
|
||||
const [range, setRange] = useState<ChartRange>("live")
|
||||
const histQ = useMetricHistory(range === "live" ? 60 : RANGE_MINUTES[range], range !== "live")
|
||||
|
||||
// Live = 3-s-Browser-Store (~2 min); 1h/24h = Backend-Ringpuffer mit echten Zeitstempeln.
|
||||
const chartData = useMemo(() => {
|
||||
if (range === "live") return hist
|
||||
return (histQ.data?.points ?? []).map((p) => ({ t: p.t * 1000, cpu: p.cpu, ram: p.ram, gpu: p.gpu, disk: p.disk }))
|
||||
}, [range, hist, histQ.data])
|
||||
|
||||
const hasGpu = !!(sys?.gpu && sys.gpu.busy_percent != null && sys.gpu.gtt_used != null && sys.gpu.gtt_total != null)
|
||||
const activeSeries = SERIES.filter((s) => s.key !== "gpu" || hasGpu)
|
||||
@@ -33,9 +44,12 @@ export function SystemStatusCard() {
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<Cpu className="h-4.5 w-4.5 text-primary" />
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wide text-foreground">System-Status</h2>
|
||||
{range === "live" && (
|
||||
<span className="ml-1 inline-flex items-center gap-1 text-[10px] font-medium text-muted-foreground/70">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500 animate-pulse" /> live
|
||||
</span>
|
||||
)}
|
||||
<div className="ml-auto"><RangeSwitch value={range} onChange={setRange} /></div>
|
||||
</div>
|
||||
|
||||
{sys ? (
|
||||
@@ -50,7 +64,14 @@ export function SystemStatusCard() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<LiveAreaChart data={hist} series={activeSeries} unit="%" yMode="percent" height={176} />
|
||||
{range !== "live" && chartData.length === 0 && (
|
||||
<div className="flex h-44 items-center justify-center text-xs text-muted-foreground">
|
||||
{histQ.isLoading ? "Verlauf wird geladen …" : "Noch kein Verlauf — der Sammler baut die Historie gerade erst auf."}
|
||||
</div>
|
||||
)}
|
||||
{(range === "live" || chartData.length > 0) && (
|
||||
<LiveAreaChart data={chartData} series={activeSeries} unit="%" yMode="percent" height={176} showTime />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-44 items-center justify-center text-xs text-muted-foreground">Lade Systemdaten…</div>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useMemo, useState } from "react"
|
||||
import { Activity } from "lucide-react"
|
||||
import { useTokenStats } from "@/lib/queries"
|
||||
import { useTokenStats, useMetricHistory } from "@/lib/queries"
|
||||
import { useTokHistory } from "@/lib/metricsStore"
|
||||
import { LiveAreaChart, type ChartSeries } from "./LiveAreaChart"
|
||||
import { RangeSwitch, RANGE_MINUTES, type ChartRange } from "./RangeSwitch"
|
||||
|
||||
const SERIES: ChartSeries[] = [
|
||||
{ key: "prompt", label: "Prompt", color: "#f59e0b" },
|
||||
@@ -10,7 +12,27 @@ const SERIES: ChartSeries[] = [
|
||||
|
||||
export function TokenPerformanceCard() {
|
||||
const { data: ts } = useTokenStats()
|
||||
const hist = useTokHistory() // Durchsatz-Verlauf aus dem modul-globalen Store
|
||||
const hist = useTokHistory() // Live-Durchsatz aus dem modul-globalen Store (~2 min)
|
||||
const [range, setRange] = useState<ChartRange>("live")
|
||||
const histQ = useMetricHistory(range === "live" ? 60 : RANGE_MINUTES[range], range !== "live")
|
||||
|
||||
// 1h/24h: Raten aus den Deltas der Gesamtzähler (tok/s zwischen zwei Sample-Punkten).
|
||||
const chartData = useMemo(() => {
|
||||
if (range === "live") return hist
|
||||
const pts = histQ.data?.points ?? []
|
||||
const out: { t: number; prompt: number; completion: number }[] = []
|
||||
for (let i = 1; i < pts.length; i++) {
|
||||
const a = pts[i - 1], b = pts[i]
|
||||
if (a.tp == null || b.tp == null || a.tc == null || b.tc == null) continue
|
||||
const dt = Math.max(b.t - a.t, 1)
|
||||
out.push({
|
||||
t: b.t * 1000,
|
||||
prompt: Math.max(0, (b.tp - a.tp) / dt),
|
||||
completion: Math.max(0, (b.tc - a.tc) / dt),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}, [range, hist, histQ.data])
|
||||
|
||||
const last = hist[hist.length - 1]
|
||||
const curRate = last ? Math.round(last.prompt + last.completion) : 0
|
||||
@@ -22,9 +44,11 @@ export function TokenPerformanceCard() {
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="h-4.5 w-4.5 text-primary" />
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wide text-foreground">Token-Durchsatz</h2>
|
||||
{range === "live" && (
|
||||
<span className="ml-1 inline-flex items-center gap-1 text-[10px] font-medium text-muted-foreground/70">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500 animate-pulse" /> live
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{ts && (
|
||||
<div className="mt-2 flex items-baseline gap-2">
|
||||
@@ -45,7 +69,9 @@ export function TokenPerformanceCard() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex shrink-0 flex-col items-end gap-1.5 pt-1">
|
||||
<div className="flex shrink-0 flex-col items-end gap-2 pt-1">
|
||||
<RangeSwitch value={range} onChange={setRange} />
|
||||
<div className="flex flex-col items-end gap-1.5">
|
||||
{SERIES.map((s) => (
|
||||
<div key={s.key} className="flex items-center gap-1.5">
|
||||
<span className="h-2 w-2 rounded-full" style={{ background: s.color }} />
|
||||
@@ -57,9 +83,14 @@ export function TokenPerformanceCard() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ts ? (
|
||||
<LiveAreaChart data={hist} series={SERIES} unit=" tok/s" yMode="auto" height={150} />
|
||||
{range !== "live" && chartData.length === 0 ? (
|
||||
<div className="flex h-[150px] items-center justify-center text-xs text-muted-foreground">
|
||||
{histQ.isLoading ? "Verlauf wird geladen …" : "Noch kein Verlauf — der Sammler baut die Historie gerade erst auf."}
|
||||
</div>
|
||||
) : ts ? (
|
||||
<LiveAreaChart data={chartData} series={SERIES} unit=" tok/s" yMode="auto" height={150} showTime />
|
||||
) : (
|
||||
<div className="flex h-[150px] items-center justify-center text-xs text-muted-foreground">Lade Durchsatz…</div>
|
||||
)}
|
||||
|
||||
@@ -417,6 +417,22 @@ export interface Health {
|
||||
brain?: { role: string; model: string | null; ready: boolean }
|
||||
}
|
||||
|
||||
// 24-h-Metrik-Verlauf (GET /api/system/history?minutes=N) — Basis der Cockpit-Zeitachse.
|
||||
// tp/tc sind Token-GESAMTZÄHLER; Raten rechnet das Frontend aus den Deltas.
|
||||
export interface HistoryPoint {
|
||||
t: number // Epoch-Sekunden
|
||||
cpu: number | null
|
||||
ram: number | null
|
||||
gpu: number | null
|
||||
disk: number | null
|
||||
tp: number | null
|
||||
tc: number | null
|
||||
}
|
||||
export interface HistoryResp {
|
||||
sample_s: number
|
||||
points: HistoryPoint[]
|
||||
}
|
||||
|
||||
export interface TokenStats {
|
||||
prompt_tokens: number
|
||||
completion_tokens: number
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
type GroupsResp,
|
||||
type HermesBrainResp,
|
||||
type Health,
|
||||
type HistoryResp,
|
||||
type IdeenLogResp,
|
||||
type IdeenResp,
|
||||
type Job,
|
||||
@@ -169,6 +170,16 @@ export const useHealth = () =>
|
||||
export const useSystemStatus = (refetchInterval: number = TAKT.graph) =>
|
||||
useQuery({ queryKey: qk.systemStatus, queryFn: () => api<SystemStatus>("/api/system/status"), refetchInterval })
|
||||
|
||||
// Metrik-Verlauf (Cockpit-Zeitachse): 1h/24h aus dem Backend-Ringpuffer. Nur aktiv,
|
||||
// wenn eine Karte gerade diesen Bereich zeigt (enabled) — Live bleibt beim 3s-Store.
|
||||
export const useMetricHistory = (minutes: number, enabled = true) =>
|
||||
useQuery({
|
||||
queryKey: ["metrics-history", minutes],
|
||||
queryFn: () => api<HistoryResp>(`/api/system/history?minutes=${minutes}`),
|
||||
enabled,
|
||||
refetchInterval: 60_000,
|
||||
})
|
||||
|
||||
export const useServices = (refetchInterval: number = TAKT.normal) =>
|
||||
useQuery({ queryKey: qk.services, queryFn: () => api<ServicesResp>("/api/system/services"), refetchInterval })
|
||||
|
||||
|
||||
@@ -72,8 +72,16 @@ function CodeWindow({
|
||||
)
|
||||
}
|
||||
|
||||
// Die Box-Adresse kennt der Browser schon — er ist ja über sie verbunden. Nur im
|
||||
// Dev-Modus (localhost + Vite-Proxy) greift der bekannte LAN-Fallback.
|
||||
function defaultHost(): string {
|
||||
const h = window.location.hostname
|
||||
if (h && h !== "localhost" && h !== "127.0.0.1") return h
|
||||
return "192.168.178.151"
|
||||
}
|
||||
|
||||
export function ConnectView() {
|
||||
const [host, setHost] = useState(localStorage.getItem("mc_host") || "192.168.178.151")
|
||||
const [host, setHost] = useState(localStorage.getItem("mc_host") || defaultHost())
|
||||
const [mcpPath, setMcpPath] = useState(localStorage.getItem("mc_mcp_path") || "")
|
||||
// Legacy-Tools (roher /v1-Zugang) leben im Ausklapper — Hermes Desktop ist der Hauptweg.
|
||||
const [active, setActive] = useState("kilo")
|
||||
@@ -187,11 +195,12 @@ export function ConnectView() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Gemeinsame Variablen */}
|
||||
<div className="grid gap-4 md:grid-cols-2 p-5 mc-card">
|
||||
<div className="space-y-1.5">
|
||||
{/* Box-Adresse — vorbefüllt aus der Browser-URL; anfassen nur, wenn sich die Box-IP ändert. */}
|
||||
<div className="p-5 mc-card">
|
||||
<div className="space-y-1.5 max-w-md">
|
||||
<label className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground flex items-center gap-1.5">
|
||||
<Globe className="h-3.5 w-3.5 text-primary" /> Box LAN IP-Adresse
|
||||
<span className="normal-case font-semibold tracking-normal text-muted-foreground/60">— vorbefüllt aus der Adresse dieser Seite</span>
|
||||
</label>
|
||||
<input
|
||||
value={host}
|
||||
@@ -201,21 +210,9 @@ export function ConnectView() {
|
||||
placeholder="z.B. 192.168.178.151"
|
||||
className="w-full h-9 rounded-lg border border-border/60 bg-background/40 px-3 font-mono text-xs outline-none focus:ring-1 focus:ring-primary/50 text-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground flex items-center gap-1.5">
|
||||
<FolderOpen className="h-3.5 w-3.5 text-violet-400" /> Lokaler MCP-Scriptpfad
|
||||
<span className="text-violet-400/70 normal-case font-semibold tracking-normal">(nur Leitung 2)</span>
|
||||
</label>
|
||||
<input
|
||||
value={mcpPath}
|
||||
onChange={(e) => saveMcpPath(e.target.value)}
|
||||
aria-label="Lokaler MCP-Scriptpfad"
|
||||
spellCheck={false}
|
||||
placeholder="z.B. F:\Coding Stuff\mission-control-2\mcp\mcp_memory.py"
|
||||
className="w-full h-9 rounded-lg border border-border/60 bg-background/40 px-3 font-mono text-xs outline-none focus:ring-1 focus:ring-violet-500/50 text-foreground"
|
||||
/>
|
||||
<p className="text-[10px] text-muted-foreground/70">
|
||||
Wird in alle Snippets eingebacken. Ändern musst du sie nur, wenn die Box eine neue IP bekommt.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -332,6 +329,26 @@ export function ConnectView() {
|
||||
<span>{data.memory.note}</span>
|
||||
</div>
|
||||
|
||||
{/* Der Pfad wirkt NUR auf dieses Snippet — deshalb wohnt das Feld hier,
|
||||
nicht mehr prominent in der Kopfzeile („Wann brauche ich das?"). */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground flex items-center gap-1.5">
|
||||
<FolderOpen className="h-3.5 w-3.5 text-violet-400" /> Wo liegt mcp_memory.py auf DIESEM PC?
|
||||
</label>
|
||||
<input
|
||||
value={mcpPath}
|
||||
onChange={(e) => saveMcpPath(e.target.value)}
|
||||
aria-label="Lokaler MCP-Scriptpfad"
|
||||
spellCheck={false}
|
||||
placeholder="z.B. F:\Coding Stuff\mission-control-2\mcp\mcp_memory.py"
|
||||
className="w-full h-9 rounded-lg border border-border/60 bg-background/40 px-3 font-mono text-xs outline-none focus:ring-1 focus:ring-violet-500/50 text-foreground"
|
||||
/>
|
||||
<p className="text-[10px] text-muted-foreground/70">
|
||||
Nur nötig, wenn du das Gedächtnis in einem PC-Tool anschließt — das Script läuft lokal
|
||||
und spricht mit der Box. Der Pfad landet direkt im Snippet unten.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<CodeWindow
|
||||
tool={data.memory}
|
||||
fileName="mcp.json"
|
||||
|
||||
@@ -259,14 +259,18 @@ function Detail({
|
||||
{lockUnload ? "🔒 bleibt geladen" : warm ? "Entladen" : "Ins Warm-Set laden"}
|
||||
</button>
|
||||
|
||||
{/* Rolle wählen */}
|
||||
{/* Rolle wählen — bei lebenswichtigen Rollen (Hirn/Gedächtnis) gesperrt: die Rolle
|
||||
wandert nur, indem man sie einem ANDEREN Modell zuweist (Backend erzwingt das auch). */}
|
||||
<div className="rounded-xl border border-border/30 bg-background/20 p-3">
|
||||
<div className="mb-2 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">Rolle</div>
|
||||
<div className="mb-2 flex items-center justify-between text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
<span>Rolle</span>
|
||||
{protectedRole && <span className="normal-case font-medium tracking-normal text-muted-foreground/70">🔒 lebenswichtig — zum Tausch die Rolle woanders zuweisen</span>}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
<RoleChip active={!m.role} onClick={() => onRole("")} dot="bg-slate-400" label="keine" />
|
||||
<RoleChip active={!m.role} onClick={() => onRole("")} dot="bg-slate-400" label="keine" disabled={protectedRole} />
|
||||
{ROLE_META.map((r) => (
|
||||
<RoleChip key={r.role} active={m.role === r.role} onClick={() => onRole(r.role)}
|
||||
dot={roleBarColor(r.role).dot} label={r.short} />
|
||||
dot={roleBarColor(r.role).dot} label={r.short} disabled={protectedRole && m.role !== r.role} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -322,11 +326,16 @@ function Detail({
|
||||
)
|
||||
}
|
||||
|
||||
function RoleChip({ active, onClick, dot, label }: { active: boolean; onClick: () => void; dot: string; label: string }) {
|
||||
function RoleChip({ active, onClick, dot, label, disabled }: {
|
||||
active: boolean; onClick: () => void; dot: string; label: string; disabled?: boolean
|
||||
}) {
|
||||
return (
|
||||
<button onClick={onClick}
|
||||
className={cn("flex items-center gap-1.5 rounded-lg border px-2 py-1 text-[10px] font-semibold transition-all cursor-pointer",
|
||||
active ? "border-primary/60 bg-primary/15 text-foreground" : "border-border/40 bg-background/20 text-muted-foreground hover:border-primary/30")}>
|
||||
<button onClick={onClick} disabled={disabled}
|
||||
title={disabled ? "Gesperrt: dieses Modell hält eine lebenswichtige Rolle." : undefined}
|
||||
className={cn("flex items-center gap-1.5 rounded-lg border px-2 py-1 text-[10px] font-semibold transition-all",
|
||||
disabled ? "cursor-not-allowed opacity-40 border-border/40 bg-background/20 text-muted-foreground"
|
||||
: active ? "cursor-pointer border-primary/60 bg-primary/15 text-foreground"
|
||||
: "cursor-pointer border-border/40 bg-background/20 text-muted-foreground hover:border-primary/30")}>
|
||||
<span className={cn("h-2 w-2 rounded-sm", dot)} />
|
||||
{label}
|
||||
{active && <Check className="h-3 w-3 text-primary" />}
|
||||
|
||||
Reference in New Issue
Block a user