feat(palette): P6 — die Palette wird zum Bedienwerkzeug (§4.6)
Bisher listete Strg+K die elf Navigations-Eintraege: ein Sprungbrett. Jetzt tippt
man, WAS man will, nicht wo es liegt.
Gehe zu die 11 Ziele — direkt aus NAV, damit es nie zwei Listen gibt
Tun Wartung/Logs oeffnen, Updates suchen, Snapshot, Motor neu starten
Finden das Getippte in Chronik oder Wissens-Vault suchen
An Lucy das Getippte als Idee erfassen
## Die Sicherheitsregel ist verdrahtet, nicht nur beschrieben
Die Palette ist schnell genug, dass "Strg+K, mot, Enter" den Motor neu startet,
bevor man es zu Ende gedacht hat. Jeder eingreifende Befehl traegt darum
`rueckfrage`; die Oberflaeche haelt dann an und sagt erst, was passieren WIRD:
"Alle geladenen Modelle fallen dabei aus dem Speicher und muessen neu aufwaermen —
das dauert je nach Modell ueber 20 Sekunden. Laufende Antworten brechen ab."
Ein Test prueft die Regel generisch: Jeder Befehl, dessen Label auf "neu starten",
"loeschen", "deploy", "reboot" o. AE. passt, MUSS eine Rueckfrage haben. Wer kuenftig
einen eingreifenden Befehl ohne Rueckfrage ergaenzt, macht die Ampel rot.
"An Lucy" erfasst mit `art: "idee"` — durchdenken, nicht bauen. `"projekt"` wuerde
sofort ein Repo anlegen; das darf aus einem Tastendruck nie passieren. Auch das ist
getestet.
## Die "Finden"-Eintraege fuehren nicht ins Leere
Sie brauchten erst ein Ziel — sonst haette ich genau die leere Leitung gebaut, die ich
beim Werkzeug-Verlauf vermieden habe:
ChronikView neues Suchfeld, filtert ueber Betreff/Text/Quelle, Wort in der URL
(/chronik?q=…), Trefferzahl in der Ueberschrift, "Filter aufheben"
im Leer-Zustand
WissenView hatte das Suchfeld schon, aber nur lokal — jetzt aus /wissen?suche=
gespeist und damit teilbar
## Verifiziert im Browser
Strg+K oeffnet, "Gehe zu" (11) + "Tun" (5); leere Gruppen bleiben verborgen
Text tippen -> "Finden" (2) und "An Lucy" (1) erscheinen
"Motor neu starten" traegt den Chip "fragt nach"; Auswahl zeigt die Rueckfrage
mit vollem Folgentext — es wird NICHTS gestartet
"Abbrechen" fuehrt zurueck in die Liste, keine Meldung, kein Aufruf
54 Tests gruen (10 neue) · ESLint 0 Fehler · Einstieg 118 762 B gzip / Budget 125 000.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a494d320d7
commit
24a0694b80
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as B,M as T,j as e,N as p,W as g,b as n,J as D,T as G,z as L,O as $,f as R,k as F,u as I,r as q,P as y,Q as v,t as w,X as U,C as k,n as _,q as u}from"./index-DvtGY8-b.js";import{S}from"./SectionLabel-D0h88ho8.js";import{C as K}from"./clock-geHux9wJ.js";import{E as O}from"./external-link-DPeXL_3q.js";import{S as Q}from"./shield-DuTFhOFM.js";/**
|
||||
import{c as B,M as T,j as e,N as p,W as g,b as n,J as D,T as G,z as L,O as $,f as R,k as F,u as I,r as q,P as y,Q as v,t as w,X as U,C as k,n as _,q as u}from"./index-C3tzC6TG.js";import{S}from"./SectionLabel-CWIt51pc.js";import{C as K}from"./clock-Dgmp--tr.js";import{E as O}from"./external-link-A-cVlKY7.js";import{S as Q}from"./shield-DhFhR3yI.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as W,z as G,a2 as H,a3 as T,u as F,k as O,r as u,j as e,b as N,L as m,C as E,$ as P,a1 as z,a4 as R,X as I,T as B,G as V,n as $,a5 as _,q}from"./index-DvtGY8-b.js";import{S}from"./SectionLabel-D0h88ho8.js";import{R as U}from"./refresh-cw-CTp9Wnlo.js";import{A as Q}from"./arrow-right-DrtT4MFG.js";import{C as K}from"./clock-geHux9wJ.js";import{C as M}from"./chevron-down-hb9Pt4vA.js";import{H as Z}from"./hammer-CE6Zn7Q-.js";/**
|
||||
import{c as W,z as G,a2 as H,a3 as T,u as F,k as O,r as u,j as e,b as N,L as m,C as E,$ as P,a1 as z,a4 as R,X as I,T as B,G as V,n as $,a5 as _,q}from"./index-C3tzC6TG.js";import{S}from"./SectionLabel-CWIt51pc.js";import{R as U}from"./refresh-cw-COF4ZaAx.js";import{A as Q}from"./arrow-right-BuSI6b_d.js";import{C as K}from"./clock-Dgmp--tr.js";import{C as M}from"./chevron-down-DCnAAqHq.js";import{H as Z}from"./hammer-B3FRhAF9.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+31
File diff suppressed because one or more lines are too long
-31
File diff suppressed because one or more lines are too long
-41
File diff suppressed because one or more lines are too long
+51
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as f,r as m,D as G,F as H,j as e,t as I,B as M,I as y,G as z,b as S,L as B,J as _,K as E,C as P}from"./index-DvtGY8-b.js";import{A as u}from"./arrow-right-DrtT4MFG.js";import{C as K}from"./chevron-down-hb9Pt4vA.js";/**
|
||||
import{c as f,r as m,D as G,F as H,j as e,t as I,B as M,I as y,G as z,b as S,L as B,J as _,K as E,C as P}from"./index-C3tzC6TG.js";import{A as u}from"./arrow-right-BuSI6b_d.js";import{C as K}from"./chevron-down-DCnAAqHq.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as u,j as e,r as M,t as C,Y as z,Z as k,_ as y,Q as v,$ as L,a0 as G,W as D,b as f,a1 as T}from"./index-DvtGY8-b.js";import{a as w,L as B,C as P,Z as E}from"./zap-fipupct_.js";import{L as R}from"./layers-DGDzGr16.js";import{S as I}from"./shield-DuTFhOFM.js";/**
|
||||
import{c as u,j as e,r as M,t as C,Y as z,Z as k,_ as y,Q as v,$ as L,a0 as G,W as D,b as f,a1 as T}from"./index-C3tzC6TG.js";import{a as w,L as B,C as P,Z as E}from"./zap-BmFVNuAf.js";import{L as R}from"./layers-y7VoVedk.js";import{S as I}from"./shield-DhFhR3yI.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as R,$ as O,r as u,n as z,j as e,b as S,L as D,G as T,X as Q,a3 as Se,u as De,k as Ce,z as Ke,A as ze,a6 as Ee,a7 as xe,C as Ae,T as Le,a4 as Pe,R as We,q as $e}from"./index-DvtGY8-b.js";import{S as Re}from"./SectionLabel-D0h88ho8.js";import{C as ee}from"./code-xml-CWTq52sX.js";import{H as X}from"./hammer-CE6Zn7Q-.js";import{R as ge}from"./refresh-cw-CTp9Wnlo.js";import{F as fe}from"./file-text-BN2uI732.js";import{L as Be}from"./layers-DGDzGr16.js";import{C as U}from"./chevron-down-hb9Pt4vA.js";import{C as he}from"./clock-geHux9wJ.js";/**
|
||||
import{c as R,$ as O,r as u,n as z,j as e,b as S,L as D,G as T,X as Q,a3 as Se,u as De,k as Ce,z as Ke,A as ze,a6 as Ee,a7 as xe,C as Ae,T as Le,a4 as Pe,R as We,q as $e}from"./index-C3tzC6TG.js";import{S as Re}from"./SectionLabel-CWIt51pc.js";import{C as ee}from"./code-xml-CizGFoHx.js";import{H as X}from"./hammer-B3FRhAF9.js";import{R as ge}from"./refresh-cw-COF4ZaAx.js";import{F as fe}from"./file-text-CgoPL5rz.js";import{L as Be}from"./layers-y7VoVedk.js";import{C as U}from"./chevron-down-DCnAAqHq.js";import{C as he}from"./clock-Dgmp--tr.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{u,ag as m,k as x,j as s,y as n,ah as b,b as p,n as f,q as h}from"./index-DvtGY8-b.js";function y(){const d=u(),{data:t=[]}=m(),{showAlert:l,dialogElement:o}=x();async function i(e){try{await f(`/api/jobs/${e}/cancel`,{method:"POST"}),d.invalidateQueries({queryKey:h.jobs})}catch(c){l("Fehler",c.message)}}const a=t.filter(e=>e.state==="running"||e.state==="queued"),r=t.filter(e=>e.state!=="running"&&e.state!=="queued").slice(-3);return a.length===0&&r.length===0?null:s.jsxs("div",{className:"space-y-3 mc-card p-4",children:[s.jsx("div",{className:"text-[11px] font-semibold text-muted-foreground uppercase tracking-wider",children:"Aktive Downloads"}),a.map(e=>s.jsxs("div",{className:"space-y-1.5 p-3 rounded-xl bg-background/20 border border-border/40",children:[s.jsxs("div",{className:"flex justify-between items-center text-xs",children:[s.jsx("span",{className:"font-semibold truncate max-w-[250px]",children:e.label}),s.jsxs("div",{className:"flex items-center gap-3",children:[s.jsxs("span",{className:"text-muted-foreground font-mono",children:[e.progress??0,"% • ",n(e.done_bytes),"/",n(e.total_bytes),e.eta_s?` • ETA ${b(e.eta_s)}`:""]}),s.jsx("button",{onClick:()=>i(e.id),className:"text-[10px] text-red-400 hover:text-red-300 font-semibold border border-red-500/25 bg-red-500/5 px-2 py-0.5 rounded transition-all cursor-pointer",children:"Abbrechen"})]})]}),s.jsx("div",{className:"h-1.5 overflow-hidden rounded-full bg-muted",children:s.jsx("div",{className:"h-full rounded-full bg-primary transition-all duration-500",style:{width:`${e.progress??0}%`}})})]},e.id)),r.map(e=>s.jsxs("div",{className:"flex justify-between items-center text-xs text-muted-foreground px-1",children:[s.jsx("span",{className:"truncate",children:e.label}),s.jsx("span",{className:p("font-semibold text-[10px] px-1.5 py-0.5 rounded uppercase font-mono",e.state==="done"?"bg-emerald-500/10 text-emerald-400":"bg-amber-500/10 text-amber-400"),children:e.state})]},e.id)),o]})}export{y as J};
|
||||
import{u,ag as m,k as x,j as s,y as n,ah as b,b as p,n as f,q as h}from"./index-C3tzC6TG.js";function y(){const d=u(),{data:t=[]}=m(),{showAlert:l,dialogElement:o}=x();async function i(e){try{await f(`/api/jobs/${e}/cancel`,{method:"POST"}),d.invalidateQueries({queryKey:h.jobs})}catch(c){l("Fehler",c.message)}}const a=t.filter(e=>e.state==="running"||e.state==="queued"),r=t.filter(e=>e.state!=="running"&&e.state!=="queued").slice(-3);return a.length===0&&r.length===0?null:s.jsxs("div",{className:"space-y-3 mc-card p-4",children:[s.jsx("div",{className:"text-[11px] font-semibold text-muted-foreground uppercase tracking-wider",children:"Aktive Downloads"}),a.map(e=>s.jsxs("div",{className:"space-y-1.5 p-3 rounded-xl bg-background/20 border border-border/40",children:[s.jsxs("div",{className:"flex justify-between items-center text-xs",children:[s.jsx("span",{className:"font-semibold truncate max-w-[250px]",children:e.label}),s.jsxs("div",{className:"flex items-center gap-3",children:[s.jsxs("span",{className:"text-muted-foreground font-mono",children:[e.progress??0,"% • ",n(e.done_bytes),"/",n(e.total_bytes),e.eta_s?` • ETA ${b(e.eta_s)}`:""]}),s.jsx("button",{onClick:()=>i(e.id),className:"text-[10px] text-red-400 hover:text-red-300 font-semibold border border-red-500/25 bg-red-500/5 px-2 py-0.5 rounded transition-all cursor-pointer",children:"Abbrechen"})]})]}),s.jsx("div",{className:"h-1.5 overflow-hidden rounded-full bg-muted",children:s.jsx("div",{className:"h-full rounded-full bg-primary transition-all duration-500",style:{width:`${e.progress??0}%`}})})]},e.id)),r.map(e=>s.jsxs("div",{className:"flex justify-between items-center text-xs text-muted-foreground px-1",children:[s.jsx("span",{className:"truncate",children:e.label}),s.jsx("span",{className:p("font-semibold text-[10px] px-1.5 py-0.5 rounded uppercase font-mono",e.state==="done"?"bg-emerald-500/10 text-emerald-400":"bg-amber-500/10 text-amber-400"),children:e.state})]},e.id)),o]})}export{y as J};
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
import{O as h,u as g,P as p,r as d,j as e,b as x,T as j,S as N,n as v,U as m,V as w,q as f}from"./index-DvtGY8-b.js";import{E as k}from"./external-link-DPeXL_3q.js";import{R as y}from"./refresh-cw-CTp9Wnlo.js";function K(){const{data:t}=h(),u=g(),a=t!=null&&t.box_console_url?p(t.box_console_url):void 0,n=t==null?void 0:t.box_console_reachable,[o,i]=d.useState(!1),[c,l]=d.useState("");async function b(){i(!0),l("");try{const s=await v("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})}),r=s.ok?"Konsolen-Dienst neu gestartet — einen Moment, dann lädt das Terminal.":`Neustart fehlgeschlagen: ${s.err||"Unbekannter Fehler"}`;l(r),m(s.ok?"erfolg":"fehler",r),w(u,f.agentStatus,f.services)}catch(s){const r=`Neustart fehlgeschlagen: ${(s==null?void 0:s.message)||s}`;l(r),m("fehler",r)}finally{i(!1)}}return e.jsxs("div",{className:"flex h-full flex-col gap-4",children:[e.jsxs("div",{className:"flex flex-wrap items-end justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Konsole"}),e.jsx("p",{className:"flex items-center gap-2 text-sm text-muted-foreground",children:"Direkte Shell auf der Box — wie ein SSH-Fenster, mitten im Browser."})]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground",children:[e.jsx("span",{className:x("h-2 w-2 rounded-full",n?"bg-emerald-500 animate-pulse":"bg-amber-500")}),n?"online":"offline"]}),a&&e.jsxs("a",{href:a,target:"_blank",rel:"noopener",className:"flex h-8 items-center gap-1.5 rounded-lg border border-border/60 bg-background/20 px-3 text-xs font-semibold text-muted-foreground transition-all hover:border-primary/50 hover:text-foreground",children:[e.jsx(k,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),a?e.jsxs("div",{className:"relative min-h-[58vh] flex-1 overflow-hidden rounded-2xl border border-border/60 bg-black/50 shadow-lg shadow-black/25",children:[n===!1&&e.jsxs("div",{className:"absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/70 text-center",children:[e.jsx(j,{className:"h-8 w-8 text-amber-400"}),e.jsx("div",{className:"text-sm font-semibold text-amber-300",children:"Konsole nicht erreichbar"}),e.jsxs("p",{className:"max-w-sm text-[11px] leading-normal text-muted-foreground",children:["Der Terminal-Dienst (",e.jsx("code",{className:"font-mono text-primary",children:"box-console"}),") läuft gerade nicht."]}),e.jsxs("button",{onClick:b,disabled:o,className:"flex h-9 items-center gap-1.5 rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 text-[11px] font-bold uppercase tracking-wide text-amber-300 transition-all hover:bg-amber-500/20 cursor-pointer disabled:opacity-50",children:[e.jsx(y,{className:x("h-3.5 w-3.5",o&&"animate-spin")})," Dienst neu starten"]}),c&&e.jsx("p",{className:"max-w-sm text-[11px] text-muted-foreground",children:c})]}),e.jsx("iframe",{src:a,title:"Box-Konsole",className:"h-full w-full border-0",style:{minHeight:"58vh"}})]}):e.jsxs("div",{className:"flex min-h-[58vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground",children:[e.jsx(N,{className:"mr-2 h-4 w-4"})," Lade Konsole…"]})]})}export{K as KonsoleView};
|
||||
import{O as h,u as g,P as p,r as d,j as e,b as x,T as j,S as N,n as v,U as m,V as w,q as f}from"./index-C3tzC6TG.js";import{E as k}from"./external-link-A-cVlKY7.js";import{R as y}from"./refresh-cw-COF4ZaAx.js";function K(){const{data:t}=h(),u=g(),a=t!=null&&t.box_console_url?p(t.box_console_url):void 0,n=t==null?void 0:t.box_console_reachable,[o,i]=d.useState(!1),[c,l]=d.useState("");async function b(){i(!0),l("");try{const s=await v("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})}),r=s.ok?"Konsolen-Dienst neu gestartet — einen Moment, dann lädt das Terminal.":`Neustart fehlgeschlagen: ${s.err||"Unbekannter Fehler"}`;l(r),m(s.ok?"erfolg":"fehler",r),w(u,f.agentStatus,f.services)}catch(s){const r=`Neustart fehlgeschlagen: ${(s==null?void 0:s.message)||s}`;l(r),m("fehler",r)}finally{i(!1)}}return e.jsxs("div",{className:"flex h-full flex-col gap-4",children:[e.jsxs("div",{className:"flex flex-wrap items-end justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Konsole"}),e.jsx("p",{className:"flex items-center gap-2 text-sm text-muted-foreground",children:"Direkte Shell auf der Box — wie ein SSH-Fenster, mitten im Browser."})]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground",children:[e.jsx("span",{className:x("h-2 w-2 rounded-full",n?"bg-emerald-500 animate-pulse":"bg-amber-500")}),n?"online":"offline"]}),a&&e.jsxs("a",{href:a,target:"_blank",rel:"noopener",className:"flex h-8 items-center gap-1.5 rounded-lg border border-border/60 bg-background/20 px-3 text-xs font-semibold text-muted-foreground transition-all hover:border-primary/50 hover:text-foreground",children:[e.jsx(k,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),a?e.jsxs("div",{className:"relative min-h-[58vh] flex-1 overflow-hidden rounded-2xl border border-border/60 bg-black/50 shadow-lg shadow-black/25",children:[n===!1&&e.jsxs("div",{className:"absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/70 text-center",children:[e.jsx(j,{className:"h-8 w-8 text-amber-400"}),e.jsx("div",{className:"text-sm font-semibold text-amber-300",children:"Konsole nicht erreichbar"}),e.jsxs("p",{className:"max-w-sm text-[11px] leading-normal text-muted-foreground",children:["Der Terminal-Dienst (",e.jsx("code",{className:"font-mono text-primary",children:"box-console"}),") läuft gerade nicht."]}),e.jsxs("button",{onClick:b,disabled:o,className:"flex h-9 items-center gap-1.5 rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 text-[11px] font-bold uppercase tracking-wide text-amber-300 transition-all hover:bg-amber-500/20 cursor-pointer disabled:opacity-50",children:[e.jsx(y,{className:x("h-3.5 w-3.5",o&&"animate-spin")})," Dienst neu starten"]}),c&&e.jsx("p",{className:"max-w-sm text-[11px] text-muted-foreground",children:c})]}),e.jsx("iframe",{src:a,title:"Box-Konsole",className:"h-full w-full border-0",style:{minHeight:"58vh"}})]}):e.jsxs("div",{className:"flex min-h-[58vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground",children:[e.jsx(N,{className:"mr-2 h-4 w-4"})," Lade Konsole…"]})]})}export{K as KonsoleView};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as E,u as he,a as qe,r as v,j as e,C as Y,L as Fe,b as k,d as Re,q as V,R as Ie,B as re,e as Pe,E as ze,f as le,g as Be,h as Ae,i as Oe,k as Ge,l as ee,X as xe,H as be,T as se,s as Te,m as He,n as q,o as U,p as Qe,t as Ue,v as Ve,w as We,x as Ze,y as Je,z as Xe,A as Ye}from"./index-DvtGY8-b.js";import{J as et}from"./JobsBar-B0XdGtEh.js";import{C as tt}from"./code-xml-CWTq52sX.js";import{C as ue,Z as ie,L as rt,a as st}from"./zap-fipupct_.js";import{S as ge}from"./search-CES9eee3.js";import{P as nt}from"./power-t1doBEG9.js";import{C as $e}from"./chevron-down-hb9Pt4vA.js";import{L as at}from"./layers-DGDzGr16.js";/**
|
||||
import{c as E,u as he,a as qe,r as v,j as e,C as Y,L as Fe,b as k,d as Re,q as V,R as Ie,B as re,e as Pe,E as ze,f as le,g as Be,h as Ae,i as Oe,k as Ge,l as ee,X as xe,H as be,T as se,s as Te,m as He,n as q,o as U,p as Qe,t as Ue,v as Ve,w as We,x as Ze,y as Je,z as Xe,A as Ye}from"./index-C3tzC6TG.js";import{J as et}from"./JobsBar-DqqfGkDz.js";import{C as tt}from"./code-xml-CizGFoHx.js";import{C as ue,Z as ie,L as rt,a as st}from"./zap-BmFVNuAf.js";import{S as ge}from"./search-CMGuC7tW.js";import{P as nt}from"./power-cDUHwtRA.js";import{C as $e}from"./chevron-down-DCnAAqHq.js";import{L as at}from"./layers-y7VoVedk.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
import{j as e}from"./index-DvtGY8-b.js";function n({icon:t,children:s}){return e.jsxs("p",{className:"flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/60",children:[e.jsx(t,{className:"h-3.5 w-3.5"})," ",s]})}export{n as S};
|
||||
import{j as e}from"./index-C3tzC6TG.js";function n({icon:t,children:s}){return e.jsxs("p",{className:"flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/60",children:[e.jsx(t,{className:"h-3.5 w-3.5"})," ",s]})}export{n as S};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
var E=r=>{throw TypeError(r)};var O=(r,e,t)=>e.has(r)||E("Cannot "+t);var a=(r,e,t)=>(O(r,e,"read from private field"),t?t.call(r):e.get(r)),y=(r,e,t)=>e.has(r)?E("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(r):e.set(r,t),v=(r,e,t,n)=>(O(r,e,"write to private field"),n?n.call(r,t):e.set(r,t),t),j=(r,e,t)=>(O(r,e,"access private method"),t);import{a8 as Q,a9 as A,aa as M,ab as B,ac as P,u as J,r as f,ad as F,ae as I,c as T,af as U,j as s,J as D,T as H,b as K,L,Q as V,q as _,n as R}from"./index-DvtGY8-b.js";import{J as z}from"./JobsBar-B0XdGtEh.js";var c,g,o,h,m,w,C,q,G=(q=class extends Q{constructor(e,t){super();y(this,m);y(this,c);y(this,g);y(this,o);y(this,h);v(this,c,e),this.setOptions(t),this.bindMethods(),j(this,m,w).call(this)}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(e){var n;const t=this.options;this.options=a(this,c).defaultMutationOptions(e),A(this.options,t)||a(this,c).getMutationCache().notify({type:"observerOptionsUpdated",mutation:a(this,o),observer:this}),t!=null&&t.mutationKey&&this.options.mutationKey&&M(t.mutationKey)!==M(this.options.mutationKey)?this.reset():((n=a(this,o))==null?void 0:n.state.status)==="pending"&&a(this,o).setOptions(this.options)}onUnsubscribe(){var e;this.hasListeners()||(e=a(this,o))==null||e.removeObserver(this)}onMutationUpdate(e){j(this,m,w).call(this),j(this,m,C).call(this,e)}getCurrentResult(){return a(this,g)}reset(){var e;(e=a(this,o))==null||e.removeObserver(this),v(this,o,void 0),j(this,m,w).call(this),j(this,m,C).call(this)}mutate(e,t){var n;return v(this,h,t),(n=a(this,o))==null||n.removeObserver(this),v(this,o,a(this,c).getMutationCache().build(a(this,c),this.options)),a(this,o).addObserver(this),a(this,o).execute(e)}},c=new WeakMap,g=new WeakMap,o=new WeakMap,h=new WeakMap,m=new WeakSet,w=function(){var t;const e=((t=a(this,o))==null?void 0:t.state)??B();v(this,g,{...e,isPending:e.status==="pending",isSuccess:e.status==="success",isError:e.status==="error",isIdle:e.status==="idle",mutate:this.mutate,reset:this.reset})},C=function(e){P.batch(()=>{var t,n,l,x,d,p,k,i;if(a(this,h)&&this.hasListeners()){const u=a(this,g).variables,N=a(this,g).context,S={client:a(this,c),meta:this.options.meta,mutationKey:this.options.mutationKey};if((e==null?void 0:e.type)==="success"){try{(n=(t=a(this,h)).onSuccess)==null||n.call(t,e.data,u,N,S)}catch(b){Promise.reject(b)}try{(x=(l=a(this,h)).onSettled)==null||x.call(l,e.data,null,u,N,S)}catch(b){Promise.reject(b)}}else if((e==null?void 0:e.type)==="error"){try{(p=(d=a(this,h)).onError)==null||p.call(d,e.error,u,N,S)}catch(b){Promise.reject(b)}try{(i=(k=a(this,h)).onSettled)==null||i.call(k,void 0,e.error,u,N,S)}catch(b){Promise.reject(b)}}}this.listeners.forEach(u=>{u(a(this,g))})})},q);function W(r,e){const t=J(),[n]=f.useState(()=>new G(t,r));f.useEffect(()=>{n.setOptions(r)},[n,r]);const l=f.useSyncExternalStore(f.useCallback(d=>n.subscribe(P.batchCalls(d)),[n]),()=>n.getCurrentResult(),()=>n.getCurrentResult()),x=f.useCallback((d,p)=>{n.mutate(d,p).catch(F)},[n]);if(l.error&&I(n.options.throwOnError,[l.error]))throw l.error;return{...l,mutate:x,mutateAsync:l.mutate}}/**
|
||||
var E=r=>{throw TypeError(r)};var O=(r,e,t)=>e.has(r)||E("Cannot "+t);var a=(r,e,t)=>(O(r,e,"read from private field"),t?t.call(r):e.get(r)),y=(r,e,t)=>e.has(r)?E("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(r):e.set(r,t),v=(r,e,t,n)=>(O(r,e,"write to private field"),n?n.call(r,t):e.set(r,t),t),j=(r,e,t)=>(O(r,e,"access private method"),t);import{a8 as Q,a9 as A,aa as M,ab as B,ac as P,u as J,r as f,ad as F,ae as I,c as T,af as U,j as s,J as D,T as H,b as K,L,Q as V,q as _,n as R}from"./index-C3tzC6TG.js";import{J as z}from"./JobsBar-DqqfGkDz.js";var c,g,o,h,m,w,C,q,G=(q=class extends Q{constructor(e,t){super();y(this,m);y(this,c);y(this,g);y(this,o);y(this,h);v(this,c,e),this.setOptions(t),this.bindMethods(),j(this,m,w).call(this)}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(e){var n;const t=this.options;this.options=a(this,c).defaultMutationOptions(e),A(this.options,t)||a(this,c).getMutationCache().notify({type:"observerOptionsUpdated",mutation:a(this,o),observer:this}),t!=null&&t.mutationKey&&this.options.mutationKey&&M(t.mutationKey)!==M(this.options.mutationKey)?this.reset():((n=a(this,o))==null?void 0:n.state.status)==="pending"&&a(this,o).setOptions(this.options)}onUnsubscribe(){var e;this.hasListeners()||(e=a(this,o))==null||e.removeObserver(this)}onMutationUpdate(e){j(this,m,w).call(this),j(this,m,C).call(this,e)}getCurrentResult(){return a(this,g)}reset(){var e;(e=a(this,o))==null||e.removeObserver(this),v(this,o,void 0),j(this,m,w).call(this),j(this,m,C).call(this)}mutate(e,t){var n;return v(this,h,t),(n=a(this,o))==null||n.removeObserver(this),v(this,o,a(this,c).getMutationCache().build(a(this,c),this.options)),a(this,o).addObserver(this),a(this,o).execute(e)}},c=new WeakMap,g=new WeakMap,o=new WeakMap,h=new WeakMap,m=new WeakSet,w=function(){var t;const e=((t=a(this,o))==null?void 0:t.state)??B();v(this,g,{...e,isPending:e.status==="pending",isSuccess:e.status==="success",isError:e.status==="error",isIdle:e.status==="idle",mutate:this.mutate,reset:this.reset})},C=function(e){P.batch(()=>{var t,n,l,x,d,p,k,i;if(a(this,h)&&this.hasListeners()){const u=a(this,g).variables,N=a(this,g).context,S={client:a(this,c),meta:this.options.meta,mutationKey:this.options.mutationKey};if((e==null?void 0:e.type)==="success"){try{(n=(t=a(this,h)).onSuccess)==null||n.call(t,e.data,u,N,S)}catch(b){Promise.reject(b)}try{(x=(l=a(this,h)).onSettled)==null||x.call(l,e.data,null,u,N,S)}catch(b){Promise.reject(b)}}else if((e==null?void 0:e.type)==="error"){try{(p=(d=a(this,h)).onError)==null||p.call(d,e.error,u,N,S)}catch(b){Promise.reject(b)}try{(i=(k=a(this,h)).onSettled)==null||i.call(k,void 0,e.error,u,N,S)}catch(b){Promise.reject(b)}}}this.listeners.forEach(u=>{u(a(this,g))})})},q);function W(r,e){const t=J(),[n]=f.useState(()=>new G(t,r));f.useEffect(()=>{n.setOptions(r)},[n,r]);const l=f.useSyncExternalStore(f.useCallback(d=>n.subscribe(P.batchCalls(d)),[n]),()=>n.getCurrentResult(),()=>n.getCurrentResult()),x=f.useCallback((d,p)=>{n.mutate(d,p).catch(F)},[n]);if(l.error&&I(n.options.throwOnError,[l.error]))throw l.error;return{...l,mutate:x,mutateAsync:l.mutate}}/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
Vendored
+10
-20
File diff suppressed because one or more lines are too long
+15
-15
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as r}from"./index-DvtGY8-b.js";/**
|
||||
import{c as r}from"./index-C3tzC6TG.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as o}from"./index-DvtGY8-b.js";/**
|
||||
import{c as o}from"./index-C3tzC6TG.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c}from"./index-DvtGY8-b.js";/**
|
||||
import{c}from"./index-C3tzC6TG.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import{c as a}from"./index-C3tzC6TG.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const c=a("Camera",[["path",{d:"M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z",key:"1tc9qg"}],["circle",{cx:"12",cy:"13",r:"3",key:"1vg3eu"}]]);/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/const o=a("CloudDownload",[["path",{d:"M12 13v8l-4-4",key:"1f5nwf"}],["path",{d:"m12 21 4-4",key:"1lfcce"}],["path",{d:"M4.393 15.269A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.436 8.284",key:"ui1hmy"}]]);export{o as C,c as a};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as e}from"./index-DvtGY8-b.js";/**
|
||||
import{c as e}from"./index-C3tzC6TG.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as a}from"./index-DvtGY8-b.js";/**
|
||||
import{c as a}from"./index-C3tzC6TG.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as e}from"./index-DvtGY8-b.js";/**
|
||||
import{c as e}from"./index-C3tzC6TG.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as a}from"./index-DvtGY8-b.js";/**
|
||||
import{c as a}from"./index-C3tzC6TG.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
@@ -0,0 +1 @@
|
||||
import{aA as r}from"./index-C3tzC6TG.js";var o=r();export{o as r};
|
||||
-1
File diff suppressed because one or more lines are too long
+17
-17
File diff suppressed because one or more lines are too long
-1
@@ -1 +0,0 @@
|
||||
import{ay as r}from"./index-DvtGY8-b.js";var o=r();export{o as r};
|
||||
+1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as a}from"./index-DvtGY8-b.js";/**
|
||||
import{c as a}from"./index-C3tzC6TG.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as o}from"./index-DvtGY8-b.js";/**
|
||||
import{c as o}from"./index-C3tzC6TG.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as e}from"./index-DvtGY8-b.js";/**
|
||||
import{c as e}from"./index-C3tzC6TG.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c}from"./index-DvtGY8-b.js";/**
|
||||
import{c}from"./index-C3tzC6TG.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as a}from"./index-DvtGY8-b.js";/**
|
||||
import{c as a}from"./index-C3tzC6TG.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as a}from"./index-DvtGY8-b.js";/**
|
||||
import{c as a}from"./index-C3tzC6TG.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-DvtGY8-b.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-C0bSwoXg.css">
|
||||
<script type="module" crossorigin src="/assets/index-C3tzC6TG.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-mvq7yy8w.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>␍
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { befehle, suchtext, type BefehlsKontext } from "./register"
|
||||
import { NAV } from "@/nav"
|
||||
|
||||
function kontext(ueberschreiben: Partial<BefehlsKontext> = {}): BefehlsKontext {
|
||||
return {
|
||||
text: "",
|
||||
navigate: vi.fn(),
|
||||
schublade: vi.fn(),
|
||||
melden: vi.fn(),
|
||||
api: vi.fn(async () => ({}) as never),
|
||||
...ueberschreiben,
|
||||
}
|
||||
}
|
||||
|
||||
describe("Befehls-Register", () => {
|
||||
it("führt jedes Navigations-Ziel auf — genau einmal", () => {
|
||||
const ziele = befehle("").filter((b) => b.gruppe === "gehe")
|
||||
expect(ziele).toHaveLength(NAV.length)
|
||||
expect(new Set(ziele.map((z) => z.id)).size).toBe(NAV.length)
|
||||
})
|
||||
|
||||
it("zeigt Suche und „An Lucy“ erst, wenn etwas getippt ist", () => {
|
||||
const leer = befehle("")
|
||||
expect(leer.some((b) => b.gruppe === "finden")).toBe(false)
|
||||
expect(leer.some((b) => b.gruppe === "lucy")).toBe(false)
|
||||
|
||||
const getippt = befehle("Wissensgraph-Ansicht bauen")
|
||||
expect(getippt.filter((b) => b.gruppe === "finden")).toHaveLength(2)
|
||||
expect(getippt.filter((b) => b.gruppe === "lucy")).toHaveLength(1)
|
||||
})
|
||||
|
||||
// Unter acht Zeichen ist es kein Auftrag, sondern ein Tippfehler auf dem Weg zu
|
||||
// einem Sprungziel. Sonst böte die Palette bei „mod" an, daraus eine Idee zu machen.
|
||||
it("bietet kurze Eingaben NICHT als Idee an", () => {
|
||||
expect(befehle("mod").some((b) => b.gruppe === "lucy")).toBe(false)
|
||||
expect(befehle("modelle").some((b) => b.gruppe === "lucy")).toBe(false)
|
||||
expect(befehle("modellwechsel").some((b) => b.gruppe === "lucy")).toBe(true)
|
||||
})
|
||||
|
||||
// ── Die Sicherheitsregel ──────────────────────────────────────────────────
|
||||
// Die Palette ist schnell genug, dass „Strg+K, mot, Enter" einen Dienst neu startet,
|
||||
// bevor man es zu Ende gedacht hat. Alles Eingreifende MUSS deshalb `rueckfrage`
|
||||
// tragen — die Oberfläche hält dann an und sagt erst, was passieren wird.
|
||||
it("verlangt für jeden eingreifenden Befehl eine Rückfrage", () => {
|
||||
const eingreifend = /neu starten|löschen|loeschen|deploy|zurücksetzen|reboot|entfernen/i
|
||||
for (const b of befehle("etwas getipptes")) {
|
||||
if (eingreifend.test(b.label)) {
|
||||
expect(b.rueckfrage, `„${b.label}" muss nachfragen`).toBeTruthy()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it("nennt in der Rückfrage die Folge, nicht nur die Aktion", () => {
|
||||
const motor = befehle("").find((b) => b.id === "tun:engine-neustart")!
|
||||
expect(motor.rueckfrage).toBeTruthy()
|
||||
// Der Text muss sagen, was der Nutzer DANACH merkt.
|
||||
expect(motor.rueckfrage).toMatch(/Modelle/)
|
||||
expect(motor.rueckfrage).toMatch(/aufwärmen|abbrechen|abbrechen/i)
|
||||
})
|
||||
|
||||
it("lässt harmlose Befehle ohne Rückfrage durch", () => {
|
||||
const wartung = befehle("").find((b) => b.id === "tun:wartung")!
|
||||
expect(wartung.rueckfrage).toBeUndefined()
|
||||
})
|
||||
|
||||
// ── Verhalten einzelner Befehle ───────────────────────────────────────────
|
||||
it("öffnet die Schublade über die URL, nicht über ein Ereignis", () => {
|
||||
const ctx = kontext()
|
||||
befehle("").find((b) => b.id === "tun:logs")!.ausfuehren(ctx)
|
||||
expect(ctx.schublade).toHaveBeenCalledWith("logs")
|
||||
})
|
||||
|
||||
it("erfasst eine Idee zum Durchdenken — und startet keinen Bau", async () => {
|
||||
const ctx = kontext({ text: "Wissensgraph-Ansicht bauen" })
|
||||
await befehle(ctx.text).find((b) => b.gruppe === "lucy")!.ausfuehren(ctx)
|
||||
|
||||
expect(ctx.api).toHaveBeenCalledTimes(1)
|
||||
const [pfad, init] = (ctx.api as ReturnType<typeof vi.fn>).mock.calls[0]
|
||||
expect(pfad).toBe("/api/ideen")
|
||||
const rumpf = JSON.parse((init as RequestInit).body as string)
|
||||
// `art: "idee"` heisst durchdenken. `"projekt"` wuerde sofort ein Repo anlegen —
|
||||
// das darf die Palette niemals aus einem Tastendruck heraus tun.
|
||||
expect(rumpf.art).toBe("idee")
|
||||
expect(rumpf.titel).toBe("Wissensgraph-Ansicht bauen")
|
||||
})
|
||||
|
||||
it("kürzt einen überlangen Ideen-Titel und hebt den vollen Text in die Notiz", async () => {
|
||||
const lang = "a".repeat(200)
|
||||
const ctx = kontext({ text: lang })
|
||||
await befehle(lang).find((b) => b.gruppe === "lucy")!.ausfuehren(ctx)
|
||||
const rumpf = JSON.parse(
|
||||
((ctx.api as ReturnType<typeof vi.fn>).mock.calls[0][1] as RequestInit).body as string,
|
||||
)
|
||||
expect(rumpf.titel.length).toBeLessThanOrEqual(160)
|
||||
expect(rumpf.notiz).toBe(lang)
|
||||
})
|
||||
|
||||
it("packt Suchwörter in den Suchtext, damit cmdk sie findet", () => {
|
||||
const wartung = befehle("").find((b) => b.id === "tun:wartung")!
|
||||
expect(suchtext(wartung)).toMatch(/neustart/) // steht nur in den Stichworten
|
||||
expect(suchtext(wartung)).toMatch(/System-Wartung/)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,194 @@
|
||||
import type { LucideIcon } from "lucide-react"
|
||||
import {
|
||||
Camera, CloudDownload, FileText, Lightbulb, RefreshCw, ScrollText, Search, Wrench,
|
||||
} from "lucide-react"
|
||||
import { NAV } from "@/nav"
|
||||
|
||||
// Befehls-Register der Palette (v3-Umbau P6, Spezifikation §4.6).
|
||||
//
|
||||
// Bis hierher listete die Palette die elf Navigations-Einträge — sie war ein Sprungbrett.
|
||||
// Jetzt ist sie das primäre Bedienwerkzeug: Man tippt, was man will, nicht wo es liegt.
|
||||
//
|
||||
// VIER KATEGORIEN, in der Reihenfolge, in der man sie braucht:
|
||||
// Gehe zu Sprungziele (wie bisher)
|
||||
// Tun Aktionen — Dienste, Modelle, Wartung
|
||||
// Finden Suche in Chronik und Wissens-Vault
|
||||
// An Lucy den getippten Text direkt als Idee erfassen
|
||||
//
|
||||
// SICHERHEITSREGEL (aus der Spezifikation, hier hart verdrahtet): Alles, was neu startet,
|
||||
// löscht oder deployt, trägt `rueckfrage` und wird zweistufig bestätigt. Die Palette ist
|
||||
// zu schnell für einen unbedachten Treffer — zwei Tastendrücke und ein Dienst wäre weg.
|
||||
|
||||
export type BefehlsGruppe = "gehe" | "tun" | "finden" | "lucy"
|
||||
|
||||
export interface Befehl {
|
||||
id: string
|
||||
gruppe: BefehlsGruppe
|
||||
label: string
|
||||
hinweis?: string
|
||||
icon: LucideIcon
|
||||
/** Zusätzliche Wörter für die Suche — was der Nutzer tippen könnte, aber nicht dasteht. */
|
||||
stichworte?: string
|
||||
/** Gesetzt = zweistufige Rückfrage mit diesem Text, bevor irgendetwas passiert. */
|
||||
rueckfrage?: string
|
||||
/** Was der Befehl tut. `text` ist die aktuelle Eingabe der Palette. */
|
||||
ausfuehren: (ctx: BefehlsKontext) => void | Promise<void>
|
||||
}
|
||||
|
||||
export interface BefehlsKontext {
|
||||
text: string
|
||||
navigate: (pfad: string) => void
|
||||
schublade: (tab: "wartung" | "logs") => void
|
||||
melden: (art: "info" | "erfolg" | "warnung" | "fehler", text: string) => void
|
||||
api: <T>(pfad: string, init?: RequestInit) => Promise<T>
|
||||
}
|
||||
|
||||
export const GRUPPEN_TITEL: Record<BefehlsGruppe, string> = {
|
||||
gehe: "Gehe zu",
|
||||
tun: "Tun",
|
||||
finden: "Finden",
|
||||
lucy: "An Lucy",
|
||||
}
|
||||
|
||||
/** Sprungziele — direkt aus der Navigations-Tabelle, damit es nie zwei Listen gibt. */
|
||||
const ZIELE: Befehl[] = NAV.map((n) => ({
|
||||
id: `gehe:${n.id}`,
|
||||
gruppe: "gehe" as const,
|
||||
label: n.label,
|
||||
hinweis: n.hint,
|
||||
icon: n.icon,
|
||||
ausfuehren: ({ navigate }) => navigate(n.pfad),
|
||||
}))
|
||||
|
||||
/** Aktionen. Bewusst wenige: nur was man wirklich aus dem Stand heraus tut. */
|
||||
const AKTIONEN: Befehl[] = [
|
||||
{
|
||||
id: "tun:wartung",
|
||||
gruppe: "tun",
|
||||
label: "System-Wartung öffnen",
|
||||
hinweis: "Dienste, Updates, Snapshot",
|
||||
icon: Wrench,
|
||||
stichworte: "pflege dienste neustart update",
|
||||
ausfuehren: ({ schublade }) => schublade("wartung"),
|
||||
},
|
||||
{
|
||||
id: "tun:logs",
|
||||
gruppe: "tun",
|
||||
label: "System-Logs öffnen",
|
||||
hinweis: "Diagnose je Dienst",
|
||||
icon: ScrollText,
|
||||
stichworte: "fehler journal diagnose",
|
||||
ausfuehren: ({ schublade }) => schublade("logs"),
|
||||
},
|
||||
{
|
||||
id: "tun:updates",
|
||||
gruppe: "tun",
|
||||
label: "Nach Updates suchen",
|
||||
hinweis: "apt + Engine + Agent prüfen",
|
||||
icon: CloudDownload,
|
||||
stichworte: "aktualisieren apt engine",
|
||||
ausfuehren: async ({ api, melden }) => {
|
||||
await api("/api/maintenance/check-updates", { method: "POST" })
|
||||
melden("info", "Update-Suche läuft — das Ergebnis erscheint in der Wartung.")
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "tun:snapshot",
|
||||
gruppe: "tun",
|
||||
label: "Snapshot anlegen",
|
||||
hinweis: "Sicherung vor riskanten Schritten",
|
||||
icon: Camera,
|
||||
stichworte: "backup sicherung zeitmaschine",
|
||||
ausfuehren: async ({ api, melden }) => {
|
||||
await api("/api/system/backup", { method: "POST" })
|
||||
melden("erfolg", "Snapshot angestoßen.")
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "tun:engine-neustart",
|
||||
gruppe: "tun",
|
||||
label: "Motor neu starten",
|
||||
hinweis: "llama-swap — alle Modelle fallen kurz weg",
|
||||
icon: RefreshCw,
|
||||
stichworte: "engine llama swap modell neu starten",
|
||||
// Zweistufig: Ein Neustart wirft jedes geladene Modell aus dem Speicher, und ein
|
||||
// grosses Modell braucht danach ueber 20 Sekunden zum Aufwaermen.
|
||||
rueckfrage:
|
||||
"Der Motor (llama-swap) wird neu gestartet. Alle geladenen Modelle fallen dabei aus " +
|
||||
"dem Speicher und müssen neu aufwärmen — das dauert je nach Modell über 20 Sekunden. " +
|
||||
"Laufende Antworten brechen ab.",
|
||||
ausfuehren: async ({ api, melden }) => {
|
||||
const r = await api<{ ok: boolean; err?: string }>("/api/maintenance/restart", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ service: "llama-swap" }),
|
||||
})
|
||||
melden(r.ok ? "erfolg" : "fehler",
|
||||
r.ok ? "Motor neu gestartet." : `Neustart fehlgeschlagen: ${r.err || "Unbekannter Fehler"}`)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
/** Suchen — führen in die jeweilige Ansicht, mit dem Suchwort in der URL. */
|
||||
const SUCHEN = (text: string): Befehl[] => {
|
||||
const q = text.trim()
|
||||
if (!q) return []
|
||||
return [
|
||||
{
|
||||
id: "finden:chronik",
|
||||
gruppe: "finden",
|
||||
label: `„${q}“ in der Chronik`,
|
||||
hinweis: "Was die Box getan hat",
|
||||
icon: Search,
|
||||
ausfuehren: ({ navigate }) => navigate(`/chronik?q=${encodeURIComponent(q)}`),
|
||||
},
|
||||
{
|
||||
id: "finden:wissen",
|
||||
gruppe: "finden",
|
||||
label: `„${q}“ im Wissens-Vault`,
|
||||
hinweis: "Lucys Notizen",
|
||||
icon: FileText,
|
||||
ausfuehren: ({ navigate }) => navigate(`/wissen?suche=${encodeURIComponent(q)}`),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/** An Lucy — den getippten Text als Idee erfassen. Erfasst nur, startet nichts. */
|
||||
const AN_LUCY = (text: string): Befehl[] => {
|
||||
const q = text.trim()
|
||||
if (q.length < 8) return [] // unter acht Zeichen ist es kein Auftrag, sondern ein Tippfehler
|
||||
return [
|
||||
{
|
||||
id: "lucy:idee",
|
||||
gruppe: "lucy",
|
||||
label: `Als Idee erfassen: „${q.length > 60 ? q.slice(0, 57) + "…" : q}“`,
|
||||
hinweis: "Wird nur notiert — gebaut wird erst auf deinen Klick",
|
||||
icon: Lightbulb,
|
||||
ausfuehren: async ({ api, melden, navigate }) => {
|
||||
// Rumpf exakt wie im Ideen-Verfasser (routers/ideen.py → IdeeIn). `art: "idee"`
|
||||
// heisst durchdenken, nicht bauen — die Palette startet nie einen Bau.
|
||||
await api("/api/ideen", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
titel: q.length > 160 ? q.slice(0, 157) + "…" : q,
|
||||
notiz: q.length > 160 ? q : "",
|
||||
welt: "ide",
|
||||
tiefe: "",
|
||||
ziel: "",
|
||||
art: "idee",
|
||||
}),
|
||||
})
|
||||
melden("erfolg", "Als Idee erfasst.")
|
||||
navigate("/ideen")
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/** Das vollständige Register für die aktuelle Eingabe. */
|
||||
export function befehle(text: string): Befehl[] {
|
||||
return [...ZIELE, ...AKTIONEN, ...SUCHEN(text), ...AN_LUCY(text)]
|
||||
}
|
||||
|
||||
/** Suchtext eines Befehls — cmdk filtert darüber. */
|
||||
export const suchtext = (b: Befehl) =>
|
||||
[b.label, b.hinweis, b.stichworte, GRUPPEN_TITEL[b.gruppe]].filter(Boolean).join(" ")
|
||||
@@ -101,7 +101,20 @@ function CockpitSeite() {
|
||||
const cockpitRoute = ansicht("/cockpit", () => <CockpitSeite />)
|
||||
const auftraegeRoute = ansicht("/auftraege", () => <AuftragsbuchView />)
|
||||
const skillsRoute = ansicht("/skills", () => <SkillsView />)
|
||||
const chronikRoute = ansicht("/chronik", () => <ChronikView />)
|
||||
/** Chronik: `?q=` filtert den Verlauf — Ziel der „Finden"-Einträge der Palette (P6). */
|
||||
export interface ChronikSuche {
|
||||
q?: string
|
||||
}
|
||||
const chronikRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/chronik",
|
||||
validateSearch: (roh: Record<string, unknown>): ChronikSuche =>
|
||||
typeof roh.q === "string" && roh.q ? { q: roh.q } : {},
|
||||
component: () => <ChronikView />,
|
||||
errorComponent: ({ error, reset }) => (
|
||||
<RouteFehler titel="Die Chronik ist abgestürzt" text="Der Rest läuft weiter." fehler={error} onErneut={reset} />
|
||||
),
|
||||
})
|
||||
const verbindenRoute = ansicht("/verbinden", () => <ConnectView />)
|
||||
const konsoleRoute = ansicht("/konsole", () => <KonsoleView />)
|
||||
const anleitungRoute = ansicht("/anleitung", () => <GuideView />)
|
||||
@@ -124,15 +137,19 @@ const ideenRoute = createRoute({
|
||||
),
|
||||
})
|
||||
|
||||
/** Wissen: `?datei=<pfad>` macht jede Vault-Notiz verlinkbar. */
|
||||
/** Wissen: `?datei=` macht jede Notiz verlinkbar, `?suche=` trägt das Suchwort
|
||||
* aus der Befehlspalette herein (P6). */
|
||||
export interface WissenSuche {
|
||||
datei?: string
|
||||
suche?: string
|
||||
}
|
||||
const wissenRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/wissen",
|
||||
validateSearch: (roh: Record<string, unknown>): WissenSuche =>
|
||||
typeof roh.datei === "string" && roh.datei ? { datei: roh.datei } : {},
|
||||
validateSearch: (roh: Record<string, unknown>): WissenSuche => ({
|
||||
...(typeof roh.datei === "string" && roh.datei ? { datei: roh.datei } : {}),
|
||||
...(typeof roh.suche === "string" && roh.suche ? { suche: roh.suche } : {}),
|
||||
}),
|
||||
component: () => <WissenView />,
|
||||
errorComponent: ({ error, reset }) => (
|
||||
<RouteFehler titel="Die Wissens-Ansicht ist abgestürzt" text="Der Rest läuft weiter." fehler={error} onErneut={reset} />
|
||||
|
||||
@@ -1,72 +1,160 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { useCallback, useMemo, useState } from "react"
|
||||
import { Command } from "cmdk"
|
||||
import { useNavigate } from "@tanstack/react-router"
|
||||
import { NAV } from "@/nav"
|
||||
import { AlertTriangle, CornerDownLeft } from "lucide-react"
|
||||
import { useMC, usePaletteOffen } from "@/app/store"
|
||||
import { api } from "@/lib/api"
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
GRUPPEN_TITEL, befehle, suchtext,
|
||||
type Befehl, type BefehlsGruppe,
|
||||
} from "@/app/palette/register"
|
||||
|
||||
// Befehlspalette 2.0 (v3-Umbau P6, Spezifikation §4.6).
|
||||
//
|
||||
// Vorher listete sie die elf Navigations-Einträge — ein Sprungbrett. Jetzt tippt man,
|
||||
// WAS man will, nicht wo es liegt: springen, tun, finden, oder den Text direkt als Idee
|
||||
// an Lucy geben. Das Register steht in app/palette/register.ts.
|
||||
//
|
||||
// Die Rückfrage ist keine Höflichkeit, sondern Absicht: Die Palette ist schnell genug,
|
||||
// dass „Strg+K, mot, Enter" den Motor neu startet, bevor man es zu Ende gedacht hat.
|
||||
// Befehle mit `rueckfrage` halten deshalb an und sagen erst, was passieren wird.
|
||||
|
||||
const GRUPPEN_REIHE: BefehlsGruppe[] = ["gehe", "tun", "finden", "lucy"]
|
||||
|
||||
export function CommandPalette() {
|
||||
const [open, setOpen] = useState(false)
|
||||
const offen = usePaletteOffen()
|
||||
const palette = useMC((s) => s.palette)
|
||||
const melden = useMC((s) => s.melden)
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
|
||||
e.preventDefault()
|
||||
setOpen((o) => !o)
|
||||
}
|
||||
}
|
||||
document.addEventListener("keydown", onKey)
|
||||
return () => document.removeEventListener("keydown", onKey)
|
||||
}, [])
|
||||
const [text, setText] = useState("")
|
||||
const [nachfrage, setNachfrage] = useState<Befehl | null>(null)
|
||||
const [laeuft, setLaeuft] = useState(false)
|
||||
|
||||
// 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)
|
||||
}, [])
|
||||
const schliessen = useCallback(() => {
|
||||
palette(false)
|
||||
setText("")
|
||||
setNachfrage(null)
|
||||
}, [palette])
|
||||
|
||||
const ausfuehren = useCallback(async (b: Befehl) => {
|
||||
setLaeuft(true)
|
||||
try {
|
||||
await b.ausfuehren({
|
||||
text,
|
||||
navigate: (pfad) => navigate({ to: pfad }),
|
||||
schublade: (tab) => navigate({ to: ".", search: (a) => ({ ...a, system: tab }) }),
|
||||
melden,
|
||||
api,
|
||||
})
|
||||
schliessen()
|
||||
} catch (e) {
|
||||
// Der echte Grund aus ApiError.detail (P1/B-14) — nicht „Fehler".
|
||||
melden("fehler", e instanceof Error ? e.message : String(e))
|
||||
setNachfrage(null)
|
||||
} finally {
|
||||
setLaeuft(false)
|
||||
}
|
||||
}, [text, navigate, melden, schliessen])
|
||||
|
||||
const liste = useMemo(() => befehle(text), [text])
|
||||
|
||||
return (
|
||||
<Command.Dialog
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
open={offen}
|
||||
onOpenChange={(o) => (o ? palette(true) : schliessen())}
|
||||
label="Befehlspalette"
|
||||
className="fixed inset-0 z-50 flex items-start justify-center bg-black/50 p-4 pt-[12vh] overscroll-contain"
|
||||
onClick={() => setOpen(false)}
|
||||
className="fixed inset-0 z-50 flex items-start justify-center overscroll-contain bg-black/50 p-4 pt-[12vh]"
|
||||
onClick={schliessen}
|
||||
shouldFilter={!nachfrage}
|
||||
>
|
||||
<div
|
||||
className="w-full max-w-lg overflow-hidden rounded-xl border border-border bg-popover text-popover-foreground shadow-2xl"
|
||||
className="w-full max-w-xl overflow-hidden rounded-xl border border-border bg-popover text-popover-foreground shadow-2xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{nachfrage ? (
|
||||
// ── Zweistufige Rückfrage ────────────────────────────────────────
|
||||
<div className="space-y-4 p-5">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<AlertTriangle className="h-5 w-5 shrink-0 text-amber-400" aria-hidden="true" />
|
||||
<h2 className="font-space text-sm font-bold tracking-tight">{nachfrage.label}</h2>
|
||||
</div>
|
||||
<p className="text-xs leading-relaxed text-muted-foreground">{nachfrage.rueckfrage}</p>
|
||||
<div className="flex justify-end gap-2 pt-1">
|
||||
<button
|
||||
onClick={() => setNachfrage(null)}
|
||||
className="h-9 cursor-pointer rounded-lg border border-border/60 bg-background/20 px-4 text-xs font-semibold text-muted-foreground transition-colors hover:bg-accent"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
autoFocus
|
||||
disabled={laeuft}
|
||||
onClick={() => ausfuehren(nachfrage)}
|
||||
className="h-9 cursor-pointer rounded-lg bg-amber-500/90 px-4 text-xs font-bold text-black transition-colors hover:bg-amber-500 disabled:opacity-50"
|
||||
>
|
||||
{laeuft ? "Läuft …" : "Ja, ausführen"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Command.Input
|
||||
autoFocus
|
||||
placeholder="Springe zu… (Modelle, Routing, System …)"
|
||||
value={text}
|
||||
onValueChange={setText}
|
||||
placeholder="Springen, tun, finden — oder schreib eine Idee …"
|
||||
className="w-full border-b border-border bg-transparent px-4 py-3 text-sm outline-none placeholder:text-muted-foreground"
|
||||
/>
|
||||
<Command.List className="max-h-80 overflow-y-auto p-2">
|
||||
<Command.List className="max-h-[26rem] overflow-y-auto p-2 scrollbar-thin">
|
||||
<Command.Empty className="px-3 py-6 text-center text-sm text-muted-foreground">
|
||||
Nichts gefunden.
|
||||
</Command.Empty>
|
||||
<Command.Group heading="Bereiche" className="px-1 py-1 text-xs text-muted-foreground">
|
||||
{NAV.map((item) => (
|
||||
|
||||
{GRUPPEN_REIHE.map((gruppe) => {
|
||||
const eintraege = liste.filter((b) => b.gruppe === gruppe)
|
||||
if (!eintraege.length) return null
|
||||
return (
|
||||
<Command.Group
|
||||
key={gruppe}
|
||||
heading={GRUPPEN_TITEL[gruppe]}
|
||||
className="px-1 py-1 text-[10px] font-bold uppercase tracking-widest text-muted-foreground/50"
|
||||
>
|
||||
{eintraege.map((b) => (
|
||||
<Command.Item
|
||||
key={item.id}
|
||||
value={`${item.label} ${item.hint}`}
|
||||
onSelect={() => {
|
||||
navigate({ to: item.pfad })
|
||||
setOpen(false)
|
||||
}}
|
||||
key={b.id}
|
||||
value={suchtext(b)}
|
||||
onSelect={() => (b.rueckfrage ? setNachfrage(b) : ausfuehren(b))}
|
||||
className="flex cursor-pointer items-center gap-3 rounded-md px-3 py-2 text-sm text-foreground aria-selected:bg-accent aria-selected:text-accent-foreground"
|
||||
>
|
||||
<item.icon className="h-4 w-4 text-primary" />
|
||||
<span>{item.label}</span>
|
||||
<span className="ml-auto truncate text-xs text-muted-foreground">{item.hint}</span>
|
||||
<b.icon
|
||||
className={cn("h-4 w-4 shrink-0", b.rueckfrage ? "text-amber-400" : "text-primary")}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="truncate">{b.label}</span>
|
||||
{b.rueckfrage && (
|
||||
<span className="shrink-0 rounded bg-amber-500/15 px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-wide text-amber-300">
|
||||
fragt nach
|
||||
</span>
|
||||
)}
|
||||
{b.hinweis && (
|
||||
<span className="ml-auto truncate pl-3 text-xs text-muted-foreground">{b.hinweis}</span>
|
||||
)}
|
||||
</Command.Item>
|
||||
))}
|
||||
</Command.Group>
|
||||
)
|
||||
})}
|
||||
</Command.List>
|
||||
|
||||
<div className="flex items-center gap-3 border-t border-border/50 px-4 py-2 font-mono text-[10px] text-muted-foreground">
|
||||
<span className="flex items-center gap-1">↑↓ wählen</span>
|
||||
<span className="flex items-center gap-1"><CornerDownLeft className="h-3 w-3" aria-hidden="true" /> ausführen</span>
|
||||
<span className="ml-auto">esc schließt</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Command.Dialog>
|
||||
)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { useMemo, useState } from "react"
|
||||
import {
|
||||
History, HeartPulse, ShieldAlert, Bell, Moon, Radar, Inbox, Loader2,
|
||||
Archive, RotateCcw, AlarmClock, MessageCircle, Sparkles, type LucideIcon,
|
||||
} from "lucide-react"
|
||||
import { AlarmClock, Archive, Bell, HeartPulse, History, Inbox, Loader2, MessageCircle, Moon, Radar, RotateCcw, Search, ShieldAlert, Sparkles, type LucideIcon } from "lucide-react"
|
||||
import { api, type ChronikItem, type Snapshot } from "@/lib/api"
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router"
|
||||
import { useChronik, useZeitmaschine, useQueryClient, qk } from "@/lib/queries"
|
||||
import { useDialog } from "@/lib/useDialog"
|
||||
import { cn } from "@/lib/utils"
|
||||
@@ -30,9 +28,25 @@ function dayKey(ts: number): string {
|
||||
}
|
||||
|
||||
export function ChronikView() {
|
||||
const { data: items = [], isLoading } = useChronik()
|
||||
const { data: alle = [], isLoading } = useChronik()
|
||||
const [limitPerDay, setLimitPerDay] = useState(true)
|
||||
|
||||
// v3-Umbau P6: Das Suchwort steht in der URL (/chronik?q=…). So landet der
|
||||
// „Finden"-Eintrag der Befehlspalette hier — und ein Treffer ist teilbar.
|
||||
const navigate = useNavigate()
|
||||
const { q = "" } = useSearch({ from: "/chronik" })
|
||||
const suchen = (wert: string) =>
|
||||
navigate({ to: "/chronik", search: wert ? { q: wert } : {}, replace: true })
|
||||
|
||||
const items = useMemo(() => {
|
||||
const ql = q.trim().toLowerCase()
|
||||
if (!ql) return alle
|
||||
return alle.filter((it) =>
|
||||
it.subject.toLowerCase().includes(ql)
|
||||
|| it.text.toLowerCase().includes(ql)
|
||||
|| it.source.toLowerCase().includes(ql))
|
||||
}, [alle, q])
|
||||
|
||||
const grouped = useMemo(() => {
|
||||
const g: { day: string; items: ChronikItem[] }[] = []
|
||||
for (const it of items) {
|
||||
@@ -58,9 +72,23 @@ export function ChronikView() {
|
||||
<Zeitmaschine />
|
||||
|
||||
<section className="space-y-4">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<p className="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/60">
|
||||
<History className="h-3.5 w-3.5" /> Verlauf
|
||||
{q && <span className="normal-case text-primary">· {items.length} von {alle.length}</span>}
|
||||
</p>
|
||||
<div className="relative ml-auto w-full sm:w-64">
|
||||
<Search className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" aria-hidden="true" />
|
||||
<input
|
||||
type="search"
|
||||
value={q}
|
||||
onChange={(e) => suchen(e.target.value)}
|
||||
placeholder="Verlauf durchsuchen …"
|
||||
aria-label="Chronik durchsuchen"
|
||||
className="h-8 w-full rounded-lg border border-border/60 bg-background/40 pl-8 pr-3 text-xs text-foreground outline-none transition-colors focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex items-center gap-2 rounded-2xl border border-border/60 bg-card/30 p-6 text-xs text-muted-foreground">
|
||||
@@ -70,7 +98,9 @@ export function ChronikView() {
|
||||
|
||||
{!isLoading && items.length === 0 && (
|
||||
<div className="rounded-2xl border border-dashed border-border/60 bg-card/20 p-8 text-center text-xs text-muted-foreground">
|
||||
Noch keine Einträge. Sobald die Box etwas von allein tut oder meldet (Updates, Wächter, Träume), steht es hier.
|
||||
{q
|
||||
? <>Nichts zu „{q}" im Verlauf. <button onClick={() => suchen("")} className="cursor-pointer text-primary underline underline-offset-2">Filter aufheben</button></>
|
||||
: "Noch keine Einträge. Sobald die Box etwas von allein tut oder meldet (Updates, Wächter, Träume), steht es hier."}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router"
|
||||
import { Library, FileText, Sparkles, Loader2, FolderOpen, Search } from "lucide-react"
|
||||
import { api, type WissenDatei, type WissenFile } from "@/lib/api"
|
||||
import { useWissen, useMemoryGraph } from "@/lib/queries"
|
||||
@@ -24,7 +25,14 @@ export function WissenView() {
|
||||
const [active, setActive] = useState<string | null>(null)
|
||||
const [doc, setDoc] = useState<WissenDatei | null>(null)
|
||||
const [docLoading, setDocLoading] = useState(false)
|
||||
const [q, setQ] = useState("")
|
||||
// v3-Umbau P6: Das Suchfeld gab es schon, aber nur lokal. Jetzt steht das Wort in
|
||||
// der URL (/wissen?suche=…) — so landet der „Finden"-Eintrag der Palette hier, und
|
||||
// ein Suchergebnis ist teilbar.
|
||||
const navigate = useNavigate()
|
||||
const { suche = "" } = useSearch({ from: "/wissen" })
|
||||
const q = suche
|
||||
const setQ = (wert: string) =>
|
||||
navigate({ to: "/wissen", search: (a) => ({ ...a, suche: wert || undefined }), replace: true })
|
||||
const [showGraph, setShowGraph] = useState(false)
|
||||
|
||||
const { data: graphData, isLoading: graphLoading } = useMemoryGraph(showGraph)
|
||||
|
||||
Reference in New Issue
Block a user