feat(frontend): v3-Umbau P2 — echtes Routing, Unterzustand in der URL
Dritte Etappe. Die Ansicht lag bisher im URL-Hash (`#models`), gespeist aus einem
useState in App.tsx. Das war fuer Reload und Lesezeichen brauchbar — aber nur auf
View-Ebene; fuer den Zustand DARUNTER war kein Platz.
## Was jetzt in der Adresse steht (Befund B-05)
/modelle?reiter=routing der Reiter war lokaler State, nicht teilbar
/ideen?offen=<id> die aufgeklappte Karte; wer waehrend eines
laufenden Workers neu lud, verlor seinen Platz
/wissen?datei=<pfad> jede Vault-Notiz verlinkbar
<ueberall>?system=wartung|logs die System-Schublade inkl. Reiter
Alle Suchparameter werden validiert (app/router.tsx) — ein Link mit Unsinn darin
faellt auf den Normalfall zurueck, statt die Ansicht in einen halben Zustand zu
bringen.
## Alte Lesezeichen brechen nicht
lib/hashUmleitung.ts biegt `#models` per replaceState auf `/modelle`, EINMAL vor
dem Router-Mount. Fuenf Tests decken das ab (jeder NAV-Eintrag, die Schreibweise
`#/models`, erhaltene Suchparameter, unbekannter Hash, kein Hash) — die Bruecke
sieht sonst wie toter Code aus und waere ein naheliegender Kandidat zum Wegraeumen.
Das Backend brauchte KEINE Zeile: der SPA-Catch-all in app.py:148-168 beantwortet
jede unbekannte Route schon immer mit index.html. Vorher geprueft, nicht gehofft.
## Fehlergrenze pro Route (Befund B-13)
Vorher hing eine einzige Grenze in main.tsx um alles — ein Renderfehler im
Modell-Manager nahm Cockpit, Konsole und Chronik mit. Jetzt faengt jede Route fuer
sich (components/RouteFehler.tsx: was passiert ist, was man tun kann, technische
Einzelheiten aufklappbar). AppErrorBoundary bleibt als letztes Netz fuer Fehler
ausserhalb jeder Route. Dazu eine 404-Seite, die Schiene und Kopfzeile stehen
laesst — im Browser geprueft.
## Budget: der Router kostete 29 kB, sie sind wieder drin
TanStack Router hob den Start-Chunk von 111 auf 141 kB gzip und riss damit das in
P0 gesetzte Ampel-Budget (125 kB). Statt das Budget hochzusetzen — was es als
Signal entwertet haette — den Platz zurueckgeholt: SystemDrawer und CommandPalette
sind beim Start UNSICHTBAR und liegen jetzt hinter lazy(). Die Palette laedt beim
ersten Strg+K und bleibt dann gemountet.
Start-Chunk gzip 111 411 -> 114 978 B (Budget 125 000, weiter scharf)
dist gesamt 1 565 521 B (Budget 3 145 728)
## Nebenbei
· Befund B-09 erledigt: Der "Suchen"-Knopf baute ein GEFAELSCHTES KeyboardEvent
(`new KeyboardEvent("keydown", { metaKey: true })`) und feuerte es aufs
Dokument. Jetzt ein eigener Kanal; in P3 loest der Store auch den ab.
· Der Schliessen-Knopf der System-Schublade hatte kein aria-label — ein reiner
Icon-Knopf ohne Namen. Nachgeholt (beim Pruefen aufgefallen).
· nav.ts fuehrt jetzt den Pfad je Eintrag + navFuerPfad() mit Laengen-Sortierung,
damit /ideen/<id> die Sidebar bei "Ideen" markiert. Mit Tests.
## Verifiziert im Browser (lokales Backend, echtes Produktions-Buendel)
/ -> /cockpit, Sidebar markiert
/#models -> /modelle, Sidebar markiert
/modelle?reiter=routing -> Reiter "Routing & Warm-Set" aktiv
/cockpit?system=logs -> Schublade offen, Reiter "System-Logs" aktiv;
Schliessen entfernt den Parameter wieder
/gibtsnicht -> 404-Seite, Schiene + Kopfzeile intakt
Zurueck-Taste -> voriger Pfad
26/26 Tests gruen · ESLint 0 Fehler · tsc sauber.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ae69c5ce31
commit
464b0d20b8
+2
-2
File diff suppressed because one or more lines are too long
+5
-5
File diff suppressed because one or more lines are too long
Vendored
+2
-2
File diff suppressed because one or more lines are too long
+41
File diff suppressed because one or more lines are too long
Vendored
+2
-2
File diff suppressed because one or more lines are too long
+3
-3
File diff suppressed because one or more lines are too long
+11
-11
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{u,ah as m,k as x,j as s,y as n,ai as b,b as p,n as f,q as h}from"./index-BYlMs6Dj.js";function y(){const d=u(),{data:t=[]}=m(),{showAlert:l,dialogElement:o}=x();async function i(e){try{await f(`/api/jobs/${e}/cancel`,{method:"POST"}),d.invalidateQueries({queryKey:h.jobs})}catch(c){l("Fehler",c.message)}}const a=t.filter(e=>e.state==="running"||e.state==="queued"),r=t.filter(e=>e.state!=="running"&&e.state!=="queued").slice(-3);return a.length===0&&r.length===0?null:s.jsxs("div",{className:"space-y-3 mc-card p-4",children:[s.jsx("div",{className:"text-[11px] font-semibold text-muted-foreground uppercase tracking-wider",children:"Aktive Downloads"}),a.map(e=>s.jsxs("div",{className:"space-y-1.5 p-3 rounded-xl bg-background/20 border border-border/40",children:[s.jsxs("div",{className:"flex justify-between items-center text-xs",children:[s.jsx("span",{className:"font-semibold truncate max-w-[250px]",children:e.label}),s.jsxs("div",{className:"flex items-center gap-3",children:[s.jsxs("span",{className:"text-muted-foreground font-mono",children:[e.progress??0,"% • ",n(e.done_bytes),"/",n(e.total_bytes),e.eta_s?` • ETA ${b(e.eta_s)}`:""]}),s.jsx("button",{onClick:()=>i(e.id),className:"text-[10px] text-red-400 hover:text-red-300 font-semibold border border-red-500/25 bg-red-500/5 px-2 py-0.5 rounded transition-all cursor-pointer",children:"Abbrechen"})]})]}),s.jsx("div",{className:"h-1.5 overflow-hidden rounded-full bg-muted",children:s.jsx("div",{className:"h-full rounded-full bg-primary transition-all duration-500",style:{width:`${e.progress??0}%`}})})]},e.id)),r.map(e=>s.jsxs("div",{className:"flex justify-between items-center text-xs text-muted-foreground px-1",children:[s.jsx("span",{className:"truncate",children:e.label}),s.jsx("span",{className:p("font-semibold text-[10px] px-1.5 py-0.5 rounded uppercase font-mono",e.state==="done"?"bg-emerald-500/10 text-emerald-400":"bg-amber-500/10 text-amber-400"),children:e.state})]},e.id)),o]})}export{y as J};
|
||||
import{u,ab as m,k as x,j as s,y as n,ac as b,b as p,n as f,q as h}from"./index-Bb8GyPHo.js";function y(){const d=u(),{data:t=[]}=m(),{showAlert:l,dialogElement:o}=x();async function i(e){try{await f(`/api/jobs/${e}/cancel`,{method:"POST"}),d.invalidateQueries({queryKey:h.jobs})}catch(c){l("Fehler",c.message)}}const a=t.filter(e=>e.state==="running"||e.state==="queued"),r=t.filter(e=>e.state!=="running"&&e.state!=="queued").slice(-3);return a.length===0&&r.length===0?null:s.jsxs("div",{className:"space-y-3 mc-card p-4",children:[s.jsx("div",{className:"text-[11px] font-semibold text-muted-foreground uppercase tracking-wider",children:"Aktive Downloads"}),a.map(e=>s.jsxs("div",{className:"space-y-1.5 p-3 rounded-xl bg-background/20 border border-border/40",children:[s.jsxs("div",{className:"flex justify-between items-center text-xs",children:[s.jsx("span",{className:"font-semibold truncate max-w-[250px]",children:e.label}),s.jsxs("div",{className:"flex items-center gap-3",children:[s.jsxs("span",{className:"text-muted-foreground font-mono",children:[e.progress??0,"% • ",n(e.done_bytes),"/",n(e.total_bytes),e.eta_s?` • ETA ${b(e.eta_s)}`:""]}),s.jsx("button",{onClick:()=>i(e.id),className:"text-[10px] text-red-400 hover:text-red-300 font-semibold border border-red-500/25 bg-red-500/5 px-2 py-0.5 rounded transition-all cursor-pointer",children:"Abbrechen"})]})]}),s.jsx("div",{className:"h-1.5 overflow-hidden rounded-full bg-muted",children:s.jsx("div",{className:"h-full rounded-full bg-primary transition-all duration-500",style:{width:`${e.progress??0}%`}})})]},e.id)),r.map(e=>s.jsxs("div",{className:"flex justify-between items-center text-xs text-muted-foreground px-1",children:[s.jsx("span",{className:"truncate",children:e.label}),s.jsx("span",{className:p("font-semibold text-[10px] px-1.5 py-0.5 rounded uppercase font-mono",e.state==="done"?"bg-emerald-500/10 text-emerald-400":"bg-amber-500/10 text-amber-400"),children:e.state})]},e.id)),o]})}export{y as J};
|
||||
@@ -0,0 +1 @@
|
||||
import{I as b,u as f,J as h,r as d,j as e,b as c,T as g,S as p,n as j,N,q as x}from"./index-Bb8GyPHo.js";import{E as v}from"./external-link-P1_1JELD.js";import{R as w}from"./refresh-cw-C3saW9zg.js";function T(){const{data:s}=b(),m=f(),t=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 r=await j("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})});n(r.ok?"Neu gestartet — einen Moment, dann lädt das Terminal.":`Fehlgeschlagen: ${r.err||"Unbekannter Fehler"}`),N(m,x.agentStatus,x.services)}catch(r){n(`Fehlgeschlagen: ${(r==null?void 0:r.message)||r}`)}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"]}),t&&e.jsxs("a",{href:t,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(v,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),t?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(g,{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(w,{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:t,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(p,{className:"mr-2 h-4 w-4"})," Lade Konsole…"]})]})}export{T as KonsoleView};
|
||||
@@ -1 +0,0 @@
|
||||
import{I as b,u as f,K as h,r as d,j as e,b as c,J as g,T as p,R as j,Q as N,n as v,U as w,q as x}from"./index-BYlMs6Dj.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};
|
||||
+33
-49
File diff suppressed because one or more lines are too long
+15
-15
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as s,j as e}from"./index-BYlMs6Dj.js";/**
|
||||
import{c as s,j as e}from"./index-Bb8GyPHo.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
var E=r=>{throw TypeError(r)};var C=(r,e,t)=>e.has(r)||E("Cannot "+t);var a=(r,e,t)=>(C(r,e,"read from private field"),t?t.call(r):e.get(r)),y=(r,e,t)=>e.has(r)?E("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(r):e.set(r,t),v=(r,e,t,n)=>(C(r,e,"write to private field"),n?n.call(r,t):e.set(r,t),t),j=(r,e,t)=>(C(r,e,"access private method"),t);import{a3 as B,a4 as F,a5 as K,a6 as I,a7 as P,u as A,r as f,a8 as J,a9 as Q,c as T,aa as U,j as s,T as D,b as M,L,K as H,q as V,n as R}from"./index-Bb8GyPHo.js";import{J as _}from"./JobsBar-D2vfhT2b.js";import{C as z}from"./circle-check-Crmfz00W.js";var c,g,o,h,m,w,O,q,G=(q=class extends B{constructor(e,t){super();y(this,m);y(this,c);y(this,g);y(this,o);y(this,h);v(this,c,e),this.setOptions(t),this.bindMethods(),j(this,m,w).call(this)}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(e){var n;const t=this.options;this.options=a(this,c).defaultMutationOptions(e),F(this.options,t)||a(this,c).getMutationCache().notify({type:"observerOptionsUpdated",mutation:a(this,o),observer:this}),t!=null&&t.mutationKey&&this.options.mutationKey&&K(t.mutationKey)!==K(this.options.mutationKey)?this.reset():((n=a(this,o))==null?void 0:n.state.status)==="pending"&&a(this,o).setOptions(this.options)}onUnsubscribe(){var e;this.hasListeners()||(e=a(this,o))==null||e.removeObserver(this)}onMutationUpdate(e){j(this,m,w).call(this),j(this,m,O).call(this,e)}getCurrentResult(){return a(this,g)}reset(){var e;(e=a(this,o))==null||e.removeObserver(this),v(this,o,void 0),j(this,m,w).call(this),j(this,m,O).call(this)}mutate(e,t){var n;return v(this,h,t),(n=a(this,o))==null||n.removeObserver(this),v(this,o,a(this,c).getMutationCache().build(a(this,c),this.options)),a(this,o).addObserver(this),a(this,o).execute(e)}},c=new WeakMap,g=new WeakMap,o=new WeakMap,h=new WeakMap,m=new WeakSet,w=function(){var t;const e=((t=a(this,o))==null?void 0:t.state)??I();v(this,g,{...e,isPending:e.status==="pending",isSuccess:e.status==="success",isError:e.status==="error",isIdle:e.status==="idle",mutate:this.mutate,reset:this.reset})},O=function(e){P.batch(()=>{var t,n,l,x,d,p,k,i;if(a(this,h)&&this.hasListeners()){const u=a(this,g).variables,N=a(this,g).context,S={client:a(this,c),meta:this.options.meta,mutationKey:this.options.mutationKey};if((e==null?void 0:e.type)==="success"){try{(n=(t=a(this,h)).onSuccess)==null||n.call(t,e.data,u,N,S)}catch(b){Promise.reject(b)}try{(x=(l=a(this,h)).onSettled)==null||x.call(l,e.data,null,u,N,S)}catch(b){Promise.reject(b)}}else if((e==null?void 0:e.type)==="error"){try{(p=(d=a(this,h)).onError)==null||p.call(d,e.error,u,N,S)}catch(b){Promise.reject(b)}try{(i=(k=a(this,h)).onSettled)==null||i.call(k,void 0,e.error,u,N,S)}catch(b){Promise.reject(b)}}}this.listeners.forEach(u=>{u(a(this,g))})})},q);function W(r,e){const t=A(),[n]=f.useState(()=>new G(t,r));f.useEffect(()=>{n.setOptions(r)},[n,r]);const l=f.useSyncExternalStore(f.useCallback(d=>n.subscribe(P.batchCalls(d)),[n]),()=>n.getCurrentResult(),()=>n.getCurrentResult()),x=f.useCallback((d,p)=>{n.mutate(d,p).catch(J)},[n]);if(l.error&&Q(n.options.throwOnError,[l.error]))throw l.error;return{...l,mutate:x,mutateAsync:l.mutate}}/**
|
||||
* @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 X=T("Play",[["polygon",{points:"6 3 20 12 6 21 6 3",key:"1oa8hb"}]]);function te(){const r=A(),{data:e,isLoading:t,isError:n}=U({queryKey:["skills-list"],queryFn:()=>R("/api/skills"),refetchInterval:5e3}),[l,x]=f.useState(null),[d,p]=f.useState(null),k=W({mutationFn:i=>R("/api/skills/run",{method:"POST",body:JSON.stringify({skill_name:i})}),onMutate:i=>{x(i),p(null)},onSuccess:(i,u)=>{x(null),p({name:u,ok:i.ok,msg:i.msg||i.err||"Unbekannter Fehler"}),r.invalidateQueries({queryKey:V.jobs}),r.invalidateQueries({queryKey:["skills-list"]})},onError:(i,u)=>{x(null),p({name:u,ok:!1,msg:String(i)})}});return s.jsxs("div",{className:"space-y-6",children:[s.jsx("div",{className:"flex flex-col sm:flex-row justify-between sm:items-center gap-4",children:s.jsxs("div",{children:[s.jsx("h1",{className:"text-2xl font-space font-bold tracking-tight bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text text-transparent",children:"Skills & Jobs"}),s.jsx("p",{className:"text-sm text-muted-foreground",children:"Löst autonome Agent-Skills manuell auf der Box aus. Die Ausführung erfolgt im Hintergrund durch Hermes."})]})}),s.jsx(_,{}),d&&s.jsxs("div",{className:M("p-4 rounded-lg border flex gap-3 items-start",d.ok?"bg-emerald-500/10 border-emerald-500/20 text-emerald-600 dark:text-emerald-400":"bg-red-500/10 border-red-500/20 text-red-600 dark:text-red-400"),children:[d.ok?s.jsx(z,{className:"h-5 w-5 mt-0.5 shrink-0"}):s.jsx(D,{className:"h-5 w-5 mt-0.5 shrink-0"}),s.jsxs("div",{children:[s.jsx("div",{className:"font-semibold text-sm",children:d.name}),s.jsx("div",{className:"text-sm opacity-90",children:d.msg})]})]}),t&&s.jsx("div",{className:"flex h-32 items-center justify-center",children:s.jsx(L,{className:"h-6 w-6 animate-spin text-muted-foreground"})}),n&&s.jsx("div",{className:"text-red-500 p-4 mc-card text-sm",children:"Fehler beim Laden der Skills. Ist das Backend erreichbar?"}),(e==null?void 0:e.skills)&&s.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4",children:[e.skills.map(i=>s.jsxs("div",{className:"mc-card p-5 flex flex-col hover:border-primary/40 transition-colors group",children:[s.jsxs("div",{className:"flex items-start justify-between gap-3 mb-2",children:[s.jsxs("div",{className:"flex items-center gap-2 font-semibold text-base font-space",children:[s.jsx(H,{className:"h-5 w-5 text-primary/80"}),i.name]}),s.jsxs("button",{onClick:()=>k.mutate(i.name),disabled:l===i.name||i.running||k.isPending,className:M("shrink-0 h-8 px-3 rounded-md text-xs font-semibold uppercase tracking-wider flex items-center gap-1.5 transition-all cursor-pointer",l===i.name||i.running?"bg-sky-500/10 border border-sky-500/40 text-sky-400 cursor-not-allowed":"bg-primary/10 text-primary hover:bg-primary hover:text-primary-foreground group-hover:shadow-md group-hover:shadow-primary/20"),children:[l===i.name||i.running?s.jsx(L,{className:"h-3.5 w-3.5 animate-spin"}):s.jsx(X,{className:"h-3.5 w-3.5"}),l===i.name||i.running?"Läuft …":"Starten"]})]}),s.jsx("div",{className:"text-sm text-muted-foreground flex-1",children:i.description})]},i.name)),e.skills.length===0&&s.jsxs("div",{className:"col-span-full p-8 text-center text-muted-foreground mc-card border-dashed border-2",children:["Keine Skills im Ordner ",s.jsx("code",{className:"text-xs text-foreground bg-muted px-1.5 py-0.5 rounded",children:"deploy/skills"})," gefunden."]})]})]})}export{te as SkillsView};
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
var M=r=>{throw TypeError(r)};var O=(r,e,t)=>e.has(r)||M("Cannot "+t);var a=(r,e,t)=>(O(r,e,"read from private field"),t?t.call(r):e.get(r)),y=(r,e,t)=>e.has(r)?M("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(r):e.set(r,t),v=(r,e,t,n)=>(O(r,e,"write to private field"),n?n.call(r,t):e.set(r,t),t),j=(r,e,t)=>(O(r,e,"access private method"),t);import{a9 as B,aa as F,ab as E,ac as I,ad as P,u as A,r as f,ae as J,af as Q,c as T,ag as U,j as s,G as D,T as H,b as K,L,M as G,q as V,n as R}from"./index-BYlMs6Dj.js";import{J as _}from"./JobsBar-B1nmMx5i.js";var c,g,o,h,m,w,C,q,z=(q=class extends B{constructor(e,t){super();y(this,m);y(this,c);y(this,g);y(this,o);y(this,h);v(this,c,e),this.setOptions(t),this.bindMethods(),j(this,m,w).call(this)}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(e){var n;const t=this.options;this.options=a(this,c).defaultMutationOptions(e),F(this.options,t)||a(this,c).getMutationCache().notify({type:"observerOptionsUpdated",mutation:a(this,o),observer:this}),t!=null&&t.mutationKey&&this.options.mutationKey&&E(t.mutationKey)!==E(this.options.mutationKey)?this.reset():((n=a(this,o))==null?void 0:n.state.status)==="pending"&&a(this,o).setOptions(this.options)}onUnsubscribe(){var e;this.hasListeners()||(e=a(this,o))==null||e.removeObserver(this)}onMutationUpdate(e){j(this,m,w).call(this),j(this,m,C).call(this,e)}getCurrentResult(){return a(this,g)}reset(){var e;(e=a(this,o))==null||e.removeObserver(this),v(this,o,void 0),j(this,m,w).call(this),j(this,m,C).call(this)}mutate(e,t){var n;return v(this,h,t),(n=a(this,o))==null||n.removeObserver(this),v(this,o,a(this,c).getMutationCache().build(a(this,c),this.options)),a(this,o).addObserver(this),a(this,o).execute(e)}},c=new WeakMap,g=new WeakMap,o=new WeakMap,h=new WeakMap,m=new WeakSet,w=function(){var t;const e=((t=a(this,o))==null?void 0:t.state)??I();v(this,g,{...e,isPending:e.status==="pending",isSuccess:e.status==="success",isError:e.status==="error",isIdle:e.status==="idle",mutate:this.mutate,reset:this.reset})},C=function(e){P.batch(()=>{var t,n,l,x,d,p,k,i;if(a(this,h)&&this.hasListeners()){const u=a(this,g).variables,N=a(this,g).context,S={client:a(this,c),meta:this.options.meta,mutationKey:this.options.mutationKey};if((e==null?void 0:e.type)==="success"){try{(n=(t=a(this,h)).onSuccess)==null||n.call(t,e.data,u,N,S)}catch(b){Promise.reject(b)}try{(x=(l=a(this,h)).onSettled)==null||x.call(l,e.data,null,u,N,S)}catch(b){Promise.reject(b)}}else if((e==null?void 0:e.type)==="error"){try{(p=(d=a(this,h)).onError)==null||p.call(d,e.error,u,N,S)}catch(b){Promise.reject(b)}try{(i=(k=a(this,h)).onSettled)==null||i.call(k,void 0,e.error,u,N,S)}catch(b){Promise.reject(b)}}}this.listeners.forEach(u=>{u(a(this,g))})})},q);function W(r,e){const t=A(),[n]=f.useState(()=>new z(t,r));f.useEffect(()=>{n.setOptions(r)},[n,r]);const l=f.useSyncExternalStore(f.useCallback(d=>n.subscribe(P.batchCalls(d)),[n]),()=>n.getCurrentResult(),()=>n.getCurrentResult()),x=f.useCallback((d,p)=>{n.mutate(d,p).catch(J)},[n]);if(l.error&&Q(n.options.throwOnError,[l.error]))throw l.error;return{...l,mutate:x,mutateAsync:l.mutate}}/**
|
||||
* @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 X=T("Play",[["polygon",{points:"6 3 20 12 6 21 6 3",key:"1oa8hb"}]]);function ee(){const r=A(),{data:e,isLoading:t,isError:n}=U({queryKey:["skills-list"],queryFn:()=>R("/api/skills"),refetchInterval:5e3}),[l,x]=f.useState(null),[d,p]=f.useState(null),k=W({mutationFn:i=>R("/api/skills/run",{method:"POST",body:JSON.stringify({skill_name:i})}),onMutate:i=>{x(i),p(null)},onSuccess:(i,u)=>{x(null),p({name:u,ok:i.ok,msg:i.msg||i.err||"Unbekannter Fehler"}),r.invalidateQueries({queryKey:V.jobs}),r.invalidateQueries({queryKey:["skills-list"]})},onError:(i,u)=>{x(null),p({name:u,ok:!1,msg:String(i)})}});return s.jsxs("div",{className:"space-y-6",children:[s.jsx("div",{className:"flex flex-col sm:flex-row justify-between sm:items-center gap-4",children:s.jsxs("div",{children:[s.jsx("h1",{className:"text-2xl font-space font-bold tracking-tight bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text text-transparent",children:"Skills & Jobs"}),s.jsx("p",{className:"text-sm text-muted-foreground",children:"Löst autonome Agent-Skills manuell auf der Box aus. Die Ausführung erfolgt im Hintergrund durch Hermes."})]})}),s.jsx(_,{}),d&&s.jsxs("div",{className:K("p-4 rounded-lg border flex gap-3 items-start",d.ok?"bg-emerald-500/10 border-emerald-500/20 text-emerald-600 dark:text-emerald-400":"bg-red-500/10 border-red-500/20 text-red-600 dark:text-red-400"),children:[d.ok?s.jsx(D,{className:"h-5 w-5 mt-0.5 shrink-0"}):s.jsx(H,{className:"h-5 w-5 mt-0.5 shrink-0"}),s.jsxs("div",{children:[s.jsx("div",{className:"font-semibold text-sm",children:d.name}),s.jsx("div",{className:"text-sm opacity-90",children:d.msg})]})]}),t&&s.jsx("div",{className:"flex h-32 items-center justify-center",children:s.jsx(L,{className:"h-6 w-6 animate-spin text-muted-foreground"})}),n&&s.jsx("div",{className:"text-red-500 p-4 mc-card text-sm",children:"Fehler beim Laden der Skills. Ist das Backend erreichbar?"}),(e==null?void 0:e.skills)&&s.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4",children:[e.skills.map(i=>s.jsxs("div",{className:"mc-card p-5 flex flex-col hover:border-primary/40 transition-colors group",children:[s.jsxs("div",{className:"flex items-start justify-between gap-3 mb-2",children:[s.jsxs("div",{className:"flex items-center gap-2 font-semibold text-base font-space",children:[s.jsx(G,{className:"h-5 w-5 text-primary/80"}),i.name]}),s.jsxs("button",{onClick:()=>k.mutate(i.name),disabled:l===i.name||i.running||k.isPending,className:K("shrink-0 h-8 px-3 rounded-md text-xs font-semibold uppercase tracking-wider flex items-center gap-1.5 transition-all cursor-pointer",l===i.name||i.running?"bg-sky-500/10 border border-sky-500/40 text-sky-400 cursor-not-allowed":"bg-primary/10 text-primary hover:bg-primary hover:text-primary-foreground group-hover:shadow-md group-hover:shadow-primary/20"),children:[l===i.name||i.running?s.jsx(L,{className:"h-3.5 w-3.5 animate-spin"}):s.jsx(X,{className:"h-3.5 w-3.5"}),l===i.name||i.running?"Läuft …":"Starten"]})]}),s.jsx("div",{className:"text-sm text-muted-foreground flex-1",children:i.description})]},i.name)),e.skills.length===0&&s.jsxs("div",{className:"col-span-full p-8 text-center text-muted-foreground mc-card border-dashed border-2",children:["Keine Skills im Ordner ",s.jsx("code",{className:"text-xs text-foreground bg-muted px-1.5 py-0.5 rounded",children:"deploy/skills"})," gefunden."]})]})]})}export{ee as SkillsView};
|
||||
+52
File diff suppressed because one or more lines are too long
+4
-4
File diff suppressed because one or more lines are too long
@@ -0,0 +1,6 @@
|
||||
import{c as r}from"./index-Bb8GyPHo.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 o=r("ArrowRight",[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"m12 5 7 7-7 7",key:"xquz4c"}]]);export{o as A};
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as o}from"./index-BYlMs6Dj.js";/**
|
||||
import{c as o}from"./index-Bb8GyPHo.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -0,0 +1,6 @@
|
||||
import{c}from"./index-Bb8GyPHo.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("CircleCheck",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]]);export{r as C};
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import{c}from"./index-Bb8GyPHo.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 o=c("Clock",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["polyline",{points:"12 6 12 12 16 14",key:"68esgv"}]]);export{o as C};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as e}from"./index-BYlMs6Dj.js";/**
|
||||
import{c as e}from"./index-Bb8GyPHo.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -0,0 +1,6 @@
|
||||
import{c as a}from"./index-Bb8GyPHo.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 e=a("ExternalLink",[["path",{d:"M15 3h6v6",key:"1q9fwt"}],["path",{d:"M10 14 21 3",key:"gplh6r"}],["path",{d:"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6",key:"a6xqqp"}]]);export{e as E};
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import{c as e}from"./index-Bb8GyPHo.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 t=e("FileText",[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z",key:"1rqfz7"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}],["path",{d:"M10 9H8",key:"b1mrlr"}],["path",{d:"M16 13H8",key:"t4e002"}],["path",{d:"M16 17H8",key:"z1uh3a"}]]);export{t as F};
|
||||
-1
File diff suppressed because one or more lines are too long
-360
File diff suppressed because one or more lines are too long
+245
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
||||
import{at as r}from"./index-Bb8GyPHo.js";var o=r();export{o as r};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as a}from"./index-BYlMs6Dj.js";/**
|
||||
import{c as a}from"./index-Bb8GyPHo.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import{c as o}from"./index-Bb8GyPHo.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 a=o("Power",[["path",{d:"M12 2v10",key:"mnfbl"}],["path",{d:"M18.4 6.6a9 9 0 1 1-12.77.04",key:"obofu9"}]]);export{a as P};
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import{c as e}from"./index-Bb8GyPHo.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 t=e("RefreshCw",[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]]);export{t as R};
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
import{c as t}from"./index-BYlMs6Dj.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 e=t("RotateCcw",[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",key:"1357e3"}],["path",{d:"M3 3v5h5",key:"1xhq8a"}]]);export{e as R};
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import{c}from"./index-Bb8GyPHo.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("Search",[["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}],["path",{d:"m21 21-4.3-4.3",key:"1qie3q"}]]);export{r as S};
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import{c as a}from"./index-Bb8GyPHo.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const c=a("Shield",[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z",key:"oel41y"}]]);export{c as S};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as a}from"./index-BYlMs6Dj.js";/**
|
||||
import{c as a}from"./index-Bb8GyPHo.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
Vendored
+2
-2
@@ -7,8 +7,8 @@
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>Mission Control 2.0</title>
|
||||
<script type="module" crossorigin src="/assets/index-BYlMs6Dj.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-8N0vA58P.css">
|
||||
<script type="module" crossorigin src="/assets/index-Bb8GyPHo.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DErfN6d3.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Generated
+120
@@ -12,6 +12,7 @@
|
||||
"@fontsource/jetbrains-mono": "^5.2.8",
|
||||
"@fontsource/space-grotesk": "^5.2.10",
|
||||
"@tanstack/react-query": "^5.101.1",
|
||||
"@tanstack/react-router": "^1.170.32",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"d3-force": "^3.0.0",
|
||||
@@ -2340,6 +2341,19 @@
|
||||
"vite": "^5.2.0 || ^6 || ^7 || ^8"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/history": {
|
||||
"version": "1.162.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.162.1.tgz",
|
||||
"integrity": "sha512-DR9t6lfLVdrjgCwpglrR9DR7Ok8/HlXjcOE+goWXF3zyuLUO/ug7vMbSFxTqrQTtbRghJfyhmIZ0S6LhPIy44w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.19"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.101.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.1.tgz",
|
||||
@@ -2366,6 +2380,76 @@
|
||||
"react": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-router": {
|
||||
"version": "1.170.32",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.170.32.tgz",
|
||||
"integrity": "sha512-SIpxvaTKco100a5ZR3ePmArbhtm3XOx+w1dpGYY9gxHDta4iXSKDdQuhLonwJbIMkVJsU1rwXf0UDHMrF/1snw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/history": "1.162.1",
|
||||
"@tanstack/react-store": "^0.9.3",
|
||||
"@tanstack/router-core": "1.171.27",
|
||||
"isbot": "^5.1.22"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.0.0 || >=19.0.0",
|
||||
"react-dom": ">=18.0.0 || >=19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-store": {
|
||||
"version": "0.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.9.3.tgz",
|
||||
"integrity": "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/store": "0.9.3",
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/router-core": {
|
||||
"version": "1.171.27",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.171.27.tgz",
|
||||
"integrity": "sha512-wDwSLvoLwIaNcnx9UNcN9Mb7Y8QwCYq1U1RQZwyN186gnkIoIYI2SOxy8VqH1vFigbkHkk4FmwMAQlghPgDK2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/history": "1.162.1",
|
||||
"cookie-es": "^3.0.0",
|
||||
"seroval": "^1.6.2",
|
||||
"seroval-plugins": "^1.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/store": {
|
||||
"version": "0.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.9.3.tgz",
|
||||
"integrity": "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom": {
|
||||
"version": "10.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
|
||||
@@ -3354,6 +3438,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie-es": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz",
|
||||
"integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -4401,6 +4491,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/isbot": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/isbot/-/isbot-5.2.2.tgz",
|
||||
"integrity": "sha512-iQcBXcd+Rv/pkubRyGh2utW2j1oPG5hZY6TUhVPpqK4G+o3IbxpJNx04hgksjc/N7GK5pEorUxDeg31cFgEk/w==",
|
||||
"license": "Unlicense",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
@@ -5568,6 +5667,27 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/seroval": {
|
||||
"version": "1.6.4",
|
||||
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.6.4.tgz",
|
||||
"integrity": "sha512-LErWMNS2RRFdu2RMA5u/PA59/IWs0XsikyEXGQ2/36iEWFrdG0ABmg17E17cikrv76891kOAMq3TkTFXpwAHXw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/seroval-plugins": {
|
||||
"version": "1.6.4",
|
||||
"resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.6.4.tgz",
|
||||
"integrity": "sha512-R0f1U9hmn38+dFMz6b6ab8lwucmw4AtiY7St+JPWudy1dm+Bs3g884nyrsH9Cy6rKpZKLYayXuMda9GZ/fl8JQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"seroval": "^1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"@fontsource/jetbrains-mono": "^5.2.8",
|
||||
"@fontsource/space-grotesk": "^5.2.10",
|
||||
"@tanstack/react-query": "^5.101.1",
|
||||
"@tanstack/react-router": "^1.170.32",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"d3-force": "^3.0.0",
|
||||
|
||||
+88
-74
@@ -1,29 +1,22 @@
|
||||
import { useEffect, useState, useCallback, lazy, Suspense } from "react"
|
||||
import { useEffect, useState, useCallback, Suspense, lazy } from "react"
|
||||
import { Link, Outlet, useNavigate, useRouterState } from "@tanstack/react-router"
|
||||
import { Command as CommandIcon, ChevronLeft, ChevronRight, Loader2, Menu, X } from "lucide-react"
|
||||
import { NAV, type ViewId } from "@/nav"
|
||||
import { CommandPalette } from "@/components/CommandPalette"
|
||||
import { NAV, navFuerPfad } from "@/nav"
|
||||
import { ExpertToggle } from "@/components/ExpertToggle"
|
||||
import { SystemDrawer } from "@/components/SystemDrawer"
|
||||
|
||||
import { useHealth, useSystemStatus } from "@/lib/queries"
|
||||
import type { Health, SystemStatus } from "@/lib/api"
|
||||
import { useMetricsFeeder } from "@/lib/metricsStore"
|
||||
import { useEventStream } from "@/lib/events"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CockpitView } from "@/views/cockpit/CockpitView"
|
||||
|
||||
// Code-Splitting: nur das Cockpit (Startseite) lädt sofort, jede weitere View
|
||||
// kommt als eigenes Chunk erst beim ersten Aufruf — kleineres Start-Bundle.
|
||||
const ModelsView = lazy(() => import("@/views/ModelsView").then((m) => ({ default: m.ModelsView })))
|
||||
const ConnectView = lazy(() => import("@/views/ConnectView").then((m) => ({ default: m.ConnectView })))
|
||||
const AgentView = lazy(() => import("@/views/AgentView").then((m) => ({ default: m.AgentView })))
|
||||
const KonsoleView = lazy(() => import("@/views/KonsoleView").then((m) => ({ default: m.KonsoleView })))
|
||||
const GuideView = lazy(() => import("@/views/GuideView").then((m) => ({ default: m.GuideView })))
|
||||
const AuftragsbuchView = lazy(() => import("@/views/AuftragsbuchView").then((m) => ({ default: m.AuftragsbuchView })))
|
||||
const IdeenView = lazy(() => import("@/views/IdeenView").then((m) => ({ default: m.IdeenView })))
|
||||
const SkillsView = lazy(() => import("@/views/SkillsView").then((m) => ({ default: m.SkillsView })))
|
||||
const ChronikView = lazy(() => import("@/views/ChronikView").then((m) => ({ default: m.ChronikView })))
|
||||
|
||||
const WissenView = lazy(() => import("@/views/WissenView").then((m) => ({ default: m.WissenView })))
|
||||
// Beide sind beim Start UNSICHTBAR und gehoeren deshalb nicht ins Start-Buendel (P2):
|
||||
// die Schublade zieht Wartung, Logs, Einstellungen und den Update-Dialog mit, die Palette
|
||||
// bringt cmdk mit. Zusammen waren das rund 30 kB gzip, die niemand beim ersten Bild braucht.
|
||||
// Der Router kostete P2 ungefaehr dasselbe — so bleibt das Buendel-Budget der Ampel scharf,
|
||||
// statt es einfach hochzusetzen.
|
||||
const SystemDrawer = lazy(() => import("@/components/SystemDrawer").then((m) => ({ default: m.SystemDrawer })))
|
||||
const CommandPalette = lazy(() => import("@/components/CommandPalette").then((m) => ({ default: m.CommandPalette })))
|
||||
|
||||
// Dezenter Lade-Zustand während eine View nachgeladen wird (lokales Netz: kaum sichtbar).
|
||||
function ViewLoading() {
|
||||
@@ -37,22 +30,50 @@ function ViewLoading() {
|
||||
// Tastatur-Hinweis passend zum Betriebssystem (Palette hört auf Ctrl UND Cmd).
|
||||
const IS_MAC = /Mac|iPhone|iPad/.test(navigator.platform)
|
||||
|
||||
export default function App() {
|
||||
/**
|
||||
* Die Shell: Schiene, Kopfzeile, System-Schublade — alles, was über JEDER Ansicht liegt.
|
||||
*
|
||||
* v3-Umbau P2 (28.08.2026): Hier stand vorher der Ansichts-Umschalter
|
||||
* (`{view === "ideen" && <IdeenView />}`), gespeist aus einem `useState`, das am URL-Hash
|
||||
* hing. Das funktionierte für Reload und Lesezeichen — aber nur auf View-Ebene. Jetzt
|
||||
* rendert der Router in den `<Outlet/>`, und der Zustand DARUNTER (offene Idee, Reiter,
|
||||
* Schublade) liegt in echten Suchparametern. Siehe app/router.tsx.
|
||||
*/
|
||||
export function Shell() {
|
||||
useMetricsFeeder() // sammelt Live-Verlauf global, unabhängig vom aktiven Tab
|
||||
useEventStream() // SSE-Invalidation-Bus (P3a): sofortige Updates, Poller nur Netz
|
||||
// View ist im URL-Hash (#models) verankert → Reload/Teilen/Cmd-Klick funktionieren.
|
||||
const [view, setView] = useState<ViewId>(() => {
|
||||
const h = window.location.hash.slice(1) as ViewId
|
||||
return NAV.some((n) => n.id === h) ? h : "dashboard"
|
||||
|
||||
const navigate = useNavigate()
|
||||
const pfad = useRouterState({ select: (s) => s.location.pathname })
|
||||
// Die Schublade liegt über allem und gehört deshalb in die Wurzel-Suche (?system=).
|
||||
// Vorher war sie lokaler State und wurde per CustomEvent von außen aufgestoßen —
|
||||
// nicht teilbar, nicht reload-fest.
|
||||
const drawerTab = useRouterState({
|
||||
select: (s) => (s.location.search as { system?: "wartung" | "logs" }).system,
|
||||
})
|
||||
const navigate = useCallback((v: ViewId) => {
|
||||
if (window.location.hash.slice(1) === v) setView(v)
|
||||
else window.location.hash = v
|
||||
}, [])
|
||||
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => localStorage.getItem("mc_sidebar_collapsed") === "true")
|
||||
const [mobileNavOpen, setMobileNavOpen] = useState(false)
|
||||
const [drawerOpen, setDrawerOpen] = useState(false)
|
||||
const [drawerTab, setDrawerTab] = useState<"maintenance" | "logs">("maintenance")
|
||||
// Die Palette wird beim ERSTEN Ruf geladen und bleibt dann gemountet — sie haelt ihren
|
||||
// eigenen Offen-Zustand und soll nicht bei jedem Strg+K neu nachgeladen werden.
|
||||
const [paletteBereit, setPaletteBereit] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const wecken = (e: KeyboardEvent) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") setPaletteBereit(true)
|
||||
}
|
||||
const rufen = () => setPaletteBereit(true)
|
||||
document.addEventListener("keydown", wecken)
|
||||
window.addEventListener("mc-palette-open", rufen)
|
||||
return () => {
|
||||
document.removeEventListener("keydown", wecken)
|
||||
window.removeEventListener("mc-palette-open", rufen)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const schubladeSetzen = useCallback((tab: "wartung" | "logs" | undefined) => {
|
||||
navigate({ to: ".", search: (alt: Record<string, unknown>) => ({ ...alt, system: tab }), replace: true })
|
||||
}, [navigate])
|
||||
|
||||
// isError mitnehmen: React Query behält bei Fehlern die letzten Daten — ohne den
|
||||
// Fehler-Blick zeigte die Sidebar bei totem Backend ewig den letzten grünen Stand.
|
||||
@@ -63,38 +84,33 @@ export default function App() {
|
||||
document.documentElement.classList.add("dark")
|
||||
}, [])
|
||||
|
||||
// Zwei Alt-Kanäle aus der Zeit vor dem Router. Sie bleiben in P2 bestehen, damit die
|
||||
// Aufrufer (Cockpit-Kacheln, Agent-Ansicht, Auftragsbuch) unverändert weiterlaufen —
|
||||
// gefüttert wird jetzt aber der Router, nicht mehr ein lokales useState.
|
||||
// In P3 lösen Store und Palette diese Kanäle ganz ab (Befund B-08).
|
||||
useEffect(() => {
|
||||
const handleOpen = (e: Event) => {
|
||||
const customEvent = e as CustomEvent
|
||||
setDrawerTab(customEvent.detail?.tab || "maintenance")
|
||||
setDrawerOpen(true)
|
||||
const tab = (e as CustomEvent).detail?.tab
|
||||
schubladeSetzen(tab === "logs" ? "logs" : "wartung")
|
||||
}
|
||||
window.addEventListener("open-system-drawer", handleOpen)
|
||||
return () => window.removeEventListener("open-system-drawer", handleOpen)
|
||||
}, [])
|
||||
}, [schubladeSetzen])
|
||||
|
||||
useEffect(() => {
|
||||
const handleNav = (e: Event) => {
|
||||
const v = (e as CustomEvent).detail?.view as ViewId | undefined
|
||||
if (v) navigate(v)
|
||||
const v = (e as CustomEvent).detail?.view as string | undefined
|
||||
const ziel = NAV.find((n) => n.id === v)
|
||||
if (ziel) navigate({ to: ziel.pfad })
|
||||
}
|
||||
window.addEventListener("mc-navigate", handleNav)
|
||||
return () => window.removeEventListener("mc-navigate", handleNav)
|
||||
}, [navigate])
|
||||
|
||||
useEffect(() => {
|
||||
const onHash = () => {
|
||||
const h = window.location.hash.slice(1) as ViewId
|
||||
if (NAV.some((n) => n.id === h)) setView(h)
|
||||
}
|
||||
window.addEventListener("hashchange", onHash)
|
||||
return () => window.removeEventListener("hashchange", onHash)
|
||||
}, [])
|
||||
|
||||
// Mobile-Navigation schließt nach jedem View-Wechsel (ein Tipp = ein Ziel).
|
||||
useEffect(() => { setMobileNavOpen(false) }, [view])
|
||||
useEffect(() => { setMobileNavOpen(false) }, [pfad])
|
||||
|
||||
const active = NAV.find((n) => n.id === view)!
|
||||
const active = navFuerPfad(pfad)
|
||||
|
||||
return (
|
||||
<div className="flex h-full relative">
|
||||
@@ -112,8 +128,17 @@ export default function App() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<CommandPalette onNavigate={navigate} />
|
||||
<SystemDrawer open={drawerOpen} onClose={() => setDrawerOpen(false)} defaultTab={drawerTab} />
|
||||
{/* Erst laden, wenn wirklich gebraucht — ohne Platzhalter, beide sind Overlays. */}
|
||||
<Suspense fallback={null}>
|
||||
{paletteBereit && <CommandPalette />}
|
||||
{drawerTab && (
|
||||
<SystemDrawer
|
||||
open
|
||||
onClose={() => schubladeSetzen(undefined)}
|
||||
defaultTab={drawerTab === "logs" ? "logs" : "maintenance"}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
|
||||
{/* Sidebar (Desktop, ab md) */}
|
||||
<aside className={cn(
|
||||
@@ -129,7 +154,7 @@ export default function App() {
|
||||
return next
|
||||
})
|
||||
}}
|
||||
view={view}
|
||||
pfad={pfad}
|
||||
health={health}
|
||||
backendDown={backendDown}
|
||||
sysStatus={sysStatus}
|
||||
@@ -152,7 +177,7 @@ export default function App() {
|
||||
<SidebarContent
|
||||
collapsed={false}
|
||||
onClose={() => setMobileNavOpen(false)}
|
||||
view={view}
|
||||
pfad={pfad}
|
||||
health={health}
|
||||
backendDown={backendDown}
|
||||
sysStatus={sysStatus}
|
||||
@@ -171,7 +196,7 @@ export default function App() {
|
||||
>
|
||||
<Menu className="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
<div className="truncate text-xs font-medium text-muted-foreground tracking-wide uppercase font-sans">{active.hint}</div>
|
||||
<div className="truncate text-xs font-medium text-muted-foreground tracking-wide uppercase font-sans">{active?.hint}</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<ExpertToggle />
|
||||
@@ -186,10 +211,7 @@ export default function App() {
|
||||
</a>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
const ev = new KeyboardEvent("keydown", { key: "k", metaKey: true })
|
||||
document.dispatchEvent(ev)
|
||||
}}
|
||||
onClick={() => window.dispatchEvent(new CustomEvent("mc-palette-open"))}
|
||||
className="flex items-center gap-2 rounded-md border border-border/40 bg-background/40 px-2.5 py-1.5 text-xs text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-all cursor-pointer"
|
||||
>
|
||||
<CommandIcon className="h-3.5 w-3.5" />
|
||||
@@ -201,18 +223,7 @@ export default function App() {
|
||||
|
||||
<main className="flex-1 overflow-y-auto p-4 md:p-6 scrollbar-thin">
|
||||
<Suspense fallback={<ViewLoading />}>
|
||||
{view === "dashboard" && <CockpitView onNavigate={(v) => navigate(v as ViewId)} />}
|
||||
{view === "ideen" && <IdeenView />}
|
||||
{view === "auftraege" && <AuftragsbuchView />}
|
||||
{view === "skills" && <SkillsView />}
|
||||
{view === "models" && <ModelsView />}
|
||||
{view === "connect" && <ConnectView />}
|
||||
{view === "wissen" && <WissenView />}
|
||||
{view === "chronik" && <ChronikView />}
|
||||
|
||||
{view === "agent" && <AgentView />}
|
||||
{view === "konsole" && <KonsoleView />}
|
||||
{view === "guide" && <GuideView />}
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</main>
|
||||
</div>
|
||||
@@ -223,16 +234,18 @@ export default function App() {
|
||||
// Sidebar-Inhalt (Logo, Navigation, Status-Fuß) — geteilt zwischen Desktop-Leiste
|
||||
// und Mobile-Off-Canvas, damit beide immer identisch bleiben.
|
||||
function SidebarContent({
|
||||
collapsed, onToggleCollapse, onClose, view, health, backendDown, sysStatus,
|
||||
collapsed, onToggleCollapse, onClose, pfad, health, backendDown, sysStatus,
|
||||
}: {
|
||||
collapsed: boolean
|
||||
onToggleCollapse?: () => void
|
||||
onClose?: () => void
|
||||
view: ViewId
|
||||
pfad: string
|
||||
health: Health | undefined
|
||||
backendDown: boolean
|
||||
sysStatus: SystemStatus | undefined
|
||||
}) {
|
||||
const aktiv = navFuerPfad(pfad)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cn("flex items-center py-4 border-b border-border/40 shrink-0", collapsed ? "flex-col gap-3 px-2" : "justify-between px-5")}>
|
||||
@@ -272,6 +285,7 @@ function SidebarContent({
|
||||
return NAV.map((item) => {
|
||||
const showGroupHeader = item.group !== lastGroup
|
||||
lastGroup = item.group
|
||||
const istAktiv = aktiv?.id === item.id
|
||||
return (
|
||||
<div key={item.id} className="space-y-1">
|
||||
{showGroupHeader && (
|
||||
@@ -283,14 +297,14 @@ function SidebarContent({
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
<a
|
||||
href={`#${item.id}`}
|
||||
<Link
|
||||
to={item.pfad}
|
||||
onClick={onClose}
|
||||
aria-current={view === item.id ? "page" : undefined}
|
||||
aria-current={istAktiv ? "page" : undefined}
|
||||
className={cn(
|
||||
"flex w-full items-center rounded-md text-sm transition-all cursor-pointer",
|
||||
collapsed ? "justify-center p-2.5" : "gap-3 px-3 py-2",
|
||||
view === item.id
|
||||
istAktiv
|
||||
? (collapsed ? "nav-active-collapsed" : "nav-active")
|
||||
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
|
||||
)}
|
||||
@@ -299,7 +313,7 @@ function SidebarContent({
|
||||
>
|
||||
<item.icon className="h-4.5 w-4.5 shrink-0" aria-hidden="true" />
|
||||
{!collapsed && <span className="truncate">{item.label}</span>}
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import { lazy } from "react"
|
||||
import {
|
||||
createRootRoute,
|
||||
createRoute,
|
||||
createRouter,
|
||||
redirect,
|
||||
useNavigate,
|
||||
} from "@tanstack/react-router"
|
||||
import { Shell } from "@/App"
|
||||
import { NAV } from "@/nav"
|
||||
import { RouteFehler } from "@/components/RouteFehler"
|
||||
import { CockpitView } from "@/views/cockpit/CockpitView"
|
||||
|
||||
// Routenbaum (v3-Umbau P2).
|
||||
//
|
||||
// Bewusst code-based statt dateibasiert: Es sind zwölf Routen, und sie stehen dadurch
|
||||
// alle an EINER Stelle — inklusive dem, was in den Suchparametern erlaubt ist. Eine
|
||||
// Datei-Konvention würde diese Übersicht auf zwölf Dateien verteilen, ohne dass jemand
|
||||
// etwas davon hätte.
|
||||
//
|
||||
// Code-Splitting bleibt exakt wie bisher: die Views hängen an React.lazy, das Cockpit als
|
||||
// Startseite lädt eifrig. Kein Chunk verschiebt sich durch den Router.
|
||||
const ModelsView = lazy(() => import("@/views/ModelsView").then((m) => ({ default: m.ModelsView })))
|
||||
const ConnectView = lazy(() => import("@/views/ConnectView").then((m) => ({ default: m.ConnectView })))
|
||||
const AgentView = lazy(() => import("@/views/AgentView").then((m) => ({ default: m.AgentView })))
|
||||
const KonsoleView = lazy(() => import("@/views/KonsoleView").then((m) => ({ default: m.KonsoleView })))
|
||||
const GuideView = lazy(() => import("@/views/GuideView").then((m) => ({ default: m.GuideView })))
|
||||
const AuftragsbuchView = lazy(() => import("@/views/AuftragsbuchView").then((m) => ({ default: m.AuftragsbuchView })))
|
||||
const IdeenView = lazy(() => import("@/views/IdeenView").then((m) => ({ default: m.IdeenView })))
|
||||
const SkillsView = lazy(() => import("@/views/SkillsView").then((m) => ({ default: m.SkillsView })))
|
||||
const ChronikView = lazy(() => import("@/views/ChronikView").then((m) => ({ default: m.ChronikView })))
|
||||
const WissenView = lazy(() => import("@/views/WissenView").then((m) => ({ default: m.WissenView })))
|
||||
|
||||
// ── Suchparameter ───────────────────────────────────────────────────────────
|
||||
// Regel für das, was hier steht: Alles, was ein Kollege per Link teilen können soll und
|
||||
// was einen Reload überleben muss. Nichts Flüchtiges (Hover, Entwürfe im Eingabefeld).
|
||||
|
||||
/** Wurzel: die System-Schublade. Gehört hierher, weil sie über JEDER Ansicht liegt. */
|
||||
export interface WurzelSuche {
|
||||
system?: "wartung" | "logs"
|
||||
}
|
||||
|
||||
const wurzelSuche = (roh: Record<string, unknown>): WurzelSuche => {
|
||||
const s = roh.system
|
||||
return s === "wartung" || s === "logs" ? { system: s } : {}
|
||||
}
|
||||
|
||||
export const rootRoute = createRootRoute({
|
||||
component: Shell,
|
||||
validateSearch: wurzelSuche,
|
||||
notFoundComponent: () => (
|
||||
<RouteFehler
|
||||
titel="Diese Seite gibt es nicht"
|
||||
text="Der Link zeigt auf eine Ansicht, die Mission Control nicht kennt. Vielleicht stammt er aus einer älteren Fassung."
|
||||
/>
|
||||
),
|
||||
})
|
||||
|
||||
// Wurzelpfad → Cockpit. `redirect` statt einer Kopie der Startseite: sonst gäbe es
|
||||
// zwei URLs für denselben Inhalt und die aktive Markierung in der Sidebar flackert.
|
||||
const indexRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/",
|
||||
beforeLoad: () => {
|
||||
throw redirect({ to: "/cockpit" })
|
||||
},
|
||||
})
|
||||
|
||||
/** Eine Route je Ansicht — kurze Schreibweise, weil sich nur Pfad und Bauteil ändern.
|
||||
* Generisch über den Pfad, damit TanStack Router den Literal-Typ behält: mit
|
||||
* `path: string` verlöre `navigate({ to: "/cockpit" })` seine Typprüfung. */
|
||||
function ansicht<P extends string>(path: P, component: () => React.ReactNode) {
|
||||
return createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path,
|
||||
component,
|
||||
// Fehlergrenze PRO Route (Befund B-13): Vorher hing eine einzige Grenze in main.tsx
|
||||
// um die ganze App — ein Renderfehler in den Modellen nahm Cockpit, Konsole und
|
||||
// Chronik mit. Jetzt bleiben Schiene, Kopfzeile und Statusanzeige stehen.
|
||||
errorComponent: ({ error, reset }) => (
|
||||
<RouteFehler
|
||||
titel="Diese Ansicht ist abgestürzt"
|
||||
text="Der Rest von Mission Control läuft weiter. Erneut versuchen — oder in eine andere Ansicht wechseln."
|
||||
fehler={error}
|
||||
onErneut={reset}
|
||||
/>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
// Das Cockpit verlinkt in andere Bereiche und bekam den Sprung bisher als Prop von der
|
||||
// App gereicht. Jetzt kommt er aus dem Router — die Ansicht selbst bleibt unverändert.
|
||||
function CockpitSeite() {
|
||||
const navigate = useNavigate()
|
||||
return <CockpitView onNavigate={(v) => {
|
||||
const ziel = NAV.find((n) => n.id === v)
|
||||
if (ziel) navigate({ to: ziel.pfad })
|
||||
}} />
|
||||
}
|
||||
|
||||
const cockpitRoute = ansicht("/cockpit", () => <CockpitSeite />)
|
||||
const auftraegeRoute = ansicht("/auftraege", () => <AuftragsbuchView />)
|
||||
const skillsRoute = ansicht("/skills", () => <SkillsView />)
|
||||
const chronikRoute = ansicht("/chronik", () => <ChronikView />)
|
||||
const verbindenRoute = ansicht("/verbinden", () => <ConnectView />)
|
||||
const konsoleRoute = ansicht("/konsole", () => <KonsoleView />)
|
||||
const anleitungRoute = ansicht("/anleitung", () => <GuideView />)
|
||||
const agentRoute = ansicht("/agent", () => <AgentView />)
|
||||
|
||||
/** Ideen: `?offen=<id>` hält die aufgeklappte Karte fest (Befund B-05).
|
||||
* Vorher lag das in `useState` — wer während eines laufenden Workers neu lud,
|
||||
* verlor seinen Platz in der Liste. */
|
||||
export interface IdeenSuche {
|
||||
offen?: string
|
||||
}
|
||||
const ideenRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/ideen",
|
||||
validateSearch: (roh: Record<string, unknown>): IdeenSuche =>
|
||||
typeof roh.offen === "string" && roh.offen ? { offen: roh.offen } : {},
|
||||
component: () => <IdeenView />,
|
||||
errorComponent: ({ error, reset }) => (
|
||||
<RouteFehler titel="Die Ideen-Ansicht ist abgestürzt" text="Der Rest läuft weiter." fehler={error} onErneut={reset} />
|
||||
),
|
||||
})
|
||||
|
||||
/** Wissen: `?datei=<pfad>` macht jede Vault-Notiz verlinkbar. */
|
||||
export interface WissenSuche {
|
||||
datei?: string
|
||||
}
|
||||
const wissenRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/wissen",
|
||||
validateSearch: (roh: Record<string, unknown>): WissenSuche =>
|
||||
typeof roh.datei === "string" && roh.datei ? { datei: roh.datei } : {},
|
||||
component: () => <WissenView />,
|
||||
errorComponent: ({ error, reset }) => (
|
||||
<RouteFehler titel="Die Wissens-Ansicht ist abgestürzt" text="Der Rest läuft weiter." fehler={error} onErneut={reset} />
|
||||
),
|
||||
})
|
||||
|
||||
/** Modelle: `?reiter=` — der Reiter war lokaler State und damit nicht teilbar. */
|
||||
export type ModellReiter = "werkbank" | "routing" | "discover"
|
||||
export interface ModelleSuche {
|
||||
reiter?: ModellReiter
|
||||
}
|
||||
const modelleRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/modelle",
|
||||
validateSearch: (roh: Record<string, unknown>): ModelleSuche => {
|
||||
const r = roh.reiter
|
||||
return r === "werkbank" || r === "routing" || r === "discover" ? { reiter: r } : {}
|
||||
},
|
||||
component: () => <ModelsView />,
|
||||
errorComponent: ({ error, reset }) => (
|
||||
<RouteFehler titel="Der Modell-Manager ist abgestürzt" text="Der Rest läuft weiter." fehler={error} onErneut={reset} />
|
||||
),
|
||||
})
|
||||
|
||||
export const routeTree = rootRoute.addChildren([
|
||||
indexRoute,
|
||||
cockpitRoute,
|
||||
ideenRoute,
|
||||
auftraegeRoute,
|
||||
skillsRoute,
|
||||
chronikRoute,
|
||||
wissenRoute,
|
||||
verbindenRoute,
|
||||
konsoleRoute,
|
||||
anleitungRoute,
|
||||
modelleRoute,
|
||||
agentRoute,
|
||||
])
|
||||
|
||||
export const router = createRouter({
|
||||
routeTree,
|
||||
// Ein LAN-Gerät: der Verlauf muss nicht scrollen-restaurieren, aber die Position
|
||||
// oben zu haben ist beim Ansichtswechsel richtig.
|
||||
scrollRestoration: true,
|
||||
defaultPreload: false,
|
||||
})
|
||||
|
||||
declare module "@tanstack/react-router" {
|
||||
interface Register {
|
||||
router: typeof router
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { Command } from "cmdk"
|
||||
import { NAV, type ViewId } from "@/nav"
|
||||
import { useNavigate } from "@tanstack/react-router"
|
||||
import { NAV } from "@/nav"
|
||||
|
||||
export function CommandPalette({ onNavigate }: { onNavigate: (v: ViewId) => void }) {
|
||||
export function CommandPalette() {
|
||||
const [open, setOpen] = useState(false)
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
@@ -16,6 +18,16 @@ export function CommandPalette({ onNavigate }: { onNavigate: (v: ViewId) => void
|
||||
return () => document.removeEventListener("keydown", onKey)
|
||||
}, [])
|
||||
|
||||
// Der „Suchen"-Knopf in der Kopfzeile (v3-Umbau P2, Befund B-09).
|
||||
// Er baute vorher ein GEFÄLSCHTES Tastatur-Ereignis (`new KeyboardEvent("keydown",
|
||||
// { metaKey: true })`) und feuerte es aufs Dokument, damit der Zuhörer oben anspringt.
|
||||
// Jetzt ein eigener, ehrlicher Kanal. In P3 löst der Store auch den ab.
|
||||
useEffect(() => {
|
||||
const onOpen = () => setOpen(true)
|
||||
window.addEventListener("mc-palette-open", onOpen)
|
||||
return () => window.removeEventListener("mc-palette-open", onOpen)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Command.Dialog
|
||||
open={open}
|
||||
@@ -43,7 +55,7 @@ export function CommandPalette({ onNavigate }: { onNavigate: (v: ViewId) => void
|
||||
key={item.id}
|
||||
value={`${item.label} ${item.hint}`}
|
||||
onSelect={() => {
|
||||
onNavigate(item.id)
|
||||
navigate({ to: item.pfad })
|
||||
setOpen(false)
|
||||
}}
|
||||
className="flex cursor-pointer items-center gap-3 rounded-md px-3 py-2 text-sm text-foreground aria-selected:bg-accent aria-selected:text-accent-foreground"
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { AlertTriangle, RotateCcw } from "lucide-react"
|
||||
|
||||
// Fehlerfläche EINER Ansicht (v3-Umbau P2, Befund B-13).
|
||||
//
|
||||
// Vorher hing genau eine Fehlergrenze in main.tsx um die gesamte App: Ein Renderfehler
|
||||
// im Modell-Manager nahm Cockpit, Konsole und Chronik gleich mit — die Seite war weiß.
|
||||
// Jetzt fängt jede Route für sich; Schiene, Kopfzeile und Statusanzeige bleiben stehen,
|
||||
// und der Nutzer kann einfach woanders hin.
|
||||
//
|
||||
// Die Meldung sagt, was passiert ist und was man tun kann. Der technische Text steht
|
||||
// aufklappbar darunter — nicht versteckt (man braucht ihn für einen Fehlerbericht),
|
||||
// aber auch nicht als Erstes im Gesicht.
|
||||
export function RouteFehler({ titel, text, fehler, onErneut }: {
|
||||
titel: string
|
||||
text: string
|
||||
fehler?: unknown
|
||||
onErneut?: () => void
|
||||
}) {
|
||||
const detail = fehler instanceof Error ? (fehler.stack || fehler.message) : fehler ? String(fehler) : null
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[50vh] items-center justify-center p-6">
|
||||
<div className="mc-card max-w-lg space-y-4 p-6">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<AlertTriangle className="h-5 w-5 shrink-0 text-amber-400" aria-hidden="true" />
|
||||
<h1 className="font-space text-lg font-bold tracking-tight">{titel}</h1>
|
||||
</div>
|
||||
|
||||
<p className="text-sm leading-relaxed text-muted-foreground">{text}</p>
|
||||
|
||||
<div className="flex flex-wrap gap-2 pt-1">
|
||||
{onErneut && (
|
||||
<button
|
||||
onClick={onErneut}
|
||||
className="flex h-9 cursor-pointer items-center gap-1.5 rounded-lg bg-primary px-4 text-xs font-semibold text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
>
|
||||
<RotateCcw className="h-3.5 w-3.5" aria-hidden="true" /> Erneut versuchen
|
||||
</button>
|
||||
)}
|
||||
<a
|
||||
href="/cockpit"
|
||||
className="flex h-9 items-center rounded-lg border border-border/60 bg-background/20 px-4 text-xs font-semibold text-muted-foreground transition-colors hover:bg-accent"
|
||||
>
|
||||
Zum Cockpit
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{detail && (
|
||||
<details className="pt-1">
|
||||
<summary className="cursor-pointer text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70 hover:text-foreground">
|
||||
Technische Einzelheiten
|
||||
</summary>
|
||||
<pre className="scrollbar-thin mt-2 max-h-52 overflow-auto rounded-lg border border-border/40 bg-background/40 p-3 font-mono text-[10px] leading-relaxed text-muted-foreground">
|
||||
{detail}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -311,9 +311,10 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-lg border border-border/40 text-muted-foreground hover:bg-accent transition-colors"
|
||||
aria-label="System-Schublade schließen"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-lg border border-border/40 text-muted-foreground hover:bg-accent transition-colors cursor-pointer"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<X className="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest"
|
||||
import { hashUmleiten } from "./hashUmleitung"
|
||||
import { NAV, navFuerPfad } from "@/nav"
|
||||
|
||||
function adresseSetzen(url: string) {
|
||||
window.history.replaceState(null, "", url)
|
||||
}
|
||||
|
||||
// Diese Brücke ist die einzige Verbindung zu Lesezeichen aus der Hash-Zeit. Sie sieht
|
||||
// nach totem Code aus und wäre damit ein naheliegender Kandidat zum Wegräumen — die
|
||||
// Tests machen sichtbar, was dabei kaputtginge.
|
||||
describe("hashUmleiten()", () => {
|
||||
beforeEach(() => adresseSetzen("/"))
|
||||
|
||||
it("biegt jedes alte Hash-Lesezeichen auf seinen neuen Pfad", () => {
|
||||
for (const eintrag of NAV) {
|
||||
adresseSetzen("/#" + eintrag.id)
|
||||
hashUmleiten()
|
||||
expect(window.location.pathname, `#${eintrag.id}`).toBe(eintrag.pfad)
|
||||
}
|
||||
})
|
||||
|
||||
it("verträgt die Schreibweise mit Schrägstrich (#/models)", () => {
|
||||
adresseSetzen("/#/models")
|
||||
hashUmleiten()
|
||||
expect(window.location.pathname).toBe("/modelle")
|
||||
})
|
||||
|
||||
it("behält vorhandene Suchparameter", () => {
|
||||
adresseSetzen("/?system=logs#models")
|
||||
hashUmleiten()
|
||||
expect(window.location.pathname).toBe("/modelle")
|
||||
expect(window.location.search).toBe("?system=logs")
|
||||
})
|
||||
|
||||
it("fasst einen unbekannten Hash nicht an", () => {
|
||||
adresseSetzen("/cockpit#irgendwas")
|
||||
hashUmleiten()
|
||||
expect(window.location.pathname).toBe("/cockpit")
|
||||
})
|
||||
|
||||
it("tut ohne Hash gar nichts", () => {
|
||||
adresseSetzen("/modelle?reiter=routing")
|
||||
hashUmleiten()
|
||||
expect(window.location.pathname + window.location.search).toBe("/modelle?reiter=routing")
|
||||
})
|
||||
})
|
||||
|
||||
describe("navFuerPfad()", () => {
|
||||
it("findet den Eintrag zum exakten Pfad", () => {
|
||||
expect(navFuerPfad("/modelle")?.id).toBe("models")
|
||||
expect(navFuerPfad("/cockpit")?.id).toBe("dashboard")
|
||||
})
|
||||
|
||||
// Damit /ideen/<id> die Sidebar weiterhin bei „Ideen" markiert und nicht ins Leere fällt.
|
||||
it("ordnet Unterpfade dem übergeordneten Eintrag zu", () => {
|
||||
expect(navFuerPfad("/ideen/id_9f3c")?.id).toBe("ideen")
|
||||
})
|
||||
|
||||
it("liefert nichts für einen unbekannten Pfad", () => {
|
||||
expect(navFuerPfad("/gibtsnicht")).toBeUndefined()
|
||||
})
|
||||
|
||||
// Der Grund für die Längen-Sortierung: ein kurzer Pfad darf einen längeren nicht
|
||||
// überstimmen, sonst gewinnt der erstbeste Präfix-Treffer.
|
||||
it("nimmt bei mehreren Treffern den längsten Pfad", () => {
|
||||
const treffer = navFuerPfad("/modelle")
|
||||
expect(treffer?.pfad).toBe("/modelle")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
// Alte Hash-Lesezeichen auf die neuen Pfade umbiegen (v3-Umbau P2).
|
||||
//
|
||||
// Bis zum 28.08.2026 lebte die Ansicht im URL-Hash (`#models`). Das war teilbar und
|
||||
// reload-fest — aber nur auf View-Ebene; für den Zustand DARUNTER (offene Idee, Reiter,
|
||||
// Drawer) war kein Platz. Mit dem Router zieht das in echte Pfade um.
|
||||
//
|
||||
// Wer ein altes Lesezeichen hat, soll nichts merken: Diese Funktion läuft EINMAL vor dem
|
||||
// Router-Mount und ersetzt `#models` durch `/modelle` — per `replaceState`, damit die
|
||||
// Zurück-Taste nicht in einer Umleitungs-Schleife landet.
|
||||
//
|
||||
// Die Tabelle darf mit der Zeit schrumpfen, aber nicht kommentarlos: Solange jemand ein
|
||||
// Lesezeichen von früher haben könnte, ist sie die einzige Brücke dorthin.
|
||||
|
||||
import { NAV, type ViewId } from "@/nav"
|
||||
|
||||
const ALT_ZU_PFAD: Record<ViewId, string> = Object.fromEntries(
|
||||
NAV.map((n) => [n.id, n.pfad]),
|
||||
) as Record<ViewId, string>
|
||||
|
||||
export function hashUmleiten(): void {
|
||||
const hash = window.location.hash.replace(/^#\/?/, "")
|
||||
if (!hash) return
|
||||
const ziel = ALT_ZU_PFAD[hash as ViewId]
|
||||
if (!ziel) return
|
||||
window.history.replaceState(null, "", ziel + window.location.search)
|
||||
}
|
||||
+13
-4
@@ -1,13 +1,19 @@
|
||||
import React from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||
import App from "./App"
|
||||
import { RouterProvider } from "@tanstack/react-router"
|
||||
import { router } from "./app/router"
|
||||
import { AppErrorBoundary } from "./components/AppErrorBoundary"
|
||||
import { hashUmleiten } from "./lib/hashUmleitung"
|
||||
import { veralteteVersionAbfangen } from "./lib/veralteteVersion"
|
||||
import "./index.css"
|
||||
|
||||
// Muss VOR dem ersten Rendern hängen: Nach einem Deploy sucht ein offener Tab
|
||||
// Nachlade-Chunks des alten Builds, die es nicht mehr gibt (siehe lib/veralteteVersion.ts).
|
||||
// Beides MUSS vor dem ersten Rendern laufen:
|
||||
// · hashUmleiten — alte Lesezeichen (#models) landen auf dem neuen Pfad (/modelle),
|
||||
// bevor der Router die Adresse zum ersten Mal liest (P2).
|
||||
// · veralteteVersionAbfangen — nach einem Deploy sucht ein offener Tab Nachlade-Chunks
|
||||
// des alten Builds, die es nicht mehr gibt (P1).
|
||||
hashUmleiten()
|
||||
veralteteVersionAbfangen()
|
||||
|
||||
// Zentraler Daten-Layer: Caching, Dedup, Retry, Polling pro Query-Hook.
|
||||
@@ -21,11 +27,14 @@ const queryClient = new QueryClient({
|
||||
},
|
||||
})
|
||||
|
||||
// AppErrorBoundary bleibt als LETZTES Netz stehen — für Fehler außerhalb jeder Route
|
||||
// (Provider, Router selbst). Innerhalb der Routen fängt seit P2 jede Ansicht für sich
|
||||
// (app/router.tsx → errorComponent), damit ein Absturz nicht die ganze Shell mitnimmt.
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<AppErrorBoundary>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
<RouterProvider router={router} />
|
||||
</QueryClientProvider>
|
||||
</AppErrorBoundary>
|
||||
</React.StrictMode>,
|
||||
|
||||
+55
-46
@@ -1,46 +1,55 @@
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Boxes,
|
||||
Inbox,
|
||||
History,
|
||||
Library,
|
||||
Plug,
|
||||
Bot,
|
||||
SquareTerminal,
|
||||
HelpCircle,
|
||||
Lightbulb,
|
||||
type LucideIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
export type ViewId = "dashboard" | "ideen" | "auftraege" | "skills" | "models" | "wissen" | "chronik" | "connect" | "agent" | "konsole" | "guide"
|
||||
|
||||
export type NavGroup = "Operativ" | "Wissen" | "Werkzeuge" | "Wartung"
|
||||
|
||||
export interface NavItem {
|
||||
id: ViewId
|
||||
label: string
|
||||
hint: string
|
||||
icon: LucideIcon
|
||||
group: NavGroup
|
||||
}
|
||||
|
||||
// Eine Quelle der Wahrheit für Sidebar UND Command-Palette.
|
||||
export const NAV: NavItem[] = [
|
||||
{ id: "dashboard", label: "Cockpit", hint: "Deine Box auf einen Blick", icon: LayoutDashboard, group: "Operativ" },
|
||||
// Der Haupteingang: Idee → Konzept → Repo → Zed. Steht bewusst direkt unter dem
|
||||
// Cockpit — hier fängt jede Arbeit an (eigene Seite seit 25.07.2026).
|
||||
// Hinweis bewusst kurz: die App zeigt ihn oben in Großbuchstaben, direkt über der
|
||||
// Seitenüberschrift „Ideen" — ein langer Satz stand dort dreifach gedoppelt.
|
||||
{ id: "ideen", label: "Ideen", hint: "Schreib hin, was entstehen soll", icon: Lightbulb, group: "Operativ" },
|
||||
{ id: "auftraege", label: "Auftragsbuch", hint: "Vorschläge der Box — annehmen oder ablehnen", icon: Inbox, group: "Operativ" },
|
||||
{ id: "skills", label: "Skills & Jobs", hint: "Autonome Skills manuell auslösen", icon: Bot, group: "Operativ" },
|
||||
{ id: "chronik", label: "Chronik", hint: "Was die Box von allein getan hat + Zeitmaschine", icon: History, group: "Operativ" },
|
||||
{ id: "wissen", label: "Wissen", hint: "Lucys Wissens-Vault (Traum-Notizen)", icon: Library, group: "Wissen" },
|
||||
{ id: "connect", label: "Verbinden", hint: "IDE-/Agent-Configs erzeugen", icon: Plug, group: "Werkzeuge" },
|
||||
{ id: "konsole", label: "Konsole", hint: "Direkte Box-Shell (SSH-artig)", icon: SquareTerminal, group: "Werkzeuge" },
|
||||
{ id: "guide", label: "Anleitung", hint: "Einrichten & Vibe-Coding", icon: HelpCircle, group: "Werkzeuge" },
|
||||
{ id: "models", label: "Modelle", hint: "Speicher, laden & Rollen", icon: Boxes, group: "Wartung" },
|
||||
{ id: "agent", label: "Hermes", hint: "Agent-Status & Verdrahtung", icon: Bot, group: "Wartung" },
|
||||
]
|
||||
|
||||
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Boxes,
|
||||
Inbox,
|
||||
History,
|
||||
Library,
|
||||
Plug,
|
||||
Bot,
|
||||
SquareTerminal,
|
||||
HelpCircle,
|
||||
Lightbulb,
|
||||
type LucideIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
export type ViewId = "dashboard" | "ideen" | "auftraege" | "skills" | "models" | "wissen" | "chronik" | "connect" | "agent" | "konsole" | "guide"
|
||||
|
||||
export type NavGroup = "Operativ" | "Wissen" | "Werkzeuge" | "Wartung"
|
||||
|
||||
export interface NavItem {
|
||||
id: ViewId
|
||||
/** Echter URL-Pfad (v3-Umbau P2). Vorher war die Ansicht nur ein Hash (#models) —
|
||||
* teilbar auf View-Ebene, aber ohne Platz für den Zustand DARUNTER. */
|
||||
pfad: string
|
||||
label: string
|
||||
hint: string
|
||||
icon: LucideIcon
|
||||
group: NavGroup
|
||||
}
|
||||
|
||||
// Eine Quelle der Wahrheit für Sidebar UND Command-Palette.
|
||||
export const NAV: NavItem[] = [
|
||||
{ id: "dashboard", pfad: "/cockpit", label: "Cockpit", hint: "Deine Box auf einen Blick", icon: LayoutDashboard, group: "Operativ" },
|
||||
// Der Haupteingang: Idee → Konzept → Repo → IDE. Steht bewusst direkt unter dem
|
||||
// Cockpit — hier fängt jede Arbeit an (eigene Seite seit 25.07.2026).
|
||||
// Hinweis bewusst kurz: die App zeigt ihn oben in Großbuchstaben, direkt über der
|
||||
// Seitenüberschrift „Ideen" — ein langer Satz stand dort dreifach gedoppelt.
|
||||
{ id: "ideen", pfad: "/ideen", label: "Ideen", hint: "Schreib hin, was entstehen soll", icon: Lightbulb, group: "Operativ" },
|
||||
{ id: "auftraege", pfad: "/auftraege", label: "Auftragsbuch", hint: "Vorschläge der Box — annehmen oder ablehnen", icon: Inbox, group: "Operativ" },
|
||||
{ id: "skills", pfad: "/skills", label: "Skills & Jobs", hint: "Autonome Skills manuell auslösen", icon: Bot, group: "Operativ" },
|
||||
{ id: "chronik", pfad: "/chronik", label: "Chronik", hint: "Was die Box von allein getan hat + Zeitmaschine", icon: History, group: "Operativ" },
|
||||
{ id: "wissen", pfad: "/wissen", label: "Wissen", hint: "Lucys Wissens-Vault (Traum-Notizen)", icon: Library, group: "Wissen" },
|
||||
{ id: "connect", pfad: "/verbinden", label: "Verbinden", hint: "IDE-/Agent-Configs erzeugen", icon: Plug, group: "Werkzeuge" },
|
||||
{ id: "konsole", pfad: "/konsole", label: "Konsole", hint: "Direkte Box-Shell (SSH-artig)", icon: SquareTerminal, group: "Werkzeuge" },
|
||||
{ id: "guide", pfad: "/anleitung", label: "Anleitung", hint: "Einrichten & Vibe-Coding", icon: HelpCircle, group: "Werkzeuge" },
|
||||
{ id: "models", pfad: "/modelle", label: "Modelle", hint: "Speicher, laden & Rollen", icon: Boxes, group: "Wartung" },
|
||||
{ id: "agent", pfad: "/agent", label: "Hermes", hint: "Agent-Status & Verdrahtung", icon: Bot, group: "Wartung" },
|
||||
]
|
||||
|
||||
/** Nav-Eintrag zu einem Pfad — für Kopfzeile und aktive Markierung.
|
||||
* Längster Treffer gewinnt, damit /ideen/<id> beim Eintrag „Ideen" landet. */
|
||||
export function navFuerPfad(pfad: string): NavItem | undefined {
|
||||
return [...NAV]
|
||||
.sort((a, b) => b.pfad.length - a.pfad.length)
|
||||
.find((n) => pfad === n.pfad || pfad.startsWith(n.pfad + "/"))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router"
|
||||
import {
|
||||
Loader2, AlertTriangle, ChevronDown, ChevronRight, Clock, Hammer, Lightbulb, Send,
|
||||
HelpCircle, RotateCcw, Square, ArrowUp, ArrowDown, Code2, FileText, GitFork, X, Layers,
|
||||
@@ -114,7 +115,14 @@ function IdeenSection() {
|
||||
const [ziel, setZiel] = useState<"lxc" | "docker">("lxc")
|
||||
const [sending, setSending] = useState(false)
|
||||
const [busy, setBusy] = useState<Record<string, boolean>>({})
|
||||
const [openLog, setOpenLog] = useState<string | null>(null)
|
||||
// v3-Umbau P2 (Befund B-05): Die aufgeklappte Karte lag in useState. Wer waehrend
|
||||
// eines laufenden Workers neu lud — und genau dann schaut man hin — verlor seinen
|
||||
// Platz in der Liste. Jetzt steht sie in der URL (/ideen?offen=<id>) und ist teilbar.
|
||||
const navigate = useNavigate()
|
||||
const suche = useSearch({ from: "/ideen" })
|
||||
const openLog = suche.offen ?? null
|
||||
const setOpenLog = (id: string | null) =>
|
||||
navigate({ to: "/ideen", search: (alt) => ({ ...alt, offen: id ?? undefined }), replace: true })
|
||||
const [openKonzept, setOpenKonzept] = useState<string | null>(null)
|
||||
const [antworten, setAntworten] = useState<Record<string, string>>({})
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react"
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { JobsBar } from "@/components/models/JobsBar"
|
||||
import { LaneEditor } from "@/components/models/LaneEditor"
|
||||
@@ -16,7 +16,14 @@ const TABS = [
|
||||
type TabId = (typeof TABS)[number]["id"]
|
||||
|
||||
export function ModelsView() {
|
||||
const [tab, setTab] = useState<TabId>("werkbank")
|
||||
// v3-Umbau P2: Der Reiter lag in useState und war damit nicht teilbar und nicht
|
||||
// reload-fest — wer jemandem „schau dir das Routing an" schicken wollte, schickte
|
||||
// die Werkbank. Jetzt steht er in der URL (/modelle?reiter=routing).
|
||||
const navigate = useNavigate()
|
||||
const { reiter } = useSearch({ from: "/modelle" })
|
||||
const tab: TabId = reiter ?? "werkbank"
|
||||
const setTab = (t: TabId) =>
|
||||
navigate({ to: "/modelle", search: (alt) => ({ ...alt, reiter: t }), replace: true })
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col sm:flex-row justify-between sm:items-center gap-4">
|
||||
|
||||
Reference in New Issue
Block a user