Zeitachse (24h-Historie), Verbinden-Autofill, Modelltausch abgesichert

Paket 1 - Metrik-Historie mit Zeitachse (User: 'WANN war Last?'):
- services/metrics_history.py: 10s-Sampler (CPU/RAM/GPU/Disk + Token-Totale) in
  24h-Ringpuffer (aelteres faellt automatisch raus, nichts waechst unbegrenzt),
  persistiert alle 5 min -> uebersteht Deploys; GET /api/system/history?minutes
  mit Downsampling auf ~300 Punkte; Lifespan-Task in app.py
- Cockpit-Karten System-Status + Token-Durchsatz: Bereichs-Schalter Live/1h/24h,
  sichtbare Zeitachse (HH:MM:SS bei kurzen, HH:MM bei langen Fenstern), Tooltip
  zeigt Uhrzeit; Token-Raten in 1h/24h aus Totale-Deltas

Paket 2 - Verbinden selbsterklaerend:
- Box-IP vorbefuellt aus window.location.hostname (Dev-Fallback bleibt)
- MCP-Scriptpfad-Feld aus der Kopfzeile in die 'Gedaechtnis anbinden'-Karte
  verschoben, mit Erklaerung WANN man es braucht

Paket 3 - Modelltausch-Loecher gestopft (Review 16.07.):
- install: Registrierung OHNE Alias-Umzug; Rolle wird erst NACH erfolgreichem
  Download uebernommen (on_done) - hermes dabei durch den warm-bewussten
  set_agent_brain-Flow statt rohem Alias-Move (Lucy waere sonst bis Download-
  Ende tot gewesen); Re-Install erhaelt bestehende Aliase
- set_role_alias: lebenswichtige Aliase (hermes/embed) des Ziel-Modells
  ueberleben jeden Rollen-Klick (Qwen3.6 haelt live hermes+fast!)
- Rollen-Endpoint verweigert Rollen-Wechsel am Halter geschuetzter Aliase
  mit klarer Anleitung; Werkbank sperrt die Rollen-Chips sichtbar

