Compare commits
2 Commits
main
...
31d6beabbc
| Author | SHA1 | Date | |
|---|---|---|---|
| 31d6beabbc | |||
| c697356f5a |
+11
-1
@@ -19,13 +19,14 @@ from fastapi.responses import FileResponse
|
|||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
|
|
||||||
from config import FRONTEND_DIST, VERSION
|
from config import FRONTEND_DIST, V1_UPSTREAM, VERSION
|
||||||
from routers import (
|
from routers import (
|
||||||
agent,
|
agent,
|
||||||
auftragsbuch,
|
auftragsbuch,
|
||||||
chronik,
|
chronik,
|
||||||
connect,
|
connect,
|
||||||
console,
|
console,
|
||||||
|
eigenleben,
|
||||||
gateway_proxy,
|
gateway_proxy,
|
||||||
health,
|
health,
|
||||||
hermes_ui,
|
hermes_ui,
|
||||||
@@ -121,11 +122,20 @@ app.include_router(
|
|||||||
app.include_router(
|
app.include_router(
|
||||||
reminders_router.router
|
reminders_router.router
|
||||||
) # Erinnerungen/Routinen (A3) — feuern in den Briefkasten
|
) # Erinnerungen/Routinen (A3) — feuern in den Briefkasten
|
||||||
|
# /v1-Datenpfad: Nach dem Gateway-Auszug (UMBAU v3 P1) läuft der eigentliche Gateway als
|
||||||
|
# eigener Prozess (mc2-gateway, Loopback :9010) — MC2 reicht /v1 dann nur roh durch, damit
|
||||||
|
# LAN-Clients (IDE-Lane) weiter über :9001 kommen. Ohne MC_V1_UPSTREAM (vor dem ersten
|
||||||
|
# Deploy der neuen Unit / nach Rollback) bedient MC2 /v1 wie bisher selbst.
|
||||||
|
if V1_UPSTREAM:
|
||||||
|
from routers import gateway_forward
|
||||||
|
app.include_router(gateway_forward.router) # dünner Roh-Weiterleiter → mc2-gateway
|
||||||
|
else:
|
||||||
app.include_router(gateway_proxy.router) # OpenAI-kompatibler /v1-Gateway (model:auto)
|
app.include_router(gateway_proxy.router) # OpenAI-kompatibler /v1-Gateway (model:auto)
|
||||||
app.include_router(maintenance.router)
|
app.include_router(maintenance.router)
|
||||||
app.include_router(auftragsbuch.router) # Vorschlags-Inbox (Mensch-Gate als Klick)
|
app.include_router(auftragsbuch.router) # Vorschlags-Inbox (Mensch-Gate als Klick)
|
||||||
app.include_router(ideen.router) # Ideen-Queue (natives Hermes-Kanban) — Tür der Zentrale
|
app.include_router(ideen.router) # Ideen-Queue (natives Hermes-Kanban) — Tür der Zentrale
|
||||||
app.include_router(chronik.router) # Timeline der autonomen Taten (Announce-Store)
|
app.include_router(chronik.router) # Timeline der autonomen Taten (Announce-Store)
|
||||||
|
app.include_router(eigenleben.router) # „Von allein": Skills + Vorschlags-Bilanz der Box
|
||||||
app.include_router(wissen.router) # Wissens-Vault (Traum-Notizen) read-only
|
app.include_router(wissen.router) # Wissens-Vault (Traum-Notizen) read-only
|
||||||
app.include_router(zeitmaschine.router) # Snapshots ansehen + Ein-Klick-Restore (detached)
|
app.include_router(zeitmaschine.router) # Snapshots ansehen + Ein-Klick-Restore (detached)
|
||||||
app.include_router(
|
app.include_router(
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ HF_DOWNLOAD_ENV = {"HF_HUB_DISABLE_XET": "1"}
|
|||||||
# MC2 IST der Gateway (services/gateway.py + routers/gateway_proxy.py). KEIN externer
|
# MC2 IST der Gateway (services/gateway.py + routers/gateway_proxy.py). KEIN externer
|
||||||
# LiteLLM-Dienst (scheitert auf Python 3.14). Daher keine Gateway-Config-Datei mehr.
|
# LiteLLM-Dienst (scheitert auf Python 3.14). Daher keine Gateway-Config-Datei mehr.
|
||||||
GATEWAY_URL = os.environ.get("MC_GATEWAY_URL", f"http://127.0.0.1:{os.environ.get('MC_PORT', '9000')}").rstrip("/")
|
GATEWAY_URL = os.environ.get("MC_GATEWAY_URL", f"http://127.0.0.1:{os.environ.get('MC_PORT', '9000')}").rstrip("/")
|
||||||
|
# Gateway-Auszug (UMBAU v3 P1): Ist MC_V1_UPSTREAM gesetzt (Unit-Env, z. B.
|
||||||
|
# http://127.0.0.1:9010), bedient der eigenständige mc2-gateway-Prozess den /v1-Pfad
|
||||||
|
# und MC2 reicht /v1 nur noch roh durch (routers/gateway_forward.py). Leer = altes
|
||||||
|
# Verhalten, MC2 bedient /v1 selbst — die Zeile aus der Unit nehmen ist der Rollback.
|
||||||
|
V1_UPSTREAM = os.environ.get("MC_V1_UPSTREAM", "").rstrip("/")
|
||||||
|
|
||||||
# --- Hermes Agent (eigener Dienst auf der Box) -------------------------------
|
# --- Hermes Agent (eigener Dienst auf der Box) -------------------------------
|
||||||
# Gateway (OpenAI-API des Agenten) + interaktives Web-Terminal (ttyd → `hermes chat`).
|
# Gateway (OpenAI-API des Agenten) + interaktives Web-Terminal (ttyd → `hermes chat`).
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"""
|
||||||
|
MC2-Gateway — der /v1-Datenpfad als EIGENER Prozess (UMBAU v3, P1).
|
||||||
|
|
||||||
|
Befund der Live-Inspektion 15.07.2026: Jeder LLM-Aufruf der Box (Lucys Haupt-Hirn,
|
||||||
|
Nacht-Crons, Worker-Delegation, Vision, Decomposer/Specifier/Curator) lief durch den
|
||||||
|
Steuerpult-Prozess auf :9001 — den am häufigsten neu gestarteten Dienst des Stacks.
|
||||||
|
Dieser Einstieg hebt denselben Gateway-Router (routers/gateway_proxy.py) UNVERÄNDERT
|
||||||
|
in einen bewusst winzigen, langweiligen Prozess: Unit mc2-gateway.service, Loopback
|
||||||
|
:9010, Restart=always. Das Steuerpult darf beliebig neu starten — die Wirbelsäule steht.
|
||||||
|
|
||||||
|
Bewusst NICHT hier: weitere Router, Hintergrund-Loops, CORS, Frontend-Auslieferung.
|
||||||
|
LAN-Clients (IDE-Lane) erreichen /v1 weiter über MC2 :9001, das roh hierher
|
||||||
|
durchreicht (routers/gateway_forward.py, MC_V1_UPSTREAM).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from typing import AsyncGenerator
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from fastapi import FastAPI, Request
|
||||||
|
|
||||||
|
from config import LLAMA_SWAP_URL, VERSION
|
||||||
|
from routers import gateway_proxy
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=os.environ.get("MC_LOG_LEVEL", "INFO").upper(),
|
||||||
|
format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
|
||||||
|
)
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||||
|
# Gleiche Client-Parameter wie zuvor in app.py: Keep-Alive/Pooling statt neuer
|
||||||
|
# Client pro Anfrage (Sockets/TIME_WAIT unter parallelen Agent-Strömen).
|
||||||
|
app.state.gw_client = httpx.AsyncClient(
|
||||||
|
timeout=httpx.Timeout(connect=10.0, read=None, write=None, pool=10.0),
|
||||||
|
limits=httpx.Limits(max_keepalive_connections=100, max_connections=200),
|
||||||
|
)
|
||||||
|
log.info("mc2-gateway bereit (Engine: %s)", LLAMA_SWAP_URL)
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
await app.state.gw_client.aclose()
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI(title="MC2 Gateway", version=VERSION, lifespan=lifespan)
|
||||||
|
app.include_router(gateway_proxy.router)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/gw/health")
|
||||||
|
async def health(request: Request):
|
||||||
|
"""Eigener Health-Pfad (nicht /api/health — das gehört dem Steuerpult):
|
||||||
|
beweist Prozess UND Engine-Erreichbarkeit, für deploy.sh/stack-postcheck.sh."""
|
||||||
|
engine = False
|
||||||
|
try:
|
||||||
|
r = await request.app.state.gw_client.get(f"{LLAMA_SWAP_URL}/v1/models", timeout=5.0)
|
||||||
|
engine = r.status_code == 200
|
||||||
|
except httpx.HTTPError:
|
||||||
|
pass
|
||||||
|
return {"status": "ok", "service": "mc2-gateway", "version": VERSION,
|
||||||
|
"engine_reachable": engine}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
"""Eigenleben-Endpoints — „Von allein"-Ansicht (Skills + Vorschlags-Bilanz), read-only."""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
|
from services import eigenleben
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/eigenleben")
|
||||||
|
def get_overview() -> dict:
|
||||||
|
return eigenleben.overview()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/eigenleben/skill/{skill_id}")
|
||||||
|
def get_skill(skill_id: str) -> dict:
|
||||||
|
text = eigenleben.skill_text(skill_id)
|
||||||
|
if text is None:
|
||||||
|
raise HTTPException(404, "Skill nicht gefunden.")
|
||||||
|
return {"id": skill_id, "text": text}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
"""
|
||||||
|
Dünner /v1-Roh-Weiterleiter (UMBAU v3, P1).
|
||||||
|
|
||||||
|
Wenn MC_V1_UPSTREAM gesetzt ist (Unit-Env), reicht das Steuerpult /v1 unangefasst an
|
||||||
|
den eigenständigen mc2-gateway-Prozess (Loopback :9010) durch — LAN-Clients wie die
|
||||||
|
IDE-Lane erreichen den Gateway sonst nicht. Hier passiert BEWUSST nichts: kein
|
||||||
|
Routing, keine Bild-Weiche, keine Token-Zählung — all das macht genau einmal der
|
||||||
|
Gateway-Prozess (routers/gateway_proxy.py). Rollback = Env-Zeile aus der Unit
|
||||||
|
entfernen → app.py bindet wieder den lokalen Gateway ein.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Request
|
||||||
|
from fastapi.responses import JSONResponse, StreamingResponse
|
||||||
|
|
||||||
|
from config import V1_UPSTREAM
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/v1")
|
||||||
|
|
||||||
|
# Hop-by-hop-Header dürfen nicht blind weitergereicht werden; content-length wird von
|
||||||
|
# httpx (Request) bzw. Starlette-Chunking (Response) neu bestimmt.
|
||||||
|
_HOP_HEADERS = {
|
||||||
|
"host", "content-length", "connection", "keep-alive", "transfer-encoding",
|
||||||
|
"upgrade", "proxy-authenticate", "proxy-authorization", "te", "trailer",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _clean(headers) -> dict:
|
||||||
|
return {k: v for k, v in headers.items() if k.lower() not in _HOP_HEADERS}
|
||||||
|
|
||||||
|
|
||||||
|
@router.api_route("/{path:path}", methods=["GET", "POST"])
|
||||||
|
async def forward(path: str, request: Request):
|
||||||
|
client = request.app.state.gw_client # geteilter Keep-Alive-Client (app.py lifespan)
|
||||||
|
url = f"{V1_UPSTREAM}/v1/{path}"
|
||||||
|
if request.url.query:
|
||||||
|
url = f"{url}?{request.url.query}"
|
||||||
|
body = await request.body()
|
||||||
|
req = client.build_request(
|
||||||
|
request.method, url, content=body or None, headers=_clean(request.headers),
|
||||||
|
timeout=None,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
upstream = await client.send(req, stream=True)
|
||||||
|
except Exception as exc: # Gateway-Prozess weg → ehrlicher 502 statt Hänger
|
||||||
|
log.warning("/v1-Weiterleitung an %s fehlgeschlagen: %s", V1_UPSTREAM, exc)
|
||||||
|
return JSONResponse(
|
||||||
|
{"error": {"message": f"mc2-gateway ({V1_UPSTREAM}) nicht erreichbar: {exc}",
|
||||||
|
"type": "gateway_unavailable"}},
|
||||||
|
status_code=502,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def gen():
|
||||||
|
try:
|
||||||
|
async for chunk in upstream.aiter_raw():
|
||||||
|
yield chunk
|
||||||
|
finally:
|
||||||
|
await upstream.aclose()
|
||||||
|
|
||||||
|
# Streaming-Passthrough für BEIDE Fälle (SSE + normale JSON-Antwort): Starlette
|
||||||
|
# chunkt selbst, Status/Header (inkl. x-mc-routed-to) kommen vom Gateway.
|
||||||
|
return StreamingResponse(
|
||||||
|
gen(), status_code=upstream.status_code, headers=_clean(upstream.headers)
|
||||||
|
)
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
"""Eigenleben — was die Box von allein gebaut und vorgeschlagen hat.
|
||||||
|
|
||||||
|
Beantwortet die User-Frage vom 15.07.2026: „Es gibt keinen Viewpoint, welche Skills
|
||||||
|
erstellt wurden, was die Box vorgeschlagen hat, welche Skills Lucy hat und wie die
|
||||||
|
funktionieren." Reine LESE-Schicht, keine Schreib-Operationen:
|
||||||
|
|
||||||
|
- Skills aus ~/.hermes/skills (= Lucys Skill-Satz; die Worker-Profile werkstatt/betrieb
|
||||||
|
tragen Kopien desselben Satzes). Herkunft dreistufig klassifiziert:
|
||||||
|
selbst → weder mitgeliefert noch aus unserem Repo = die Box/Lucy hat ihn erzeugt
|
||||||
|
repo → liegt in ~/mission-control-v2/deploy/skills (von uns gebaut + deployt)
|
||||||
|
bundled → liegt in ~/.hermes/hermes-agent/skills (kam mit Hermes mit)
|
||||||
|
- Nutzungszahlen aus ~/.hermes/skills/.usage.json (Hermes' eigener Zähler).
|
||||||
|
- Bilanz aus Auftragsbuch-Chronik (/srv/models/mc2-auftragsbuch.json, angenommene
|
||||||
|
Branches) + Ablehnungs-Lern-Journal (/srv/models/mc2-ablehnungen.jsonl).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SKILLS_DIR = Path.home() / ".hermes" / "skills"
|
||||||
|
BUNDLED_DIR = Path.home() / ".hermes" / "hermes-agent" / "skills"
|
||||||
|
REPO_DIR = Path.home() / "mission-control-v2" / "deploy" / "skills"
|
||||||
|
USAGE_FILE = SKILLS_DIR / ".usage.json"
|
||||||
|
AUFTRAGSBUCH_FILE = Path("/srv/models/mc2-auftragsbuch.json")
|
||||||
|
ABLEHNUNGEN_FILE = Path("/srv/models/mc2-ablehnungen.jsonl")
|
||||||
|
|
||||||
|
_HERKUNFT_LABEL = {
|
||||||
|
"selbst": "Selbst erstellt",
|
||||||
|
"repo": "Von uns gebaut",
|
||||||
|
"bundled": "Mit Hermes mitgeliefert",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _frontmatter(text: str) -> dict:
|
||||||
|
"""Sehr kleiner Frontmatter-Leser: nur die flachen key: value-Zeilen des ersten
|
||||||
|
---Blocks (name/description/version/author reichen hier; kein YAML-Import nötig)."""
|
||||||
|
out: dict = {}
|
||||||
|
if not text.startswith("---"):
|
||||||
|
return out
|
||||||
|
for line in text.split("\n", 1)[1].split("\n"):
|
||||||
|
if line.strip() == "---":
|
||||||
|
break
|
||||||
|
m = re.match(r"^([A-Za-z_][\w-]*):\s*(.*)$", line)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
val = m.group(2).strip().strip('"').strip("'")
|
||||||
|
if val:
|
||||||
|
out[m.group(1)] = val
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _norm(s: str) -> str:
|
||||||
|
return re.sub(r"[\s_-]+", "", (s or "").lower())
|
||||||
|
|
||||||
|
|
||||||
|
def list_skills() -> list[dict]:
|
||||||
|
"""Alle Skills mit Beschreibung, Herkunft und Nutzungszahlen (leer im Dev-Modus)."""
|
||||||
|
if not SKILLS_DIR.is_dir():
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
usage_raw = json.loads(USAGE_FILE.read_text(encoding="utf-8"))
|
||||||
|
except Exception:
|
||||||
|
usage_raw = {}
|
||||||
|
usage = {_norm(k): v for k, v in usage_raw.items()}
|
||||||
|
|
||||||
|
skills: list[dict] = []
|
||||||
|
for d in sorted(SKILLS_DIR.iterdir()):
|
||||||
|
md = d / "SKILL.md"
|
||||||
|
if not d.is_dir() or not md.is_file():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
fm = _frontmatter(md.read_text(encoding="utf-8", errors="replace"))
|
||||||
|
except Exception:
|
||||||
|
fm = {}
|
||||||
|
if (BUNDLED_DIR / d.name).is_dir():
|
||||||
|
herkunft = "bundled"
|
||||||
|
elif (REPO_DIR / d.name).is_dir():
|
||||||
|
herkunft = "repo"
|
||||||
|
else:
|
||||||
|
herkunft = "selbst"
|
||||||
|
u = usage.get(_norm(d.name)) or usage.get(_norm(fm.get("name", ""))) or {}
|
||||||
|
skills.append({
|
||||||
|
"id": d.name,
|
||||||
|
"name": fm.get("name") or d.name,
|
||||||
|
"beschreibung": fm.get("description") or "",
|
||||||
|
"version": fm.get("version") or "",
|
||||||
|
"autor": fm.get("author") or "",
|
||||||
|
"herkunft": herkunft,
|
||||||
|
"herkunft_label": _HERKUNFT_LABEL[herkunft],
|
||||||
|
"genutzt": int(u.get("use_count") or 0),
|
||||||
|
"zuletzt_genutzt": u.get("last_used_at") or None,
|
||||||
|
"erstellt": u.get("created_at") or None,
|
||||||
|
"status": u.get("state") or "active",
|
||||||
|
"geaendert": md.stat().st_mtime,
|
||||||
|
})
|
||||||
|
# Selbst erstellte zuerst — das ist der Star der Ansicht.
|
||||||
|
order = {"selbst": 0, "repo": 1, "bundled": 2}
|
||||||
|
skills.sort(key=lambda s: (order[s["herkunft"]], -s["genutzt"], s["name"]))
|
||||||
|
return skills
|
||||||
|
|
||||||
|
|
||||||
|
def skill_text(skill_id: str) -> str | None:
|
||||||
|
"""Voller SKILL.md-Text („wie funktioniert der Skill") — nur Namen ohne Pfad-Tricks."""
|
||||||
|
if not re.fullmatch(r"[\w.-]+", skill_id or ""):
|
||||||
|
return None
|
||||||
|
md = SKILLS_DIR / skill_id / "SKILL.md"
|
||||||
|
if not md.is_file():
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return md.read_text(encoding="utf-8", errors="replace")
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def bilanz() -> dict:
|
||||||
|
"""Vorschlags-Bilanz: was die Box vorschlug und was daraus wurde (eine Timeline)."""
|
||||||
|
eintraege: list[dict] = []
|
||||||
|
try:
|
||||||
|
branches = json.loads(AUFTRAGSBUCH_FILE.read_text(encoding="utf-8")).get("branches") or {}
|
||||||
|
for branch, info in branches.items():
|
||||||
|
eintraege.append({
|
||||||
|
"art": "angenommen" if info.get("state") == "eingespielt" else info.get("state", "?"),
|
||||||
|
"titel": branch,
|
||||||
|
"detail": info.get("detail") or "",
|
||||||
|
"ts": float(info.get("ts") or 0),
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
for line in ABLEHNUNGEN_FILE.read_text(encoding="utf-8").splitlines():
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
j = json.loads(line)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
eintraege.append({
|
||||||
|
"art": "abgelehnt",
|
||||||
|
"titel": j.get("subject") or j.get("branch") or "?",
|
||||||
|
"detail": j.get("grund") or "",
|
||||||
|
"ts": float(j.get("ts") or 0),
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
eintraege.sort(key=lambda e: -e["ts"])
|
||||||
|
return {
|
||||||
|
"eintraege": eintraege,
|
||||||
|
"angenommen": sum(1 for e in eintraege if e["art"] == "angenommen"),
|
||||||
|
"abgelehnt": sum(1 for e in eintraege if e["art"] == "abgelehnt"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def overview() -> dict:
|
||||||
|
skills = list_skills()
|
||||||
|
b = bilanz()
|
||||||
|
return {
|
||||||
|
"available": SKILLS_DIR.is_dir(),
|
||||||
|
"skills": skills,
|
||||||
|
"selbst_erstellt": sum(1 for s in skills if s["herkunft"] == "selbst"),
|
||||||
|
"bilanz": b,
|
||||||
|
}
|
||||||
@@ -26,9 +26,22 @@ _lock = threading.Lock()
|
|||||||
_stats: dict | None = None
|
_stats: dict | None = None
|
||||||
_dirty = False
|
_dirty = False
|
||||||
_last_flush = 0.0
|
_last_flush = 0.0
|
||||||
|
# mtime der Datei beim letzten eigenen Laden/Schreiben — seit dem Gateway-Auszug
|
||||||
|
# (UMBAU v3 P1) schreibt der mc2-gateway-Prozess die Datei, das Steuerpult liest nur
|
||||||
|
# noch: ohne mtime-Vergleich zeigte es ab Prozessstart eingefrorene Zahlen.
|
||||||
|
_disk_mtime: float | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _stat_mtime() -> float | None:
|
||||||
|
try:
|
||||||
|
return STATS_FILE.stat().st_mtime
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _load_from_disk() -> dict:
|
def _load_from_disk() -> dict:
|
||||||
|
global _disk_mtime
|
||||||
|
_disk_mtime = _stat_mtime()
|
||||||
if not STATS_FILE.exists():
|
if not STATS_FILE.exists():
|
||||||
return dict(_BASELINE)
|
return dict(_BASELINE)
|
||||||
try:
|
try:
|
||||||
@@ -51,19 +64,31 @@ def _ensure_loaded() -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def _write(stats: dict) -> None:
|
def _write(stats: dict) -> None:
|
||||||
|
global _disk_mtime
|
||||||
try:
|
try:
|
||||||
STATS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
STATS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
tmp = STATS_FILE.with_suffix(".tmp")
|
tmp = STATS_FILE.with_suffix(".tmp")
|
||||||
with open(tmp, "w", encoding="utf-8") as f:
|
with open(tmp, "w", encoding="utf-8") as f:
|
||||||
json.dump(stats, f)
|
json.dump(stats, f)
|
||||||
tmp.replace(STATS_FILE)
|
tmp.replace(STATS_FILE)
|
||||||
|
_disk_mtime = _stat_mtime() # eigener Write ist kein Fremd-Update
|
||||||
except OSError:
|
except OSError:
|
||||||
log.warning("token_stats: Schreiben fehlgeschlagen", exc_info=True)
|
log.warning("token_stats: Schreiben fehlgeschlagen", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
def get_stats() -> dict:
|
def get_stats() -> dict:
|
||||||
"""Aktueller Stand (inkl. noch nicht geflushter Inkremente) als Kopie."""
|
"""Aktueller Stand (inkl. noch nicht geflushter Inkremente) als Kopie.
|
||||||
|
|
||||||
|
Multi-Prozess-fähig: Hat ein ANDERER Prozess (mc2-gateway) die Datei inzwischen
|
||||||
|
geschrieben und liegen hier keine ungeflushten Inkremente, wird frisch geladen.
|
||||||
|
Im Gateway-Prozess selbst ist nach jedem Flush Datei == Speicher → der
|
||||||
|
mtime-Vergleich lädt dort nie unnötig nach."""
|
||||||
|
global _stats
|
||||||
with _lock:
|
with _lock:
|
||||||
|
if _stats is not None and not _dirty:
|
||||||
|
mtime = _stat_mtime()
|
||||||
|
if mtime is not None and mtime != _disk_mtime:
|
||||||
|
_stats = _load_from_disk()
|
||||||
return json.loads(json.dumps(_ensure_loaded()))
|
return json.loads(json.dumps(_ensure_loaded()))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ systemctl --user disable --now hermes-webui 2>/dev/null || true
|
|||||||
rm -f "$HOME/.config/systemd/user/hermes-terminal.service" "$HOME/.config/systemd/user/hermes-webui.service"
|
rm -f "$HOME/.config/systemd/user/hermes-terminal.service" "$HOME/.config/systemd/user/hermes-webui.service"
|
||||||
# Box-Konsole (ttyd -> Login-Shell auf :7682), in MC2 als Konsole-Seite eingebettet.
|
# Box-Konsole (ttyd -> Login-Shell auf :7682), in MC2 als Konsole-Seite eingebettet.
|
||||||
cp "$SRC/deploy/box-console.service" "$HOME/.config/systemd/user/box-console.service"
|
cp "$SRC/deploy/box-console.service" "$HOME/.config/systemd/user/box-console.service"
|
||||||
|
# MC2-Gateway (UMBAU v3 P1): der /v1-Datenpfad als eigener Prozess (Loopback :9010,
|
||||||
|
# Restart=always) — Lucys Hirn + Nacht-Autonomie überleben damit jeden MC2-Neustart.
|
||||||
|
cp "$SRC/deploy/mc2-gateway.service" "$HOME/.config/systemd/user/mc2-gateway.service"
|
||||||
# Mem0-Sidecar-Unit (nur wenn das venv existiert).
|
# Mem0-Sidecar-Unit (nur wenn das venv existiert).
|
||||||
[ -x "$HOME/.mem0/venv/bin/python" ] && cp "$SRC/deploy/mem0-service.service" "$HOME/.config/systemd/user/mem0-service.service"
|
[ -x "$HOME/.mem0/venv/bin/python" ] && cp "$SRC/deploy/mem0-service.service" "$HOME/.config/systemd/user/mem0-service.service"
|
||||||
# Voice-Sidecar-Unit (nur wenn das venv existiert).
|
# Voice-Sidecar-Unit (nur wenn das venv existiert).
|
||||||
@@ -171,6 +174,7 @@ if [ -d "$_HUI_WEB" ] && [ -x "$HOME/.hermes/node/bin/npx" ]; then
|
|||||||
fi
|
fi
|
||||||
systemctl --user daemon-reload
|
systemctl --user daemon-reload
|
||||||
systemctl --user enable mission-control-2 >/dev/null 2>&1 || true
|
systemctl --user enable mission-control-2 >/dev/null 2>&1 || true
|
||||||
|
systemctl --user enable mc2-gateway >/dev/null 2>&1 || true
|
||||||
systemctl --user enable box-console >/dev/null 2>&1 || true
|
systemctl --user enable box-console >/dev/null 2>&1 || true
|
||||||
systemctl --user enable mem0-service >/dev/null 2>&1 || true
|
systemctl --user enable mem0-service >/dev/null 2>&1 || true
|
||||||
systemctl --user enable voice-service >/dev/null 2>&1 || true
|
systemctl --user enable voice-service >/dev/null 2>&1 || true
|
||||||
@@ -191,11 +195,22 @@ loginctl enable-linger "$USER" >/dev/null 2>&1 || true
|
|||||||
# reset-failed, damit ein zuvor am Start-Limit gestorbener Dienst wieder anläuft.
|
# reset-failed, damit ein zuvor am Start-Limit gestorbener Dienst wieder anläuft.
|
||||||
systemctl --user reset-failed hermes-builtin-ui 2>/dev/null || true
|
systemctl --user reset-failed hermes-builtin-ui 2>/dev/null || true
|
||||||
systemctl --user restart hermes-builtin-ui 2>/dev/null || true
|
systemctl --user restart hermes-builtin-ui 2>/dev/null || true
|
||||||
|
# Gateway VOR dem Steuerpult (re)starten — MC2s /v1-Weiterleiter braucht ihn sofort.
|
||||||
|
# KEIN `|| true`: ohne Gateway ist der LLM-Datenpfad tot, das MUSS den Deploy stoppen.
|
||||||
|
systemctl --user restart mc2-gateway
|
||||||
systemctl --user restart mission-control-2
|
systemctl --user restart mission-control-2
|
||||||
command -v ttyd >/dev/null 2>&1 && systemctl --user restart box-console 2>/dev/null || true
|
command -v ttyd >/dev/null 2>&1 && systemctl --user restart box-console 2>/dev/null || true
|
||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
echo "--- Health ---"
|
echo "--- Health ---"
|
||||||
|
# Gateway-Kaltstart-Fenster: bis ~12 s tolerieren, DANN hart scheitern (ohne Gateway
|
||||||
|
# ist der LLM-Datenpfad tot — das muss den Karten-Runner rot machen).
|
||||||
|
for _i in $(seq 1 10); do
|
||||||
|
curl -sf -m 3 http://127.0.0.1:9010/gw/health >/dev/null 2>&1 && break
|
||||||
|
[ "$_i" -eq 10 ] && { echo "FEHLER: mc2-gateway (:9010) antwortet nicht"; exit 1; }
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
curl -sf http://127.0.0.1:9010/gw/health && echo
|
||||||
curl -sf http://127.0.0.1:9001/api/health && echo
|
curl -sf http://127.0.0.1:9001/api/health && echo
|
||||||
|
|
||||||
# Warm-Set (Hirn + Augen/vision + Gedächtnis/embed) nach dem Deploy nachladen — ein Deploy bzw.
|
# Warm-Set (Hirn + Augen/vision + Gedächtnis/embed) nach dem Deploy nachladen — ein Deploy bzw.
|
||||||
|
|||||||
Executable
+59
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Gateway-Cutover (UMBAU v3 P1, Stufe 2 = Unabhängigkeit):
|
||||||
|
# Hermes' LLM-Endpunkte vom Steuerpult-Umweg (127.0.0.1:9001/v1) auf den
|
||||||
|
# eigenständigen Gateway (127.0.0.1:9010/v1) umstellen — Top-Level-config.yaml
|
||||||
|
# UND alle Profil-Configs (werkstatt/betrieb). Danach überleben Lucys Hirn und
|
||||||
|
# die komplette Nacht-Autonomie jeden MC2-Neustart.
|
||||||
|
#
|
||||||
|
# BEWUSST NICHT in deploy.sh: ~/.hermes/config.yaml ist Zwei-Schreiber-sensibel
|
||||||
|
# (Hermes schreibt sie selbst). Einmalig von Hand bzw. per angenommener Karte:
|
||||||
|
# bash ~/mission-control-v2/deploy/gateway-cutover.sh # umstellen
|
||||||
|
# bash ~/mission-control-v2/deploy/gateway-cutover.sh --revert # zurück
|
||||||
|
# Je geänderter Datei entsteht ein Backup *.bak-gwcut-<Zeitstempel>.
|
||||||
|
# Danach: hermes-gateway-Neustart + kurzer Funktionsnachweis.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ "${1:-}" = "--revert" ]; then
|
||||||
|
ALT="127.0.0.1:9010/v1"; NEU="127.0.0.1:9001/v1"
|
||||||
|
PROBE="http://127.0.0.1:9001/api/health" # Rückweg: MC2 muss /v1 wieder selbst bedienen
|
||||||
|
else
|
||||||
|
ALT="127.0.0.1:9001/v1"; NEU="127.0.0.1:9010/v1"
|
||||||
|
PROBE="http://127.0.0.1:9010/gw/health" # Hinweg: Gateway-Prozess muss leben
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sicherung: NIE auf ein totes Ziel umstellen — das würde Lucy das Hirn abschneiden.
|
||||||
|
if ! curl -sf -m 5 "$PROBE" >/dev/null 2>&1; then
|
||||||
|
echo "ABBRUCH: Ziel $NEU antwortet nicht ($PROBE). Nichts geändert."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
ALT_RE="${ALT//./\\.}" # Punkte für sed escapen
|
||||||
|
geaendert=0
|
||||||
|
for f in "$HOME/.hermes/config.yaml" "$HOME"/.hermes/profiles/*/config.yaml; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
grep -q "$ALT" "$f" || continue
|
||||||
|
cp "$f" "$f.bak-gwcut-$STAMP"
|
||||||
|
sed -i "s|$ALT_RE|$NEU|g" "$f"
|
||||||
|
echo "umgestellt: $f (Backup: $f.bak-gwcut-$STAMP)"
|
||||||
|
geaendert=1
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$geaendert" -eq 0 ]; then
|
||||||
|
echo "Nichts zu tun — '$ALT' kommt in keiner Hermes-Config vor."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "hermes-gateway neu starten …"
|
||||||
|
systemctl --user restart hermes-gateway
|
||||||
|
sleep 3
|
||||||
|
if systemctl --user is-active --quiet hermes-gateway; then
|
||||||
|
echo "hermes-gateway läuft."
|
||||||
|
else
|
||||||
|
echo "WARN: hermes-gateway ist nach dem Neustart NICHT aktiv!"
|
||||||
|
echo " journalctl --user -u hermes-gateway -n 30 und ggf.: $0 --revert"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Empfohlener Nachweis: cd ~/.hermes/hermes-agent && venv/bin/hermes doctor"
|
||||||
|
echo "FERTIG. Rückweg jederzeit: $0 --revert"
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# systemd-USER-Unit für den MC2-Gateway — der /v1-Datenpfad als eigener Prozess (UMBAU v3 P1).
|
||||||
|
# Durch diesen Prozess läuft JEDER LLM-Aufruf der Box (Lucys Hirn, Nacht-Crons, Worker-
|
||||||
|
# Delegation, Vision) — er ist bewusst winzig, wird praktisch nie angefasst und startet
|
||||||
|
# sich immer selbst neu (Restart=always: die Wirbelsäule steht, bevor es jemand merkt).
|
||||||
|
# Loopback-only (:9010): alle kritischen Verbraucher sind box-lokal; LAN-Clients
|
||||||
|
# (IDE-Lane) kommen weiter über MC2 :9001/v1, das roh hierher durchreicht.
|
||||||
|
# Sudo-frei wie alle MC2-Units: ~/.config/systemd/user/ + systemctl --user.
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=MC2 Gateway (/v1-Datenpfad: model:auto-Routing + Bild-Weiche)
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=%h/mission-control-v2/backend
|
||||||
|
ExecStart=%h/mission-control-v2/backend/.venv/bin/python -m uvicorn gateway_app:app --host 127.0.0.1 --port 9010
|
||||||
|
Environment=PYTHONPATH=%h/mission-control-v2
|
||||||
|
Environment=MC_LLAMA_SWAP_URL=http://127.0.0.1:8080
|
||||||
|
Environment=MC_CONFIG_PATH=/etc/llama-swap/config.yaml
|
||||||
|
Environment=MC_MODELS_DIR=/srv/models
|
||||||
|
Restart=always
|
||||||
|
RestartSec=2
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -21,6 +21,10 @@ Environment=MC_CONFIG_PATH=/etc/llama-swap/config.yaml
|
|||||||
Environment=MC_MODELS_DIR=/srv/models
|
Environment=MC_MODELS_DIR=/srv/models
|
||||||
# Geteiltes Gedächtnis = die bestehende v1-DB (Kontinuität bis/über Cutover).
|
# Geteiltes Gedächtnis = die bestehende v1-DB (Kontinuität bis/über Cutover).
|
||||||
Environment=MC_MEMORY_DB=/srv/models/mission-control-memory.db
|
Environment=MC_MEMORY_DB=/srv/models/mission-control-memory.db
|
||||||
|
# Gateway-Auszug (UMBAU v3 P1): /v1 roh an den eigenständigen mc2-gateway-Prozess
|
||||||
|
# durchreichen. Diese Zeile entfernen (+ daemon-reload + restart) = Rollback, MC2
|
||||||
|
# bedient /v1 wieder selbst.
|
||||||
|
Environment=MC_V1_UPSTREAM=http://127.0.0.1:9010
|
||||||
# KEIN MC_ENGINE_UPDATE_CMD-Override mehr: Das alte /usr/local/bin/update-llamacpp zog den
|
# KEIN MC_ENGINE_UPDATE_CMD-Override mehr: Das alte /usr/local/bin/update-llamacpp zog den
|
||||||
# ROCm-Build nach /opt/llamacpp (totes Rollback-Dir) statt des aktiven Vulkan-Builds → Updates
|
# ROCm-Build nach /opt/llamacpp (totes Rollback-Dir) statt des aktiven Vulkan-Builds → Updates
|
||||||
# liefen ins Leere ("DONE", aber nichts passierte). Ohne Override nutzt das Backend den Default
|
# liefen ins Leere ("DONE", aber nichts passierte). Ohne Override nutzt das Backend den Default
|
||||||
|
|||||||
@@ -65,6 +65,17 @@ else
|
|||||||
echo "FAIL · MC2 /api/health meldet Engine nicht erreichbar"; fail=1
|
echo "FAIL · MC2 /api/health meldet Engine nicht erreichbar"; fail=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 4b. MC2-Gateway (eigenständiger /v1-Datenpfad, UMBAU v3 P1) gesund? Nur prüfen, wenn
|
||||||
|
# die Unit enabled ist — Postchecks können auf Ständen vor dem Gateway-Auszug laufen.
|
||||||
|
GW_URL="${MC2_GATEWAY_URL:-http://127.0.0.1:9010}"
|
||||||
|
if systemctl --user is-enabled --quiet mc2-gateway 2>/dev/null; then
|
||||||
|
if curl -sf -m 8 "$GW_URL/gw/health" 2>/dev/null | grep -qE '"engine_reachable":[[:space:]]*true'; then
|
||||||
|
echo "PASS · mc2-gateway gesund (Prozess + Engine)"
|
||||||
|
else
|
||||||
|
echo "FAIL · mc2-gateway antwortet nicht/Engine weg — LLM-DATENPFAD BETROFFEN"; fail=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# 5. Mem0-Sidecar (Gedächtnis) erreichbar?
|
# 5. Mem0-Sidecar (Gedächtnis) erreichbar?
|
||||||
if curl -sf -m 5 "$MEM0_URL/health" >/dev/null 2>&1; then
|
if curl -sf -m 5 "$MEM0_URL/health" >/dev/null 2>&1; then
|
||||||
echo "PASS · Mem0-Sidecar erreichbar"
|
echo "PASS · Mem0-Sidecar erreichbar"
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ Recherche). DIE Arbeitsliste für die Zeit nach Claude. Leser: der User, die Box
|
|||||||
- Warm-Set schlank: Hirn 17,5 GB + embed 2 GB + reranker 2,2 GB ≈ 22 GB.
|
- Warm-Set schlank: Hirn 17,5 GB + embed 2 GB + reranker 2,2 GB ≈ 22 GB.
|
||||||
- Repo: lokal = Gitea = Box @ main. Live-Checkout wieder sauber auf `main`.
|
- Repo: lokal = Gitea = Box @ main. Live-Checkout wieder sauber auf `main`.
|
||||||
- **Autonomie-Beweis der Nacht 14.07.:** Idle-Radar erhob Journal-Befund (stop-sigterm-Timeout) → Decomposer zerlegte → werkstatt baute Restart-Skript+systemd-Override → betrieb testete live (~6 s Recovery) → done. Ohne Menschen, ohne Claude.
|
- **Autonomie-Beweis der Nacht 14.07.:** Idle-Radar erhob Journal-Befund (stop-sigterm-Timeout) → Decomposer zerlegte → werkstatt baute Restart-Skript+systemd-Override → betrieb testete live (~6 s Recovery) → done. Ohne Menschen, ohne Claude.
|
||||||
|
- **★ Nachmittags-Befund (Greenfield-Review + Live-Inspektion 15.07.):** JEDER LLM-Aufruf der Box läuft durch den EINEN MC2-Prozess auf :9001 — Lucys Haupt-Hirn (`model.base_url`, config.yaml ~Z. 430), alle auxiliary-Rollen (vision/curator/kanban_decomposer/triage_specifier), die komplette `delegation` (jeder Worker-Auftrag) und 3 MCP-Server (MC_URL). Der am häufigsten neu gestartete Dienst ist zugleich die Wirbelsäule des gesamten Stacks; das `Restart=always`-Override ist rückblickend ein Symptom dieser Kopplung. Konsequenz: **Abschnitt 3b · UMBAU v3.**
|
||||||
|
|
||||||
## 1 · Heute im Review erledigt (15.07.)
|
## 1 · Heute im Review erledigt (15.07.)
|
||||||
|
|
||||||
@@ -49,6 +50,43 @@ c) **sudo-Runde (einmal Passwort):**
|
|||||||
2. Root-Warmup-Kopie aktualisieren: `sudo install -m 0755 ~/mission-control-v2/deploy/warmup.sh /usr/local/bin/llama-swap-warmup.sh` (stale seit 07.07. — sonst wärmt ein llama-swap-Restart falsch)
|
2. Root-Warmup-Kopie aktualisieren: `sudo install -m 0755 ~/mission-control-v2/deploy/warmup.sh /usr/local/bin/llama-swap-warmup.sh` (stale seit 07.07. — sonst wärmt ein llama-swap-Restart falsch)
|
||||||
d) **Daily-Briefing-Cron** (dein eigener, 08:00): steht auf `Repeat: 9/41` — **läuft nach 41 Läufen aus**. Wenn dauerhaft gewollt: auf ∞ stellen (`hermes cron edit 095d53c1e99e`).
|
d) **Daily-Briefing-Cron** (dein eigener, 08:00): steht auf `Repeat: 9/41` — **läuft nach 41 Läufen aus**. Wenn dauerhaft gewollt: auf ∞ stellen (`hermes cron edit 095d53c1e99e`).
|
||||||
|
|
||||||
|
## 3b · UMBAU v3 — Architektur-Feinschliff (NEU 15.07. nachmittags)
|
||||||
|
|
||||||
|
**Woher:** Greenfield-Gedankenexperiment („wie sähe ein Komplett-Rework aus?") + Live-Inspektion
|
||||||
|
der Box. Ergebnis: KEIN Rewrite (die Box wartet sich nachweislich selbst — während der Inspektion
|
||||||
|
lief Karte `t_ab6754f1` und lud das Gemma-4-Bench-Modell), sondern vier Inkremente, die je für
|
||||||
|
sich Wert haben. Reihenfolge ist von der Topologie diktiert, nicht verhandelbar: P1 ent-riskiert
|
||||||
|
alles Weitere. Recherche-Verdikte dazu (15.07.): FastAPI bleibt (Litestar erwogen — Ökosystem/
|
||||||
|
Coder-Modell-Wissen gewinnen), React bleibt (Svelte/Solid schneller, aber irrelevant für
|
||||||
|
1-Nutzer-LAN), SSE ist 2026 der klare Dashboard-Standard (einweg, Auto-Reconnect, ~95 % der Fälle).
|
||||||
|
|
||||||
|
- **P1 · Gateway-Auszug** ✅ GEBAUT (Branch `umbau/p1-gateway-auszug`, wartet auf Klick):
|
||||||
|
Der /v1-Datenpfad wird eigener Prozess `mc2-gateway.service` (Loopback **:9010**,
|
||||||
|
`Restart=always`, gleicher Router-Code `routers/gateway_proxy.py`, neuer Einstieg
|
||||||
|
`backend/gateway_app.py`). MC2 :9001/v1 wird dünner Roh-Weiterleiter (`MC_V1_UPSTREAM` in der
|
||||||
|
Unit; Zeile entfernen = Rollback), damit LAN-Clients (IDE-Lane) nichts merken. Token-Zählung
|
||||||
|
passiert genau EINMAL (im Gateway); `token_stats.get_stats()` lädt bei Fremd-Änderung per
|
||||||
|
mtime nach (Steuerpult zeigt sonst eingefrorene Zahlen). **Stufe 2 = Unabhängigkeit:**
|
||||||
|
`deploy/gateway-cutover.sh` stellt ~/.hermes-Configs (Top-Level + beide Profile) von
|
||||||
|
`127.0.0.1:9001/v1` auf `127.0.0.1:9010/v1` um (Backups, `--revert`, bewusst NICHT im
|
||||||
|
deploy.sh — config.yaml ist Zwei-Schreiber-sensibel). Danach überlebt Lucys Hirn + die ganze
|
||||||
|
Nacht-Autonomie jeden MC2-Neustart. Postcheck prüft den Gateway mit (nur wenn Unit enabled).
|
||||||
|
- **P2 · Steward** (Karte, nach P1): die 4 Endlos-Loops (warmer, sentry, reminders, Mem0-dedupe)
|
||||||
|
aus dem Webserver-Lifespan in einen eigenen Mini-Dienst — ein Steuerpult-Neustart darf keinem
|
||||||
|
Wächter Timing/Gedächtnis nehmen. Danach gibt es genau ZWEI Zeitplan-Systeme mit klaren Rollen:
|
||||||
|
systemd-Timer (Box-Pflege) + Hermes-Crons (Agenten-Arbeit).
|
||||||
|
- **P3 · Steuerpult-Modernisierung** (Kartenserie, reine UI/API-Arbeit, berührt nach P1 nie mehr
|
||||||
|
den Datenpfad): (a) EIN SSE-Eventstrom `/api/events` ersetzt die 53 `refetchInterval`-Poller in
|
||||||
|
`queries.ts`; (b) TanStack Router → echte URLs (`/modelle/coder`, `/auftrag/t_…`), Deep-Links,
|
||||||
|
Browser-Zurück; (c) TypeScript-Typen aus OpenAPI GENERIEREN (openapi-typescript) statt
|
||||||
|
handgepflegtem `api.ts` (stiller Drift-Fehlerherd).
|
||||||
|
- **P4 · State-Konsolidierung** (optional, NUR bei Schmerz): die 10 `mc2-*.json/jsonl` in
|
||||||
|
/srv/models → eine SQLite (WAL) mit Event-Tabelle (Chronik/Zeitmaschine = Abfragen statt
|
||||||
|
Eigenbau; Backup = eine Datei). Nutzen real, aber Risiko/Aufwand hoch — bewusst hinten.
|
||||||
|
|
||||||
|
**Bewusst NICHT geändert:** FastAPI, React/Vite/Tailwind, kein Electron, kein Chat in MC2,
|
||||||
|
LAN-Trust ohne Auth, Appliance-Philosophie (user-services, deploy.sh, Postcheck, Rollback).
|
||||||
|
|
||||||
## 4 · ROBUSTHEIT (Priorität 1 — Karten für die Queue)
|
## 4 · ROBUSTHEIT (Priorität 1 — Karten für die Queue)
|
||||||
|
|
||||||
- **R1 · Worker-Protokoll-Erinnerung** (klein): `box-steckbrief-inject.sh` um einen Satz ergänzen: „Am Ende IMMER kanban_complete oder kanban_block aufrufen — sauberes Text-Ende zählt nicht." Genau daran blockierte die Nacht-Karte.
|
- **R1 · Worker-Protokoll-Erinnerung** (klein): `box-steckbrief-inject.sh` um einen Satz ergänzen: „Am Ende IMMER kanban_complete oder kanban_block aufrufen — sauberes Text-Ende zählt nicht." Genau daran blockierte die Nacht-Karte.
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as k,R as _,m as S,b as C,u as H,r as E,j as e,U as A,e as a,V as u,W as p,Y as B,s as h,Z as D,Q as P,X as M,C as g,g as b,q as i}from"./index-CgQUayy8.js";/**
|
import{c as k,R as _,m as S,b as C,u as H,r as E,j as e,U as A,e as a,V as u,W as p,Y as B,s as h,Z as D,Q as P,X as M,C as g,g as b,q as i}from"./index-KOW4xETl.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,a4 as X,u as _,b as Q,r as h,j as e,a5 as Y,e as S,L as g,C as q,N as U,a6 as ee,a7 as te,O as V,X as I,w as R,F,a8 as re,T as z,g as B,a9 as se,q as J}from"./index-CgQUayy8.js";import{L as ae}from"./lightbulb-Dj1hbza2.js";import{S as G}from"./send-BjlsoGEB.js";/**
|
import{c as O,a4 as X,u as _,b as Q,r as h,j as e,a5 as Y,e as S,L as g,C as q,N as U,a6 as ee,a7 as te,O as V,X as I,w as R,F,a8 as re,T as z,g as B,a9 as se,q as J}from"./index-KOW4xETl.js";import{L as ae}from"./lightbulb-NhH1RZ1n.js";import{S as G}from"./send-Ca4K5Qji.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as h,aa as S,r as c,j as e,ab as z,L as g,N as M,ac as C,a9 as D,ad as A,ae as B,af as Z,e as E,ag as L,u as q,b as R,g as k,q as T}from"./index-CgQUayy8.js";/**
|
import{c as h,aa as S,r as c,j as e,ab as z,L as g,N as M,ac as C,a9 as D,ad as A,ae as B,af as Z,e as E,ag as L,u as q,b as R,g as k,q as T}from"./index-KOW4xETl.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
+4
-9
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as p,j as e,r as M,s as C,a0 as z,a1 as k,a2 as y,W as v,Q as L,a3 as G,Z as T,e as f,N as D}from"./index-CgQUayy8.js";import{B as w}from"./book-open-ClD73ijg.js";import{a as N,b as B,L as P,C as E,Z as R}from"./zap-B7eYUqmc.js";import{L as I}from"./lightbulb-Dj1hbza2.js";/**
|
import{c as p,j as e,r as M,s as C,a0 as z,a1 as k,a2 as y,W as v,Q as L,a3 as G,Z as T,e as f,N as D}from"./index-KOW4xETl.js";import{B as w}from"./book-open-DIahvumE.js";import{a as N,b as B,L as P,C as E,Z as R}from"./zap-D4PbbO5d.js";import{L as I}from"./lightbulb-NhH1RZ1n.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
import{R as l,V as o,j as e,e as a,U as n,T as d,$ as i}from"./index-CgQUayy8.js";function c(){const{data:s}=l(),r=s!=null&&s.box_console_url?o(s.box_console_url):void 0,t=s==null?void 0:s.box_console_reachable;return e.jsxs("div",{className:"flex h-full flex-col gap-4",children:[e.jsxs("div",{className:"flex flex-wrap items-end justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Konsole"}),e.jsx("p",{className:"flex items-center gap-2 text-sm text-muted-foreground",children:"Direkte Shell auf der Box — wie ein SSH-Fenster, mitten im Browser."})]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground",children:[e.jsx("span",{className:a("h-2 w-2 rounded-full",t?"bg-emerald-500 animate-pulse":"bg-amber-500")}),t?"online":"offline"]}),r&&e.jsxs("a",{href:r,target:"_blank",rel:"noopener",className:"flex h-8 items-center gap-1.5 rounded-lg border border-border/60 bg-background/20 px-3 text-xs font-semibold text-muted-foreground transition-all hover:border-primary/50 hover:text-foreground",children:[e.jsx(n,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),r?e.jsxs("div",{className:"relative min-h-[58vh] flex-1 overflow-hidden rounded-2xl border border-border/60 bg-black/50 shadow-lg shadow-black/25",children:[t===!1&&e.jsxs("div",{className:"absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/70 text-center",children:[e.jsx(d,{className:"h-8 w-8 text-amber-400"}),e.jsx("div",{className:"text-sm font-semibold text-amber-300",children:"Konsole nicht erreichbar"}),e.jsxs("p",{className:"max-w-sm text-[11px] leading-normal text-muted-foreground",children:["Der ",e.jsx("code",{className:"font-mono text-primary",children:"box-console"}),"-Dienst (ttyd) läuft nicht. Auf der Box: ",e.jsx("code",{className:"font-mono",children:"systemctl --user restart box-console"}),"."]})]}),e.jsx("iframe",{src:r,title:"Box-Konsole",className:"h-full w-full border-0",style:{minHeight:"58vh"}})]}):e.jsxs("div",{className:"flex min-h-[58vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground",children:[e.jsx(i,{className:"mr-2 h-4 w-4"})," Lade Konsole…"]})]})}export{c as KonsoleView};
|
import{R as l,V as o,j as e,e as a,U as n,T as d,$ as i}from"./index-KOW4xETl.js";function c(){const{data:s}=l(),r=s!=null&&s.box_console_url?o(s.box_console_url):void 0,t=s==null?void 0:s.box_console_reachable;return e.jsxs("div",{className:"flex h-full flex-col gap-4",children:[e.jsxs("div",{className:"flex flex-wrap items-end justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Konsole"}),e.jsx("p",{className:"flex items-center gap-2 text-sm text-muted-foreground",children:"Direkte Shell auf der Box — wie ein SSH-Fenster, mitten im Browser."})]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground",children:[e.jsx("span",{className:a("h-2 w-2 rounded-full",t?"bg-emerald-500 animate-pulse":"bg-amber-500")}),t?"online":"offline"]}),r&&e.jsxs("a",{href:r,target:"_blank",rel:"noopener",className:"flex h-8 items-center gap-1.5 rounded-lg border border-border/60 bg-background/20 px-3 text-xs font-semibold text-muted-foreground transition-all hover:border-primary/50 hover:text-foreground",children:[e.jsx(n,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),r?e.jsxs("div",{className:"relative min-h-[58vh] flex-1 overflow-hidden rounded-2xl border border-border/60 bg-black/50 shadow-lg shadow-black/25",children:[t===!1&&e.jsxs("div",{className:"absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/70 text-center",children:[e.jsx(d,{className:"h-8 w-8 text-amber-400"}),e.jsx("div",{className:"text-sm font-semibold text-amber-300",children:"Konsole nicht erreichbar"}),e.jsxs("p",{className:"max-w-sm text-[11px] leading-normal text-muted-foreground",children:["Der ",e.jsx("code",{className:"font-mono text-primary",children:"box-console"}),"-Dienst (ttyd) läuft nicht. Auf der Box: ",e.jsx("code",{className:"font-mono",children:"systemctl --user restart box-console"}),"."]})]}),e.jsx("iframe",{src:r,title:"Box-Konsole",className:"h-full w-full border-0",style:{minHeight:"58vh"}})]}):e.jsxs("div",{className:"flex min-h-[58vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground",children:[e.jsx(i,{className:"mr-2 h-4 w-4"})," Lade Konsole…"]})]})}export{c as KonsoleView};
|
||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/GraphView-BrCeW1FA.js","assets/index-CgQUayy8.js","assets/index-Bpukq9Y7.css"])))=>i.map(i=>d[i]);
|
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/GraphView-Dqr19v9r.js","assets/index-KOW4xETl.js","assets/index-BAToQqxX.css"])))=>i.map(i=>d[i]);
|
||||||
var ke=Object.defineProperty;var ve=(d,i,c)=>i in d?ke(d,i,{enumerable:!0,configurable:!0,writable:!0,value:c}):d[i]=c;var U=(d,i,c)=>ve(d,typeof i!="symbol"?i+"":i,c);import{c as h,r as s,j as e,u as Ne,b as Se,I as J,J as Ce,C as W,K as Ee,S as Me,M as ze,N as _,e as x,O as _e,Q as Oe,X,v as Z,_ as Te,g as j}from"./index-CgQUayy8.js";import{C as Y}from"./copy-B1SWMsx4.js";import{S as Ae}from"./send-BjlsoGEB.js";import{B as De}from"./book-open-ClD73ijg.js";/**
|
var ke=Object.defineProperty;var ve=(d,i,c)=>i in d?ke(d,i,{enumerable:!0,configurable:!0,writable:!0,value:c}):d[i]=c;var U=(d,i,c)=>ve(d,typeof i!="symbol"?i+"":i,c);import{c as h,r as s,j as e,u as Ne,b as Se,I as J,J as Ce,C as W,K as Ee,S as Me,M as ze,N as _,e as x,O as _e,Q as Oe,X,v as Z,_ as Te,g as j}from"./index-KOW4xETl.js";import{C as Y}from"./copy-DZwsKvPS.js";import{S as Ae}from"./send-Ca4K5Qji.js";import{B as De}from"./book-open-DIahvumE.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
@@ -34,7 +34,7 @@ var ke=Object.defineProperty;var ve=(d,i,c)=>i in d?ke(d,i,{enumerable:!0,config
|
|||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
* See the LICENSE file in the root directory of this source tree.
|
* See the LICENSE file in the root directory of this source tree.
|
||||||
*/const Re=h("User",[["path",{d:"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2",key:"975kel"}],["circle",{cx:"12",cy:"7",r:"4",key:"17ys0d"}]]);class re extends s.Component{constructor(){super(...arguments);U(this,"state",{error:null})}static getDerivedStateFromError(c){return{error:c}}render(){return this.state.error?e.jsxs("div",{className:"h-[480px] rounded-2xl border border-red-500/20 bg-red-500/5 p-6 text-xs text-red-400 overflow-auto",children:[e.jsx("div",{className:"font-semibold mb-2",children:"Graph konnte nicht gerendert werden"}),e.jsx("pre",{className:"whitespace-pre-wrap break-words text-[11px] leading-relaxed",children:this.state.error.message})]}):this.props.children}}const te=s.lazy(()=>Te(()=>import("./GraphView-BrCeW1FA.js"),__vite__mapDeps([0,1,2])).then(d=>({default:d.GraphView}))),k=["identity","knowledge","rules","events"],se=new Set(["auto","agent","hermes"]),O={identity:{label:"Identität",icon:Re,bg:"bg-cyan-500/10",text:"text-cyan-400"},knowledge:{label:"Wissen",icon:Oe,bg:"bg-indigo-500/10",text:"text-indigo-400"},rules:{label:"Regeln",icon:Pe,bg:"bg-violet-500/10",text:"text-violet-400"},events:{label:"Ereignisse",icon:_e,bg:"bg-amber-500/10",text:"text-amber-400"}},ne={label:"Gedächtnis",icon:De,text:"text-muted-foreground"},Ve={identity:"border-l-cyan-500/80",knowledge:"border-l-indigo-500/80",rules:"border-l-violet-500/80",events:"border-l-amber-500/80"},ae=`Hi Hermes! Lass uns ein kurzes Onboarding machen, damit du dich künftig an mich erinnerst. Stell mir nacheinander ein paar kurze Fragen zu:
|
*/const Re=h("User",[["path",{d:"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2",key:"975kel"}],["circle",{cx:"12",cy:"7",r:"4",key:"17ys0d"}]]);class re extends s.Component{constructor(){super(...arguments);U(this,"state",{error:null})}static getDerivedStateFromError(c){return{error:c}}render(){return this.state.error?e.jsxs("div",{className:"h-[480px] rounded-2xl border border-red-500/20 bg-red-500/5 p-6 text-xs text-red-400 overflow-auto",children:[e.jsx("div",{className:"font-semibold mb-2",children:"Graph konnte nicht gerendert werden"}),e.jsx("pre",{className:"whitespace-pre-wrap break-words text-[11px] leading-relaxed",children:this.state.error.message})]}):this.props.children}}const te=s.lazy(()=>Te(()=>import("./GraphView-Dqr19v9r.js"),__vite__mapDeps([0,1,2])).then(d=>({default:d.GraphView}))),k=["identity","knowledge","rules","events"],se=new Set(["auto","agent","hermes"]),O={identity:{label:"Identität",icon:Re,bg:"bg-cyan-500/10",text:"text-cyan-400"},knowledge:{label:"Wissen",icon:Oe,bg:"bg-indigo-500/10",text:"text-indigo-400"},rules:{label:"Regeln",icon:Pe,bg:"bg-violet-500/10",text:"text-violet-400"},events:{label:"Ereignisse",icon:_e,bg:"bg-amber-500/10",text:"text-amber-400"}},ne={label:"Gedächtnis",icon:De,text:"text-muted-foreground"},Ve={identity:"border-l-cyan-500/80",knowledge:"border-l-indigo-500/80",rules:"border-l-violet-500/80",events:"border-l-amber-500/80"},ae=`Hi Hermes! Lass uns ein kurzes Onboarding machen, damit du dich künftig an mich erinnerst. Stell mir nacheinander ein paar kurze Fragen zu:
|
||||||
1) wer ich bin und woran ich gerade arbeite,
|
1) wer ich bin und woran ich gerade arbeite,
|
||||||
2) wie ich angesprochen werden möchte,
|
2) wie ich angesprochen werden möchte,
|
||||||
3) meine bevorzugten Tools, Sprachen und Arbeitsweise,
|
3) meine bevorzugten Tools, Sprachen und Arbeitsweise,
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as D,u as Ce,a as Ee,b as Se,j as e,f as ce,d as De,e as g,g as B,q as te,B as ae,h as qe,E as _e,i as Ge,r as y,X as Re,C as J,T as re,k as Z,H as Ae,l as P,m as me,n as Fe,o as Ke,p as Oe,S as pe,s as Ie,P as Te,t as Ue,v as He,w as Le,x as Pe,y as Qe}from"./index-CgQUayy8.js";import{C as xe,Z as ue,L as Ve,a as We,b as Je}from"./zap-B7eYUqmc.js";/**
|
import{c as D,u as Ce,a as Ee,b as Se,j as e,f as ce,d as De,e as g,g as B,q as te,B as ae,h as qe,E as _e,i as Ge,r as y,X as Re,C as J,T as re,k as Z,H as Ae,l as P,m as me,n as Fe,o as Ke,p as Oe,S as pe,s as Ie,P as Te,t as Ue,v as He,w as Le,x as Pe,y as Qe}from"./index-KOW4xETl.js";import{C as xe,Z as ue,L as Ve,a as We,b as Je}from"./zap-D4PbbO5d.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{ah as y,r as x,g as L,j as e,L as v,S,ai as W,N as C,e as w,aj as E}from"./index-CgQUayy8.js";import{F as M}from"./folder-open-tYLNcEQ3.js";const D={"":"Allgemein",traeume:"Träume",muster:"Muster","skill-kandidaten":"Skill-Kandidaten","skill-kandidaten/beauftragt":"Skill-Kandidaten · beauftragt","skill-kandidaten/verworfen":"Skill-Kandidaten · verworfen"};function T(){const{data:o,isLoading:h}=y(),[l,f]=x.useState(null),[d,r]=x.useState(null),[u,i]=x.useState(!1),[a,p]=x.useState(""),t=x.useMemo(()=>(o==null?void 0:o.files)??[],[o]),m=x.useMemo(()=>{const s=new Map;for(const n of t)s.set(n.name.toLowerCase(),n.path);return s},[t]),g=x.useMemo(()=>{if(!a.trim())return t;const s=a.toLowerCase();return t.filter(n=>n.title.toLowerCase().includes(s)||n.path.toLowerCase().includes(s))},[t,a]),b=x.useMemo(()=>{const s=[];for(const n of g){const c=s.find(k=>k.dir===n.dir);c?c.files.push(n):s.push({dir:n.dir,files:[n]})}return s.sort((n,c)=>n.dir===""?-1:c.dir===""?1:n.dir.localeCompare(c.dir))},[g]);x.useEffect(()=>{if(!l&&t.length){const s=t.find(n=>n.path.toLowerCase()==="index.md");f((s==null?void 0:s.path)??t[0].path)}},[t,l]),x.useEffect(()=>{l&&(i(!0),L(`/api/wissen/datei?pfad=${encodeURIComponent(l)}`).then(r).catch(()=>r({path:l,content:"*(Notiz nicht ladbar)*",mtime:0})).finally(()=>i(!1)))},[l]);const j=s=>{const n=m.get(s.toLowerCase());n&&f(n)};return e.jsxs("div",{className:"space-y-5",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Wissen"}),e.jsx("p",{className:"text-sm text-muted-foreground",children:"Was sich die Box nachts erschließt — Traum-Notizen, Muster und Skill-Ideen aus dem Wissens-Vault, verlinkt wie ein Wiki."})]}),o&&!o.available&&e.jsx("div",{className:"rounded-2xl border border-amber-500/30 bg-amber-500/5 p-4 text-xs text-amber-300",children:"Der Wissens-Vault liegt auf der Box (~/wissens-vault) — im lokalen Dev-Modus nicht verfügbar."}),h&&e.jsxs("div",{className:"flex items-center gap-2 rounded-2xl border border-border/60 bg-card/30 p-6 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Vault wird geladen …"]}),(o==null?void 0:o.available)&&t.length===0&&e.jsx("div",{className:"rounded-2xl border border-dashed border-border/60 bg-card/20 p-8 text-center text-xs text-muted-foreground",children:"Noch keine Notizen. Der nächtliche Traum (03:15) legt sie an — morgen früh steht hier die erste."}),t.length>0&&e.jsxs("div",{className:"flex flex-col gap-4 lg:flex-row",children:[e.jsxs("aside",{className:"w-full shrink-0 space-y-3 lg:w-72",children:[e.jsxs("div",{className:"relative",children:[e.jsx("input",{value:a,onChange:s=>p(s.target.value),type:"search",placeholder:"Notizen durchsuchen…","aria-label":"Notizen durchsuchen",className:"h-9 w-full rounded-lg border border-border/60 bg-card/45 pl-8 pr-3 text-xs text-foreground outline-none focus:ring-1 focus:ring-primary/50"}),e.jsx(S,{className:"absolute left-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground"})]}),e.jsx("div",{className:"max-h-[70vh] space-y-3 overflow-y-auto pr-1 scrollbar-thin",children:b.map(({dir:s,files:n})=>e.jsxs("div",{children:[e.jsxs("p",{className:"mb-1 flex items-center gap-1 text-[10px] font-bold uppercase tracking-wider text-muted-foreground/60",children:[e.jsx(M,{className:"h-3 w-3"})," ",D[s]??s]}),e.jsx("div",{className:"space-y-0.5",children:n.map(c=>e.jsxs("button",{onClick:()=>f(c.path),className:w("flex w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left text-xs transition-all cursor-pointer",l===c.path?"bg-primary/15 text-primary":"text-muted-foreground hover:bg-accent hover:text-foreground"),children:[e.jsx(W,{className:"h-3.5 w-3.5 shrink-0"}),e.jsx("span",{className:"truncate",title:c.title,children:c.name}),c.neu&&e.jsxs("span",{className:"ml-auto flex items-center gap-0.5 rounded bg-violet-500/15 px-1 py-0.5 text-[8px] font-bold uppercase text-violet-300",children:[e.jsx(C,{className:"h-2.5 w-2.5"}),"neu"]})]},c.path))})]},s||"__root"))})]}),e.jsx("div",{className:"min-w-0 flex-1 mc-card p-5",children:u?e.jsxs("div",{className:"flex items-center gap-2 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Notiz wird geladen …"]}):d?e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"mb-3 flex items-center justify-between border-b border-border/40 pb-2",children:[e.jsxs("span",{className:"flex items-center gap-1.5 font-mono text-[11px] text-muted-foreground",children:[e.jsx(E,{className:"h-3.5 w-3.5"})," ",d.path]}),d.mtime>0&&e.jsxs("span",{className:"text-[10px] text-muted-foreground/60",children:["Stand ",new Date(d.mtime*1e3).toLocaleString("de-DE",{day:"2-digit",month:"2-digit",hour:"2-digit",minute:"2-digit"})]})]}),e.jsx(z,{text:d.content,onWikiLink:j,known:m})]}):null})]})]})}function z({text:o,onWikiLink:h,known:l}){const f=o.split(`
|
import{aj as y,r as x,g as L,j as e,L as v,S,ak as W,N as C,e as w,al as E}from"./index-KOW4xETl.js";import{F as M}from"./folder-open-C6effGop.js";const D={"":"Allgemein",traeume:"Träume",muster:"Muster","skill-kandidaten":"Skill-Kandidaten","skill-kandidaten/beauftragt":"Skill-Kandidaten · beauftragt","skill-kandidaten/verworfen":"Skill-Kandidaten · verworfen"};function T(){const{data:o,isLoading:h}=y(),[l,f]=x.useState(null),[d,r]=x.useState(null),[u,i]=x.useState(!1),[a,p]=x.useState(""),t=x.useMemo(()=>(o==null?void 0:o.files)??[],[o]),m=x.useMemo(()=>{const s=new Map;for(const n of t)s.set(n.name.toLowerCase(),n.path);return s},[t]),g=x.useMemo(()=>{if(!a.trim())return t;const s=a.toLowerCase();return t.filter(n=>n.title.toLowerCase().includes(s)||n.path.toLowerCase().includes(s))},[t,a]),b=x.useMemo(()=>{const s=[];for(const n of g){const c=s.find(k=>k.dir===n.dir);c?c.files.push(n):s.push({dir:n.dir,files:[n]})}return s.sort((n,c)=>n.dir===""?-1:c.dir===""?1:n.dir.localeCompare(c.dir))},[g]);x.useEffect(()=>{if(!l&&t.length){const s=t.find(n=>n.path.toLowerCase()==="index.md");f((s==null?void 0:s.path)??t[0].path)}},[t,l]),x.useEffect(()=>{l&&(i(!0),L(`/api/wissen/datei?pfad=${encodeURIComponent(l)}`).then(r).catch(()=>r({path:l,content:"*(Notiz nicht ladbar)*",mtime:0})).finally(()=>i(!1)))},[l]);const j=s=>{const n=m.get(s.toLowerCase());n&&f(n)};return e.jsxs("div",{className:"space-y-5",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Wissen"}),e.jsx("p",{className:"text-sm text-muted-foreground",children:"Was sich die Box nachts erschließt — Traum-Notizen, Muster und Skill-Ideen aus dem Wissens-Vault, verlinkt wie ein Wiki."})]}),o&&!o.available&&e.jsx("div",{className:"rounded-2xl border border-amber-500/30 bg-amber-500/5 p-4 text-xs text-amber-300",children:"Der Wissens-Vault liegt auf der Box (~/wissens-vault) — im lokalen Dev-Modus nicht verfügbar."}),h&&e.jsxs("div",{className:"flex items-center gap-2 rounded-2xl border border-border/60 bg-card/30 p-6 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Vault wird geladen …"]}),(o==null?void 0:o.available)&&t.length===0&&e.jsx("div",{className:"rounded-2xl border border-dashed border-border/60 bg-card/20 p-8 text-center text-xs text-muted-foreground",children:"Noch keine Notizen. Der nächtliche Traum (03:15) legt sie an — morgen früh steht hier die erste."}),t.length>0&&e.jsxs("div",{className:"flex flex-col gap-4 lg:flex-row",children:[e.jsxs("aside",{className:"w-full shrink-0 space-y-3 lg:w-72",children:[e.jsxs("div",{className:"relative",children:[e.jsx("input",{value:a,onChange:s=>p(s.target.value),type:"search",placeholder:"Notizen durchsuchen…","aria-label":"Notizen durchsuchen",className:"h-9 w-full rounded-lg border border-border/60 bg-card/45 pl-8 pr-3 text-xs text-foreground outline-none focus:ring-1 focus:ring-primary/50"}),e.jsx(S,{className:"absolute left-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground"})]}),e.jsx("div",{className:"max-h-[70vh] space-y-3 overflow-y-auto pr-1 scrollbar-thin",children:b.map(({dir:s,files:n})=>e.jsxs("div",{children:[e.jsxs("p",{className:"mb-1 flex items-center gap-1 text-[10px] font-bold uppercase tracking-wider text-muted-foreground/60",children:[e.jsx(M,{className:"h-3 w-3"})," ",D[s]??s]}),e.jsx("div",{className:"space-y-0.5",children:n.map(c=>e.jsxs("button",{onClick:()=>f(c.path),className:w("flex w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left text-xs transition-all cursor-pointer",l===c.path?"bg-primary/15 text-primary":"text-muted-foreground hover:bg-accent hover:text-foreground"),children:[e.jsx(W,{className:"h-3.5 w-3.5 shrink-0"}),e.jsx("span",{className:"truncate",title:c.title,children:c.name}),c.neu&&e.jsxs("span",{className:"ml-auto flex items-center gap-0.5 rounded bg-violet-500/15 px-1 py-0.5 text-[8px] font-bold uppercase text-violet-300",children:[e.jsx(C,{className:"h-2.5 w-2.5"}),"neu"]})]},c.path))})]},s||"__root"))})]}),e.jsx("div",{className:"min-w-0 flex-1 mc-card p-5",children:u?e.jsxs("div",{className:"flex items-center gap-2 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Notiz wird geladen …"]}):d?e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"mb-3 flex items-center justify-between border-b border-border/40 pb-2",children:[e.jsxs("span",{className:"flex items-center gap-1.5 font-mono text-[11px] text-muted-foreground",children:[e.jsx(E,{className:"h-3.5 w-3.5"})," ",d.path]}),d.mtime>0&&e.jsxs("span",{className:"text-[10px] text-muted-foreground/60",children:["Stand ",new Date(d.mtime*1e3).toLocaleString("de-DE",{day:"2-digit",month:"2-digit",hour:"2-digit",minute:"2-digit"})]})]}),e.jsx(z,{text:d.content,onWikiLink:j,known:m})]}):null})]})]})}function z({text:o,onWikiLink:h,known:l}){const f=o.split(`
|
||||||
`),d=[];let r=[],u=null;const i=a=>{r.length&&(d.push(e.jsx("ul",{className:"mb-3 ml-4 list-disc space-y-1",children:r},a)),r=[])};return f.forEach((a,p)=>{const t=`l${p}`;if(u!==null){a.trimEnd()==="```"?(d.push(e.jsx("pre",{className:"mb-3 overflow-x-auto rounded-xl border border-border/50 bg-background/50 p-3 font-mono text-[11px] leading-relaxed text-foreground/85",children:u.join(`
|
`),d=[];let r=[],u=null;const i=a=>{r.length&&(d.push(e.jsx("ul",{className:"mb-3 ml-4 list-disc space-y-1",children:r},a)),r=[])};return f.forEach((a,p)=>{const t=`l${p}`;if(u!==null){a.trimEnd()==="```"?(d.push(e.jsx("pre",{className:"mb-3 overflow-x-auto rounded-xl border border-border/50 bg-background/50 p-3 font-mono text-[11px] leading-relaxed text-foreground/85",children:u.join(`
|
||||||
`)},t)),u=null):u.push(a);return}if(a.trimStart().startsWith("```")){i(t),u=[];return}const m=a.trimEnd();if(!m.trim()){i(t);return}const g=m.match(/^(#{1,4})\s+(.*)$/);if(g){i(t);const j=g[1].length,s=j===1?"text-lg font-bold mt-1 mb-3":j===2?"text-base font-bold mt-4 mb-2":"text-sm font-bold mt-3 mb-1.5";d.push(e.jsx("p",{className:w(s,"font-space text-foreground"),children:N(g[2],h,l,t)},t));return}const b=m.match(/^\s*[-*•]\s+(.*)$/);if(b){r.push(e.jsx("li",{className:"text-xs leading-relaxed text-foreground/85",children:N(b[1],h,l,t)},t));return}i(t),d.push(e.jsx("p",{className:"mb-2 text-xs leading-relaxed text-foreground/85",children:N(m,h,l,t)},t))}),i("end"),u!==null&&d.push(e.jsx("pre",{className:"mb-3 overflow-x-auto rounded-xl border border-border/50 bg-background/50 p-3 font-mono text-[11px]",children:u.join(`
|
`)},t)),u=null):u.push(a);return}if(a.trimStart().startsWith("```")){i(t),u=[];return}const m=a.trimEnd();if(!m.trim()){i(t);return}const g=m.match(/^(#{1,4})\s+(.*)$/);if(g){i(t);const j=g[1].length,s=j===1?"text-lg font-bold mt-1 mb-3":j===2?"text-base font-bold mt-4 mb-2":"text-sm font-bold mt-3 mb-1.5";d.push(e.jsx("p",{className:w(s,"font-space text-foreground"),children:N(g[2],h,l,t)},t));return}const b=m.match(/^\s*[-*•]\s+(.*)$/);if(b){r.push(e.jsx("li",{className:"text-xs leading-relaxed text-foreground/85",children:N(b[1],h,l,t)},t));return}i(t),d.push(e.jsx("p",{className:"mb-2 text-xs leading-relaxed text-foreground/85",children:N(m,h,l,t)},t))}),i("end"),u!==null&&d.push(e.jsx("pre",{className:"mb-3 overflow-x-auto rounded-xl border border-border/50 bg-background/50 p-3 font-mono text-[11px]",children:u.join(`
|
||||||
`)},"code-end")),e.jsx("div",{className:"max-w-3xl",children:d})}function N(o,h,l,f){return o.split(/(\[\[[^\]]+\]\]|\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g).map((r,u)=>{const i=`${f}-${u}`,a=r.match(/^\[\[([^\]|]+)(?:\|([^\]]+))?\]\]$/);if(a){const p=a[1].trim(),t=(a[2]??a[1]).trim();return l.has(p.toLowerCase())?e.jsx("button",{onClick:()=>h(p),className:"rounded bg-primary/10 px-1 text-primary underline decoration-primary/40 underline-offset-2 hover:bg-primary/20 cursor-pointer",children:t},i):e.jsx("span",{className:"rounded bg-background/40 px-1 text-muted-foreground",title:"Notiz existiert (noch) nicht",children:t},i)}return r.startsWith("**")&&r.endsWith("**")?e.jsx("b",{className:"font-semibold text-foreground",children:r.slice(2,-2)},i):r.startsWith("*")&&r.endsWith("*")&&r.length>2?e.jsx("i",{children:r.slice(1,-1)},i):r.startsWith("`")&&r.endsWith("`")?e.jsx("code",{className:"rounded bg-background/50 px-1 font-mono text-[11px] text-teal-300",children:r.slice(1,-1)},i):e.jsx("span",{children:r},i)})}export{T as WissenView};
|
`)},"code-end")),e.jsx("div",{className:"max-w-3xl",children:d})}function N(o,h,l,f){return o.split(/(\[\[[^\]]+\]\]|\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g).map((r,u)=>{const i=`${f}-${u}`,a=r.match(/^\[\[([^\]|]+)(?:\|([^\]]+))?\]\]$/);if(a){const p=a[1].trim(),t=(a[2]??a[1]).trim();return l.has(p.toLowerCase())?e.jsx("button",{onClick:()=>h(p),className:"rounded bg-primary/10 px-1 text-primary underline decoration-primary/40 underline-offset-2 hover:bg-primary/20 cursor-pointer",children:t},i):e.jsx("span",{className:"rounded bg-background/40 px-1 text-muted-foreground",title:"Notiz existiert (noch) nicht",children:t},i)}return r.startsWith("**")&&r.endsWith("**")?e.jsx("b",{className:"font-semibold text-foreground",children:r.slice(2,-2)},i):r.startsWith("*")&&r.endsWith("*")&&r.length>2?e.jsx("i",{children:r.slice(1,-1)},i):r.startsWith("`")&&r.endsWith("`")?e.jsx("code",{className:"rounded bg-background/50 px-1 font-mono text-[11px] text-teal-300",children:r.slice(1,-1)},i):e.jsx("span",{children:r},i)})}export{T as WissenView};
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as a}from"./index-CgQUayy8.js";/**
|
import{c as a}from"./index-KOW4xETl.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}from"./index-KOW4xETl.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=c("CircleX",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m15 9-6 6",key:"1uzhvr"}],["path",{d:"m9 9 6 6",key:"z0biqf"}]]);export{r as C};
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c}from"./index-CgQUayy8.js";/**
|
import{c}from"./index-KOW4xETl.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-CgQUayy8.js";/**
|
import{c as a}from"./index-KOW4xETl.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
+75
-75
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as t}from"./index-CgQUayy8.js";/**
|
import{c as t}from"./index-KOW4xETl.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-CgQUayy8.js";/**
|
import{c as a}from"./index-KOW4xETl.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-CgQUayy8.js";/**
|
import{c as a}from"./index-KOW4xETl.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-CgQUayy8.js"></script>
|
<script type="module" crossorigin src="/assets/index-KOW4xETl.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-Bpukq9Y7.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-BAToQqxX.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const KonsoleView = lazy(() => import("@/views/KonsoleView").then((m) => ({ defa
|
|||||||
const GuideView = lazy(() => import("@/views/GuideView").then((m) => ({ default: m.GuideView })))
|
const GuideView = lazy(() => import("@/views/GuideView").then((m) => ({ default: m.GuideView })))
|
||||||
const AuftragsbuchView = lazy(() => import("@/views/AuftragsbuchView").then((m) => ({ default: m.AuftragsbuchView })))
|
const AuftragsbuchView = lazy(() => import("@/views/AuftragsbuchView").then((m) => ({ default: m.AuftragsbuchView })))
|
||||||
const ChronikView = lazy(() => import("@/views/ChronikView").then((m) => ({ default: m.ChronikView })))
|
const ChronikView = lazy(() => import("@/views/ChronikView").then((m) => ({ default: m.ChronikView })))
|
||||||
|
const EigenlebenView = lazy(() => import("@/views/EigenlebenView").then((m) => ({ default: m.EigenlebenView })))
|
||||||
const WissenView = lazy(() => import("@/views/WissenView").then((m) => ({ default: m.WissenView })))
|
const WissenView = lazy(() => import("@/views/WissenView").then((m) => ({ default: m.WissenView })))
|
||||||
|
|
||||||
// Dezenter Lade-Zustand während eine View nachgeladen wird (lokales Netz: kaum sichtbar).
|
// Dezenter Lade-Zustand während eine View nachgeladen wird (lokales Netz: kaum sichtbar).
|
||||||
@@ -201,10 +202,11 @@ export default function App() {
|
|||||||
{view === "memory" && <MemoryView />}
|
{view === "memory" && <MemoryView />}
|
||||||
{view === "wissen" && <WissenView />}
|
{view === "wissen" && <WissenView />}
|
||||||
{view === "chronik" && <ChronikView />}
|
{view === "chronik" && <ChronikView />}
|
||||||
|
{view === "eigenleben" && <EigenlebenView />}
|
||||||
{view === "agent" && <AgentView />}
|
{view === "agent" && <AgentView />}
|
||||||
{view === "konsole" && <KonsoleView />}
|
{view === "konsole" && <KonsoleView />}
|
||||||
{view === "guide" && <GuideView />}
|
{view === "guide" && <GuideView />}
|
||||||
{!["dashboard", "auftraege", "models", "connect", "memory", "wissen", "chronik", "agent", "konsole", "guide"].includes(view) && (
|
{!["dashboard", "auftraege", "eigenleben", "models", "connect", "memory", "wissen", "chronik", "agent", "konsole", "guide"].includes(view) && (
|
||||||
<Placeholder title={active.label} hint={active.hint} />
|
<Placeholder title={active.label} hint={active.hint} />
|
||||||
)}
|
)}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -520,6 +520,34 @@ export interface ChronikResp {
|
|||||||
items: ChronikItem[]
|
items: ChronikItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Eigenleben („Von allein": Skills + Vorschlags-Bilanz der Box) ────────────
|
||||||
|
export interface EigenlebenSkill {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
beschreibung: string
|
||||||
|
version: string
|
||||||
|
autor: string
|
||||||
|
herkunft: "selbst" | "repo" | "bundled"
|
||||||
|
herkunft_label: string
|
||||||
|
genutzt: number
|
||||||
|
zuletzt_genutzt: string | null
|
||||||
|
erstellt: string | null
|
||||||
|
status: string
|
||||||
|
geaendert: number
|
||||||
|
}
|
||||||
|
export interface EigenlebenEintrag {
|
||||||
|
art: string // "angenommen" | "abgelehnt" | sonstiger Chronik-State
|
||||||
|
titel: string
|
||||||
|
detail: string
|
||||||
|
ts: number
|
||||||
|
}
|
||||||
|
export interface EigenlebenResp {
|
||||||
|
available: boolean
|
||||||
|
skills: EigenlebenSkill[]
|
||||||
|
selbst_erstellt: number
|
||||||
|
bilanz: { eintraege: EigenlebenEintrag[]; angenommen: number; abgelehnt: number }
|
||||||
|
}
|
||||||
|
|
||||||
// ── Wissens-Vault (Traum-Notizen, read-only) ─────────────────────────────────
|
// ── Wissens-Vault (Traum-Notizen, read-only) ─────────────────────────────────
|
||||||
export interface WissenFile {
|
export interface WissenFile {
|
||||||
path: string
|
path: string
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
type ConnectHealth,
|
type ConnectHealth,
|
||||||
type DiscoverResp,
|
type DiscoverResp,
|
||||||
type DraftsResp,
|
type DraftsResp,
|
||||||
|
type EigenlebenResp,
|
||||||
type GroupsResp,
|
type GroupsResp,
|
||||||
type HermesBrainResp,
|
type HermesBrainResp,
|
||||||
type Health,
|
type Health,
|
||||||
@@ -57,6 +58,7 @@ export const qk = {
|
|||||||
auftragsbuch: ["auftragsbuch"] as const,
|
auftragsbuch: ["auftragsbuch"] as const,
|
||||||
ideen: ["ideen"] as const,
|
ideen: ["ideen"] as const,
|
||||||
chronik: ["chronik"] as const,
|
chronik: ["chronik"] as const,
|
||||||
|
eigenleben: ["eigenleben"] as const,
|
||||||
wissen: ["wissen"] as const,
|
wissen: ["wissen"] as const,
|
||||||
zeitmaschine: ["zeitmaschine"] as const,
|
zeitmaschine: ["zeitmaschine"] as const,
|
||||||
reminders: ["reminders"] as const,
|
reminders: ["reminders"] as const,
|
||||||
@@ -113,6 +115,13 @@ export const useChronik = (limit = 150, refetchInterval: number = TAKT.gemuetlic
|
|||||||
export const useWissen = () =>
|
export const useWissen = () =>
|
||||||
useQuery({ queryKey: qk.wissen, queryFn: () => api<WissenResp>("/api/wissen") })
|
useQuery({ queryKey: qk.wissen, queryFn: () => api<WissenResp>("/api/wissen") })
|
||||||
|
|
||||||
|
export const useEigenleben = (refetchInterval: number = TAKT.traege) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: qk.eigenleben,
|
||||||
|
queryFn: () => api<EigenlebenResp>("/api/eigenleben"),
|
||||||
|
refetchInterval,
|
||||||
|
})
|
||||||
|
|
||||||
export const useZeitmaschine = (refetchInterval: number = TAKT.traege) =>
|
export const useZeitmaschine = (refetchInterval: number = TAKT.traege) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: qk.zeitmaschine,
|
queryKey: qk.zeitmaschine,
|
||||||
|
|||||||
+3
-1
@@ -7,12 +7,13 @@ import {
|
|||||||
Library,
|
Library,
|
||||||
Plug,
|
Plug,
|
||||||
Bot,
|
Bot,
|
||||||
|
Sparkles,
|
||||||
SquareTerminal,
|
SquareTerminal,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
|
|
||||||
export type ViewId = "dashboard" | "auftraege" | "models" | "memory" | "wissen" | "chronik" | "connect" | "agent" | "konsole" | "guide"
|
export type ViewId = "dashboard" | "auftraege" | "eigenleben" | "models" | "memory" | "wissen" | "chronik" | "connect" | "agent" | "konsole" | "guide"
|
||||||
|
|
||||||
export type NavGroup = "Operativ" | "Wissen" | "Werkzeuge" | "Wartung"
|
export type NavGroup = "Operativ" | "Wissen" | "Werkzeuge" | "Wartung"
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ export interface NavItem {
|
|||||||
export const NAV: NavItem[] = [
|
export const NAV: NavItem[] = [
|
||||||
{ id: "dashboard", label: "Cockpit", hint: "Deine Box auf einen Blick", icon: LayoutDashboard, group: "Operativ" },
|
{ id: "dashboard", label: "Cockpit", hint: "Deine Box auf einen Blick", icon: LayoutDashboard, group: "Operativ" },
|
||||||
{ id: "auftraege", label: "Auftragsbuch", hint: "Vorschläge der Box — annehmen oder ablehnen", icon: Inbox, group: "Operativ" },
|
{ id: "auftraege", label: "Auftragsbuch", hint: "Vorschläge der Box — annehmen oder ablehnen", icon: Inbox, group: "Operativ" },
|
||||||
|
{ id: "eigenleben", label: "Von allein", hint: "Was die Box selbst gebaut hat — Skills & Bilanz", icon: Sparkles, group: "Operativ" },
|
||||||
{ id: "chronik", label: "Chronik", hint: "Was die Box von allein getan hat + Zeitmaschine", icon: History, group: "Operativ" },
|
{ id: "chronik", label: "Chronik", hint: "Was die Box von allein getan hat + Zeitmaschine", icon: History, group: "Operativ" },
|
||||||
{ id: "memory", label: "Gedächtnis", hint: "Geteiltes Memory verwalten", icon: Brain, group: "Wissen" },
|
{ id: "memory", label: "Gedächtnis", hint: "Geteiltes Memory verwalten", icon: Brain, group: "Wissen" },
|
||||||
{ id: "wissen", label: "Wissen", hint: "Lucys Wissens-Vault (Traum-Notizen)", icon: Library, group: "Wissen" },
|
{ id: "wissen", label: "Wissen", hint: "Lucys Wissens-Vault (Traum-Notizen)", icon: Library, group: "Wissen" },
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
import { useMemo, useState } from "react"
|
||||||
|
import {
|
||||||
|
Sparkles, Wrench, Package, Loader2, CheckCircle2, XCircle, CircleDot,
|
||||||
|
ChevronDown, ChevronRight, BookOpen, type LucideIcon,
|
||||||
|
} from "lucide-react"
|
||||||
|
import { api, type EigenlebenEintrag, type EigenlebenSkill } from "@/lib/api"
|
||||||
|
import { useEigenleben } from "@/lib/queries"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
// „Von allein" — die Antwort auf die User-Frage vom 15.07.2026: EIN Ort, der zeigt,
|
||||||
|
// was die Box selbst geleistet hat. Oben die Skills (Lucys Fähigkeiten-Regal, selbst
|
||||||
|
// erstellte zuerst, mit Klartext-Beschreibung + Nutzungszähler), darunter die
|
||||||
|
// Vorschlags-Bilanz (angenommen/abgelehnt mit Grund) — ohne Chronik-Rauschen.
|
||||||
|
|
||||||
|
const HERKUNFT_UI: Record<EigenlebenSkill["herkunft"], { icon: LucideIcon; cls: string; erklaerung: string }> = {
|
||||||
|
selbst: {
|
||||||
|
icon: Sparkles,
|
||||||
|
cls: "text-emerald-300 bg-emerald-500/10 border-emerald-500/30",
|
||||||
|
erklaerung: "Diese Skills hat sich die Box selbst geschrieben — niemand hat sie ihr installiert.",
|
||||||
|
},
|
||||||
|
repo: {
|
||||||
|
icon: Wrench,
|
||||||
|
cls: "text-primary bg-primary/10 border-primary/25",
|
||||||
|
erklaerung: "Von uns gebaut und über das Repo installiert (deploy/skills).",
|
||||||
|
},
|
||||||
|
bundled: {
|
||||||
|
icon: Package,
|
||||||
|
cls: "text-muted-foreground bg-background/40 border-border/50",
|
||||||
|
erklaerung: "Kamen mit Hermes mit — Standard-Ausstattung.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function datumKurz(iso: string | null): string {
|
||||||
|
if (!iso) return "—"
|
||||||
|
const d = new Date(iso)
|
||||||
|
return isNaN(d.getTime()) ? "—" : d.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "2-digit" })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EigenlebenView() {
|
||||||
|
const { data, isLoading } = useEigenleben()
|
||||||
|
|
||||||
|
const gruppen = useMemo(() => {
|
||||||
|
const g: { herkunft: EigenlebenSkill["herkunft"]; label: string; skills: EigenlebenSkill[] }[] = []
|
||||||
|
for (const s of data?.skills ?? []) {
|
||||||
|
const last = g[g.length - 1]
|
||||||
|
if (last && last.herkunft === s.herkunft) last.skills.push(s)
|
||||||
|
else g.push({ herkunft: s.herkunft, label: s.herkunft_label, skills: [s] })
|
||||||
|
}
|
||||||
|
return g
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-7">
|
||||||
|
<div>
|
||||||
|
<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">
|
||||||
|
Von allein
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Was deine Box selbst geleistet hat: ihre Fähigkeiten (Skills) und was aus ihren Vorschlägen wurde.
|
||||||
|
Das ist derselbe Skill-Satz, mit dem Lucy spricht und die Nacht-Worker arbeiten.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<div className="flex items-center gap-2 rounded-2xl border border-border/60 bg-card/30 p-6 text-xs text-muted-foreground">
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" /> Wird geladen …
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data && !data.available && (
|
||||||
|
<div className="rounded-2xl border border-amber-500/30 bg-amber-500/5 p-4 text-xs text-amber-300">
|
||||||
|
Die Skills liegen auf der Box (~/.hermes/skills) — im lokalen Dev-Modus nicht verfügbar.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data?.available && (
|
||||||
|
<>
|
||||||
|
{/* Bilanz-Zähler */}
|
||||||
|
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
|
||||||
|
<Zahl label="Skills gesamt" wert={data.skills.length} />
|
||||||
|
<Zahl label="davon selbst erstellt" wert={data.selbst_erstellt} betone />
|
||||||
|
<Zahl label="Vorschläge angenommen" wert={data.bilanz.angenommen} />
|
||||||
|
<Zahl label="Vorschläge abgelehnt" wert={data.bilanz.abgelehnt} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Skills nach Herkunft */}
|
||||||
|
{gruppen.map((g) => {
|
||||||
|
const ui = HERKUNFT_UI[g.herkunft]
|
||||||
|
const Icon = ui.icon
|
||||||
|
return (
|
||||||
|
<section key={g.herkunft} className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<p className={cn("inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-[11px] font-bold", ui.cls)}>
|
||||||
|
<Icon className="h-3.5 w-3.5" /> {g.label} · {g.skills.length}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-[11px] text-muted-foreground/70">{ui.erklaerung}</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2 md:grid-cols-2">
|
||||||
|
{g.skills.map((s) => <SkillKarte key={s.id} skill={s} />)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Vorschlags-Bilanz */}
|
||||||
|
<Bilanz eintraege={data.bilanz.eintraege} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Zahl({ label, wert, betone }: { label: string; wert: number; betone?: boolean }) {
|
||||||
|
return (
|
||||||
|
<div className={cn("rounded-xl border p-3", betone ? "border-emerald-500/30 bg-emerald-500/5" : "border-border/50 bg-card/40")}>
|
||||||
|
<p className={cn("font-space text-xl font-bold", betone ? "text-emerald-300" : "text-foreground")}>{wert}</p>
|
||||||
|
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70">{label}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SkillKarte({ skill }: { skill: EigenlebenSkill }) {
|
||||||
|
const [offen, setOffen] = useState(false)
|
||||||
|
const [text, setText] = useState<string | null>(null)
|
||||||
|
const [laedt, setLaedt] = useState(false)
|
||||||
|
|
||||||
|
async function toggle() {
|
||||||
|
const next = !offen
|
||||||
|
setOffen(next)
|
||||||
|
if (next && text === null && !laedt) {
|
||||||
|
setLaedt(true)
|
||||||
|
try {
|
||||||
|
const r = await api<{ id: string; text: string }>(`/api/eigenleben/skill/${encodeURIComponent(skill.id)}`)
|
||||||
|
setText(r.text)
|
||||||
|
} catch {
|
||||||
|
setText("(Konnte den Skill-Text nicht laden.)")
|
||||||
|
} finally {
|
||||||
|
setLaedt(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-xl border border-border/50 bg-card/40 p-3 backdrop-blur-sm">
|
||||||
|
<button onClick={toggle} className="flex w-full items-start gap-2 text-left cursor-pointer">
|
||||||
|
{offen ? <ChevronDown className="mt-0.5 h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : <ChevronRight className="mt-0.5 h-3.5 w-3.5 shrink-0 text-muted-foreground" />}
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<span className="text-sm font-semibold text-foreground">{skill.name}</span>
|
||||||
|
{skill.genutzt > 0 && (
|
||||||
|
<span className="rounded bg-primary/10 px-1.5 py-0.5 text-[9px] font-bold text-primary" title={`zuletzt: ${datumKurz(skill.zuletzt_genutzt)}`}>
|
||||||
|
{skill.genutzt}× genutzt
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{skill.status !== "active" && (
|
||||||
|
<span className="rounded bg-amber-500/10 px-1.5 py-0.5 text-[9px] font-bold text-amber-300">{skill.status}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className={cn("mt-1 text-xs leading-relaxed text-foreground/75", !offen && "line-clamp-2")}>
|
||||||
|
{skill.beschreibung || "(keine Beschreibung im Skill hinterlegt)"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{offen && (
|
||||||
|
<div className="mt-2 border-t border-border/40 pt-2">
|
||||||
|
<p className="mb-1.5 flex items-center gap-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/60">
|
||||||
|
<BookOpen className="h-3 w-3" /> So funktioniert er (SKILL.md){skill.autor ? ` · Autor: ${skill.autor}` : ""}
|
||||||
|
</p>
|
||||||
|
{laedt
|
||||||
|
? <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||||
|
: <pre className="max-h-80 overflow-y-auto whitespace-pre-wrap rounded-lg bg-background/50 p-2.5 font-mono text-[10.5px] leading-relaxed text-foreground/80 scrollbar-thin">{text ?? ""}</pre>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Bilanz({ eintraege }: { eintraege: EigenlebenEintrag[] }) {
|
||||||
|
const [alle, setAlle] = useState(false)
|
||||||
|
const shown = alle ? eintraege : eintraege.slice(0, 12)
|
||||||
|
return (
|
||||||
|
<section className="space-y-3">
|
||||||
|
<p className="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/60">
|
||||||
|
<CircleDot className="h-3.5 w-3.5" /> Vorschlags-Bilanz — was die Box vorschlug und was daraus wurde
|
||||||
|
</p>
|
||||||
|
{eintraege.length === 0 && (
|
||||||
|
<div className="rounded-2xl border border-dashed border-border/60 bg-card/20 p-6 text-center text-xs text-muted-foreground">
|
||||||
|
Noch keine Vorschläge verbucht. Sobald die Box etwas vorschlägt und du klickst, steht die Entscheidung hier.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{shown.map((e, i) => {
|
||||||
|
const angenommen = e.art === "angenommen"
|
||||||
|
const Icon = angenommen ? CheckCircle2 : e.art === "abgelehnt" ? XCircle : CircleDot
|
||||||
|
return (
|
||||||
|
<div key={`${e.titel}-${e.ts}-${i}`} className="flex items-start gap-2.5 rounded-xl border border-border/50 bg-card/40 p-3">
|
||||||
|
<Icon className={cn("mt-0.5 h-4 w-4 shrink-0",
|
||||||
|
angenommen ? "text-emerald-400" : e.art === "abgelehnt" ? "text-red-400" : "text-amber-300")} />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<span className="font-mono text-xs font-semibold text-foreground">{e.titel}</span>
|
||||||
|
<span className={cn("rounded px-1.5 py-0.5 text-[9px] font-bold uppercase",
|
||||||
|
angenommen ? "bg-emerald-500/10 text-emerald-300" : e.art === "abgelehnt" ? "bg-red-500/10 text-red-300" : "bg-amber-500/10 text-amber-300")}>
|
||||||
|
{e.art}
|
||||||
|
</span>
|
||||||
|
<span className="ml-auto font-mono text-[10px] text-muted-foreground/60">
|
||||||
|
{e.ts ? new Date(e.ts * 1000).toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit" }) : ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{e.detail && <p className="mt-1 text-xs leading-relaxed text-foreground/70">{e.detail}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{!alle && eintraege.length > 12 && (
|
||||||
|
<button onClick={() => setAlle(true)} className="text-[11px] text-muted-foreground underline hover:text-foreground cursor-pointer">
|
||||||
|
Alle {eintraege.length} Einträge anzeigen
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user