Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33032f8fb0 | ||
|
|
3249c8e942 | ||
|
|
24a0694b80 | ||
|
|
a494d320d7 | ||
|
|
7957cda438 |
+22
-4
@@ -18,18 +18,36 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"runtimeExecutable": "npm",
|
"runtimeExecutable": "npm",
|
||||||
"runtimeArgs": ["run", "dev", "--", "--port", "5180", "--strictPort"],
|
"runtimeArgs": [
|
||||||
|
"run",
|
||||||
|
"dev",
|
||||||
|
"--",
|
||||||
|
"--port",
|
||||||
|
"5180",
|
||||||
|
"--strictPort"
|
||||||
|
],
|
||||||
"cwd": "F:\\Coding Stuff\\mission-control-2\\frontend",
|
"cwd": "F:\\Coding Stuff\\mission-control-2\\frontend",
|
||||||
"env": { "MC_API_TARGET": "http://192.168.178.151:9001" },
|
"env": {
|
||||||
|
"MC_API_TARGET": "http://192.168.178.151:9001"
|
||||||
|
},
|
||||||
"autoPort": false,
|
"autoPort": false,
|
||||||
"port": 5180
|
"port": 5180
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "frontend-mock",
|
"name": "frontend-mock",
|
||||||
"runtimeExecutable": "npm",
|
"runtimeExecutable": "npm",
|
||||||
"runtimeArgs": ["run", "dev", "--", "--port", "5181", "--strictPort"],
|
"runtimeArgs": [
|
||||||
|
"run",
|
||||||
|
"dev",
|
||||||
|
"--",
|
||||||
|
"--port",
|
||||||
|
"5181",
|
||||||
|
"--strictPort"
|
||||||
|
],
|
||||||
"cwd": "F:\\Coding Stuff\\mission-control-2\\frontend",
|
"cwd": "F:\\Coding Stuff\\mission-control-2\\frontend",
|
||||||
"env": { "MC_API_TARGET": "http://127.0.0.1:9000" },
|
"env": {
|
||||||
|
"MC_API_TARGET": "http://127.0.0.1:9000"
|
||||||
|
},
|
||||||
"autoPort": false,
|
"autoPort": false,
|
||||||
"port": 5181
|
"port": 5181
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,19 @@ jobs:
|
|||||||
# FRISCHE Build im Runner — kein Vergleich mit dem committeten dist (das wäre
|
# FRISCHE Build im Runner — kein Vergleich mit dem committeten dist (das wäre
|
||||||
# über Node-Versionen hinweg flatterhaft und würde dauerhaft rot leuchten).
|
# über Node-Versionen hinweg flatterhaft und würde dauerhaft rot leuchten).
|
||||||
if [ $rot -eq 0 ] && [ -d frontend/dist/assets ]; then
|
if [ $rot -eq 0 ] && [ -d frontend/dist/assets ]; then
|
||||||
budget_gz=125000 # Stand nach P0: 110 571 B gzip
|
# 28.08.2026, React 18 -> 19: Der Sprung kostete GEMESSEN 14 645 B gzip
|
||||||
|
# (118 762 -> 133 407) und riss das bis dahin geltende Budget von 125 000.
|
||||||
|
# Das Gate hat also getan, was es soll. Angehoben wurde es TROTZDEM —
|
||||||
|
# bewusst und einmalig, nicht weil es im Weg stand:
|
||||||
|
# · Der Zuwachs IST die Plattform, nicht Wildwuchs. Es gibt hier nichts
|
||||||
|
# wegzulassen, so wie beim Router (P2), wo Schublade und Palette
|
||||||
|
# hinter lazy() wanderten und das Budget scharf blieb.
|
||||||
|
# · MC2 ist eine LAN-Appliance. 14 kB sind ueber Gigabit-Ethernet keine
|
||||||
|
# messbare Wartezeit; das Budget existiert gegen DRIFT (ein 24-MB-Avatar,
|
||||||
|
# eine 95-kB-Diagramm-Bibliothek auf der Startseite), nicht gegen einen
|
||||||
|
# ueberlegten Plattform-Schritt.
|
||||||
|
# Der Abstand zum Budget bleibt derselbe wie vorher (~5 %).
|
||||||
|
budget_gz=140000 # Stand nach React 19: 133 407 B gzip
|
||||||
budget_dist=3145728 # Stand nach P0: 1 477 852 B
|
budget_dist=3145728 # Stand nach P0: 1 477 852 B
|
||||||
# Den Einstiegs-Chunk aus index.html lesen, NICHT per Glob raten:
|
# Den Einstiegs-Chunk aus index.html lesen, NICHT per Glob raten:
|
||||||
# Rollup nennt auch kleine geteilte Module "index-*.js" (gemessen: ein
|
# Rollup nennt auch kleine geteilte Module "index-*.js" (gemessen: ein
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from services import agent_aktivitaet
|
||||||
from services.agent import agent_status, hermes_brain_info, set_agent_brain, update_brain_model
|
from services.agent import agent_status, hermes_brain_info, set_agent_brain, update_brain_model
|
||||||
|
|
||||||
router = APIRouter(prefix="/api")
|
router = APIRouter(prefix="/api")
|
||||||
@@ -39,3 +40,13 @@ def set_brain(body: SetBrainReq) -> dict:
|
|||||||
def set_brain_model(body: BrainReq) -> dict:
|
def set_brain_model(body: BrainReq) -> dict:
|
||||||
ok = update_brain_model(body.model)
|
ok = update_brain_model(body.model)
|
||||||
return {"ok": ok}
|
return {"ok": ok}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/agent/aktivitaet")
|
||||||
|
def aktivitaet(limit: int = 60) -> dict:
|
||||||
|
"""Werkzeug-Verlauf des Agenten (v3-Umbau P6).
|
||||||
|
|
||||||
|
Gelesen aus Hermes' eigenem Log — MC2 patcht dort nichts, es schaut nur zu. Was
|
||||||
|
NICHT drin steht (Denkstrom, Werkzeug-Argumente), verspricht die Ansicht auch nicht.
|
||||||
|
"""
|
||||||
|
return agent_aktivitaet.uebersicht(max(1, min(limit, 300)))
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
"""Werkzeug-Verlauf des Agenten (v3-Umbau P6).
|
||||||
|
|
||||||
|
WARUM ES DAS GIBT: Lucys Arbeit war eine Blackbox mit Statuswort. Der Blueprint sah
|
||||||
|
dafür eine „Live Agent Matrix" mit Denkstrom vor (§4.4) — die ist so nicht baubar: Die
|
||||||
|
Denkschritte entstehen im Hermes-Prozess, und `AGENTS.md` verbietet es, dessen Quellcode
|
||||||
|
zu patchen.
|
||||||
|
|
||||||
|
Beim Nachsehen zeigte sich aber, dass die HÄLFTE davon längst offen daliegt: Hermes
|
||||||
|
protokolliert jeden Werkzeug-Ruf nach `~/.hermes/logs/agent.log`. Eine Log-Datei zu lesen
|
||||||
|
ist kein Patchen. Was dadurch sichtbar wird — welches Werkzeug, wie lange, mit welchem
|
||||||
|
Ergebnis, in welchem Lauf — ist für die Frage „was tut sie gerade und wo hängt es" oft
|
||||||
|
nützlicher als der Fließtext ihrer Gedanken.
|
||||||
|
|
||||||
|
WAS NICHT GEHT (und hier auch nicht so tut): der Denkstrom selbst und die Argumente eines
|
||||||
|
Werkzeug-Rufs. Beides steht nicht im Log. Die Ansicht verspricht deshalb nur, was sie
|
||||||
|
halten kann.
|
||||||
|
|
||||||
|
ES HAT SICH SOFORT GELOHNT: Beim ersten Lesen fiel auf, dass `web_extract` seit
|
||||||
|
mindestens dem 25.08. JEDEN Morgen um 07:00 scheitert (der Suchanbieter kann keine
|
||||||
|
Seiten abrufen). Vier Tage lang, ohne dass es irgendwo aufgefallen wäre.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
LOG_PFAD = Path(os.path.expanduser(
|
||||||
|
os.environ.get("MC_HERMES_AGENT_LOG", "~/.hermes/logs/agent.log")))
|
||||||
|
|
||||||
|
# Nur das Ende der Datei lesen. Sie wächst auf mehrere MB und wird rotiert; für einen
|
||||||
|
# Verlauf der letzten Stunden reicht der Schwanz — und er kostet nichts.
|
||||||
|
LESE_BYTES = 512 * 1024
|
||||||
|
|
||||||
|
# Die drei Formen, in denen Hermes einen Werkzeug-Ruf notiert (am Log gemessen, nicht
|
||||||
|
# geraten). Die Lauf-Kennung in eckigen Klammern fehlt bei Nicht-Cron-Läufen.
|
||||||
|
#
|
||||||
|
# INFO [cron_…] agent.tool_executor: tool terminal completed (1.40s, 53 chars)
|
||||||
|
# INFO agent.tool_executor: tool web_search completed (2.61s, 2944 chars)
|
||||||
|
# WARNING [cron_…] agent.tool_executor: Tool web_extract returned error (0.17s): {…}
|
||||||
|
# INFO agent.tool_executor: tool web_extract failed (0.17s): {…}
|
||||||
|
_ZEIT = r"(?P<zeit>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}),\d+"
|
||||||
|
_LAUF = r"(?:\[(?P<lauf>[^\]]+)\] )?"
|
||||||
|
|
||||||
|
_FERTIG = re.compile(
|
||||||
|
_ZEIT + r" \w+ " + _LAUF + r"agent\.tool_executor: [Tt]ool (?P<werkzeug>\S+) completed "
|
||||||
|
r"\((?P<dauer>[\d.]+)s, (?P<zeichen>\d+) chars\)")
|
||||||
|
|
||||||
|
_FEHLER = re.compile(
|
||||||
|
_ZEIT + r" \w+ " + _LAUF + r"agent\.tool_executor: [Tt]ool (?P<werkzeug>\S+) "
|
||||||
|
r"(?:failed|returned error) \((?P<dauer>[\d.]+)s\)(?::\s*(?P<detail>.*))?")
|
||||||
|
|
||||||
|
|
||||||
|
def _schwanz(pfad: Path, n: int) -> list[str]:
|
||||||
|
"""Die letzten n Bytes als Zeilen. Die erste Zeile kann angeschnitten sein und
|
||||||
|
wird verworfen — ein halber Zeitstempel passt auf kein Muster, aber sicher ist besser."""
|
||||||
|
try:
|
||||||
|
groesse = pfad.stat().st_size
|
||||||
|
with pfad.open("rb") as f:
|
||||||
|
f.seek(max(0, groesse - n))
|
||||||
|
roh = f.read()
|
||||||
|
zeilen = roh.decode("utf-8", errors="replace").splitlines()
|
||||||
|
return zeilen[1:] if groesse > n and zeilen else zeilen
|
||||||
|
except OSError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _kurz(detail: str | None) -> str | None:
|
||||||
|
"""Fehlertext des Werkzeugs auf einen lesbaren Satz eindampfen. Hermes legt dort ein
|
||||||
|
JSON ab; interessant ist daran nur das `error`-Feld."""
|
||||||
|
if not detail:
|
||||||
|
return None
|
||||||
|
treffer = re.search(r'"error"\s*:\s*"([^"]{1,300})"', detail)
|
||||||
|
text = treffer.group(1) if treffer else detail.strip()
|
||||||
|
return (text[:297] + "…") if len(text) > 300 else text
|
||||||
|
|
||||||
|
|
||||||
|
def rufe(limit: int = 60) -> list[dict]:
|
||||||
|
"""Die jüngsten Werkzeug-Rufe, ältester zuerst."""
|
||||||
|
ergebnis: list[dict] = []
|
||||||
|
for zeile in _schwanz(LOG_PFAD, LESE_BYTES):
|
||||||
|
m = _FERTIG.match(zeile)
|
||||||
|
if m:
|
||||||
|
ergebnis.append({
|
||||||
|
"zeit": _iso(m.group("zeit")),
|
||||||
|
"lauf": m.group("lauf"),
|
||||||
|
"werkzeug": m.group("werkzeug"),
|
||||||
|
"dauer_s": float(m.group("dauer")),
|
||||||
|
"zeichen": int(m.group("zeichen")),
|
||||||
|
"ok": True,
|
||||||
|
"fehler": None,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
m = _FEHLER.match(zeile)
|
||||||
|
if m:
|
||||||
|
ergebnis.append({
|
||||||
|
"zeit": _iso(m.group("zeit")),
|
||||||
|
"lauf": m.group("lauf"),
|
||||||
|
"werkzeug": m.group("werkzeug"),
|
||||||
|
"dauer_s": float(m.group("dauer")),
|
||||||
|
"zeichen": None,
|
||||||
|
"ok": False,
|
||||||
|
"fehler": _kurz(m.group("detail")),
|
||||||
|
})
|
||||||
|
return ergebnis[-limit:]
|
||||||
|
|
||||||
|
|
||||||
|
def _iso(s: str) -> str:
|
||||||
|
"""`2026-08-28 07:03:18` → ISO. Die Box läuft in Europe/Berlin; die Zeit bleibt naiv,
|
||||||
|
weil der Klient sie ohnehin nur anzeigt und nicht rechnet."""
|
||||||
|
try:
|
||||||
|
return datetime.strptime(s, "%Y-%m-%d %H:%M:%S").isoformat()
|
||||||
|
except ValueError:
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def uebersicht(limit: int = 60) -> dict:
|
||||||
|
"""Was die Agent-Ansicht braucht: die Rufe selbst, je Werkzeug eine Bilanz und die
|
||||||
|
wiederkehrenden Fehler zusammengefasst.
|
||||||
|
|
||||||
|
Die Bilanz ist der eigentliche Nutzen: Ein Werkzeug, das IMMER scheitert, verschwindet
|
||||||
|
in einer Zeitleiste — in einer Zeile „web_extract · 4 Rufe · 4 Fehler" nicht."""
|
||||||
|
liste = rufe(limit)
|
||||||
|
if not LOG_PFAD.exists():
|
||||||
|
return {"verfuegbar": False, "pfad": str(LOG_PFAD), "rufe": [],
|
||||||
|
"werkzeuge": [], "laeufe": []}
|
||||||
|
|
||||||
|
bilanz: dict[str, dict] = {}
|
||||||
|
for r in liste:
|
||||||
|
b = bilanz.setdefault(r["werkzeug"], {
|
||||||
|
"werkzeug": r["werkzeug"], "rufe": 0, "fehler": 0,
|
||||||
|
"dauer_summe": 0.0, "letzter_fehler": None,
|
||||||
|
})
|
||||||
|
b["rufe"] += 1
|
||||||
|
b["dauer_summe"] += r["dauer_s"]
|
||||||
|
if not r["ok"]:
|
||||||
|
b["fehler"] += 1
|
||||||
|
b["letzter_fehler"] = r["fehler"]
|
||||||
|
|
||||||
|
werkzeuge = sorted(
|
||||||
|
({**b, "dauer_schnitt_s": round(b["dauer_summe"] / max(b["rufe"], 1), 2)}
|
||||||
|
for b in bilanz.values()),
|
||||||
|
key=lambda b: (-b["fehler"], -b["rufe"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Läufe in der Reihenfolge ihres ersten Auftretens (nicht sortiert nach Kennung —
|
||||||
|
# die trägt zwar ein Datum, aber darauf sollte sich niemand verlassen).
|
||||||
|
laeufe: list[dict] = []
|
||||||
|
gesehen: dict[str, dict] = {}
|
||||||
|
for r in liste:
|
||||||
|
schluessel = r["lauf"] or "(interaktiv)"
|
||||||
|
if schluessel not in gesehen:
|
||||||
|
gesehen[schluessel] = {"lauf": schluessel, "von": r["zeit"], "bis": r["zeit"],
|
||||||
|
"rufe": 0, "fehler": 0}
|
||||||
|
laeufe.append(gesehen[schluessel])
|
||||||
|
e = gesehen[schluessel]
|
||||||
|
e["bis"] = r["zeit"]
|
||||||
|
e["rufe"] += 1
|
||||||
|
if not r["ok"]:
|
||||||
|
e["fehler"] += 1
|
||||||
|
|
||||||
|
return {"verfuegbar": True, "pfad": str(LOG_PFAD), "rufe": liste,
|
||||||
|
"werkzeuge": werkzeuge, "laeufe": laeufe}
|
||||||
@@ -20,7 +20,7 @@ import time
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import psutil
|
import psutil
|
||||||
from config import HERMES_API_URL, MODELS_DIR, VOICE_SERVICE_URL
|
from config import HERMES_API_KEY, HERMES_API_URL, MODELS_DIR, VOICE_SERVICE_URL
|
||||||
|
|
||||||
from services import announce, llamaswap
|
from services import announce, llamaswap
|
||||||
|
|
||||||
@@ -34,14 +34,26 @@ REMIND_S = int(os.environ.get("MC_SENTRY_REMIND_S", "21600")) # Erinnerung
|
|||||||
DISK_ALARM_PCT = float(os.environ.get("MC_SENTRY_DISK_PCT", "90"))
|
DISK_ALARM_PCT = float(os.environ.get("MC_SENTRY_DISK_PCT", "90"))
|
||||||
|
|
||||||
|
|
||||||
def _reach(url: str, path: str = "/health") -> bool:
|
def _reach(url: str, path: str = "/health", headers: dict[str, str] | None = None) -> bool:
|
||||||
|
"""Antwortet der Dienst ueberhaupt? `< 500` ist bewusst nachsichtig: Die Frage ist
|
||||||
|
"laeuft er", nicht "darf ich rein" — ein 401 beweist, dass jemand zuhoert.
|
||||||
|
|
||||||
|
`headers` gibt es seit dem 28.08.2026: Die Hermes-Probe schlug ohne API-Schluessel an
|
||||||
|
und erzeugte dabei 720 `rejected invalid API key`-Warnungen pro Tag in dessen Log
|
||||||
|
(gemessen: 30/Stunde seit dem 27.08. 15:05). Das Urteil war richtig, der Laerm nicht —
|
||||||
|
und er haette einen echten Auth-Fehler unter sich begraben."""
|
||||||
try:
|
try:
|
||||||
with httpx.Client(timeout=5.0) as c:
|
with httpx.Client(timeout=5.0) as c:
|
||||||
return c.get(f"{url}{path}").status_code < 500
|
return c.get(f"{url}{path}", headers=headers or {}).status_code < 500
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _hermes_kopf() -> dict[str, str]:
|
||||||
|
"""Bearer-Kopf fuer die Hermes-Platform, falls ein Schluessel konfiguriert ist."""
|
||||||
|
return {"Authorization": f"Bearer {HERMES_API_KEY}"} if HERMES_API_KEY else {}
|
||||||
|
|
||||||
|
|
||||||
def _check_engine() -> bool:
|
def _check_engine() -> bool:
|
||||||
return llamaswap.engine_reachable()
|
return llamaswap.engine_reachable()
|
||||||
|
|
||||||
@@ -70,7 +82,7 @@ CHECKS: dict[str, tuple] = {
|
|||||||
"brain": (_check_brain,
|
"brain": (_check_brain,
|
||||||
"Mein Gehirn lädt nicht — ich kann gerade nicht richtig denken. Ein Neustart der Engine könnte helfen.",
|
"Mein Gehirn lädt nicht — ich kann gerade nicht richtig denken. Ein Neustart der Engine könnte helfen.",
|
||||||
"Mein Gehirn ist wieder geladen. Alles klar bei mir."),
|
"Mein Gehirn ist wieder geladen. Alles klar bei mir."),
|
||||||
"hermes": (lambda: _reach(HERMES_API_URL, "/v1/models"),
|
"hermes": (lambda: _reach(HERMES_API_URL, "/v1/models", _hermes_kopf()),
|
||||||
"Der Agent-Dienst ist ausgefallen — Telegram und meine Tools gehen gerade nicht.",
|
"Der Agent-Dienst ist ausgefallen — Telegram und meine Tools gehen gerade nicht.",
|
||||||
"Der Agent-Dienst läuft wieder."),
|
"Der Agent-Dienst läuft wieder."),
|
||||||
"voice": (lambda: _reach(VOICE_SERVICE_URL),
|
"voice": (lambda: _reach(VOICE_SERVICE_URL),
|
||||||
|
|||||||
-6
File diff suppressed because one or more lines are too long
+6
File diff suppressed because one or more lines are too long
+5
-5
File diff suppressed because one or more lines are too long
-31
File diff suppressed because one or more lines are too long
+31
File diff suppressed because one or more lines are too long
-41
File diff suppressed because one or more lines are too long
+51
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as f,r as m,D as G,F as H,j as e,t as I,B as M,I as y,G as z,b as S,L as B,J as _,K as E,C as P}from"./index-C18iiJ44.js";import{A as u}from"./arrow-right-BtJkJFRf.js";import{C as K}from"./chevron-down-NnNHBt9Q.js";/**
|
import{c as f,r as m,D as G,F as H,j as e,t as I,B as M,I as y,G as z,b as S,L as B,J as _,K as E,C as P}from"./index-l6kyKCL9.js";import{A as u}from"./arrow-right-DSSXd-8y.js";import{C as K}from"./chevron-down-C5keVSfm.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.
|
||||||
+4
-4
File diff suppressed because one or more lines are too long
Vendored
+9
-9
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
|||||||
import{u,af as m,k as x,j as s,y as n,ag as b,b as p,n as f,q as h}from"./index-C18iiJ44.js";function y(){const d=u(),{data:t=[]}=m(),{showAlert:l,dialogElement:o}=x();async function i(e){try{await f(`/api/jobs/${e}/cancel`,{method:"POST"}),d.invalidateQueries({queryKey:h.jobs})}catch(c){l("Fehler",c.message)}}const a=t.filter(e=>e.state==="running"||e.state==="queued"),r=t.filter(e=>e.state!=="running"&&e.state!=="queued").slice(-3);return a.length===0&&r.length===0?null:s.jsxs("div",{className:"space-y-3 mc-card p-4",children:[s.jsx("div",{className:"text-[11px] font-semibold text-muted-foreground uppercase tracking-wider",children:"Aktive Downloads"}),a.map(e=>s.jsxs("div",{className:"space-y-1.5 p-3 rounded-xl bg-background/20 border border-border/40",children:[s.jsxs("div",{className:"flex justify-between items-center text-xs",children:[s.jsx("span",{className:"font-semibold truncate max-w-[250px]",children:e.label}),s.jsxs("div",{className:"flex items-center gap-3",children:[s.jsxs("span",{className:"text-muted-foreground font-mono",children:[e.progress??0,"% • ",n(e.done_bytes),"/",n(e.total_bytes),e.eta_s?` • ETA ${b(e.eta_s)}`:""]}),s.jsx("button",{onClick:()=>i(e.id),className:"text-[10px] text-red-400 hover:text-red-300 font-semibold border border-red-500/25 bg-red-500/5 px-2 py-0.5 rounded transition-all cursor-pointer",children:"Abbrechen"})]})]}),s.jsx("div",{className:"h-1.5 overflow-hidden rounded-full bg-muted",children:s.jsx("div",{className:"h-full rounded-full bg-primary transition-all duration-500",style:{width:`${e.progress??0}%`}})})]},e.id)),r.map(e=>s.jsxs("div",{className:"flex justify-between items-center text-xs text-muted-foreground px-1",children:[s.jsx("span",{className:"truncate",children:e.label}),s.jsx("span",{className:p("font-semibold text-[10px] px-1.5 py-0.5 rounded uppercase font-mono",e.state==="done"?"bg-emerald-500/10 text-emerald-400":"bg-amber-500/10 text-amber-400"),children:e.state})]},e.id)),o]})}export{y as J};
|
import{u,ag as m,k as x,j as s,y as n,ah as b,b as p,n as f,q as h}from"./index-l6kyKCL9.js";function y(){const d=u(),{data:t=[]}=m(),{showAlert:l,dialogElement:o}=x();async function i(e){try{await f(`/api/jobs/${e}/cancel`,{method:"POST"}),d.invalidateQueries({queryKey:h.jobs})}catch(c){l("Fehler",c.message)}}const a=t.filter(e=>e.state==="running"||e.state==="queued"),r=t.filter(e=>e.state!=="running"&&e.state!=="queued").slice(-3);return a.length===0&&r.length===0?null:s.jsxs("div",{className:"space-y-3 mc-card p-4",children:[s.jsx("div",{className:"text-[11px] font-semibold text-muted-foreground uppercase tracking-wider",children:"Aktive Downloads"}),a.map(e=>s.jsxs("div",{className:"space-y-1.5 p-3 rounded-xl bg-background/20 border border-border/40",children:[s.jsxs("div",{className:"flex justify-between items-center text-xs",children:[s.jsx("span",{className:"font-semibold truncate max-w-[250px]",children:e.label}),s.jsxs("div",{className:"flex items-center gap-3",children:[s.jsxs("span",{className:"text-muted-foreground font-mono",children:[e.progress??0,"% • ",n(e.done_bytes),"/",n(e.total_bytes),e.eta_s?` • ETA ${b(e.eta_s)}`:""]}),s.jsx("button",{onClick:()=>i(e.id),className:"text-[10px] text-red-400 hover:text-red-300 font-semibold border border-red-500/25 bg-red-500/5 px-2 py-0.5 rounded transition-all cursor-pointer",children:"Abbrechen"})]})]}),s.jsx("div",{className:"h-1.5 overflow-hidden rounded-full bg-muted",children:s.jsx("div",{className:"h-full rounded-full bg-primary transition-all duration-500",style:{width:`${e.progress??0}%`}})})]},e.id)),r.map(e=>s.jsxs("div",{className:"flex justify-between items-center text-xs text-muted-foreground px-1",children:[s.jsx("span",{className:"truncate",children:e.label}),s.jsx("span",{className:p("font-semibold text-[10px] px-1.5 py-0.5 rounded uppercase font-mono",e.state==="done"?"bg-emerald-500/10 text-emerald-400":"bg-amber-500/10 text-amber-400"),children:e.state})]},e.id)),o]})}export{y as J};
|
||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
import{M as h,u as g,N as p,r as d,j as e,b as x,T as j,S as N,n as v,Q as m,U as w,q as f}from"./index-C18iiJ44.js";import{E as k}from"./external-link-CKfa6ghp.js";import{R as y}from"./refresh-cw-Cqrl4nkL.js";function K(){const{data:t}=h(),u=g(),a=t!=null&&t.box_console_url?p(t.box_console_url):void 0,n=t==null?void 0:t.box_console_reachable,[o,i]=d.useState(!1),[c,l]=d.useState("");async function b(){i(!0),l("");try{const s=await v("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})}),r=s.ok?"Konsolen-Dienst neu gestartet — einen Moment, dann lädt das Terminal.":`Neustart fehlgeschlagen: ${s.err||"Unbekannter Fehler"}`;l(r),m(s.ok?"erfolg":"fehler",r),w(u,f.agentStatus,f.services)}catch(s){const r=`Neustart fehlgeschlagen: ${(s==null?void 0:s.message)||s}`;l(r),m("fehler",r)}finally{i(!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:x("h-2 w-2 rounded-full",n?"bg-emerald-500 animate-pulse":"bg-amber-500")}),n?"online":"offline"]}),a&&e.jsxs("a",{href:a,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(k,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),a?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:[n===!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(j,{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:b,disabled:o,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(y,{className:x("h-3.5 w-3.5",o&&"animate-spin")})," Dienst neu starten"]}),c&&e.jsx("p",{className:"max-w-sm text-[11px] text-muted-foreground",children:c})]}),e.jsx("iframe",{src:a,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{K as KonsoleView};
|
import{O as h,u as g,P as p,r as d,j as e,b as x,T as j,S as N,n as v,U as m,V as w,q as f}from"./index-l6kyKCL9.js";import{E as k}from"./external-link-ClFd8iN1.js";import{R as y}from"./refresh-cw-BV1qZMSR.js";function K(){const{data:t}=h(),u=g(),a=t!=null&&t.box_console_url?p(t.box_console_url):void 0,n=t==null?void 0:t.box_console_reachable,[o,i]=d.useState(!1),[c,l]=d.useState("");async function b(){i(!0),l("");try{const s=await v("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})}),r=s.ok?"Konsolen-Dienst neu gestartet — einen Moment, dann lädt das Terminal.":`Neustart fehlgeschlagen: ${s.err||"Unbekannter Fehler"}`;l(r),m(s.ok?"erfolg":"fehler",r),w(u,f.agentStatus,f.services)}catch(s){const r=`Neustart fehlgeschlagen: ${(s==null?void 0:s.message)||s}`;l(r),m("fehler",r)}finally{i(!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:x("h-2 w-2 rounded-full",n?"bg-emerald-500 animate-pulse":"bg-amber-500")}),n?"online":"offline"]}),a&&e.jsxs("a",{href:a,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(k,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),a?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:[n===!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(j,{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:b,disabled:o,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(y,{className:x("h-3.5 w-3.5",o&&"animate-spin")})," Dienst neu starten"]}),c&&e.jsx("p",{className:"max-w-sm text-[11px] text-muted-foreground",children:c})]}),e.jsx("iframe",{src:a,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{K as KonsoleView};
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as E,u as he,a as qe,r as v,j as e,C as Y,L as Fe,b as k,d as Re,q as V,R as Ie,B as re,e as Pe,E as ze,f as le,g as Be,h as Ae,i as Oe,k as Ge,l as ee,X as xe,H as be,T as se,s as Te,m as He,n as q,o as U,p as Qe,t as Ue,v as Ve,w as We,x as Ze,y as Je,z as Xe,A as Ye}from"./index-C18iiJ44.js";import{J as et}from"./JobsBar-BNKB17zh.js";import{C as tt}from"./code-xml-DSUsazUa.js";import{C as ue,Z as ie,L as rt,a as st}from"./zap-GT_FkD2B.js";import{S as ge}from"./search-BnlVCHow.js";import{P as nt}from"./power-DTqUyxxW.js";import{C as $e}from"./chevron-down-NnNHBt9Q.js";import{L as at}from"./layers-DJ-zOHed.js";/**
|
import{c as E,u as he,a as qe,r as v,j as e,C as Y,L as Fe,b as k,d as Re,q as V,R as Ie,B as re,e as Pe,E as ze,f as le,g as Be,h as Ae,i as Oe,k as Ge,l as ee,X as xe,H as be,T as se,s as Te,m as He,n as q,o as U,p as Qe,t as Ue,v as Ve,w as We,x as Ze,y as Je,z as Xe,A as Ye}from"./index-l6kyKCL9.js";import{J as et}from"./JobsBar-HflSaqor.js";import{C as tt}from"./code-xml-QQtsFWSM.js";import{C as ue,Z as ie,L as rt,a as st}from"./zap-CnZftvep.js";import{S as ge}from"./search-C1Zs6OOf.js";import{P as nt}from"./power-DcJoPDeQ.js";import{C as $e}from"./chevron-down-C5keVSfm.js";import{L as at}from"./layers-BFKjGswj.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,6 +0,0 @@
|
|||||||
import{c as s,j as e}from"./index-C18iiJ44.js";/**
|
|
||||||
* @license lucide-react v0.460.0 - ISC
|
|
||||||
*
|
|
||||||
* This source code is licensed under the ISC license.
|
|
||||||
* See the LICENSE file in the root directory of this source tree.
|
|
||||||
*/const r=s("Hammer",[["path",{d:"m15 12-8.373 8.373a1 1 0 1 1-3-3L12 9",key:"eefl8a"}],["path",{d:"m18 15 4-4",key:"16gjal"}],["path",{d:"m21.5 11.5-1.914-1.914A2 2 0 0 1 19 8.172V7l-2.26-2.26a6 6 0 0 0-4.202-1.756L9 2.96l.92.82A6.18 6.18 0 0 1 12 8.4V10l2 2h1.172a2 2 0 0 1 1.414.586L18.5 14.5",key:"b7pghm"}]]);function c({icon:a,children:t}){return e.jsxs("p",{className:"flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/60",children:[e.jsx(a,{className:"h-3.5 w-3.5"})," ",t]})}export{r as H,c as S};
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{j as e}from"./index-l6kyKCL9.js";function n({icon:t,children:s}){return e.jsxs("p",{className:"flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/60",children:[e.jsx(t,{className:"h-3.5 w-3.5"})," ",s]})}export{n as S};
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
var E=r=>{throw TypeError(r)};var O=(r,e,t)=>e.has(r)||E("Cannot "+t);var a=(r,e,t)=>(O(r,e,"read from private field"),t?t.call(r):e.get(r)),y=(r,e,t)=>e.has(r)?E("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(r):e.set(r,t),v=(r,e,t,n)=>(O(r,e,"write to private field"),n?n.call(r,t):e.set(r,t),t),j=(r,e,t)=>(O(r,e,"access private method"),t);import{a7 as A,a8 as B,a9 as M,aa as F,ab as P,u as J,r as f,ac as I,ad as Q,c as T,ae as U,j as s,J as D,T as H,b as K,L,O as V,q as _,n as R}from"./index-C18iiJ44.js";import{J as z}from"./JobsBar-BNKB17zh.js";var c,g,o,h,m,w,C,q,G=(q=class extends A{constructor(e,t){super();y(this,m);y(this,c);y(this,g);y(this,o);y(this,h);v(this,c,e),this.setOptions(t),this.bindMethods(),j(this,m,w).call(this)}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(e){var n;const t=this.options;this.options=a(this,c).defaultMutationOptions(e),B(this.options,t)||a(this,c).getMutationCache().notify({type:"observerOptionsUpdated",mutation:a(this,o),observer:this}),t!=null&&t.mutationKey&&this.options.mutationKey&&M(t.mutationKey)!==M(this.options.mutationKey)?this.reset():((n=a(this,o))==null?void 0:n.state.status)==="pending"&&a(this,o).setOptions(this.options)}onUnsubscribe(){var e;this.hasListeners()||(e=a(this,o))==null||e.removeObserver(this)}onMutationUpdate(e){j(this,m,w).call(this),j(this,m,C).call(this,e)}getCurrentResult(){return a(this,g)}reset(){var e;(e=a(this,o))==null||e.removeObserver(this),v(this,o,void 0),j(this,m,w).call(this),j(this,m,C).call(this)}mutate(e,t){var n;return v(this,h,t),(n=a(this,o))==null||n.removeObserver(this),v(this,o,a(this,c).getMutationCache().build(a(this,c),this.options)),a(this,o).addObserver(this),a(this,o).execute(e)}},c=new WeakMap,g=new WeakMap,o=new WeakMap,h=new WeakMap,m=new WeakSet,w=function(){var t;const e=((t=a(this,o))==null?void 0:t.state)??F();v(this,g,{...e,isPending:e.status==="pending",isSuccess:e.status==="success",isError:e.status==="error",isIdle:e.status==="idle",mutate:this.mutate,reset:this.reset})},C=function(e){P.batch(()=>{var t,n,l,x,d,p,k,i;if(a(this,h)&&this.hasListeners()){const u=a(this,g).variables,N=a(this,g).context,S={client:a(this,c),meta:this.options.meta,mutationKey:this.options.mutationKey};if((e==null?void 0:e.type)==="success"){try{(n=(t=a(this,h)).onSuccess)==null||n.call(t,e.data,u,N,S)}catch(b){Promise.reject(b)}try{(x=(l=a(this,h)).onSettled)==null||x.call(l,e.data,null,u,N,S)}catch(b){Promise.reject(b)}}else if((e==null?void 0:e.type)==="error"){try{(p=(d=a(this,h)).onError)==null||p.call(d,e.error,u,N,S)}catch(b){Promise.reject(b)}try{(i=(k=a(this,h)).onSettled)==null||i.call(k,void 0,e.error,u,N,S)}catch(b){Promise.reject(b)}}}this.listeners.forEach(u=>{u(a(this,g))})})},q);function W(r,e){const t=J(),[n]=f.useState(()=>new G(t,r));f.useEffect(()=>{n.setOptions(r)},[n,r]);const l=f.useSyncExternalStore(f.useCallback(d=>n.subscribe(P.batchCalls(d)),[n]),()=>n.getCurrentResult(),()=>n.getCurrentResult()),x=f.useCallback((d,p)=>{n.mutate(d,p).catch(I)},[n]);if(l.error&&Q(n.options.throwOnError,[l.error]))throw l.error;return{...l,mutate:x,mutateAsync:l.mutate}}/**
|
var E=r=>{throw TypeError(r)};var O=(r,e,t)=>e.has(r)||E("Cannot "+t);var a=(r,e,t)=>(O(r,e,"read from private field"),t?t.call(r):e.get(r)),y=(r,e,t)=>e.has(r)?E("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(r):e.set(r,t),v=(r,e,t,n)=>(O(r,e,"write to private field"),n?n.call(r,t):e.set(r,t),t),j=(r,e,t)=>(O(r,e,"access private method"),t);import{a8 as Q,a9 as A,aa as M,ab as B,ac as P,u as J,r as f,ad as F,ae as I,c as T,af as U,j as s,J as D,T as H,b as K,L,Q as V,q as _,n as R}from"./index-l6kyKCL9.js";import{J as z}from"./JobsBar-HflSaqor.js";var c,g,o,h,m,w,C,q,G=(q=class extends Q{constructor(e,t){super();y(this,m);y(this,c);y(this,g);y(this,o);y(this,h);v(this,c,e),this.setOptions(t),this.bindMethods(),j(this,m,w).call(this)}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(e){var n;const t=this.options;this.options=a(this,c).defaultMutationOptions(e),A(this.options,t)||a(this,c).getMutationCache().notify({type:"observerOptionsUpdated",mutation:a(this,o),observer:this}),t!=null&&t.mutationKey&&this.options.mutationKey&&M(t.mutationKey)!==M(this.options.mutationKey)?this.reset():((n=a(this,o))==null?void 0:n.state.status)==="pending"&&a(this,o).setOptions(this.options)}onUnsubscribe(){var e;this.hasListeners()||(e=a(this,o))==null||e.removeObserver(this)}onMutationUpdate(e){j(this,m,w).call(this),j(this,m,C).call(this,e)}getCurrentResult(){return a(this,g)}reset(){var e;(e=a(this,o))==null||e.removeObserver(this),v(this,o,void 0),j(this,m,w).call(this),j(this,m,C).call(this)}mutate(e,t){var n;return v(this,h,t),(n=a(this,o))==null||n.removeObserver(this),v(this,o,a(this,c).getMutationCache().build(a(this,c),this.options)),a(this,o).addObserver(this),a(this,o).execute(e)}},c=new WeakMap,g=new WeakMap,o=new WeakMap,h=new WeakMap,m=new WeakSet,w=function(){var t;const e=((t=a(this,o))==null?void 0:t.state)??B();v(this,g,{...e,isPending:e.status==="pending",isSuccess:e.status==="success",isError:e.status==="error",isIdle:e.status==="idle",mutate:this.mutate,reset:this.reset})},C=function(e){P.batch(()=>{var t,n,l,x,d,p,k,i;if(a(this,h)&&this.hasListeners()){const u=a(this,g).variables,N=a(this,g).context,S={client:a(this,c),meta:this.options.meta,mutationKey:this.options.mutationKey};if((e==null?void 0:e.type)==="success"){try{(n=(t=a(this,h)).onSuccess)==null||n.call(t,e.data,u,N,S)}catch(b){Promise.reject(b)}try{(x=(l=a(this,h)).onSettled)==null||x.call(l,e.data,null,u,N,S)}catch(b){Promise.reject(b)}}else if((e==null?void 0:e.type)==="error"){try{(p=(d=a(this,h)).onError)==null||p.call(d,e.error,u,N,S)}catch(b){Promise.reject(b)}try{(i=(k=a(this,h)).onSettled)==null||i.call(k,void 0,e.error,u,N,S)}catch(b){Promise.reject(b)}}}this.listeners.forEach(u=>{u(a(this,g))})})},q);function W(r,e){const t=J(),[n]=f.useState(()=>new G(t,r));f.useEffect(()=>{n.setOptions(r)},[n,r]);const l=f.useSyncExternalStore(f.useCallback(d=>n.subscribe(P.batchCalls(d)),[n]),()=>n.getCurrentResult(),()=>n.getCurrentResult()),x=f.useCallback((d,p)=>{n.mutate(d,p).catch(F)},[n]);if(l.error&&I(n.options.throwOnError,[l.error]))throw l.error;return{...l,mutate:x,mutateAsync:l.mutate}}/**
|
||||||
* @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
+10
-20
File diff suppressed because one or more lines are too long
+15
-15
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as r}from"./index-C18iiJ44.js";/**
|
import{c as r}from"./index-l6kyKCL9.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 o}from"./index-C18iiJ44.js";/**
|
import{c as o}from"./index-l6kyKCL9.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-C18iiJ44.js";/**
|
import{c}from"./index-l6kyKCL9.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.
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import{c as a}from"./index-l6kyKCL9.js";/**
|
||||||
|
* @license lucide-react v0.460.0 - ISC
|
||||||
|
*
|
||||||
|
* This source code is licensed under the ISC license.
|
||||||
|
* See the LICENSE file in the root directory of this source tree.
|
||||||
|
*/const c=a("Camera",[["path",{d:"M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z",key:"1tc9qg"}],["circle",{cx:"12",cy:"13",r:"3",key:"1vg3eu"}]]);/**
|
||||||
|
* @license lucide-react v0.460.0 - ISC
|
||||||
|
*
|
||||||
|
* This source code is licensed under the ISC license.
|
||||||
|
* See the LICENSE file in the root directory of this source tree.
|
||||||
|
*/const o=a("CloudDownload",[["path",{d:"M12 13v8l-4-4",key:"1f5nwf"}],["path",{d:"m12 21 4-4",key:"1lfcce"}],["path",{d:"M4.393 15.269A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.436 8.284",key:"ui1hmy"}]]);export{o as C,c as a};
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as e}from"./index-C18iiJ44.js";/**
|
import{c as e}from"./index-l6kyKCL9.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-C18iiJ44.js";/**
|
import{c as a}from"./index-l6kyKCL9.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 e}from"./index-C18iiJ44.js";/**
|
import{c as e}from"./index-l6kyKCL9.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.
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
import{c as a}from"./index-l6kyKCL9.js";/**
|
||||||
|
* @license lucide-react v0.460.0 - ISC
|
||||||
|
*
|
||||||
|
* This source code is licensed under the ISC license.
|
||||||
|
* See the LICENSE file in the root directory of this source tree.
|
||||||
|
*/const m=a("Hammer",[["path",{d:"m15 12-8.373 8.373a1 1 0 1 1-3-3L12 9",key:"eefl8a"}],["path",{d:"m18 15 4-4",key:"16gjal"}],["path",{d:"m21.5 11.5-1.914-1.914A2 2 0 0 1 19 8.172V7l-2.26-2.26a6 6 0 0 0-4.202-1.756L9 2.96l.92.82A6.18 6.18 0 0 1 12 8.4V10l2 2h1.172a2 2 0 0 1 1.414.586L18.5 14.5",key:"b7pghm"}]]);export{m as H};
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
import{ax as r}from"./index-C18iiJ44.js";var o=r();export{o as r};
|
|
||||||
-260
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
|||||||
|
import{aA as r}from"./index-l6kyKCL9.js";var o=r();export{o as r};
|
||||||
+1
File diff suppressed because one or more lines are too long
+269
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as a}from"./index-C18iiJ44.js";/**
|
import{c as a}from"./index-l6kyKCL9.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 o}from"./index-C18iiJ44.js";/**
|
import{c as o}from"./index-l6kyKCL9.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 e}from"./index-C18iiJ44.js";/**
|
import{c as e}from"./index-l6kyKCL9.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-C18iiJ44.js";/**
|
import{c}from"./index-l6kyKCL9.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-C18iiJ44.js";/**
|
import{c as a}from"./index-l6kyKCL9.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-C18iiJ44.js";/**
|
import{c as a}from"./index-l6kyKCL9.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
+2
-2
@@ -7,8 +7,8 @@
|
|||||||
<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-C18iiJ44.js"></script>
|
<script type="module" crossorigin src="/assets/index-l6kyKCL9.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DQUMn05v.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-d4K0juid.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>␍
|
<div id="root"></div>␍
|
||||||
|
|||||||
Generated
+102
-133
@@ -17,8 +17,8 @@
|
|||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"d3-force": "^3.0.0",
|
"d3-force": "^3.0.0",
|
||||||
"lucide-react": "^0.460.0",
|
"lucide-react": "^0.460.0",
|
||||||
"react": "^18.3.1",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^19.2.8",
|
||||||
"react-force-graph-2d": "^1.29.1",
|
"react-force-graph-2d": "^1.29.1",
|
||||||
"recharts": "^3.9.0",
|
"recharts": "^3.9.0",
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
@@ -32,8 +32,8 @@
|
|||||||
"@testing-library/user-event": "^14.6.6",
|
"@testing-library/user-event": "^14.6.6",
|
||||||
"@types/d3-force": "^3.0.10",
|
"@types/d3-force": "^3.0.10",
|
||||||
"@types/node": "^22.10.1",
|
"@types/node": "^22.10.1",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^19.2.18",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^19.2.5",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"eslint": "^10.9.1",
|
"eslint": "^10.9.1",
|
||||||
"eslint-plugin-react-hooks": "^7.1.1",
|
"eslint-plugin-react-hooks": "^7.1.1",
|
||||||
@@ -1285,15 +1285,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/primitive": {
|
"node_modules/@radix-ui/primitive": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz",
|
||||||
"integrity": "sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==",
|
"integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-compose-refs": {
|
"node_modules/@radix-ui/react-compose-refs": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz",
|
||||||
"integrity": "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==",
|
"integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -1306,9 +1306,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-context": {
|
"node_modules/@radix-ui/react-context": {
|
||||||
"version": "1.1.4",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.2.tgz",
|
||||||
"integrity": "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg==",
|
"integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -1321,23 +1321,24 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-dialog": {
|
"node_modules/@radix-ui/react-dialog": {
|
||||||
"version": "1.1.17",
|
"version": "1.1.23",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.17.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.23.tgz",
|
||||||
"integrity": "sha512-TDTYmpdq8dI2+Xgvgj9AJ8Ghqq+Eph/TRVEdaFQPDItIY+6QSkU7MJMeevw1568Yw/2Ijz8BTphPSP2XejKphw==",
|
"integrity": "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/primitive": "1.1.4",
|
"@radix-ui/primitive": "1.1.7",
|
||||||
"@radix-ui/react-compose-refs": "1.1.3",
|
"@radix-ui/react-compose-refs": "1.1.5",
|
||||||
"@radix-ui/react-context": "1.1.4",
|
"@radix-ui/react-context": "1.2.2",
|
||||||
"@radix-ui/react-dismissable-layer": "1.1.13",
|
"@radix-ui/react-dismissable-layer": "1.1.19",
|
||||||
"@radix-ui/react-focus-guards": "1.1.4",
|
"@radix-ui/react-focus-guards": "1.1.6",
|
||||||
"@radix-ui/react-focus-scope": "1.1.10",
|
"@radix-ui/react-focus-scope": "1.1.16",
|
||||||
"@radix-ui/react-id": "1.1.2",
|
"@radix-ui/react-id": "1.1.4",
|
||||||
"@radix-ui/react-portal": "1.1.12",
|
"@radix-ui/react-portal": "1.1.17",
|
||||||
"@radix-ui/react-presence": "1.1.6",
|
"@radix-ui/react-presence": "1.1.10",
|
||||||
"@radix-ui/react-primitive": "2.1.6",
|
"@radix-ui/react-primitive": "2.1.10",
|
||||||
"@radix-ui/react-slot": "1.3.0",
|
"@radix-ui/react-slot": "1.3.3",
|
||||||
"@radix-ui/react-use-controllable-state": "1.2.3",
|
"@radix-ui/react-use-controllable-state": "1.2.6",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.4",
|
||||||
"aria-hidden": "^1.2.4",
|
"aria-hidden": "^1.2.4",
|
||||||
"react-remove-scroll": "^2.7.2"
|
"react-remove-scroll": "^2.7.2"
|
||||||
},
|
},
|
||||||
@@ -1357,16 +1358,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-dismissable-layer": {
|
"node_modules/@radix-ui/react-dismissable-layer": {
|
||||||
"version": "1.1.13",
|
"version": "1.1.19",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.19.tgz",
|
||||||
"integrity": "sha512-2v+zNAWWe0ySxgC0D0yeXMPQ23xZVgXZTerTz+JKlmdRj6gfTqmCcR29jb6d290DezXPGgruHWDX/vYUebtErg==",
|
"integrity": "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/primitive": "1.1.4",
|
"@radix-ui/primitive": "1.1.7",
|
||||||
"@radix-ui/react-compose-refs": "1.1.3",
|
"@radix-ui/react-compose-refs": "1.1.5",
|
||||||
"@radix-ui/react-primitive": "2.1.6",
|
"@radix-ui/react-primitive": "2.1.10",
|
||||||
"@radix-ui/react-use-callback-ref": "1.1.2",
|
"@radix-ui/react-use-callback-ref": "1.1.4",
|
||||||
"@radix-ui/react-use-escape-keydown": "1.1.2"
|
"@radix-ui/react-use-effect-event": "0.0.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -1384,9 +1385,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-focus-guards": {
|
"node_modules/@radix-ui/react-focus-guards": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.6.tgz",
|
||||||
"integrity": "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==",
|
"integrity": "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -1399,14 +1400,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-focus-scope": {
|
"node_modules/@radix-ui/react-focus-scope": {
|
||||||
"version": "1.1.10",
|
"version": "1.1.16",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.10.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.16.tgz",
|
||||||
"integrity": "sha512-Fas/lXQqhVvqwAb64s5RFeHiHYElZ6SUQbZaNd6EkfhP/Al7wTIQ9WIR4QVX475tlu5yFCEdDcJH6/UwsZjMWw==",
|
"integrity": "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-compose-refs": "1.1.3",
|
"@radix-ui/react-compose-refs": "1.1.5",
|
||||||
"@radix-ui/react-primitive": "2.1.6",
|
"@radix-ui/react-primitive": "2.1.10",
|
||||||
"@radix-ui/react-use-callback-ref": "1.1.2"
|
"@radix-ui/react-use-callback-ref": "1.1.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -1424,12 +1425,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-id": {
|
"node_modules/@radix-ui/react-id": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz",
|
||||||
"integrity": "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==",
|
"integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-use-layout-effect": "1.1.2"
|
"@radix-ui/react-use-layout-effect": "1.1.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -1442,13 +1443,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-portal": {
|
"node_modules/@radix-ui/react-portal": {
|
||||||
"version": "1.1.12",
|
"version": "1.1.17",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.17.tgz",
|
||||||
"integrity": "sha512-m309havGzsjLHHaIX50G5PlvRs3xkgPCsGk/5PTvYm8D5q33yG0J7w/712PTOhid7NTaFETtnSXjngHQavvhVw==",
|
"integrity": "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-primitive": "2.1.6",
|
"@radix-ui/react-primitive": "2.1.10",
|
||||||
"@radix-ui/react-use-layout-effect": "1.1.2"
|
"@radix-ui/react-use-layout-effect": "1.1.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -1466,12 +1467,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-presence": {
|
"node_modules/@radix-ui/react-presence": {
|
||||||
"version": "1.1.6",
|
"version": "1.1.10",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.10.tgz",
|
||||||
"integrity": "sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==",
|
"integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-use-layout-effect": "1.1.2"
|
"@radix-ui/react-use-layout-effect": "1.1.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -1489,12 +1490,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-primitive": {
|
"node_modules/@radix-ui/react-primitive": {
|
||||||
"version": "2.1.6",
|
"version": "2.1.10",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz",
|
||||||
"integrity": "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g==",
|
"integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-slot": "1.3.0"
|
"@radix-ui/react-slot": "1.3.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -1512,12 +1513,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-slot": {
|
"node_modules/@radix-ui/react-slot": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz",
|
||||||
"integrity": "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==",
|
"integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-compose-refs": "1.1.3"
|
"@radix-ui/react-compose-refs": "1.1.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -1530,9 +1531,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz",
|
||||||
"integrity": "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==",
|
"integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -1545,13 +1546,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-use-controllable-state": {
|
"node_modules/@radix-ui/react-use-controllable-state": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.6.tgz",
|
||||||
"integrity": "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==",
|
"integrity": "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-use-effect-event": "0.0.3",
|
"@radix-ui/primitive": "1.1.7",
|
||||||
"@radix-ui/react-use-layout-effect": "1.1.2"
|
"@radix-ui/react-use-effect-event": "0.0.5",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -1564,30 +1566,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-use-effect-event": {
|
"node_modules/@radix-ui/react-use-effect-event": {
|
||||||
"version": "0.0.3",
|
"version": "0.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz",
|
||||||
"integrity": "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==",
|
"integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-use-layout-effect": "1.1.2"
|
"@radix-ui/react-use-layout-effect": "1.1.4"
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": "*",
|
|
||||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@radix-ui/react-use-escape-keydown": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.2.tgz",
|
|
||||||
"integrity": "sha512-2uVLvLjgO7NZCWw01/FdqRwmA42J0BcjPMUCA+koFEOAb+zjqIP7SiFz/7zWPrKnVmSqr76Omq2ALyCuX4dhLw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@radix-ui/react-use-callback-ref": "1.1.2"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -1600,9 +1584,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-use-layout-effect": {
|
"node_modules/@radix-ui/react-use-layout-effect": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz",
|
||||||
"integrity": "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==",
|
"integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -2728,32 +2712,24 @@
|
|||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/prop-types": {
|
|
||||||
"version": "15.7.15",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
|
||||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "18.3.31",
|
"version": "19.2.18",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
|
||||||
"integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
|
"integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/react-dom": {
|
"node_modules/@types/react-dom": {
|
||||||
"version": "18.3.7",
|
"version": "19.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.5.tgz",
|
||||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
"integrity": "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^18.0.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/use-sync-external-store": {
|
"node_modules/@types/use-sync-external-store": {
|
||||||
@@ -5351,28 +5327,24 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "18.3.1",
|
"version": "19.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
|
||||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
"integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
|
||||||
"loose-envify": "^1.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "18.3.1",
|
"version": "19.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
|
||||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
"integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"scheduler": "^0.27.0"
|
||||||
"scheduler": "^0.23.2"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^18.3.1"
|
"react": "^19.2.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-force-graph-2d": {
|
"node_modules/react-force-graph-2d": {
|
||||||
@@ -5650,13 +5622,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/scheduler": {
|
"node_modules/scheduler": {
|
||||||
"version": "0.23.2",
|
"version": "0.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||||
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"dependencies": {
|
|
||||||
"loose-envify": "^1.1.0"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
|
|||||||
@@ -22,8 +22,8 @@
|
|||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"d3-force": "^3.0.0",
|
"d3-force": "^3.0.0",
|
||||||
"lucide-react": "^0.460.0",
|
"lucide-react": "^0.460.0",
|
||||||
"react": "^18.3.1",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^19.2.8",
|
||||||
"react-force-graph-2d": "^1.29.1",
|
"react-force-graph-2d": "^1.29.1",
|
||||||
"recharts": "^3.9.0",
|
"recharts": "^3.9.0",
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
@@ -37,8 +37,8 @@
|
|||||||
"@testing-library/user-event": "^14.6.6",
|
"@testing-library/user-event": "^14.6.6",
|
||||||
"@types/d3-force": "^3.0.10",
|
"@types/d3-force": "^3.0.10",
|
||||||
"@types/node": "^22.10.1",
|
"@types/node": "^22.10.1",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^19.2.18",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^19.2.5",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"eslint": "^10.9.1",
|
"eslint": "^10.9.1",
|
||||||
"eslint-plugin-react-hooks": "^7.1.1",
|
"eslint-plugin-react-hooks": "^7.1.1",
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest"
|
||||||
|
import { befehle, suchtext, type BefehlsKontext } from "./register"
|
||||||
|
import { NAV } from "@/nav"
|
||||||
|
|
||||||
|
function kontext(ueberschreiben: Partial<BefehlsKontext> = {}): BefehlsKontext {
|
||||||
|
return {
|
||||||
|
text: "",
|
||||||
|
navigate: vi.fn(),
|
||||||
|
schublade: vi.fn(),
|
||||||
|
melden: vi.fn(),
|
||||||
|
api: vi.fn(async () => ({}) as never),
|
||||||
|
...ueberschreiben,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Befehls-Register", () => {
|
||||||
|
it("führt jedes Navigations-Ziel auf — genau einmal", () => {
|
||||||
|
const ziele = befehle("").filter((b) => b.gruppe === "gehe")
|
||||||
|
expect(ziele).toHaveLength(NAV.length)
|
||||||
|
expect(new Set(ziele.map((z) => z.id)).size).toBe(NAV.length)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("zeigt Suche und „An Lucy“ erst, wenn etwas getippt ist", () => {
|
||||||
|
const leer = befehle("")
|
||||||
|
expect(leer.some((b) => b.gruppe === "finden")).toBe(false)
|
||||||
|
expect(leer.some((b) => b.gruppe === "lucy")).toBe(false)
|
||||||
|
|
||||||
|
const getippt = befehle("Wissensgraph-Ansicht bauen")
|
||||||
|
expect(getippt.filter((b) => b.gruppe === "finden")).toHaveLength(2)
|
||||||
|
expect(getippt.filter((b) => b.gruppe === "lucy")).toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Unter acht Zeichen ist es kein Auftrag, sondern ein Tippfehler auf dem Weg zu
|
||||||
|
// einem Sprungziel. Sonst böte die Palette bei „mod" an, daraus eine Idee zu machen.
|
||||||
|
it("bietet kurze Eingaben NICHT als Idee an", () => {
|
||||||
|
expect(befehle("mod").some((b) => b.gruppe === "lucy")).toBe(false)
|
||||||
|
expect(befehle("modelle").some((b) => b.gruppe === "lucy")).toBe(false)
|
||||||
|
expect(befehle("modellwechsel").some((b) => b.gruppe === "lucy")).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── Die Sicherheitsregel ──────────────────────────────────────────────────
|
||||||
|
// Die Palette ist schnell genug, dass „Strg+K, mot, Enter" einen Dienst neu startet,
|
||||||
|
// bevor man es zu Ende gedacht hat. Alles Eingreifende MUSS deshalb `rueckfrage`
|
||||||
|
// tragen — die Oberfläche hält dann an und sagt erst, was passieren wird.
|
||||||
|
it("verlangt für jeden eingreifenden Befehl eine Rückfrage", () => {
|
||||||
|
const eingreifend = /neu starten|löschen|loeschen|deploy|zurücksetzen|reboot|entfernen/i
|
||||||
|
for (const b of befehle("etwas getipptes")) {
|
||||||
|
if (eingreifend.test(b.label)) {
|
||||||
|
expect(b.rueckfrage, `„${b.label}" muss nachfragen`).toBeTruthy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("nennt in der Rückfrage die Folge, nicht nur die Aktion", () => {
|
||||||
|
const motor = befehle("").find((b) => b.id === "tun:engine-neustart")!
|
||||||
|
expect(motor.rueckfrage).toBeTruthy()
|
||||||
|
// Der Text muss sagen, was der Nutzer DANACH merkt.
|
||||||
|
expect(motor.rueckfrage).toMatch(/Modelle/)
|
||||||
|
expect(motor.rueckfrage).toMatch(/aufwärmen|abbrechen|abbrechen/i)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("lässt harmlose Befehle ohne Rückfrage durch", () => {
|
||||||
|
const wartung = befehle("").find((b) => b.id === "tun:wartung")!
|
||||||
|
expect(wartung.rueckfrage).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── Verhalten einzelner Befehle ───────────────────────────────────────────
|
||||||
|
it("öffnet die Schublade über die URL, nicht über ein Ereignis", () => {
|
||||||
|
const ctx = kontext()
|
||||||
|
befehle("").find((b) => b.id === "tun:logs")!.ausfuehren(ctx)
|
||||||
|
expect(ctx.schublade).toHaveBeenCalledWith("logs")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("erfasst eine Idee zum Durchdenken — und startet keinen Bau", async () => {
|
||||||
|
const ctx = kontext({ text: "Wissensgraph-Ansicht bauen" })
|
||||||
|
await befehle(ctx.text).find((b) => b.gruppe === "lucy")!.ausfuehren(ctx)
|
||||||
|
|
||||||
|
expect(ctx.api).toHaveBeenCalledTimes(1)
|
||||||
|
const [pfad, init] = (ctx.api as ReturnType<typeof vi.fn>).mock.calls[0]
|
||||||
|
expect(pfad).toBe("/api/ideen")
|
||||||
|
const rumpf = JSON.parse((init as RequestInit).body as string)
|
||||||
|
// `art: "idee"` heisst durchdenken. `"projekt"` wuerde sofort ein Repo anlegen —
|
||||||
|
// das darf die Palette niemals aus einem Tastendruck heraus tun.
|
||||||
|
expect(rumpf.art).toBe("idee")
|
||||||
|
expect(rumpf.titel).toBe("Wissensgraph-Ansicht bauen")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("kürzt einen überlangen Ideen-Titel und hebt den vollen Text in die Notiz", async () => {
|
||||||
|
const lang = "a".repeat(200)
|
||||||
|
const ctx = kontext({ text: lang })
|
||||||
|
await befehle(lang).find((b) => b.gruppe === "lucy")!.ausfuehren(ctx)
|
||||||
|
const rumpf = JSON.parse(
|
||||||
|
((ctx.api as ReturnType<typeof vi.fn>).mock.calls[0][1] as RequestInit).body as string,
|
||||||
|
)
|
||||||
|
expect(rumpf.titel.length).toBeLessThanOrEqual(160)
|
||||||
|
expect(rumpf.notiz).toBe(lang)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("packt Suchwörter in den Suchtext, damit cmdk sie findet", () => {
|
||||||
|
const wartung = befehle("").find((b) => b.id === "tun:wartung")!
|
||||||
|
expect(suchtext(wartung)).toMatch(/neustart/) // steht nur in den Stichworten
|
||||||
|
expect(suchtext(wartung)).toMatch(/System-Wartung/)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
import type { LucideIcon } from "lucide-react"
|
||||||
|
import {
|
||||||
|
Camera, CloudDownload, FileText, Lightbulb, RefreshCw, ScrollText, Search, Wrench,
|
||||||
|
} from "lucide-react"
|
||||||
|
import { NAV } from "@/nav"
|
||||||
|
|
||||||
|
// Befehls-Register der Palette (v3-Umbau P6, Spezifikation §4.6).
|
||||||
|
//
|
||||||
|
// Bis hierher listete die Palette die elf Navigations-Einträge — sie war ein Sprungbrett.
|
||||||
|
// Jetzt ist sie das primäre Bedienwerkzeug: Man tippt, was man will, nicht wo es liegt.
|
||||||
|
//
|
||||||
|
// VIER KATEGORIEN, in der Reihenfolge, in der man sie braucht:
|
||||||
|
// Gehe zu Sprungziele (wie bisher)
|
||||||
|
// Tun Aktionen — Dienste, Modelle, Wartung
|
||||||
|
// Finden Suche in Chronik und Wissens-Vault
|
||||||
|
// An Lucy den getippten Text direkt als Idee erfassen
|
||||||
|
//
|
||||||
|
// SICHERHEITSREGEL (aus der Spezifikation, hier hart verdrahtet): Alles, was neu startet,
|
||||||
|
// löscht oder deployt, trägt `rueckfrage` und wird zweistufig bestätigt. Die Palette ist
|
||||||
|
// zu schnell für einen unbedachten Treffer — zwei Tastendrücke und ein Dienst wäre weg.
|
||||||
|
|
||||||
|
export type BefehlsGruppe = "gehe" | "tun" | "finden" | "lucy"
|
||||||
|
|
||||||
|
export interface Befehl {
|
||||||
|
id: string
|
||||||
|
gruppe: BefehlsGruppe
|
||||||
|
label: string
|
||||||
|
hinweis?: string
|
||||||
|
icon: LucideIcon
|
||||||
|
/** Zusätzliche Wörter für die Suche — was der Nutzer tippen könnte, aber nicht dasteht. */
|
||||||
|
stichworte?: string
|
||||||
|
/** Gesetzt = zweistufige Rückfrage mit diesem Text, bevor irgendetwas passiert. */
|
||||||
|
rueckfrage?: string
|
||||||
|
/** Was der Befehl tut. `text` ist die aktuelle Eingabe der Palette. */
|
||||||
|
ausfuehren: (ctx: BefehlsKontext) => void | Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BefehlsKontext {
|
||||||
|
text: string
|
||||||
|
navigate: (pfad: string) => void
|
||||||
|
schublade: (tab: "wartung" | "logs") => void
|
||||||
|
melden: (art: "info" | "erfolg" | "warnung" | "fehler", text: string) => void
|
||||||
|
api: <T>(pfad: string, init?: RequestInit) => Promise<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GRUPPEN_TITEL: Record<BefehlsGruppe, string> = {
|
||||||
|
gehe: "Gehe zu",
|
||||||
|
tun: "Tun",
|
||||||
|
finden: "Finden",
|
||||||
|
lucy: "An Lucy",
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sprungziele — direkt aus der Navigations-Tabelle, damit es nie zwei Listen gibt. */
|
||||||
|
const ZIELE: Befehl[] = NAV.map((n) => ({
|
||||||
|
id: `gehe:${n.id}`,
|
||||||
|
gruppe: "gehe" as const,
|
||||||
|
label: n.label,
|
||||||
|
hinweis: n.hint,
|
||||||
|
icon: n.icon,
|
||||||
|
ausfuehren: ({ navigate }) => navigate(n.pfad),
|
||||||
|
}))
|
||||||
|
|
||||||
|
/** Aktionen. Bewusst wenige: nur was man wirklich aus dem Stand heraus tut. */
|
||||||
|
const AKTIONEN: Befehl[] = [
|
||||||
|
{
|
||||||
|
id: "tun:wartung",
|
||||||
|
gruppe: "tun",
|
||||||
|
label: "System-Wartung öffnen",
|
||||||
|
hinweis: "Dienste, Updates, Snapshot",
|
||||||
|
icon: Wrench,
|
||||||
|
stichworte: "pflege dienste neustart update",
|
||||||
|
ausfuehren: ({ schublade }) => schublade("wartung"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tun:logs",
|
||||||
|
gruppe: "tun",
|
||||||
|
label: "System-Logs öffnen",
|
||||||
|
hinweis: "Diagnose je Dienst",
|
||||||
|
icon: ScrollText,
|
||||||
|
stichworte: "fehler journal diagnose",
|
||||||
|
ausfuehren: ({ schublade }) => schublade("logs"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tun:updates",
|
||||||
|
gruppe: "tun",
|
||||||
|
label: "Nach Updates suchen",
|
||||||
|
hinweis: "apt + Engine + Agent prüfen",
|
||||||
|
icon: CloudDownload,
|
||||||
|
stichworte: "aktualisieren apt engine",
|
||||||
|
ausfuehren: async ({ api, melden }) => {
|
||||||
|
await api("/api/maintenance/check-updates", { method: "POST" })
|
||||||
|
melden("info", "Update-Suche läuft — das Ergebnis erscheint in der Wartung.")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tun:snapshot",
|
||||||
|
gruppe: "tun",
|
||||||
|
label: "Snapshot anlegen",
|
||||||
|
hinweis: "Sicherung vor riskanten Schritten",
|
||||||
|
icon: Camera,
|
||||||
|
stichworte: "backup sicherung zeitmaschine",
|
||||||
|
ausfuehren: async ({ api, melden }) => {
|
||||||
|
await api("/api/system/backup", { method: "POST" })
|
||||||
|
melden("erfolg", "Snapshot angestoßen.")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tun:engine-neustart",
|
||||||
|
gruppe: "tun",
|
||||||
|
label: "Motor neu starten",
|
||||||
|
hinweis: "llama-swap — alle Modelle fallen kurz weg",
|
||||||
|
icon: RefreshCw,
|
||||||
|
stichworte: "engine llama swap modell neu starten",
|
||||||
|
// Zweistufig: Ein Neustart wirft jedes geladene Modell aus dem Speicher, und ein
|
||||||
|
// grosses Modell braucht danach ueber 20 Sekunden zum Aufwaermen.
|
||||||
|
rueckfrage:
|
||||||
|
"Der Motor (llama-swap) wird neu gestartet. Alle geladenen Modelle fallen dabei aus " +
|
||||||
|
"dem Speicher und müssen neu aufwärmen — das dauert je nach Modell über 20 Sekunden. " +
|
||||||
|
"Laufende Antworten brechen ab.",
|
||||||
|
ausfuehren: async ({ api, melden }) => {
|
||||||
|
const r = await api<{ ok: boolean; err?: string }>("/api/maintenance/restart", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ service: "llama-swap" }),
|
||||||
|
})
|
||||||
|
melden(r.ok ? "erfolg" : "fehler",
|
||||||
|
r.ok ? "Motor neu gestartet." : `Neustart fehlgeschlagen: ${r.err || "Unbekannter Fehler"}`)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
/** Suchen — führen in die jeweilige Ansicht, mit dem Suchwort in der URL. */
|
||||||
|
const SUCHEN = (text: string): Befehl[] => {
|
||||||
|
const q = text.trim()
|
||||||
|
if (!q) return []
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "finden:chronik",
|
||||||
|
gruppe: "finden",
|
||||||
|
label: `„${q}“ in der Chronik`,
|
||||||
|
hinweis: "Was die Box getan hat",
|
||||||
|
icon: Search,
|
||||||
|
ausfuehren: ({ navigate }) => navigate(`/chronik?q=${encodeURIComponent(q)}`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "finden:wissen",
|
||||||
|
gruppe: "finden",
|
||||||
|
label: `„${q}“ im Wissens-Vault`,
|
||||||
|
hinweis: "Lucys Notizen",
|
||||||
|
icon: FileText,
|
||||||
|
ausfuehren: ({ navigate }) => navigate(`/wissen?suche=${encodeURIComponent(q)}`),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An Lucy — den getippten Text als Idee erfassen. Erfasst nur, startet nichts. */
|
||||||
|
const AN_LUCY = (text: string): Befehl[] => {
|
||||||
|
const q = text.trim()
|
||||||
|
if (q.length < 8) return [] // unter acht Zeichen ist es kein Auftrag, sondern ein Tippfehler
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "lucy:idee",
|
||||||
|
gruppe: "lucy",
|
||||||
|
label: `Als Idee erfassen: „${q.length > 60 ? q.slice(0, 57) + "…" : q}“`,
|
||||||
|
hinweis: "Wird nur notiert — gebaut wird erst auf deinen Klick",
|
||||||
|
icon: Lightbulb,
|
||||||
|
ausfuehren: async ({ api, melden, navigate }) => {
|
||||||
|
// Rumpf exakt wie im Ideen-Verfasser (routers/ideen.py → IdeeIn). `art: "idee"`
|
||||||
|
// heisst durchdenken, nicht bauen — die Palette startet nie einen Bau.
|
||||||
|
await api("/api/ideen", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
titel: q.length > 160 ? q.slice(0, 157) + "…" : q,
|
||||||
|
notiz: q.length > 160 ? q : "",
|
||||||
|
welt: "ide",
|
||||||
|
tiefe: "",
|
||||||
|
ziel: "",
|
||||||
|
art: "idee",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
melden("erfolg", "Als Idee erfasst.")
|
||||||
|
navigate("/ideen")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Das vollständige Register für die aktuelle Eingabe. */
|
||||||
|
export function befehle(text: string): Befehl[] {
|
||||||
|
return [...ZIELE, ...AKTIONEN, ...SUCHEN(text), ...AN_LUCY(text)]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Suchtext eines Befehls — cmdk filtert darüber. */
|
||||||
|
export const suchtext = (b: Befehl) =>
|
||||||
|
[b.label, b.hinweis, b.stichworte, GRUPPEN_TITEL[b.gruppe]].filter(Boolean).join(" ")
|
||||||
@@ -101,7 +101,20 @@ function CockpitSeite() {
|
|||||||
const cockpitRoute = ansicht("/cockpit", () => <CockpitSeite />)
|
const cockpitRoute = ansicht("/cockpit", () => <CockpitSeite />)
|
||||||
const auftraegeRoute = ansicht("/auftraege", () => <AuftragsbuchView />)
|
const auftraegeRoute = ansicht("/auftraege", () => <AuftragsbuchView />)
|
||||||
const skillsRoute = ansicht("/skills", () => <SkillsView />)
|
const skillsRoute = ansicht("/skills", () => <SkillsView />)
|
||||||
const chronikRoute = ansicht("/chronik", () => <ChronikView />)
|
/** Chronik: `?q=` filtert den Verlauf — Ziel der „Finden"-Einträge der Palette (P6). */
|
||||||
|
export interface ChronikSuche {
|
||||||
|
q?: string
|
||||||
|
}
|
||||||
|
const chronikRoute = createRoute({
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
path: "/chronik",
|
||||||
|
validateSearch: (roh: Record<string, unknown>): ChronikSuche =>
|
||||||
|
typeof roh.q === "string" && roh.q ? { q: roh.q } : {},
|
||||||
|
component: () => <ChronikView />,
|
||||||
|
errorComponent: ({ error, reset }) => (
|
||||||
|
<RouteFehler titel="Die Chronik ist abgestürzt" text="Der Rest läuft weiter." fehler={error} onErneut={reset} />
|
||||||
|
),
|
||||||
|
})
|
||||||
const verbindenRoute = ansicht("/verbinden", () => <ConnectView />)
|
const verbindenRoute = ansicht("/verbinden", () => <ConnectView />)
|
||||||
const konsoleRoute = ansicht("/konsole", () => <KonsoleView />)
|
const konsoleRoute = ansicht("/konsole", () => <KonsoleView />)
|
||||||
const anleitungRoute = ansicht("/anleitung", () => <GuideView />)
|
const anleitungRoute = ansicht("/anleitung", () => <GuideView />)
|
||||||
@@ -124,15 +137,19 @@ const ideenRoute = createRoute({
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
/** Wissen: `?datei=<pfad>` macht jede Vault-Notiz verlinkbar. */
|
/** Wissen: `?datei=` macht jede Notiz verlinkbar, `?suche=` trägt das Suchwort
|
||||||
|
* aus der Befehlspalette herein (P6). */
|
||||||
export interface WissenSuche {
|
export interface WissenSuche {
|
||||||
datei?: string
|
datei?: string
|
||||||
|
suche?: string
|
||||||
}
|
}
|
||||||
const wissenRoute = createRoute({
|
const wissenRoute = createRoute({
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
path: "/wissen",
|
path: "/wissen",
|
||||||
validateSearch: (roh: Record<string, unknown>): WissenSuche =>
|
validateSearch: (roh: Record<string, unknown>): WissenSuche => ({
|
||||||
typeof roh.datei === "string" && roh.datei ? { datei: roh.datei } : {},
|
...(typeof roh.datei === "string" && roh.datei ? { datei: roh.datei } : {}),
|
||||||
|
...(typeof roh.suche === "string" && roh.suche ? { suche: roh.suche } : {}),
|
||||||
|
}),
|
||||||
component: () => <WissenView />,
|
component: () => <WissenView />,
|
||||||
errorComponent: ({ error, reset }) => (
|
errorComponent: ({ error, reset }) => (
|
||||||
<RouteFehler titel="Die Wissens-Ansicht ist abgestürzt" text="Der Rest läuft weiter." fehler={error} onErneut={reset} />
|
<RouteFehler titel="Die Wissens-Ansicht ist abgestürzt" text="Der Rest läuft weiter." fehler={error} onErneut={reset} />
|
||||||
|
|||||||
@@ -1,72 +1,160 @@
|
|||||||
import { useEffect, useState } from "react"
|
import { useCallback, useMemo, useState } from "react"
|
||||||
import { Command } from "cmdk"
|
import { Command } from "cmdk"
|
||||||
import { useNavigate } from "@tanstack/react-router"
|
import { useNavigate } from "@tanstack/react-router"
|
||||||
import { NAV } from "@/nav"
|
import { AlertTriangle, CornerDownLeft } from "lucide-react"
|
||||||
|
import { useMC, usePaletteOffen } from "@/app/store"
|
||||||
|
import { api } from "@/lib/api"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import {
|
||||||
|
GRUPPEN_TITEL, befehle, suchtext,
|
||||||
|
type Befehl, type BefehlsGruppe,
|
||||||
|
} from "@/app/palette/register"
|
||||||
|
|
||||||
|
// Befehlspalette 2.0 (v3-Umbau P6, Spezifikation §4.6).
|
||||||
|
//
|
||||||
|
// Vorher listete sie die elf Navigations-Einträge — ein Sprungbrett. Jetzt tippt man,
|
||||||
|
// WAS man will, nicht wo es liegt: springen, tun, finden, oder den Text direkt als Idee
|
||||||
|
// an Lucy geben. Das Register steht in app/palette/register.ts.
|
||||||
|
//
|
||||||
|
// Die Rückfrage ist keine Höflichkeit, sondern Absicht: Die Palette ist schnell genug,
|
||||||
|
// dass „Strg+K, mot, Enter" den Motor neu startet, bevor man es zu Ende gedacht hat.
|
||||||
|
// Befehle mit `rueckfrage` halten deshalb an und sagen erst, was passieren wird.
|
||||||
|
|
||||||
|
const GRUPPEN_REIHE: BefehlsGruppe[] = ["gehe", "tun", "finden", "lucy"]
|
||||||
|
|
||||||
export function CommandPalette() {
|
export function CommandPalette() {
|
||||||
const [open, setOpen] = useState(false)
|
const offen = usePaletteOffen()
|
||||||
|
const palette = useMC((s) => s.palette)
|
||||||
|
const melden = useMC((s) => s.melden)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
useEffect(() => {
|
const [text, setText] = useState("")
|
||||||
const onKey = (e: KeyboardEvent) => {
|
const [nachfrage, setNachfrage] = useState<Befehl | null>(null)
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
|
const [laeuft, setLaeuft] = useState(false)
|
||||||
e.preventDefault()
|
|
||||||
setOpen((o) => !o)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.addEventListener("keydown", onKey)
|
|
||||||
return () => document.removeEventListener("keydown", onKey)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Der „Suchen"-Knopf in der Kopfzeile (v3-Umbau P2, Befund B-09).
|
const schliessen = useCallback(() => {
|
||||||
// Er baute vorher ein GEFÄLSCHTES Tastatur-Ereignis (`new KeyboardEvent("keydown",
|
palette(false)
|
||||||
// { metaKey: true })`) und feuerte es aufs Dokument, damit der Zuhörer oben anspringt.
|
setText("")
|
||||||
// Jetzt ein eigener, ehrlicher Kanal. In P3 löst der Store auch den ab.
|
setNachfrage(null)
|
||||||
useEffect(() => {
|
}, [palette])
|
||||||
const onOpen = () => setOpen(true)
|
|
||||||
window.addEventListener("mc-palette-open", onOpen)
|
const ausfuehren = useCallback(async (b: Befehl) => {
|
||||||
return () => window.removeEventListener("mc-palette-open", onOpen)
|
setLaeuft(true)
|
||||||
}, [])
|
try {
|
||||||
|
await b.ausfuehren({
|
||||||
|
text,
|
||||||
|
navigate: (pfad) => navigate({ to: pfad }),
|
||||||
|
schublade: (tab) => navigate({ to: ".", search: (a) => ({ ...a, system: tab }) }),
|
||||||
|
melden,
|
||||||
|
api,
|
||||||
|
})
|
||||||
|
schliessen()
|
||||||
|
} catch (e) {
|
||||||
|
// Der echte Grund aus ApiError.detail (P1/B-14) — nicht „Fehler".
|
||||||
|
melden("fehler", e instanceof Error ? e.message : String(e))
|
||||||
|
setNachfrage(null)
|
||||||
|
} finally {
|
||||||
|
setLaeuft(false)
|
||||||
|
}
|
||||||
|
}, [text, navigate, melden, schliessen])
|
||||||
|
|
||||||
|
const liste = useMemo(() => befehle(text), [text])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Command.Dialog
|
<Command.Dialog
|
||||||
open={open}
|
open={offen}
|
||||||
onOpenChange={setOpen}
|
onOpenChange={(o) => (o ? palette(true) : schliessen())}
|
||||||
label="Befehlspalette"
|
label="Befehlspalette"
|
||||||
className="fixed inset-0 z-50 flex items-start justify-center bg-black/50 p-4 pt-[12vh] overscroll-contain"
|
className="fixed inset-0 z-50 flex items-start justify-center overscroll-contain bg-black/50 p-4 pt-[12vh]"
|
||||||
onClick={() => setOpen(false)}
|
onClick={schliessen}
|
||||||
|
shouldFilter={!nachfrage}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="w-full max-w-lg overflow-hidden rounded-xl border border-border bg-popover text-popover-foreground shadow-2xl"
|
className="w-full max-w-xl overflow-hidden rounded-xl border border-border bg-popover text-popover-foreground shadow-2xl"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
|
{nachfrage ? (
|
||||||
|
// ── Zweistufige Rückfrage ────────────────────────────────────────
|
||||||
|
<div className="space-y-4 p-5">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<AlertTriangle className="h-5 w-5 shrink-0 text-amber-400" aria-hidden="true" />
|
||||||
|
<h2 className="font-space text-sm font-bold tracking-tight">{nachfrage.label}</h2>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs leading-relaxed text-muted-foreground">{nachfrage.rueckfrage}</p>
|
||||||
|
<div className="flex justify-end gap-2 pt-1">
|
||||||
|
<button
|
||||||
|
onClick={() => setNachfrage(null)}
|
||||||
|
className="h-9 cursor-pointer rounded-lg border border-border/60 bg-background/20 px-4 text-xs font-semibold text-muted-foreground transition-colors hover:bg-accent"
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
autoFocus
|
||||||
|
disabled={laeuft}
|
||||||
|
onClick={() => ausfuehren(nachfrage)}
|
||||||
|
className="h-9 cursor-pointer rounded-lg bg-amber-500/90 px-4 text-xs font-bold text-black transition-colors hover:bg-amber-500 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{laeuft ? "Läuft …" : "Ja, ausführen"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<Command.Input
|
<Command.Input
|
||||||
autoFocus
|
autoFocus
|
||||||
placeholder="Springe zu… (Modelle, Routing, System …)"
|
value={text}
|
||||||
|
onValueChange={setText}
|
||||||
|
placeholder="Springen, tun, finden — oder schreib eine Idee …"
|
||||||
className="w-full border-b border-border bg-transparent px-4 py-3 text-sm outline-none placeholder:text-muted-foreground"
|
className="w-full border-b border-border bg-transparent px-4 py-3 text-sm outline-none placeholder:text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
<Command.List className="max-h-80 overflow-y-auto p-2">
|
<Command.List className="max-h-[26rem] overflow-y-auto p-2 scrollbar-thin">
|
||||||
<Command.Empty className="px-3 py-6 text-center text-sm text-muted-foreground">
|
<Command.Empty className="px-3 py-6 text-center text-sm text-muted-foreground">
|
||||||
Nichts gefunden.
|
Nichts gefunden.
|
||||||
</Command.Empty>
|
</Command.Empty>
|
||||||
<Command.Group heading="Bereiche" className="px-1 py-1 text-xs text-muted-foreground">
|
|
||||||
{NAV.map((item) => (
|
{GRUPPEN_REIHE.map((gruppe) => {
|
||||||
|
const eintraege = liste.filter((b) => b.gruppe === gruppe)
|
||||||
|
if (!eintraege.length) return null
|
||||||
|
return (
|
||||||
|
<Command.Group
|
||||||
|
key={gruppe}
|
||||||
|
heading={GRUPPEN_TITEL[gruppe]}
|
||||||
|
className="px-1 py-1 text-[10px] font-bold uppercase tracking-widest text-muted-foreground/50"
|
||||||
|
>
|
||||||
|
{eintraege.map((b) => (
|
||||||
<Command.Item
|
<Command.Item
|
||||||
key={item.id}
|
key={b.id}
|
||||||
value={`${item.label} ${item.hint}`}
|
value={suchtext(b)}
|
||||||
onSelect={() => {
|
onSelect={() => (b.rueckfrage ? setNachfrage(b) : ausfuehren(b))}
|
||||||
navigate({ to: item.pfad })
|
|
||||||
setOpen(false)
|
|
||||||
}}
|
|
||||||
className="flex cursor-pointer items-center gap-3 rounded-md px-3 py-2 text-sm text-foreground aria-selected:bg-accent aria-selected:text-accent-foreground"
|
className="flex cursor-pointer items-center gap-3 rounded-md px-3 py-2 text-sm text-foreground aria-selected:bg-accent aria-selected:text-accent-foreground"
|
||||||
>
|
>
|
||||||
<item.icon className="h-4 w-4 text-primary" />
|
<b.icon
|
||||||
<span>{item.label}</span>
|
className={cn("h-4 w-4 shrink-0", b.rueckfrage ? "text-amber-400" : "text-primary")}
|
||||||
<span className="ml-auto truncate text-xs text-muted-foreground">{item.hint}</span>
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span className="truncate">{b.label}</span>
|
||||||
|
{b.rueckfrage && (
|
||||||
|
<span className="shrink-0 rounded bg-amber-500/15 px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-wide text-amber-300">
|
||||||
|
fragt nach
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{b.hinweis && (
|
||||||
|
<span className="ml-auto truncate pl-3 text-xs text-muted-foreground">{b.hinweis}</span>
|
||||||
|
)}
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
))}
|
))}
|
||||||
</Command.Group>
|
</Command.Group>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</Command.List>
|
</Command.List>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 border-t border-border/50 px-4 py-2 font-mono text-[10px] text-muted-foreground">
|
||||||
|
<span className="flex items-center gap-1">↑↓ wählen</span>
|
||||||
|
<span className="flex items-center gap-1"><CornerDownLeft className="h-3 w-3" aria-hidden="true" /> ausführen</span>
|
||||||
|
<span className="ml-auto">esc schließt</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Command.Dialog>
|
</Command.Dialog>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest"
|
||||||
|
import { render, screen, waitFor } from "@testing-library/react"
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||||
|
import type { AktivitaetResp } from "@/lib/api"
|
||||||
|
|
||||||
|
// Die Query wird ersetzt, nicht das Netz: Der Test prüft die ANSICHT, nicht den Abruf.
|
||||||
|
const useWerkzeugverlauf = vi.hoisted(() => vi.fn())
|
||||||
|
vi.mock("@/lib/queries", () => ({ useWerkzeugverlauf }))
|
||||||
|
|
||||||
|
const { Werkzeugverlauf } = await import("./Werkzeugverlauf")
|
||||||
|
|
||||||
|
function zeigen(data: AktivitaetResp | undefined, isLoading = false) {
|
||||||
|
useWerkzeugverlauf.mockReturnValue({ data, isLoading })
|
||||||
|
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||||
|
return render(
|
||||||
|
<QueryClientProvider client={qc}>
|
||||||
|
<Werkzeugverlauf />
|
||||||
|
</QueryClientProvider>,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ANTWORT: AktivitaetResp = {
|
||||||
|
verfuegbar: true,
|
||||||
|
pfad: "~/.hermes/logs/agent.log",
|
||||||
|
rufe: [
|
||||||
|
{ zeit: "2026-08-28T07:01:20", lauf: "cron_a", werkzeug: "web_extract", dauer_s: 0.17, zeichen: null, ok: false, fehler: "DuckDuckGo (ddgs) is a search-only backend." },
|
||||||
|
{ zeit: "2026-08-28T07:02:36", lauf: null, werkzeug: "web_search", dauer_s: 2.61, zeichen: 2944, ok: true, fehler: null },
|
||||||
|
{ zeit: "2026-08-28T07:03:18", lauf: "cron_a", werkzeug: "terminal", dauer_s: 1.4, zeichen: 53, ok: true, fehler: null },
|
||||||
|
],
|
||||||
|
werkzeuge: [
|
||||||
|
{ werkzeug: "web_extract", rufe: 2, fehler: 2, dauer_schnitt_s: 0.17, letzter_fehler: "DuckDuckGo (ddgs) is a search-only backend." },
|
||||||
|
{ werkzeug: "web_search", rufe: 20, fehler: 0, dauer_schnitt_s: 1.77, letzter_fehler: null },
|
||||||
|
],
|
||||||
|
laeufe: [{ lauf: "cron_a", von: "2026-08-28T07:01:05", bis: "2026-08-28T07:03:18", rufe: 3, fehler: 1 }],
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Die Bilanz-Karte eines Werkzeugs — der Name steht auch in der Zeitleiste, deshalb
|
||||||
|
* wird hier gezielt die Karte gesucht und nicht irgendein Vorkommen des Namens. */
|
||||||
|
function bilanzKarte(werkzeug: string): HTMLElement {
|
||||||
|
const treffer = screen.getAllByText(werkzeug)
|
||||||
|
.map((el) => el.closest("div.mc-card-sm"))
|
||||||
|
.filter((el): el is HTMLElement => el instanceof HTMLElement && el.textContent!.includes("Rufe"))
|
||||||
|
expect(treffer.length, `genau eine Bilanz-Karte fuer ${werkzeug}`).toBe(1)
|
||||||
|
return treffer[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Werkzeugverlauf", () => {
|
||||||
|
// Der eigentliche Zweck der Ansicht: Ein Werkzeug, das IMMER scheitert, verschwindet
|
||||||
|
// in einer Ereignisliste. Genau so lief `web_extract` vier Tage lang jeden Morgen
|
||||||
|
// ins Leere, ohne dass es jemandem auffiel.
|
||||||
|
it("markiert ein durchgehend scheiterndes Werkzeug als „immer rot“", () => {
|
||||||
|
zeigen(ANTWORT)
|
||||||
|
const karte = bilanzKarte("web_extract")
|
||||||
|
expect(karte.textContent).toMatch(/immer rot/i)
|
||||||
|
expect(karte.textContent).toMatch(/2 Fehler/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("markiert ein fehlerfreies Werkzeug NICHT", () => {
|
||||||
|
zeigen(ANTWORT)
|
||||||
|
const karte = bilanzKarte("web_search")
|
||||||
|
expect(karte.textContent).not.toMatch(/immer rot/i)
|
||||||
|
expect(karte.textContent).toMatch(/0 Fehler/)
|
||||||
|
// Und die Markierung gibt es im ganzen Abschnitt genau einmal.
|
||||||
|
expect(screen.getAllByText(/immer rot/i)).toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("zeigt den echten Grund des Fehlers, nicht nur „Fehler“", () => {
|
||||||
|
zeigen(ANTWORT)
|
||||||
|
expect(bilanzKarte("web_extract").textContent).toMatch(/search-only backend/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("nennt Rufe und Fehler in der Abschnitts-Überschrift", () => {
|
||||||
|
const { container } = zeigen(ANTWORT)
|
||||||
|
// Die Überschrift setzt sich aus mehreren Textknoten zusammen (der Fehler-Teil
|
||||||
|
// erscheint nur bedingt), deshalb gegen den zusammengesetzten Text prüfen.
|
||||||
|
const kopf = container.querySelector("section")!.textContent!
|
||||||
|
expect(kopf).toMatch(/3 Rufe/) // 3 Einträge in ANTWORT.rufe
|
||||||
|
expect(kopf).toMatch(/2 Fehler/) // Summe aus der Werkzeug-Bilanz
|
||||||
|
})
|
||||||
|
|
||||||
|
it("gruppiert nach Lauf und zeigt dessen Zeitspanne", () => {
|
||||||
|
zeigen(ANTWORT)
|
||||||
|
const zeile = screen.getByText("cron_a").closest("div")!
|
||||||
|
expect(zeile.textContent).toMatch(/07:01:05–07:03:18/)
|
||||||
|
expect(zeile.textContent).toMatch(/1 Fehler/)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Auf einem Entwicklungsrechner ohne Hermes gibt es das Log nicht. Dann still
|
||||||
|
// weglassen — eine leere Karte waere eine Zusage ohne Inhalt.
|
||||||
|
it("verschwindet ganz, wenn es kein Hermes-Log gibt", () => {
|
||||||
|
const { container } = zeigen({ verfuegbar: false, pfad: "", rufe: [], werkzeuge: [], laeufe: [] })
|
||||||
|
expect(container).toBeEmptyDOMElement()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("sagt beim Laden, was es tut", async () => {
|
||||||
|
zeigen(undefined, true)
|
||||||
|
await waitFor(() => expect(screen.getByText(/Lese Hermes/)).toBeInTheDocument())
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { AlertTriangle, Activity, CheckCircle2, Clock, Wrench } from "lucide-react"
|
||||||
|
import { useWerkzeugverlauf } from "@/lib/queries"
|
||||||
|
import { SectionLabel } from "@/components/ui/SectionLabel"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
// Werkzeug-Verlauf des Agenten (v3-Umbau P6).
|
||||||
|
//
|
||||||
|
// Der Blueprint sah hier eine „Live Agent Matrix" mit Denkstrom vor (§4.4). Die ist so
|
||||||
|
// nicht baubar: Lucys Denkschritte entstehen im Hermes-Prozess, und AGENTS.md verbietet
|
||||||
|
// es, dessen Quellcode zu patchen. Was aber offen dalag: Hermes protokolliert JEDEN
|
||||||
|
// Werkzeug-Ruf in sein Log. Zuschauen ist kein Patchen.
|
||||||
|
//
|
||||||
|
// Diese Ansicht zeigt deshalb nur, was wirklich da ist — welches Werkzeug, wie lange,
|
||||||
|
// mit welchem Ergebnis, in welchem Lauf. Kein erfundener Denkstrom, keine leere Leitung.
|
||||||
|
//
|
||||||
|
// Die BILANZ steht oben, nicht die Zeitleiste. Grund: Ein Werkzeug, das immer scheitert,
|
||||||
|
// verschwindet in einer Liste von Ereignissen — in der Zeile „web_extract · 2 Rufe ·
|
||||||
|
// 2 Fehler" nicht. Genau so ist beim Bauen aufgefallen, dass `web_extract` seit
|
||||||
|
// mindestens vier Tagen jeden Morgen um 07:00 scheitert.
|
||||||
|
|
||||||
|
const uhr = (iso: string) => iso.slice(11, 19)
|
||||||
|
|
||||||
|
/** Dauer lesbar: unter einer Sekunde in ms, sonst in Sekunden. */
|
||||||
|
function dauer(s: number): string {
|
||||||
|
return s < 1 ? `${Math.round(s * 1000)} ms` : `${s.toFixed(1)} s`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Werkzeugverlauf() {
|
||||||
|
const { data, isLoading } = useWerkzeugverlauf()
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<section className="space-y-3">
|
||||||
|
<SectionLabel icon={Activity}>Werkzeug-Verlauf</SectionLabel>
|
||||||
|
<div className="mc-card-sm p-4 text-xs text-muted-foreground">Lese Hermes' Log …</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auf einem Entwicklungsrechner ohne Hermes gibt es das Log nicht. Dann still
|
||||||
|
// weglassen statt eine leere Karte zu zeigen — wie der Rest der Seite es hält.
|
||||||
|
if (!data?.verfuegbar) return null
|
||||||
|
|
||||||
|
const { rufe, werkzeuge, laeufe } = data
|
||||||
|
const fehlerhaft = werkzeuge.filter((w) => w.fehler > 0)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="space-y-3">
|
||||||
|
<SectionLabel icon={Activity}>
|
||||||
|
Werkzeug-Verlauf · {rufe.length} Rufe
|
||||||
|
{fehlerhaft.length > 0 && ` · ${fehlerhaft.reduce((n, w) => n + w.fehler, 0)} Fehler`}
|
||||||
|
</SectionLabel>
|
||||||
|
|
||||||
|
<p className="max-w-2xl text-[11px] leading-relaxed text-muted-foreground">
|
||||||
|
Gelesen aus Hermes' eigenem Log — Mission Control schaut zu, greift aber nicht ein.
|
||||||
|
Was dort <em>nicht</em> steht, steht auch hier nicht: Lucys Gedankengang und die
|
||||||
|
Argumente eines Werkzeug-Rufs bleiben unsichtbar.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Bilanz zuerst — hier faellt ein Dauerfehler auf, den eine Zeitleiste verschluckt. */}
|
||||||
|
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{werkzeuge.map((w) => {
|
||||||
|
const immerRot = w.fehler > 0 && w.fehler === w.rufe
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={w.werkzeug}
|
||||||
|
className={cn("mc-card-sm space-y-1.5 p-3",
|
||||||
|
immerRot && "border-red-500/40 bg-red-500/5")}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Wrench className={cn("h-3.5 w-3.5 shrink-0",
|
||||||
|
immerRot ? "text-red-400" : w.fehler ? "text-amber-400" : "text-primary")} aria-hidden="true" />
|
||||||
|
<span className="truncate font-mono text-xs font-semibold text-foreground">{w.werkzeug}</span>
|
||||||
|
{immerRot && (
|
||||||
|
<span className="ml-auto shrink-0 rounded bg-red-500/15 px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-wide text-red-300">
|
||||||
|
immer rot
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-x-3 gap-y-0.5 font-mono text-[10px] text-muted-foreground">
|
||||||
|
<span className="tabular-nums">{w.rufe} Rufe</span>
|
||||||
|
<span className={cn("tabular-nums", w.fehler > 0 && "font-semibold text-red-400")}>
|
||||||
|
{w.fehler} Fehler
|
||||||
|
</span>
|
||||||
|
<span className="tabular-nums">Ø {dauer(w.dauer_schnitt_s)}</span>
|
||||||
|
</div>
|
||||||
|
{w.letzter_fehler && (
|
||||||
|
<p className="border-t border-border/30 pt-1.5 text-[10px] leading-relaxed text-red-300/90">
|
||||||
|
{w.letzter_fehler}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Laeufe: welcher Cron/welche Sitzung hat wann was getan. */}
|
||||||
|
{laeufe.length > 0 && (
|
||||||
|
<div className="mc-card-sm divide-y divide-border/30">
|
||||||
|
{laeufe.map((l) => (
|
||||||
|
<div key={l.lauf} className="flex flex-wrap items-center gap-x-3 gap-y-1 px-3 py-2 font-mono text-[10px]">
|
||||||
|
<Clock className="h-3 w-3 shrink-0 text-muted-foreground" aria-hidden="true" />
|
||||||
|
<span className="truncate font-semibold text-foreground">{l.lauf}</span>
|
||||||
|
<span className="tabular-nums text-muted-foreground">{uhr(l.von)}–{uhr(l.bis)}</span>
|
||||||
|
<span className="ml-auto tabular-nums text-muted-foreground">{l.rufe} Rufe</span>
|
||||||
|
{l.fehler > 0 && (
|
||||||
|
<span className="tabular-nums font-semibold text-red-400">{l.fehler} Fehler</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Zeitleiste, juengster Ruf oben. */}
|
||||||
|
<details className="mc-card-sm">
|
||||||
|
<summary className="cursor-pointer px-3 py-2 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground hover:text-foreground">
|
||||||
|
Einzelne Rufe ({rufe.length})
|
||||||
|
</summary>
|
||||||
|
<ul className="max-h-72 divide-y divide-border/20 overflow-y-auto scrollbar-thin border-t border-border/30">
|
||||||
|
{[...rufe].reverse().map((r, i) => (
|
||||||
|
<li key={`${r.zeit}-${r.werkzeug}-${i}`} className="flex items-start gap-2 px-3 py-1.5 font-mono text-[10px]">
|
||||||
|
{r.ok
|
||||||
|
? <CheckCircle2 className="mt-px h-3 w-3 shrink-0 text-emerald-500" aria-hidden="true" />
|
||||||
|
: <AlertTriangle className="mt-px h-3 w-3 shrink-0 text-red-400" aria-hidden="true" />}
|
||||||
|
<span className="shrink-0 tabular-nums text-muted-foreground">{uhr(r.zeit)}</span>
|
||||||
|
<span className="shrink-0 font-semibold text-foreground">{r.werkzeug}</span>
|
||||||
|
<span className="shrink-0 tabular-nums text-muted-foreground">{dauer(r.dauer_s)}</span>
|
||||||
|
{r.zeichen != null && (
|
||||||
|
<span className="shrink-0 tabular-nums text-muted-foreground/60">{r.zeichen} Z.</span>
|
||||||
|
)}
|
||||||
|
{r.fehler && <span className="min-w-0 flex-1 truncate text-red-300/90" title={r.fehler}>{r.fehler}</span>}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { describe, expect, it } from "vitest"
|
||||||
|
import { render } from "@testing-library/react"
|
||||||
|
import { Dichteleiste } from "./Dichteleiste"
|
||||||
|
import type { ChronikItem } from "@/lib/api"
|
||||||
|
|
||||||
|
function eintrag(ts: number): ChronikItem {
|
||||||
|
return { id: ts, ts, subject: "x", text: "y", source: "test", priority: "normal" }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Die Balken der Leiste (ohne die Achsen-Beschriftung darunter). */
|
||||||
|
function balken(container: HTMLElement): HTMLElement[] {
|
||||||
|
return [...container.querySelectorAll('[role="img"] > div')] as HTMLElement[]
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Dichteleiste", () => {
|
||||||
|
it("zeigt nichts, wenn es nichts zu zeigen gibt", () => {
|
||||||
|
const { container } = render(<Dichteleiste items={[]} />)
|
||||||
|
expect(container).toBeEmptyDOMElement()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("verteilt die Einträge über die volle Breite", () => {
|
||||||
|
const basis = 1_800_000_000
|
||||||
|
const items = [eintrag(basis), eintrag(basis + 43_200), eintrag(basis + 86_400)]
|
||||||
|
const { container } = render(<Dichteleiste items={items} />)
|
||||||
|
const b = balken(container)
|
||||||
|
expect(b).toHaveLength(48)
|
||||||
|
// Erster, mittlerer und letzter Eimer sind belegt — dazwischen ist es leer.
|
||||||
|
expect(b[0].style.height).not.toBe("2px")
|
||||||
|
expect(b[47].style.height).not.toBe("2px")
|
||||||
|
expect(b[5].style.height).toBe("2px")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Ohne Mindesthöhe sähe ein ruhiger Tag aus wie gar keine Aktivität — genau die
|
||||||
|
// Verwechslung, die die Leiste verhindern soll.
|
||||||
|
it("macht auch einen einzelnen Eintrag sichtbar", () => {
|
||||||
|
const { container } = render(<Dichteleiste items={[eintrag(1_800_000_000)]} />)
|
||||||
|
const belegt = balken(container).filter((d) => d.style.height !== "2px")
|
||||||
|
expect(belegt).toHaveLength(1)
|
||||||
|
expect(parseFloat(belegt[0].style.height)).toBeGreaterThanOrEqual(12)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("beschriftet sich für Screenreader mit Anzahl und Zeitraum", () => {
|
||||||
|
const basis = 1_800_000_000
|
||||||
|
const { container } = render(<Dichteleiste items={[eintrag(basis), eintrag(basis + 86_400)]} />)
|
||||||
|
const label = container.querySelector('[role="img"]')!.getAttribute("aria-label")!
|
||||||
|
expect(label).toMatch(/2 Einträge/)
|
||||||
|
expect(label).toMatch(/zwischen .* und /)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("hebt die Leiste hervor, solange gefiltert wird", () => {
|
||||||
|
const items = [eintrag(1_800_000_000)]
|
||||||
|
const normal = render(<Dichteleiste items={items} />).container
|
||||||
|
const gefiltert = render(<Dichteleiste items={items} aktiv />).container
|
||||||
|
const klasse = (c: HTMLElement) =>
|
||||||
|
balken(c).find((d) => d.style.height !== "2px")!.className
|
||||||
|
expect(klasse(normal)).toMatch(/primary\/40/)
|
||||||
|
expect(klasse(gefiltert)).toMatch(/primary\/70/)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { useMemo } from "react"
|
||||||
|
import type { ChronikItem } from "@/lib/api"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
// Aktivitäts-Dichte über der Chronik (v3-Umbau P6, Spezifikation §4.5).
|
||||||
|
//
|
||||||
|
// Der eigentliche Zugewinn gegenüber einer reinen Liste: Sie zeigt, WANN die Box aktiv
|
||||||
|
// war, bevor man liest, WAS sie getan hat. Ein stiller Tag und eine durchgearbeitete
|
||||||
|
// Nacht sehen in einer Liste gleich aus — man muss scrollen, um es zu merken.
|
||||||
|
//
|
||||||
|
// Bewusst eigenes SVG statt einer Diagramm-Bibliothek: Es sind Rechtecke über einer
|
||||||
|
// Zeitachse. Recharts dafür zu laden wäre 95 kB für etwas, das in 30 Zeilen passt —
|
||||||
|
// und genau dieser Reflex hat in P0 das Startbündel aufgebläht.
|
||||||
|
|
||||||
|
const STUNDE = 3_600
|
||||||
|
const EIMER = 48 // 48 Stufen über den gezeigten Zeitraum
|
||||||
|
|
||||||
|
export function Dichteleiste({ items, aktiv }: {
|
||||||
|
items: ChronikItem[]
|
||||||
|
/** Farbe für den Fall, dass gefiltert wird — dann zeigt die Leiste nur die Treffer. */
|
||||||
|
aktiv?: boolean
|
||||||
|
}) {
|
||||||
|
const { balken, von, bis, max } = useMemo(() => {
|
||||||
|
if (!items.length) return { balken: [] as number[], von: 0, bis: 0, max: 0 }
|
||||||
|
// Die Chronik kommt neueste zuerst; für die Achse brauchen wir beide Enden.
|
||||||
|
const zeiten = items.map((i) => i.ts)
|
||||||
|
const von = Math.min(...zeiten)
|
||||||
|
const bis = Math.max(...zeiten)
|
||||||
|
const spanne = Math.max(bis - von, STUNDE)
|
||||||
|
const balken = new Array(EIMER).fill(0)
|
||||||
|
for (const t of zeiten) {
|
||||||
|
const i = Math.min(EIMER - 1, Math.floor(((t - von) / spanne) * EIMER))
|
||||||
|
balken[i]++
|
||||||
|
}
|
||||||
|
return { balken, von, bis, max: Math.max(...balken, 1) }
|
||||||
|
}, [items])
|
||||||
|
|
||||||
|
if (balken.length === 0) return null
|
||||||
|
|
||||||
|
const zeitpunkt = (ts: number) =>
|
||||||
|
new Date(ts * 1000).toLocaleDateString("de-DE", {
|
||||||
|
day: "2-digit", month: "2-digit",
|
||||||
|
...(bis - von < 2 * 86_400 ? { hour: "2-digit", minute: "2-digit" } : {}),
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div
|
||||||
|
className="flex h-8 items-end gap-px"
|
||||||
|
role="img"
|
||||||
|
aria-label={`Aktivitätsdichte: ${items.length} Einträge zwischen ${zeitpunkt(von)} und ${zeitpunkt(bis)}`}
|
||||||
|
>
|
||||||
|
{balken.map((n, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
title={n === 1 ? "1 Eintrag" : `${n} Einträge`}
|
||||||
|
className={cn(
|
||||||
|
"min-w-px flex-1 rounded-t-sm transition-colors",
|
||||||
|
n === 0 ? "bg-border/25" : aktiv ? "bg-primary/70" : "bg-primary/40",
|
||||||
|
)}
|
||||||
|
// Auch ein einzelner Eintrag muss sichtbar sein — sonst sieht ein ruhiger
|
||||||
|
// Tag aus wie gar nichts.
|
||||||
|
style={{ height: n === 0 ? "2px" : `${Math.max(12, (n / max) * 100)}%` }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between font-mono text-[9px] text-muted-foreground/60">
|
||||||
|
<span>{zeitpunkt(von)}</span>
|
||||||
|
<span className="tabular-nums">{items.length} Einträge</span>
|
||||||
|
<span>{zeitpunkt(bis)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -397,6 +397,40 @@ export interface Job {
|
|||||||
eta_s?: number
|
eta_s?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Werkzeug-Verlauf des Agenten (GET /api/agent/aktivitaet, v3-Umbau P6).
|
||||||
|
// Gelesen aus Hermes' Log — Denkstrom und Werkzeug-Argumente stehen dort NICHT und
|
||||||
|
// tauchen darum auch hier nicht auf.
|
||||||
|
export interface WerkzeugRuf {
|
||||||
|
zeit: string
|
||||||
|
lauf: string | null
|
||||||
|
werkzeug: string
|
||||||
|
dauer_s: number
|
||||||
|
zeichen: number | null
|
||||||
|
ok: boolean
|
||||||
|
fehler: string | null
|
||||||
|
}
|
||||||
|
export interface WerkzeugBilanz {
|
||||||
|
werkzeug: string
|
||||||
|
rufe: number
|
||||||
|
fehler: number
|
||||||
|
dauer_schnitt_s: number
|
||||||
|
letzter_fehler: string | null
|
||||||
|
}
|
||||||
|
export interface AgentLauf {
|
||||||
|
lauf: string
|
||||||
|
von: string
|
||||||
|
bis: string
|
||||||
|
rufe: number
|
||||||
|
fehler: number
|
||||||
|
}
|
||||||
|
export interface AktivitaetResp {
|
||||||
|
verfuegbar: boolean
|
||||||
|
pfad: string
|
||||||
|
rufe: WerkzeugRuf[]
|
||||||
|
werkzeuge: WerkzeugBilanz[]
|
||||||
|
laeufe: AgentLauf[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface Health {
|
export interface Health {
|
||||||
status: string
|
status: string
|
||||||
version: string
|
version: string
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { sseVerbunden } from "./events"
|
|||||||
import {
|
import {
|
||||||
api,
|
api,
|
||||||
type AgentStatus,
|
type AgentStatus,
|
||||||
|
type AktivitaetResp,
|
||||||
type AuftragsbuchResp,
|
type AuftragsbuchResp,
|
||||||
type ChronikResp,
|
type ChronikResp,
|
||||||
type ConnectResp,
|
type ConnectResp,
|
||||||
@@ -46,6 +47,7 @@ export const qk = {
|
|||||||
jobs: ["jobs"] as const,
|
jobs: ["jobs"] as const,
|
||||||
tokenStats: ["token-stats"] as const,
|
tokenStats: ["token-stats"] as const,
|
||||||
agentStatus: ["agent-status"] as const,
|
agentStatus: ["agent-status"] as const,
|
||||||
|
agentAktivitaet: ["agent-aktivitaet"] as const,
|
||||||
hermesBrain: ["hermes-brain"] as const,
|
hermesBrain: ["hermes-brain"] as const,
|
||||||
updates: ["updates"] as const,
|
updates: ["updates"] as const,
|
||||||
discover: ["discover"] as const,
|
discover: ["discover"] as const,
|
||||||
@@ -221,6 +223,15 @@ export const useTokenStats = (refetchInterval: number = TAKT.graph) =>
|
|||||||
export const useAgentStatus = (refetchInterval: number = TAKT.normal) =>
|
export const useAgentStatus = (refetchInterval: number = TAKT.normal) =>
|
||||||
useQuery({ queryKey: qk.agentStatus, queryFn: () => api<AgentStatus>("/api/agent/status"), refetchInterval })
|
useQuery({ queryKey: qk.agentStatus, queryFn: () => api<AgentStatus>("/api/agent/status"), refetchInterval })
|
||||||
|
|
||||||
|
// Werkzeug-Verlauf des Agenten. Liest das Ende von Hermes' Log — billig, aber es
|
||||||
|
// aendert sich nur, wenn Lucy wirklich arbeitet; der gemuetliche Takt reicht.
|
||||||
|
export const useWerkzeugverlauf = (refetchInterval: number = TAKT.gemuetlich) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: qk.agentAktivitaet,
|
||||||
|
queryFn: () => api<AktivitaetResp>("/api/agent/aktivitaet?limit=80"),
|
||||||
|
refetchInterval: relax(refetchInterval),
|
||||||
|
})
|
||||||
|
|
||||||
// Agent-Hirn (Hermes) + bestes NousResearch-Update. Selten pollen (HF-Abfrage).
|
// Agent-Hirn (Hermes) + bestes NousResearch-Update. Selten pollen (HF-Abfrage).
|
||||||
export const useHermesBrain = (refetchInterval: number = TAKT.traege) =>
|
export const useHermesBrain = (refetchInterval: number = TAKT.traege) =>
|
||||||
useQuery({ queryKey: qk.hermesBrain, queryFn: () => api<HermesBrainResp>("/api/agent/brain"), refetchInterval })
|
useQuery({ queryKey: qk.hermesBrain, queryFn: () => api<HermesBrainResp>("/api/agent/brain"), refetchInterval })
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { Bot, ExternalLink, Activity, Cpu, Wrench, Monitor, Shield, Check, X } from "lucide-react"
|
import { Bot, ExternalLink, Activity, Cpu, Wrench, Monitor, Shield, Check, X } from "lucide-react"
|
||||||
import { useNavigate } from "@tanstack/react-router"
|
import { useNavigate } from "@tanstack/react-router"
|
||||||
|
import { Werkzeugverlauf } from "@/features/agent/Werkzeugverlauf"
|
||||||
import { api } from "@/lib/api"
|
import { api } from "@/lib/api"
|
||||||
import { useAgentStatus, useModels, useQueryClient, qk } from "@/lib/queries"
|
import { useAgentStatus, useModels, useQueryClient, qk } from "@/lib/queries"
|
||||||
import { useDialog } from "@/lib/useDialog"
|
import { useDialog } from "@/lib/useDialog"
|
||||||
@@ -340,6 +341,9 @@ export function AgentView() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Werkzeug-Verlauf (v3-Umbau P6): was Lucy tatsaechlich getan hat, aus ihrem Log. */}
|
||||||
|
<Werkzeugverlauf />
|
||||||
|
|
||||||
{dialogElement}
|
{dialogElement}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useMemo, useState } from "react"
|
import { useMemo, useState } from "react"
|
||||||
import {
|
import { AlarmClock, Archive, Bell, HeartPulse, History, Inbox, Loader2, MessageCircle, Moon, Radar, RotateCcw, Search, ShieldAlert, Sparkles, type LucideIcon } from "lucide-react"
|
||||||
History, HeartPulse, ShieldAlert, Bell, Moon, Radar, Inbox, Loader2,
|
|
||||||
Archive, RotateCcw, AlarmClock, MessageCircle, Sparkles, type LucideIcon,
|
|
||||||
} from "lucide-react"
|
|
||||||
import { api, type ChronikItem, type Snapshot } from "@/lib/api"
|
import { api, type ChronikItem, type Snapshot } from "@/lib/api"
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router"
|
||||||
|
import { Dichteleiste } from "@/features/chronik/Dichteleiste"
|
||||||
import { useChronik, useZeitmaschine, useQueryClient, qk } from "@/lib/queries"
|
import { useChronik, useZeitmaschine, useQueryClient, qk } from "@/lib/queries"
|
||||||
import { useDialog } from "@/lib/useDialog"
|
import { useDialog } from "@/lib/useDialog"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
@@ -30,9 +29,25 @@ function dayKey(ts: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ChronikView() {
|
export function ChronikView() {
|
||||||
const { data: items = [], isLoading } = useChronik()
|
const { data: alle = [], isLoading } = useChronik()
|
||||||
const [limitPerDay, setLimitPerDay] = useState(true)
|
const [limitPerDay, setLimitPerDay] = useState(true)
|
||||||
|
|
||||||
|
// v3-Umbau P6: Das Suchwort steht in der URL (/chronik?q=…). So landet der
|
||||||
|
// „Finden"-Eintrag der Befehlspalette hier — und ein Treffer ist teilbar.
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { q = "" } = useSearch({ from: "/chronik" })
|
||||||
|
const suchen = (wert: string) =>
|
||||||
|
navigate({ to: "/chronik", search: wert ? { q: wert } : {}, replace: true })
|
||||||
|
|
||||||
|
const items = useMemo(() => {
|
||||||
|
const ql = q.trim().toLowerCase()
|
||||||
|
if (!ql) return alle
|
||||||
|
return alle.filter((it) =>
|
||||||
|
it.subject.toLowerCase().includes(ql)
|
||||||
|
|| it.text.toLowerCase().includes(ql)
|
||||||
|
|| it.source.toLowerCase().includes(ql))
|
||||||
|
}, [alle, q])
|
||||||
|
|
||||||
const grouped = useMemo(() => {
|
const grouped = useMemo(() => {
|
||||||
const g: { day: string; items: ChronikItem[] }[] = []
|
const g: { day: string; items: ChronikItem[] }[] = []
|
||||||
for (const it of items) {
|
for (const it of items) {
|
||||||
@@ -58,9 +73,26 @@ export function ChronikView() {
|
|||||||
<Zeitmaschine />
|
<Zeitmaschine />
|
||||||
|
|
||||||
<section className="space-y-4">
|
<section className="space-y-4">
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<p className="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/60">
|
<p className="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/60">
|
||||||
<History className="h-3.5 w-3.5" /> Verlauf
|
<History className="h-3.5 w-3.5" /> Verlauf
|
||||||
|
{q && <span className="normal-case text-primary">· {items.length} von {alle.length}</span>}
|
||||||
</p>
|
</p>
|
||||||
|
<div className="relative ml-auto w-full sm:w-64">
|
||||||
|
<Search className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" aria-hidden="true" />
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
value={q}
|
||||||
|
onChange={(e) => suchen(e.target.value)}
|
||||||
|
placeholder="Verlauf durchsuchen …"
|
||||||
|
aria-label="Chronik durchsuchen"
|
||||||
|
className="h-8 w-full rounded-lg border border-border/60 bg-background/40 pl-8 pr-3 text-xs text-foreground outline-none transition-colors focus:border-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Zeigt, WANN die Box aktiv war — bevor man liest, WAS sie getan hat (§4.5). */}
|
||||||
|
{!isLoading && items.length > 0 && <Dichteleiste items={items} aktiv={!!q} />}
|
||||||
|
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="flex items-center gap-2 rounded-2xl border border-border/60 bg-card/30 p-6 text-xs text-muted-foreground">
|
<div className="flex items-center gap-2 rounded-2xl border border-border/60 bg-card/30 p-6 text-xs text-muted-foreground">
|
||||||
@@ -70,7 +102,9 @@ export function ChronikView() {
|
|||||||
|
|
||||||
{!isLoading && items.length === 0 && (
|
{!isLoading && items.length === 0 && (
|
||||||
<div className="rounded-2xl border border-dashed border-border/60 bg-card/20 p-8 text-center text-xs text-muted-foreground">
|
<div className="rounded-2xl border border-dashed border-border/60 bg-card/20 p-8 text-center text-xs text-muted-foreground">
|
||||||
Noch keine Einträge. Sobald die Box etwas von allein tut oder meldet (Updates, Wächter, Träume), steht es hier.
|
{q
|
||||||
|
? <>Nichts zu „{q}" im Verlauf. <button onClick={() => suchen("")} className="cursor-pointer text-primary underline underline-offset-2">Filter aufheben</button></>
|
||||||
|
: "Noch keine Einträge. Sobald die Box etwas von allein tut oder meldet (Updates, Wächter, Träume), steht es hier."}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useMemo, useState } from "react"
|
import { useEffect, useMemo, useState } from "react"
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router"
|
||||||
import { Library, FileText, Sparkles, Loader2, FolderOpen, Search } from "lucide-react"
|
import { Library, FileText, Sparkles, Loader2, FolderOpen, Search } from "lucide-react"
|
||||||
import { api, type WissenDatei, type WissenFile } from "@/lib/api"
|
import { api, type WissenDatei, type WissenFile } from "@/lib/api"
|
||||||
import { useWissen, useMemoryGraph } from "@/lib/queries"
|
import { useWissen, useMemoryGraph } from "@/lib/queries"
|
||||||
@@ -24,7 +25,14 @@ export function WissenView() {
|
|||||||
const [active, setActive] = useState<string | null>(null)
|
const [active, setActive] = useState<string | null>(null)
|
||||||
const [doc, setDoc] = useState<WissenDatei | null>(null)
|
const [doc, setDoc] = useState<WissenDatei | null>(null)
|
||||||
const [docLoading, setDocLoading] = useState(false)
|
const [docLoading, setDocLoading] = useState(false)
|
||||||
const [q, setQ] = useState("")
|
// v3-Umbau P6: Das Suchfeld gab es schon, aber nur lokal. Jetzt steht das Wort in
|
||||||
|
// der URL (/wissen?suche=…) — so landet der „Finden"-Eintrag der Palette hier, und
|
||||||
|
// ein Suchergebnis ist teilbar.
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { suche = "" } = useSearch({ from: "/wissen" })
|
||||||
|
const q = suche
|
||||||
|
const setQ = (wert: string) =>
|
||||||
|
navigate({ to: "/wissen", search: (a) => ({ ...a, suche: wert || undefined }), replace: true })
|
||||||
const [showGraph, setShowGraph] = useState(false)
|
const [showGraph, setShowGraph] = useState(false)
|
||||||
|
|
||||||
const { data: graphData, isLoading: graphLoading } = useMemoryGraph(showGraph)
|
const { data: graphData, isLoading: graphLoading } = useMemoryGraph(showGraph)
|
||||||
|
|||||||
Reference in New Issue
Block a user