Verifiziert: py_compile + Sampler-Smoke (2 Punkte, Persist ok) + Alias-Logik-
Unittest (3 Faelle) + Browser (Zeitachse tickt, Schalter, Leerzustand, Verbinden).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-16 01:08:03 +02:00
parent 26224804ed
commit 71192b3db0
36 changed files with 562 additions and 213 deletions
@@ -1,4 +1,4 @@
import{c as A,Y as P,l as B,b as M,u as D,r as z,j as e,Z as T,e as a,$ as g,a0 as b,a1 as W,x as f,a2 as j,W as G,X as I,C as N,g as y,q as i}from"./index-CQgUnd1p.js";/**
import{c as A,Y as P,l as B,b as M,u as D,r as z,j as e,Z as T,e as a,$ as g,a0 as b,a1 as W,x as f,a2 as j,W as G,X as I,C as N,g as y,q as i}from"./index-BJUCwF6N.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as I,aa as Y,ab as _,u as q,b as Q,r as u,j as e,a3 as ee,e as C,L as N,C as U,U as J,ac as te,V as O,X as z,D as M,M as R,ad as re,T as K,g as E,ae as se,q as Z}from"./index-CQgUnd1p.js";import{L as ae}from"./lightbulb-Btbex65P.js";import{S as G}from"./send-COuI40dq.js";/**
import{c as I,aa as Y,ab as _,u as q,b as Q,r as u,j as e,a3 as ee,e as C,L as N,C as U,U as J,ac as te,V as O,X as z,D as M,M as R,ad as re,T as K,g as E,ae as se,q as Z}from"./index-BJUCwF6N.js";import{L as ae}from"./lightbulb-Bl7otbNF.js";import{S as G}from"./send-CLnv-s74.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as m,af as S,r as c,j as e,ag as z,L as g,U as M,ah as C,ae as D,ai as A,aj as B,ak as Z,e as E,al as L,u as R,b as q,g as k,q as T}from"./index-CQgUnd1p.js";import{R as j}from"./rotate-ccw-ChVZWN8g.js";/**
import{c as m,af as S,r as c,j as e,ag as z,L as g,U as M,ah as C,ae as D,ai as A,aj as B,ak as Z,e as E,al as L,u as R,b as q,g as k,q as T}from"./index-BJUCwF6N.js";import{R as j}from"./rotate-ccw-sjUuOvXz.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
import{c as h,am as b,r as i,j as e,L as g,an as f,a2 as j,U as k,e as o,D as N,M as v,N as w,g as y}from"./index-CQgUnd1p.js";import{B as S}from"./book-open-BFFOe9re.js";import{C as z}from"./circle-x-DKuGYlQP.js";/**
import{c as h,am as b,r as i,j as e,L as g,an as f,a2 as j,U as k,e as o,D as N,M as v,N as w,g as y}from"./index-BJUCwF6N.js";import{B as S}from"./book-open-DSKbx2re.js";import{C as z}from"./circle-x-CXUY3WgN.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
import{c as p,j as e,r as M,x as C,a6 as z,a7 as k,a8 as y,a0 as v,W as L,a9 as G,a2 as T,e as f,U as D}from"./index-CQgUnd1p.js";import{B as w}from"./book-open-BFFOe9re.js";import{a as N,b as B,L as P,C as E,Z as R}from"./zap-Df-ORHkh.js";import{L as I}from"./lightbulb-Btbex65P.js";/**
import{c as p,j as e,r as M,x as C,a6 as z,a7 as k,a8 as y,a0 as v,W as L,a9 as G,a2 as T,e as f,U as D}from"./index-BJUCwF6N.js";import{B as w}from"./book-open-DSKbx2re.js";import{a as N,b as B,L as P,C as E,Z as R}from"./zap-C9k94tAM.js";import{L as I}from"./lightbulb-Bl7otbNF.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1 +1 @@
import{Y as b,u as f,$ as h,r as d,j as e,e as c,Z as g,T as p,a3 as j,a4 as N,g as v,a5 as w,q as x}from"./index-CQgUnd1p.js";function y(){const{data:s}=b(),m=f(),r=s!=null&&s.box_console_url?h(s.box_console_url):void 0,a=s==null?void 0:s.box_console_reachable,[l,o]=d.useState(!1),[i,n]=d.useState("");async function u(){o(!0),n("");try{const t=await v("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})});n(t.ok?"Neu gestartet — einen Moment, dann lädt das Terminal.":`Fehlgeschlagen: ${t.err||"Unbekannter Fehler"}`),w(m,x.agentStatus,x.services)}catch(t){n(`Fehlgeschlagen: ${(t==null?void 0:t.message)||t}`)}finally{o(!1)}}return e.jsxs("div",{className:"flex h-full flex-col gap-4",children:[e.jsxs("div",{className:"flex flex-wrap items-end justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Konsole"}),e.jsx("p",{className:"flex items-center gap-2 text-sm text-muted-foreground",children:"Direkte Shell auf der Box — wie ein SSH-Fenster, mitten im Browser."})]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground",children:[e.jsx("span",{className:c("h-2 w-2 rounded-full",a?"bg-emerald-500 animate-pulse":"bg-amber-500")}),a?"online":"offline"]}),r&&e.jsxs("a",{href:r,target:"_blank",rel:"noopener",className:"flex h-8 items-center gap-1.5 rounded-lg border border-border/60 bg-background/20 px-3 text-xs font-semibold text-muted-foreground transition-all hover:border-primary/50 hover:text-foreground",children:[e.jsx(g,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),r?e.jsxs("div",{className:"relative min-h-[58vh] flex-1 overflow-hidden rounded-2xl border border-border/60 bg-black/50 shadow-lg shadow-black/25",children:[a===!1&&e.jsxs("div",{className:"absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/70 text-center",children:[e.jsx(p,{className:"h-8 w-8 text-amber-400"}),e.jsx("div",{className:"text-sm font-semibold text-amber-300",children:"Konsole nicht erreichbar"}),e.jsxs("p",{className:"max-w-sm text-[11px] leading-normal text-muted-foreground",children:["Der Terminal-Dienst (",e.jsx("code",{className:"font-mono text-primary",children:"box-console"}),") läuft gerade nicht."]}),e.jsxs("button",{onClick:u,disabled:l,className:"flex h-9 items-center gap-1.5 rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 text-[11px] font-bold uppercase tracking-wide text-amber-300 transition-all hover:bg-amber-500/20 cursor-pointer disabled:opacity-50",children:[e.jsx(j,{className:c("h-3.5 w-3.5",l&&"animate-spin")})," Dienst neu starten"]}),i&&e.jsx("p",{className:"max-w-sm text-[11px] text-muted-foreground",children:i})]}),e.jsx("iframe",{src:r,title:"Box-Konsole",className:"h-full w-full border-0",style:{minHeight:"58vh"}})]}):e.jsxs("div",{className:"flex min-h-[58vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground",children:[e.jsx(N,{className:"mr-2 h-4 w-4"})," Lade Konsole…"]})]})}export{y as KonsoleView};
import{Y as b,u as f,$ as h,r as d,j as e,e as c,Z as g,T as p,a3 as j,a4 as N,g as v,a5 as w,q as x}from"./index-BJUCwF6N.js";function y(){const{data:s}=b(),m=f(),r=s!=null&&s.box_console_url?h(s.box_console_url):void 0,a=s==null?void 0:s.box_console_reachable,[l,o]=d.useState(!1),[i,n]=d.useState("");async function u(){o(!0),n("");try{const t=await v("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})});n(t.ok?"Neu gestartet — einen Moment, dann lädt das Terminal.":`Fehlgeschlagen: ${t.err||"Unbekannter Fehler"}`),w(m,x.agentStatus,x.services)}catch(t){n(`Fehlgeschlagen: ${(t==null?void 0:t.message)||t}`)}finally{o(!1)}}return e.jsxs("div",{className:"flex h-full flex-col gap-4",children:[e.jsxs("div",{className:"flex flex-wrap items-end justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Konsole"}),e.jsx("p",{className:"flex items-center gap-2 text-sm text-muted-foreground",children:"Direkte Shell auf der Box — wie ein SSH-Fenster, mitten im Browser."})]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground",children:[e.jsx("span",{className:c("h-2 w-2 rounded-full",a?"bg-emerald-500 animate-pulse":"bg-amber-500")}),a?"online":"offline"]}),r&&e.jsxs("a",{href:r,target:"_blank",rel:"noopener",className:"flex h-8 items-center gap-1.5 rounded-lg border border-border/60 bg-background/20 px-3 text-xs font-semibold text-muted-foreground transition-all hover:border-primary/50 hover:text-foreground",children:[e.jsx(g,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),r?e.jsxs("div",{className:"relative min-h-[58vh] flex-1 overflow-hidden rounded-2xl border border-border/60 bg-black/50 shadow-lg shadow-black/25",children:[a===!1&&e.jsxs("div",{className:"absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/70 text-center",children:[e.jsx(p,{className:"h-8 w-8 text-amber-400"}),e.jsx("div",{className:"text-sm font-semibold text-amber-300",children:"Konsole nicht erreichbar"}),e.jsxs("p",{className:"max-w-sm text-[11px] leading-normal text-muted-foreground",children:["Der Terminal-Dienst (",e.jsx("code",{className:"font-mono text-primary",children:"box-console"}),") läuft gerade nicht."]}),e.jsxs("button",{onClick:u,disabled:l,className:"flex h-9 items-center gap-1.5 rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 text-[11px] font-bold uppercase tracking-wide text-amber-300 transition-all hover:bg-amber-500/20 cursor-pointer disabled:opacity-50",children:[e.jsx(j,{className:c("h-3.5 w-3.5",l&&"animate-spin")})," Dienst neu starten"]}),i&&e.jsx("p",{className:"max-w-sm text-[11px] text-muted-foreground",children:i})]}),e.jsx("iframe",{src:r,title:"Box-Konsole",className:"h-full w-full border-0",style:{minHeight:"58vh"}})]}):e.jsxs("div",{className:"flex min-h-[58vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground",children:[e.jsx(N,{className:"mr-2 h-4 w-4"})," Lade Konsole…"]})]})}export{y as KonsoleView};
@@ -1,5 +1,5 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/GraphView-BOVrShvN.js","assets/index-CQgUnd1p.js","assets/index-BH760w1h.css"])))=>i.map(i=>d[i]);
var je=Object.defineProperty;var ke=(d,i,c)=>i in d?je(d,i,{enumerable:!0,configurable:!0,writable:!0,value:c}):d[i]=c;var J=(d,i,c)=>ke(d,typeof i!="symbol"?i+"":i,c);import{c as h,r as s,j as e,u as ve,b as Ne,O as W,Q as Se,C as X,R as Ce,S as Ee,P as Me,U as O,e as x,V as ze,W as De,X as Z,A as Y,_ as Oe,g as j}from"./index-CQgUnd1p.js";import{C as ee}from"./copy-hJYMa3at.js";import{S as _e}from"./send-COuI40dq.js";import{B as Ge}from"./book-open-BFFOe9re.js";/**
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/GraphView-BQzGpC9Q.js","assets/index-BJUCwF6N.js","assets/index-DEL5L7Ku.css"])))=>i.map(i=>d[i]);
var je=Object.defineProperty;var ke=(d,i,c)=>i in d?je(d,i,{enumerable:!0,configurable:!0,writable:!0,value:c}):d[i]=c;var J=(d,i,c)=>ke(d,typeof i!="symbol"?i+"":i,c);import{c as h,r as s,j as e,u as ve,b as Ne,O as W,Q as Se,C as X,R as Ce,S as Ee,P as Me,U as O,e as x,V as ze,W as De,X as Z,A as Y,_ as Oe,g as j}from"./index-BJUCwF6N.js";import{C as ee}from"./copy-s7zFTglG.js";import{S as _e}from"./send-CLnv-s74.js";import{B as Ge}from"./book-open-DSKbx2re.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -34,7 +34,7 @@ var je=Object.defineProperty;var ke=(d,i,c)=>i in d?je(d,i,{enumerable:!0,config
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Pe=h("User",[["path",{d:"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2",key:"975kel"}],["circle",{cx:"12",cy:"7",r:"4",key:"17ys0d"}]]);class te extends s.Component{constructor(){super(...arguments);J(this,"state",{error:null})}static getDerivedStateFromError(c){return{error:c}}render(){return this.state.error?e.jsxs("div",{className:"h-[480px] rounded-2xl border border-red-500/20 bg-red-500/5 p-6 text-xs text-red-400 overflow-auto",children:[e.jsx("div",{className:"font-semibold mb-2",children:"Graph konnte nicht gerendert werden"}),e.jsx("pre",{className:"whitespace-pre-wrap break-words text-[11px] leading-relaxed",children:this.state.error.message})]}):this.props.children}}const se=s.lazy(()=>Oe(()=>import("./GraphView-BOVrShvN.js"),__vite__mapDeps([0,1,2])).then(d=>({default:d.GraphView}))),k=["identity","knowledge","rules","events"],v=new Set(["auto","agent","hermes"]),N={identity:{label:"Identität",icon:Pe,bg:"bg-cyan-500/10",text:"text-cyan-400"},knowledge:{label:"Wissen",icon:De,bg:"bg-indigo-500/10",text:"text-indigo-400"},rules:{label:"Regeln",icon:Le,bg:"bg-violet-500/10",text:"text-violet-400"},events:{label:"Ereignisse",icon:ze,bg:"bg-amber-500/10",text:"text-amber-400"}},ne={label:"Gedächtnis",icon:Ge,text:"text-muted-foreground"},Re={identity:"border-l-cyan-500/80",knowledge:"border-l-indigo-500/80",rules:"border-l-violet-500/80",events:"border-l-amber-500/80"},_={identity:"#00f5ff",knowledge:"#3b82f6",rules:"#d946ef",events:"#fbbf24"},ae=`Hi Hermes! Lass uns ein kurzes Onboarding machen, damit du dich künftig an mich erinnerst. Stell mir nacheinander ein paar kurze Fragen zu:
*/const Pe=h("User",[["path",{d:"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2",key:"975kel"}],["circle",{cx:"12",cy:"7",r:"4",key:"17ys0d"}]]);class te extends s.Component{constructor(){super(...arguments);J(this,"state",{error:null})}static getDerivedStateFromError(c){return{error:c}}render(){return this.state.error?e.jsxs("div",{className:"h-[480px] rounded-2xl border border-red-500/20 bg-red-500/5 p-6 text-xs text-red-400 overflow-auto",children:[e.jsx("div",{className:"font-semibold mb-2",children:"Graph konnte nicht gerendert werden"}),e.jsx("pre",{className:"whitespace-pre-wrap break-words text-[11px] leading-relaxed",children:this.state.error.message})]}):this.props.children}}const se=s.lazy(()=>Oe(()=>import("./GraphView-BQzGpC9Q.js"),__vite__mapDeps([0,1,2])).then(d=>({default:d.GraphView}))),k=["identity","knowledge","rules","events"],v=new Set(["auto","agent","hermes"]),N={identity:{label:"Identität",icon:Pe,bg:"bg-cyan-500/10",text:"text-cyan-400"},knowledge:{label:"Wissen",icon:De,bg:"bg-indigo-500/10",text:"text-indigo-400"},rules:{label:"Regeln",icon:Le,bg:"bg-violet-500/10",text:"text-violet-400"},events:{label:"Ereignisse",icon:ze,bg:"bg-amber-500/10",text:"text-amber-400"}},ne={label:"Gedächtnis",icon:Ge,text:"text-muted-foreground"},Re={identity:"border-l-cyan-500/80",knowledge:"border-l-indigo-500/80",rules:"border-l-violet-500/80",events:"border-l-amber-500/80"},_={identity:"#00f5ff",knowledge:"#3b82f6",rules:"#d946ef",events:"#fbbf24"},ae=`Hi Hermes! Lass uns ein kurzes Onboarding machen, damit du dich künftig an mich erinnerst. Stell mir nacheinander ein paar kurze Fragen zu:
1) wer ich bin und woran ich gerade arbeite,
2) wie ich angesprochen werden möchte,
3) meine bevorzugten Tools, Sprachen und Arbeitsweise,
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
import{ao as y,r as x,g as L,j as e,L as v,S,ap as W,U as C,e as w,aq as E}from"./index-CQgUnd1p.js";import{F as M}from"./folder-open-vcdogQ3l.js";const D={"":"Allgemein",traeume:"Träume",muster:"Muster","skill-kandidaten":"Skill-Kandidaten","skill-kandidaten/beauftragt":"Skill-Kandidaten · beauftragt","skill-kandidaten/verworfen":"Skill-Kandidaten · verworfen"};function T(){const{data:o,isLoading:h}=y(),[l,f]=x.useState(null),[d,r]=x.useState(null),[u,i]=x.useState(!1),[a,p]=x.useState(""),t=x.useMemo(()=>(o==null?void 0:o.files)??[],[o]),m=x.useMemo(()=>{const s=new Map;for(const n of t)s.set(n.name.toLowerCase(),n.path);return s},[t]),g=x.useMemo(()=>{if(!a.trim())return t;const s=a.toLowerCase();return t.filter(n=>n.title.toLowerCase().includes(s)||n.path.toLowerCase().includes(s))},[t,a]),b=x.useMemo(()=>{const s=[];for(const n of g){const c=s.find(k=>k.dir===n.dir);c?c.files.push(n):s.push({dir:n.dir,files:[n]})}return s.sort((n,c)=>n.dir===""?-1:c.dir===""?1:n.dir.localeCompare(c.dir))},[g]);x.useEffect(()=>{if(!l&&t.length){const s=t.find(n=>n.path.toLowerCase()==="index.md");f((s==null?void 0:s.path)??t[0].path)}},[t,l]),x.useEffect(()=>{l&&(i(!0),L(`/api/wissen/datei?pfad=${encodeURIComponent(l)}`).then(r).catch(()=>r({path:l,content:"*(Notiz nicht ladbar)*",mtime:0})).finally(()=>i(!1)))},[l]);const j=s=>{const n=m.get(s.toLowerCase());n&&f(n)};return e.jsxs("div",{className:"space-y-5",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Wissen"}),e.jsx("p",{className:"text-sm text-muted-foreground",children:"Was sich die Box nachts erschließt — Traum-Notizen, Muster und Skill-Ideen aus dem Wissens-Vault, verlinkt wie ein Wiki."})]}),o&&!o.available&&e.jsx("div",{className:"rounded-2xl border border-amber-500/30 bg-amber-500/5 p-4 text-xs text-amber-300",children:"Der Wissens-Vault liegt auf der Box (~/wissens-vault) — im lokalen Dev-Modus nicht verfügbar."}),h&&e.jsxs("div",{className:"flex items-center gap-2 rounded-2xl border border-border/60 bg-card/30 p-6 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Vault wird geladen …"]}),(o==null?void 0:o.available)&&t.length===0&&e.jsx("div",{className:"rounded-2xl border border-dashed border-border/60 bg-card/20 p-8 text-center text-xs text-muted-foreground",children:"Noch keine Notizen. Der nächtliche Traum (03:15) legt sie an — morgen früh steht hier die erste."}),t.length>0&&e.jsxs("div",{className:"flex flex-col gap-4 lg:flex-row",children:[e.jsxs("aside",{className:"w-full shrink-0 space-y-3 lg:w-72",children:[e.jsxs("div",{className:"relative",children:[e.jsx("input",{value:a,onChange:s=>p(s.target.value),type:"search",placeholder:"Notizen durchsuchen…","aria-label":"Notizen durchsuchen",className:"h-9 w-full rounded-lg border border-border/60 bg-card/45 pl-8 pr-3 text-xs text-foreground outline-none focus:ring-1 focus:ring-primary/50"}),e.jsx(S,{className:"absolute left-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground"})]}),e.jsx("div",{className:"max-h-[70vh] space-y-3 overflow-y-auto pr-1 scrollbar-thin",children:b.map(({dir:s,files:n})=>e.jsxs("div",{children:[e.jsxs("p",{className:"mb-1 flex items-center gap-1 text-[10px] font-bold uppercase tracking-wider text-muted-foreground/60",children:[e.jsx(M,{className:"h-3 w-3"})," ",D[s]??s]}),e.jsx("div",{className:"space-y-0.5",children:n.map(c=>e.jsxs("button",{onClick:()=>f(c.path),className:w("flex w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left text-xs transition-all cursor-pointer",l===c.path?"bg-primary/15 text-primary":"text-muted-foreground hover:bg-accent hover:text-foreground"),children:[e.jsx(W,{className:"h-3.5 w-3.5 shrink-0"}),e.jsx("span",{className:"truncate",title:c.title,children:c.name}),c.neu&&e.jsxs("span",{className:"ml-auto flex items-center gap-0.5 rounded bg-violet-500/15 px-1 py-0.5 text-[8px] font-bold uppercase text-violet-300",children:[e.jsx(C,{className:"h-2.5 w-2.5"}),"neu"]})]},c.path))})]},s||"__root"))})]}),e.jsx("div",{className:"min-w-0 flex-1 mc-card p-5",children:u?e.jsxs("div",{className:"flex items-center gap-2 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Notiz wird geladen …"]}):d?e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"mb-3 flex items-center justify-between border-b border-border/40 pb-2",children:[e.jsxs("span",{className:"flex items-center gap-1.5 font-mono text-[11px] text-muted-foreground",children:[e.jsx(E,{className:"h-3.5 w-3.5"})," ",d.path]}),d.mtime>0&&e.jsxs("span",{className:"text-[10px] text-muted-foreground/60",children:["Stand ",new Date(d.mtime*1e3).toLocaleString("de-DE",{day:"2-digit",month:"2-digit",hour:"2-digit",minute:"2-digit"})]})]}),e.jsx(z,{text:d.content,onWikiLink:j,known:m})]}):null})]})]})}function z({text:o,onWikiLink:h,known:l}){const f=o.split(`
import{ao as y,r as x,g as L,j as e,L as v,S,ap as W,U as C,e as w,aq as E}from"./index-BJUCwF6N.js";import{F as M}from"./folder-open-oIQVVo05.js";const D={"":"Allgemein",traeume:"Träume",muster:"Muster","skill-kandidaten":"Skill-Kandidaten","skill-kandidaten/beauftragt":"Skill-Kandidaten · beauftragt","skill-kandidaten/verworfen":"Skill-Kandidaten · verworfen"};function T(){const{data:o,isLoading:h}=y(),[l,f]=x.useState(null),[d,r]=x.useState(null),[u,i]=x.useState(!1),[a,p]=x.useState(""),t=x.useMemo(()=>(o==null?void 0:o.files)??[],[o]),m=x.useMemo(()=>{const s=new Map;for(const n of t)s.set(n.name.toLowerCase(),n.path);return s},[t]),g=x.useMemo(()=>{if(!a.trim())return t;const s=a.toLowerCase();return t.filter(n=>n.title.toLowerCase().includes(s)||n.path.toLowerCase().includes(s))},[t,a]),b=x.useMemo(()=>{const s=[];for(const n of g){const c=s.find(k=>k.dir===n.dir);c?c.files.push(n):s.push({dir:n.dir,files:[n]})}return s.sort((n,c)=>n.dir===""?-1:c.dir===""?1:n.dir.localeCompare(c.dir))},[g]);x.useEffect(()=>{if(!l&&t.length){const s=t.find(n=>n.path.toLowerCase()==="index.md");f((s==null?void 0:s.path)??t[0].path)}},[t,l]),x.useEffect(()=>{l&&(i(!0),L(`/api/wissen/datei?pfad=${encodeURIComponent(l)}`).then(r).catch(()=>r({path:l,content:"*(Notiz nicht ladbar)*",mtime:0})).finally(()=>i(!1)))},[l]);const j=s=>{const n=m.get(s.toLowerCase());n&&f(n)};return e.jsxs("div",{className:"space-y-5",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Wissen"}),e.jsx("p",{className:"text-sm text-muted-foreground",children:"Was sich die Box nachts erschließt — Traum-Notizen, Muster und Skill-Ideen aus dem Wissens-Vault, verlinkt wie ein Wiki."})]}),o&&!o.available&&e.jsx("div",{className:"rounded-2xl border border-amber-500/30 bg-amber-500/5 p-4 text-xs text-amber-300",children:"Der Wissens-Vault liegt auf der Box (~/wissens-vault) — im lokalen Dev-Modus nicht verfügbar."}),h&&e.jsxs("div",{className:"flex items-center gap-2 rounded-2xl border border-border/60 bg-card/30 p-6 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Vault wird geladen …"]}),(o==null?void 0:o.available)&&t.length===0&&e.jsx("div",{className:"rounded-2xl border border-dashed border-border/60 bg-card/20 p-8 text-center text-xs text-muted-foreground",children:"Noch keine Notizen. Der nächtliche Traum (03:15) legt sie an — morgen früh steht hier die erste."}),t.length>0&&e.jsxs("div",{className:"flex flex-col gap-4 lg:flex-row",children:[e.jsxs("aside",{className:"w-full shrink-0 space-y-3 lg:w-72",children:[e.jsxs("div",{className:"relative",children:[e.jsx("input",{value:a,onChange:s=>p(s.target.value),type:"search",placeholder:"Notizen durchsuchen…","aria-label":"Notizen durchsuchen",className:"h-9 w-full rounded-lg border border-border/60 bg-card/45 pl-8 pr-3 text-xs text-foreground outline-none focus:ring-1 focus:ring-primary/50"}),e.jsx(S,{className:"absolute left-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground"})]}),e.jsx("div",{className:"max-h-[70vh] space-y-3 overflow-y-auto pr-1 scrollbar-thin",children:b.map(({dir:s,files:n})=>e.jsxs("div",{children:[e.jsxs("p",{className:"mb-1 flex items-center gap-1 text-[10px] font-bold uppercase tracking-wider text-muted-foreground/60",children:[e.jsx(M,{className:"h-3 w-3"})," ",D[s]??s]}),e.jsx("div",{className:"space-y-0.5",children:n.map(c=>e.jsxs("button",{onClick:()=>f(c.path),className:w("flex w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left text-xs transition-all cursor-pointer",l===c.path?"bg-primary/15 text-primary":"text-muted-foreground hover:bg-accent hover:text-foreground"),children:[e.jsx(W,{className:"h-3.5 w-3.5 shrink-0"}),e.jsx("span",{className:"truncate",title:c.title,children:c.name}),c.neu&&e.jsxs("span",{className:"ml-auto flex items-center gap-0.5 rounded bg-violet-500/15 px-1 py-0.5 text-[8px] font-bold uppercase text-violet-300",children:[e.jsx(C,{className:"h-2.5 w-2.5"}),"neu"]})]},c.path))})]},s||"__root"))})]}),e.jsx("div",{className:"min-w-0 flex-1 mc-card p-5",children:u?e.jsxs("div",{className:"flex items-center gap-2 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Notiz wird geladen …"]}):d?e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"mb-3 flex items-center justify-between border-b border-border/40 pb-2",children:[e.jsxs("span",{className:"flex items-center gap-1.5 font-mono text-[11px] text-muted-foreground",children:[e.jsx(E,{className:"h-3.5 w-3.5"})," ",d.path]}),d.mtime>0&&e.jsxs("span",{className:"text-[10px] text-muted-foreground/60",children:["Stand ",new Date(d.mtime*1e3).toLocaleString("de-DE",{day:"2-digit",month:"2-digit",hour:"2-digit",minute:"2-digit"})]})]}),e.jsx(z,{text:d.content,onWikiLink:j,known:m})]}):null})]})]})}function z({text:o,onWikiLink:h,known:l}){const f=o.split(`
`),d=[];let r=[],u=null;const i=a=>{r.length&&(d.push(e.jsx("ul",{className:"mb-3 ml-4 list-disc space-y-1",children:r},a)),r=[])};return f.forEach((a,p)=>{const t=`l${p}`;if(u!==null){a.trimEnd()==="```"?(d.push(e.jsx("pre",{className:"mb-3 overflow-x-auto rounded-xl border border-border/50 bg-background/50 p-3 font-mono text-[11px] leading-relaxed text-foreground/85",children:u.join(`
`)},t)),u=null):u.push(a);return}if(a.trimStart().startsWith("```")){i(t),u=[];return}const m=a.trimEnd();if(!m.trim()){i(t);return}const g=m.match(/^(#{1,4})\s+(.*)$/);if(g){i(t);const j=g[1].length,s=j===1?"text-lg font-bold mt-1 mb-3":j===2?"text-base font-bold mt-4 mb-2":"text-sm font-bold mt-3 mb-1.5";d.push(e.jsx("p",{className:w(s,"font-space text-foreground"),children:N(g[2],h,l,t)},t));return}const b=m.match(/^\s*[-*•]\s+(.*)$/);if(b){r.push(e.jsx("li",{className:"text-xs leading-relaxed text-foreground/85",children:N(b[1],h,l,t)},t));return}i(t),d.push(e.jsx("p",{className:"mb-2 text-xs leading-relaxed text-foreground/85",children:N(m,h,l,t)},t))}),i("end"),u!==null&&d.push(e.jsx("pre",{className:"mb-3 overflow-x-auto rounded-xl border border-border/50 bg-background/50 p-3 font-mono text-[11px]",children:u.join(`
`)},"code-end")),e.jsx("div",{className:"max-w-3xl",children:d})}function N(o,h,l,f){return o.split(/(\[\[[^\]]+\]\]|\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g).map((r,u)=>{const i=`${f}-${u}`,a=r.match(/^\[\[([^\]|]+)(?:\|([^\]]+))?\]\]$/);if(a){const p=a[1].trim(),t=(a[2]??a[1]).trim();return l.has(p.toLowerCase())?e.jsx("button",{onClick:()=>h(p),className:"rounded bg-primary/10 px-1 text-primary underline decoration-primary/40 underline-offset-2 hover:bg-primary/20 cursor-pointer",children:t},i):e.jsx("span",{className:"rounded bg-background/40 px-1 text-muted-foreground",title:"Notiz existiert (noch) nicht",children:t},i)}return r.startsWith("**")&&r.endsWith("**")?e.jsx("b",{className:"font-semibold text-foreground",children:r.slice(2,-2)},i):r.startsWith("*")&&r.endsWith("*")&&r.length>2?e.jsx("i",{children:r.slice(1,-1)},i):r.startsWith("`")&&r.endsWith("`")?e.jsx("code",{className:"rounded bg-background/50 px-1 font-mono text-[11px] text-teal-300",children:r.slice(1,-1)},i):e.jsx("span",{children:r},i)})}export{T as WissenView};
@@ -1,4 +1,4 @@
import{c as a}from"./index-CQgUnd1p.js";/**
import{c as a}from"./index-BJUCwF6N.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c}from"./index-CQgUnd1p.js";/**
import{c}from"./index-BJUCwF6N.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c}from"./index-CQgUnd1p.js";/**
import{c}from"./index-BJUCwF6N.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as a}from"./index-CQgUnd1p.js";/**
import{c as a}from"./index-BJUCwF6N.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
import{c as t}from"./index-CQgUnd1p.js";/**
import{c as t}from"./index-BJUCwF6N.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as t}from"./index-CQgUnd1p.js";/**
import{c as t}from"./index-BJUCwF6N.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as a}from"./index-CQgUnd1p.js";/**
import{c as a}from"./index-BJUCwF6N.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as a}from"./index-CQgUnd1p.js";/**
import{c as a}from"./index-BJUCwF6N.js";/**
* @license lucide-react v0.460.0 - ISC
*
* This source code is licensed under the ISC license.
+2 -2
View File
@@ -7,8 +7,8 @@
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-CQgUnd1p.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BH760w1h.css">
<script type="module" crossorigin src="/assets/index-BJUCwF6N.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DEL5L7Ku.css">
</head>
<body>
<div id="root"></div>
@@ -18,10 +18,15 @@ function fmt(v: number, unit: string): string {
return `${n}${unit}`
}
function ChartTooltip({ active, payload, unit }: any) {
function ChartTooltip({ active, payload, unit, label }: any) {
if (!active || !payload?.length) return null
return (
<div className="rounded-lg border border-border/70 bg-popover/95 px-3 py-2 shadow-xl backdrop-blur">
{Number.isFinite(label) && (
<div className="mb-1 border-b border-border/40 pb-1 font-mono text-[10px] text-muted-foreground/80">
{new Date(label).toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit", second: "2-digit" })}
</div>
)}
<div className="space-y-1">
{payload.map((p: any) => (
<div key={p.dataKey} className="flex items-center gap-2 text-[11px] font-mono">
@@ -36,15 +41,17 @@ function ChartTooltip({ active, payload, unit }: any) {
}
/** Wiederverwendbarer Live-Verlaufsgraph (Recharts Area, glatte Splines, Gradient-Fill,
* Hover-Tooltip). yMode='percent' → 0..100 in 25er-Schritten; 'auto' → dynamisch (nice). */
* Hover-Tooltip). yMode='percent' → 0..100 in 25er-Schritten; 'auto' → dynamisch (nice).
* showTime=true blendet die Zeitachse ein (t in ms; Label-Format folgt der Spannweite). */
export function LiveAreaChart({
data, series, unit = "%", yMode = "percent", height = 176,
data, series, unit = "%", yMode = "percent", height = 176, showTime = false,
}: {
data: any[]
series: ChartSeries[]
unit?: string
yMode?: "percent" | "auto"
height?: number
showTime?: boolean
}) {
const peak = data.reduce(
(m, row) => series.reduce((mm, s) => Math.max(mm, Number(row[s.key]) || 0), m), 0
@@ -53,6 +60,13 @@ export function LiveAreaChart({
? Math.min(100, Math.max(25, Math.ceil((peak * 1.2) / 25) * 25))
: Math.max(niceCeil(peak * 1.15), 10)
// Kurze Fenster (Live ≈ 2 min) brauchen Sekunden, lange (1 h / 24 h) nur HH:MM.
const spanMs = data.length > 1 ? Number(data[data.length - 1]?.t) - Number(data[0]?.t) : 0
const fmtTime = (v: number) =>
new Date(v).toLocaleTimeString("de-DE",
spanMs <= 10 * 60_000 ? { hour: "2-digit", minute: "2-digit", second: "2-digit" }
: { hour: "2-digit", minute: "2-digit" })
return (
<div style={{ height }} className="w-full">
<ResponsiveContainer width="100%" height="100%">
@@ -66,7 +80,15 @@ export function LiveAreaChart({
))}
</defs>
<CartesianGrid vertical={false} stroke={GRID} />
<XAxis dataKey="t" hide />
{showTime ? (
<XAxis
dataKey="t" type="number" domain={["dataMin", "dataMax"]}
tickFormatter={fmtTime} tickCount={5} minTickGap={40} interval="preserveStartEnd"
axisLine={false} tickLine={false} tick={{ fontSize: 10, fill: AXIS }} height={18}
/>
) : (
<XAxis dataKey="t" hide />
)}
<YAxis
domain={[0, yMax]} ticks={[0, yMax / 2, yMax]} tickFormatter={(v) => fmt(v, unit)}
width={42} axisLine={false} tickLine={false} tick={{ fontSize: 10, fill: AXIS }}
@@ -0,0 +1,27 @@
import { cn } from "@/lib/utils"
// Bereichs-Schalter der Leistungs-Karten: Live (2-min-Browser-Store) vs. 1h/24h
// (Backend-Ringpuffer, /api/system/history). Bewusst winzig — kein Chart-Gebirge.
export type ChartRange = "live" | "1h" | "24h"
export const RANGE_MINUTES: Record<Exclude<ChartRange, "live">, number> = { "1h": 60, "24h": 1440 }
const LABELS: Record<ChartRange, string> = { live: "Live", "1h": "1 h", "24h": "24 h" }
export function RangeSwitch({ value, onChange }: { value: ChartRange; onChange: (r: ChartRange) => void }) {
return (
<div className="flex rounded-lg border border-border/40 bg-background/30 p-0.5">
{(Object.keys(LABELS) as ChartRange[]).map((r) => (
<button
key={r}
onClick={() => onChange(r)}
className={cn(
"rounded-md px-2 py-0.5 text-[9px] font-bold uppercase tracking-wide transition-all cursor-pointer",
value === r ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground",
)}
>
{LABELS[r]}
</button>
))}
</div>
)
}
@@ -1,7 +1,10 @@
import { useMemo, useState } from "react"
import { Cpu } from "lucide-react"
import { useSystemHistory } from "@/lib/useSystemHistory"
import { useMetricHistory } from "@/lib/queries"
import { gb } from "@/lib/format"
import { LiveAreaChart, type ChartSeries } from "./LiveAreaChart"
import { RangeSwitch, RANGE_MINUTES, type ChartRange } from "./RangeSwitch"
const SERIES: ChartSeries[] = [
{ key: "cpu", label: "CPU", color: "#2dd4bf" },
@@ -12,6 +15,14 @@ const SERIES: ChartSeries[] = [
export function SystemStatusCard() {
const { sys, hist } = useSystemHistory()
const [range, setRange] = useState<ChartRange>("live")
const histQ = useMetricHistory(range === "live" ? 60 : RANGE_MINUTES[range], range !== "live")
// Live = 3-s-Browser-Store (~2 min); 1h/24h = Backend-Ringpuffer mit echten Zeitstempeln.
const chartData = useMemo(() => {
if (range === "live") return hist
return (histQ.data?.points ?? []).map((p) => ({ t: p.t * 1000, cpu: p.cpu, ram: p.ram, gpu: p.gpu, disk: p.disk }))
}, [range, hist, histQ.data])
const hasGpu = !!(sys?.gpu && sys.gpu.busy_percent != null && sys.gpu.gtt_used != null && sys.gpu.gtt_total != null)
const activeSeries = SERIES.filter((s) => s.key !== "gpu" || hasGpu)
@@ -33,9 +44,12 @@ export function SystemStatusCard() {
<div className="mb-3 flex items-center gap-2">
<Cpu className="h-4.5 w-4.5 text-primary" />
<h2 className="text-sm font-semibold uppercase tracking-wide text-foreground">System-Status</h2>
<span className="ml-1 inline-flex items-center gap-1 text-[10px] font-medium text-muted-foreground/70">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500 animate-pulse" /> live
</span>
{range === "live" && (
<span className="ml-1 inline-flex items-center gap-1 text-[10px] font-medium text-muted-foreground/70">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500 animate-pulse" /> live
</span>
)}
<div className="ml-auto"><RangeSwitch value={range} onChange={setRange} /></div>
</div>
{sys ? (
@@ -50,7 +64,14 @@ export function SystemStatusCard() {
</div>
))}
</div>
<LiveAreaChart data={hist} series={activeSeries} unit="%" yMode="percent" height={176} />
{range !== "live" && chartData.length === 0 && (
<div className="flex h-44 items-center justify-center text-xs text-muted-foreground">
{histQ.isLoading ? "Verlauf wird geladen …" : "Noch kein Verlauf — der Sammler baut die Historie gerade erst auf."}
</div>
)}
{(range === "live" || chartData.length > 0) && (
<LiveAreaChart data={chartData} series={activeSeries} unit="%" yMode="percent" height={176} showTime />
)}
</>
) : (
<div className="flex h-44 items-center justify-center text-xs text-muted-foreground">Lade Systemdaten</div>
@@ -1,7 +1,9 @@
import { useMemo, useState } from "react"
import { Activity } from "lucide-react"
import { useTokenStats } from "@/lib/queries"
import { useTokenStats, useMetricHistory } from "@/lib/queries"
import { useTokHistory } from "@/lib/metricsStore"
import { LiveAreaChart, type ChartSeries } from "./LiveAreaChart"
import { RangeSwitch, RANGE_MINUTES, type ChartRange } from "./RangeSwitch"
const SERIES: ChartSeries[] = [
{ key: "prompt", label: "Prompt", color: "#f59e0b" },
@@ -10,7 +12,27 @@ const SERIES: ChartSeries[] = [
export function TokenPerformanceCard() {
const { data: ts } = useTokenStats()
const hist = useTokHistory() // Durchsatz-Verlauf aus dem modul-globalen Store
const hist = useTokHistory() // Live-Durchsatz aus dem modul-globalen Store (~2 min)
const [range, setRange] = useState<ChartRange>("live")
const histQ = useMetricHistory(range === "live" ? 60 : RANGE_MINUTES[range], range !== "live")
// 1h/24h: Raten aus den Deltas der Gesamtzähler (tok/s zwischen zwei Sample-Punkten).
const chartData = useMemo(() => {
if (range === "live") return hist
const pts = histQ.data?.points ?? []
const out: { t: number; prompt: number; completion: number }[] = []
for (let i = 1; i < pts.length; i++) {
const a = pts[i - 1], b = pts[i]
if (a.tp == null || b.tp == null || a.tc == null || b.tc == null) continue
const dt = Math.max(b.t - a.t, 1)
out.push({
t: b.t * 1000,
prompt: Math.max(0, (b.tp - a.tp) / dt),
completion: Math.max(0, (b.tc - a.tc) / dt),
})
}
return out
}, [range, hist, histQ.data])
const last = hist[hist.length - 1]
const curRate = last ? Math.round(last.prompt + last.completion) : 0
@@ -22,9 +44,11 @@ export function TokenPerformanceCard() {
<div className="flex items-center gap-2">
<Activity className="h-4.5 w-4.5 text-primary" />
<h2 className="text-sm font-semibold uppercase tracking-wide text-foreground">Token-Durchsatz</h2>
<span className="ml-1 inline-flex items-center gap-1 text-[10px] font-medium text-muted-foreground/70">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500 animate-pulse" /> live
</span>
{range === "live" && (
<span className="ml-1 inline-flex items-center gap-1 text-[10px] font-medium text-muted-foreground/70">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500 animate-pulse" /> live
</span>
)}
</div>
{ts && (
<div className="mt-2 flex items-baseline gap-2">
@@ -45,21 +69,28 @@ export function TokenPerformanceCard() {
</div>
)}
</div>
<div className="flex shrink-0 flex-col items-end gap-1.5 pt-1">
{SERIES.map((s) => (
<div key={s.key} className="flex items-center gap-1.5">
<span className="h-2 w-2 rounded-full" style={{ background: s.color }} />
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">{s.label}</span>
<span className="font-mono text-xs font-bold tabular-nums text-foreground">
{Math.round((last?.[s.key as "prompt" | "completion"]) ?? 0)}
</span>
</div>
))}
<div className="flex shrink-0 flex-col items-end gap-2 pt-1">
<RangeSwitch value={range} onChange={setRange} />
<div className="flex flex-col items-end gap-1.5">
{SERIES.map((s) => (
<div key={s.key} className="flex items-center gap-1.5">
<span className="h-2 w-2 rounded-full" style={{ background: s.color }} />
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">{s.label}</span>
<span className="font-mono text-xs font-bold tabular-nums text-foreground">
{Math.round((last?.[s.key as "prompt" | "completion"]) ?? 0)}
</span>
</div>
))}
</div>
</div>
</div>
{ts ? (
<LiveAreaChart data={hist} series={SERIES} unit=" tok/s" yMode="auto" height={150} />
{range !== "live" && chartData.length === 0 ? (
<div className="flex h-[150px] items-center justify-center text-xs text-muted-foreground">
{histQ.isLoading ? "Verlauf wird geladen …" : "Noch kein Verlauf — der Sammler baut die Historie gerade erst auf."}
</div>
) : ts ? (
<LiveAreaChart data={chartData} series={SERIES} unit=" tok/s" yMode="auto" height={150} showTime />
) : (
<div className="flex h-[150px] items-center justify-center text-xs text-muted-foreground">Lade Durchsatz</div>
)}
+16
View File
@@ -417,6 +417,22 @@ export interface Health {
brain?: { role: string; model: string | null; ready: boolean }
}
// 24-h-Metrik-Verlauf (GET /api/system/history?minutes=N) — Basis der Cockpit-Zeitachse.
// tp/tc sind Token-GESAMTZÄHLER; Raten rechnet das Frontend aus den Deltas.
export interface HistoryPoint {
t: number // Epoch-Sekunden
cpu: number | null
ram: number | null
gpu: number | null
disk: number | null
tp: number | null
tc: number | null
}
export interface HistoryResp {
sample_s: number
points: HistoryPoint[]
}
export interface TokenStats {
prompt_tokens: number
completion_tokens: number
+11
View File
@@ -17,6 +17,7 @@ import {
type GroupsResp,
type HermesBrainResp,
type Health,
type HistoryResp,
type IdeenLogResp,
type IdeenResp,
type Job,
@@ -169,6 +170,16 @@ export const useHealth = () =>
export const useSystemStatus = (refetchInterval: number = TAKT.graph) =>
useQuery({ queryKey: qk.systemStatus, queryFn: () => api<SystemStatus>("/api/system/status"), refetchInterval })
// Metrik-Verlauf (Cockpit-Zeitachse): 1h/24h aus dem Backend-Ringpuffer. Nur aktiv,
// wenn eine Karte gerade diesen Bereich zeigt (enabled) — Live bleibt beim 3s-Store.
export const useMetricHistory = (minutes: number, enabled = true) =>
useQuery({
queryKey: ["metrics-history", minutes],
queryFn: () => api<HistoryResp>(`/api/system/history?minutes=${minutes}`),
enabled,
refetchInterval: 60_000,
})
export const useServices = (refetchInterval: number = TAKT.normal) =>
useQuery({ queryKey: qk.services, queryFn: () => api<ServicesResp>("/api/system/services"), refetchInterval })
+36 -19
View File
@@ -72,8 +72,16 @@ function CodeWindow({
)
}
// Die Box-Adresse kennt der Browser schon — er ist ja über sie verbunden. Nur im
// Dev-Modus (localhost + Vite-Proxy) greift der bekannte LAN-Fallback.
function defaultHost(): string {
const h = window.location.hostname
if (h && h !== "localhost" && h !== "127.0.0.1") return h
return "192.168.178.151"
}
export function ConnectView() {
const [host, setHost] = useState(localStorage.getItem("mc_host") || "192.168.178.151")
const [host, setHost] = useState(localStorage.getItem("mc_host") || defaultHost())
const [mcpPath, setMcpPath] = useState(localStorage.getItem("mc_mcp_path") || "")
// Legacy-Tools (roher /v1-Zugang) leben im Ausklapper — Hermes Desktop ist der Hauptweg.
const [active, setActive] = useState("kilo")
@@ -187,11 +195,12 @@ export function ConnectView() {
</p>
</div>
{/* Gemeinsame Variablen */}
<div className="grid gap-4 md:grid-cols-2 p-5 mc-card">
<div className="space-y-1.5">
{/* Box-Adresse — vorbefüllt aus der Browser-URL; anfassen nur, wenn sich die Box-IP ändert. */}
<div className="p-5 mc-card">
<div className="space-y-1.5 max-w-md">
<label className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground flex items-center gap-1.5">
<Globe className="h-3.5 w-3.5 text-primary" /> Box LAN IP-Adresse
<span className="normal-case font-semibold tracking-normal text-muted-foreground/60"> vorbefüllt aus der Adresse dieser Seite</span>
</label>
<input
value={host}
@@ -201,21 +210,9 @@ export function ConnectView() {
placeholder="z.B. 192.168.178.151"
className="w-full h-9 rounded-lg border border-border/60 bg-background/40 px-3 font-mono text-xs outline-none focus:ring-1 focus:ring-primary/50 text-foreground"
/>
</div>
<div className="space-y-1.5">
<label className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground flex items-center gap-1.5">
<FolderOpen className="h-3.5 w-3.5 text-violet-400" /> Lokaler MCP-Scriptpfad
<span className="text-violet-400/70 normal-case font-semibold tracking-normal">(nur Leitung 2)</span>
</label>
<input
value={mcpPath}
onChange={(e) => saveMcpPath(e.target.value)}
aria-label="Lokaler MCP-Scriptpfad"
spellCheck={false}
placeholder="z.B. F:\Coding Stuff\mission-control-2\mcp\mcp_memory.py"
className="w-full h-9 rounded-lg border border-border/60 bg-background/40 px-3 font-mono text-xs outline-none focus:ring-1 focus:ring-violet-500/50 text-foreground"
/>
<p className="text-[10px] text-muted-foreground/70">
Wird in alle Snippets eingebacken. Ändern musst du sie nur, wenn die Box eine neue IP bekommt.
</p>
</div>
</div>
@@ -332,6 +329,26 @@ export function ConnectView() {
<span>{data.memory.note}</span>
</div>
{/* Der Pfad wirkt NUR auf dieses Snippet — deshalb wohnt das Feld hier,
nicht mehr prominent in der Kopfzeile („Wann brauche ich das?"). */}
<div className="space-y-1.5">
<label className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground flex items-center gap-1.5">
<FolderOpen className="h-3.5 w-3.5 text-violet-400" /> Wo liegt mcp_memory.py auf DIESEM PC?
</label>
<input
value={mcpPath}
onChange={(e) => saveMcpPath(e.target.value)}
aria-label="Lokaler MCP-Scriptpfad"
spellCheck={false}
placeholder="z.B. F:\Coding Stuff\mission-control-2\mcp\mcp_memory.py"
className="w-full h-9 rounded-lg border border-border/60 bg-background/40 px-3 font-mono text-xs outline-none focus:ring-1 focus:ring-violet-500/50 text-foreground"
/>
<p className="text-[10px] text-muted-foreground/70">
Nur nötig, wenn du das Gedächtnis in einem PC-Tool anschließt das Script läuft lokal
und spricht mit der Box. Der Pfad landet direkt im Snippet unten.
</p>
</div>
<CodeWindow
tool={data.memory}
fileName="mcp.json"
@@ -259,14 +259,18 @@ function Detail({
{lockUnload ? "🔒 bleibt geladen" : warm ? "Entladen" : "Ins Warm-Set laden"}
</button>
{/* Rolle wählen */}
{/* Rolle wählen — bei lebenswichtigen Rollen (Hirn/Gedächtnis) gesperrt: die Rolle
wandert nur, indem man sie einem ANDEREN Modell zuweist (Backend erzwingt das auch). */}
<div className="rounded-xl border border-border/30 bg-background/20 p-3">
<div className="mb-2 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">Rolle</div>
<div className="mb-2 flex items-center justify-between text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
<span>Rolle</span>
{protectedRole && <span className="normal-case font-medium tracking-normal text-muted-foreground/70">🔒 lebenswichtig zum Tausch die Rolle woanders zuweisen</span>}
</div>
<div className="flex flex-wrap gap-1.5">
<RoleChip active={!m.role} onClick={() => onRole("")} dot="bg-slate-400" label="keine" />
<RoleChip active={!m.role} onClick={() => onRole("")} dot="bg-slate-400" label="keine" disabled={protectedRole} />
{ROLE_META.map((r) => (
<RoleChip key={r.role} active={m.role === r.role} onClick={() => onRole(r.role)}
dot={roleBarColor(r.role).dot} label={r.short} />
dot={roleBarColor(r.role).dot} label={r.short} disabled={protectedRole && m.role !== r.role} />
))}
</div>
</div>
@@ -322,11 +326,16 @@ function Detail({
)
}
function RoleChip({ active, onClick, dot, label }: { active: boolean; onClick: () => void; dot: string; label: string }) {
function RoleChip({ active, onClick, dot, label, disabled }: {
active: boolean; onClick: () => void; dot: string; label: string; disabled?: boolean
}) {
return (
<button onClick={onClick}
className={cn("flex items-center gap-1.5 rounded-lg border px-2 py-1 text-[10px] font-semibold transition-all cursor-pointer",
active ? "border-primary/60 bg-primary/15 text-foreground" : "border-border/40 bg-background/20 text-muted-foreground hover:border-primary/30")}>
<button onClick={onClick} disabled={disabled}
title={disabled ? "Gesperrt: dieses Modell hält eine lebenswichtige Rolle." : undefined}
className={cn("flex items-center gap-1.5 rounded-lg border px-2 py-1 text-[10px] font-semibold transition-all",
disabled ? "cursor-not-allowed opacity-40 border-border/40 bg-background/20 text-muted-foreground"
: active ? "cursor-pointer border-primary/60 bg-primary/15 text-foreground"
: "cursor-pointer border-border/40 bg-background/20 text-muted-foreground hover:border-primary/30")}>
<span className={cn("h-2 w-2 rounded-sm", dot)} />
{label}
{active && <Check className="h-3 w-3 text-primary" />}