refactor(ideen): v3-Umbau P5 — der Monolith bekommt einen eigenen Bereich
Ampel / ampel (push) Successful in 34s
Ampel / ampel (push) Successful in 34s
Sechste Etappe, erster Bereich. views/IdeenView.tsx hatte 1031 Zeilen, 26 useState
und sechs Komponenten in derselben Datei (Befund B-06) — der Haupteingang des
Systems, und niemand konnte ihn ohne Vollbild-Scrollen ueberblicken.
## Der Schnitt
Er folgt den Grenzen, die im Monolithen ohnehin schon lagen: Jede Top-Level-Funktion
wird eine Datei. Der INHALT ist unveraendert — nur die Datei ist neu, damit der Diff
pruefbar bleibt.
features/ideen/zustaende.ts 55 Zustands-/Art-Tabellen + Sortier-Rang
features/ideen/quote.ts 22 Nutzungs-Quote der Eingabe
features/ideen/Wahl.tsx 27 eine Auswahlkachel
features/ideen/ErgebnisPanel.tsx 55
features/ideen/KonzeptPanel.tsx 73
features/ideen/ProjektGruppe.tsx 86
features/ideen/WeiterLeiste.tsx 113
features/ideen/IdeenAnsicht.tsx 676 Verfasser + Liste + Zeilen-Renderer
views/IdeenView.tsx bleibt als vierzeiliger Wegweiser eine Fassung lang stehen,
damit ein uebersehener Import nicht stillschweigend bricht; der Router zeigt bereits
direkt auf features/ideen/.
## Was BEWUSST noch nicht geschnitten ist
Der Zeilen-Renderer `zeile()` (188 Zeilen) bleibt in IdeenAnsicht.tsx. Er greift auf
16 Werte des umgebenden Zustands zu (busy, openLog, openKonzept, antworten, logData,
steuer, archive, answer, …). Ihn "herauszuloesen" hiesse, eine Props-Liste mit 16
Eintraegen zu schreiben — dieselbe Verflechtung in anderer Schreibweise. Das ist eine
Frage der Zustands-Zugehoerigkeit, keine Verschiebe-Uebung, und verdient eine eigene
Runde statt eine Beifaenger-Aenderung. Steht so auch im Kopfkommentar der Datei.
## Nebenbefund beim Pruefen: die eigene Anzeige log
Gegen die noch nicht aktualisierte Box zeigte die Statusleiste "Getrennt" — obwohl
die Poller sauber lieferten und alle Zahlen stimmten. Grund: Dort gibt es /api/stream
noch nicht, der SPA-Catch-all beantwortet den Pfad mit index.html, und EventSource
scheitert daran.
Die Anzeige verwechselte damit zwei Dinge. Jetzt trennt sie sie:
Getrennt = die Zentrale antwortet nicht, die Zahlen sind wirklich alt
Nachlauf = kein Strom, aber die Zentrale antwortet — die Zahlen stimmen,
sie kommen im Takt statt sofort
Live = Strom steht
Eine Anzeige, die vor korrekten Daten warnt, ist genauso falsch wie eine, die
veraltete verschweigt.
## Verifiziert
Gegen das lokale Backend: /ideen rendert, keine Fehlerflaeche
Gegen die ECHTE Box (altes Backend, kein /api/stream): Leiste sagt "Nachlauf",
zeigt die echten Werte (40,7/122,7 GB Unified Memory, GPU 0 %, 37 °C,
532 748 115 Token, 1023 € gespart) — und blendet die Betriebszeit AUS, weil das
alte Backend sie nicht liefert, statt eine Zahl zu erfinden.
Das neue Frontend degradiert also sauber gegen den alten Stand.
37/37 Tests gruen · ESLint 0 Fehler · Einstieg 118 606 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
b74e98ffbb
commit
dcee0096fe
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as A,z as M,M as B,f as z,k as D,u as T,r as W,j as e,b as a,N as g,O as b,P as G,t as f,W as j,X as I,C as N,n as y,q as i}from"./index-Cx7RCLVH.js";import{E as q}from"./external-link-CgE4twbA.js";import{S as U}from"./shield-BEuf-cEX.js";/**
|
||||
import{c as A,z as M,M as B,f as z,k as D,u as T,r as W,j as e,b as a,N as g,O as b,P as G,t as f,W as j,X as I,C as N,n as y,q as i}from"./index-C18iiJ44.js";import{E as q}from"./external-link-CKfa6ghp.js";import{S as U}from"./shield-BsCXu4e-.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,a1 as H,a2 as T,u as F,k as _,r as u,j as e,b as N,L as m,C as E,_ as O,a0 as z,a3 as R,X as I,T as B,G as V,n as K,a4 as P,q}from"./index-Cx7RCLVH.js";import{S,H as U}from"./SectionLabel-DUeBkmth.js";import{R as Q}from"./refresh-cw-C02wnoZb.js";import{A as Z}from"./arrow-right-CBVuzArl.js";import{C as $}from"./clock-DF_nrSqC.js";import{C as M}from"./chevron-down-IhuTdfx8.js";/**
|
||||
import{c as W,z as G,a1 as H,a2 as T,u as F,k as _,r as u,j as e,b as N,L as m,C as E,_ as O,a0 as z,a3 as R,X as I,T as B,G as V,n as K,a4 as P,q}from"./index-C18iiJ44.js";import{S,H as U}from"./SectionLabel-DOZxjhNS.js";import{R as Q}from"./refresh-cw-Cqrl4nkL.js";import{A as Z}from"./arrow-right-BtJkJFRf.js";import{C as $}from"./clock-BHNQaiMJ.js";import{C as M}from"./chevron-down-NnNHBt9Q.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 d,ah as S,r as h,j as e,ai as M,L as p,a0 as z,R as j,a4 as C,aj as D,ak as A,b as B,al as Z,u as E,k as L,n as k,q}from"./index-Cx7RCLVH.js";/**
|
||||
import{c as d,ah as S,r as h,j as e,ai as M,L as p,a0 as z,R as j,a4 as C,aj as D,ak as A,b as B,al as Z,u as E,k as L,n as k,q}from"./index-C18iiJ44.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
Vendored
+1
-1
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-Cx7RCLVH.js";import{A as u}from"./arrow-right-CBVuzArl.js";import{C as K}from"./chevron-down-IhuTdfx8.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-C18iiJ44.js";import{A as u}from"./arrow-right-BtJkJFRf.js";import{C as K}from"./chevron-down-NnNHBt9Q.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,V as z,Y as k,Z as y,O as v,_ as L,$ as G,W as D,b as f,a0 as T}from"./index-Cx7RCLVH.js";import{a as w,L as B,C as P,Z as E}from"./zap-CDvJ9oJD.js";import{L as R}from"./layers-DUCYNtcW.js";import{S as I}from"./shield-BEuf-cEX.js";/**
|
||||
import{c as u,j as e,r as M,t as C,V as z,Y as k,Z as y,O as v,_ as L,$ as G,W as D,b as f,a0 as T}from"./index-C18iiJ44.js";import{a as w,L as B,C as P,Z as E}from"./zap-GT_FkD2B.js";import{L as R}from"./layers-DJ-zOHed.js";import{S as I}from"./shield-BsCXu4e-.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+35
File diff suppressed because one or more lines are too long
-35
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{u,af as m,k as x,j as s,y as n,ag as b,b as p,n as f,q as h}from"./index-Cx7RCLVH.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,af as m,k as x,j as s,y as n,ag as b,b as p,n as f,q as h}from"./index-C18iiJ44.js";function y(){const d=u(),{data:t=[]}=m(),{showAlert:l,dialogElement:o}=x();async function i(e){try{await f(`/api/jobs/${e}/cancel`,{method:"POST"}),d.invalidateQueries({queryKey:h.jobs})}catch(c){l("Fehler",c.message)}}const a=t.filter(e=>e.state==="running"||e.state==="queued"),r=t.filter(e=>e.state!=="running"&&e.state!=="queued").slice(-3);return a.length===0&&r.length===0?null:s.jsxs("div",{className:"space-y-3 mc-card p-4",children:[s.jsx("div",{className:"text-[11px] font-semibold text-muted-foreground uppercase tracking-wider",children:"Aktive Downloads"}),a.map(e=>s.jsxs("div",{className:"space-y-1.5 p-3 rounded-xl bg-background/20 border border-border/40",children:[s.jsxs("div",{className:"flex justify-between items-center text-xs",children:[s.jsx("span",{className:"font-semibold truncate max-w-[250px]",children:e.label}),s.jsxs("div",{className:"flex items-center gap-3",children:[s.jsxs("span",{className:"text-muted-foreground font-mono",children:[e.progress??0,"% • ",n(e.done_bytes),"/",n(e.total_bytes),e.eta_s?` • ETA ${b(e.eta_s)}`:""]}),s.jsx("button",{onClick:()=>i(e.id),className:"text-[10px] text-red-400 hover:text-red-300 font-semibold border border-red-500/25 bg-red-500/5 px-2 py-0.5 rounded transition-all cursor-pointer",children:"Abbrechen"})]})]}),s.jsx("div",{className:"h-1.5 overflow-hidden rounded-full bg-muted",children:s.jsx("div",{className:"h-full rounded-full bg-primary transition-all duration-500",style:{width:`${e.progress??0}%`}})})]},e.id)),r.map(e=>s.jsxs("div",{className:"flex justify-between items-center text-xs text-muted-foreground px-1",children:[s.jsx("span",{className:"truncate",children:e.label}),s.jsx("span",{className:p("font-semibold text-[10px] px-1.5 py-0.5 rounded uppercase font-mono",e.state==="done"?"bg-emerald-500/10 text-emerald-400":"bg-amber-500/10 text-amber-400"),children:e.state})]},e.id)),o]})}export{y as J};
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
import{M as h,u as g,N as p,r as d,j as e,b as x,T as j,S as N,n as v,Q as m,U as w,q as f}from"./index-Cx7RCLVH.js";import{E as k}from"./external-link-CgE4twbA.js";import{R as y}from"./refresh-cw-C02wnoZb.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{M as h,u as g,N as p,r as d,j as e,b as x,T as j,S as N,n as v,Q as m,U as w,q as f}from"./index-C18iiJ44.js";import{E as k}from"./external-link-CKfa6ghp.js";import{R as y}from"./refresh-cw-Cqrl4nkL.js";function K(){const{data:t}=h(),u=g(),a=t!=null&&t.box_console_url?p(t.box_console_url):void 0,n=t==null?void 0:t.box_console_reachable,[o,i]=d.useState(!1),[c,l]=d.useState("");async function b(){i(!0),l("");try{const s=await v("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})}),r=s.ok?"Konsolen-Dienst neu gestartet — einen Moment, dann lädt das Terminal.":`Neustart fehlgeschlagen: ${s.err||"Unbekannter Fehler"}`;l(r),m(s.ok?"erfolg":"fehler",r),w(u,f.agentStatus,f.services)}catch(s){const r=`Neustart fehlgeschlagen: ${(s==null?void 0:s.message)||s}`;l(r),m("fehler",r)}finally{i(!1)}}return e.jsxs("div",{className:"flex h-full flex-col gap-4",children:[e.jsxs("div",{className:"flex flex-wrap items-end justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Konsole"}),e.jsx("p",{className:"flex items-center gap-2 text-sm text-muted-foreground",children:"Direkte Shell auf der Box — wie ein SSH-Fenster, mitten im Browser."})]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground",children:[e.jsx("span",{className:x("h-2 w-2 rounded-full",n?"bg-emerald-500 animate-pulse":"bg-amber-500")}),n?"online":"offline"]}),a&&e.jsxs("a",{href:a,target:"_blank",rel:"noopener",className:"flex h-8 items-center gap-1.5 rounded-lg border border-border/60 bg-background/20 px-3 text-xs font-semibold text-muted-foreground transition-all hover:border-primary/50 hover:text-foreground",children:[e.jsx(k,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),a?e.jsxs("div",{className:"relative min-h-[58vh] flex-1 overflow-hidden rounded-2xl border border-border/60 bg-black/50 shadow-lg shadow-black/25",children:[n===!1&&e.jsxs("div",{className:"absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/70 text-center",children:[e.jsx(j,{className:"h-8 w-8 text-amber-400"}),e.jsx("div",{className:"text-sm font-semibold text-amber-300",children:"Konsole nicht erreichbar"}),e.jsxs("p",{className:"max-w-sm text-[11px] leading-normal text-muted-foreground",children:["Der Terminal-Dienst (",e.jsx("code",{className:"font-mono text-primary",children:"box-console"}),") läuft gerade nicht."]}),e.jsxs("button",{onClick:b,disabled:o,className:"flex h-9 items-center gap-1.5 rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 text-[11px] font-bold uppercase tracking-wide text-amber-300 transition-all hover:bg-amber-500/20 cursor-pointer disabled:opacity-50",children:[e.jsx(y,{className:x("h-3.5 w-3.5",o&&"animate-spin")})," Dienst neu starten"]}),c&&e.jsx("p",{className:"max-w-sm text-[11px] text-muted-foreground",children:c})]}),e.jsx("iframe",{src:a,title:"Box-Konsole",className:"h-full w-full border-0",style:{minHeight:"58vh"}})]}):e.jsxs("div",{className:"flex min-h-[58vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground",children:[e.jsx(N,{className:"mr-2 h-4 w-4"})," Lade Konsole…"]})]})}export{K as KonsoleView};
|
||||
+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-Cx7RCLVH.js";import{J as et}from"./JobsBar-DJ6T2S2l.js";import{C as tt}from"./code-xml-B-fOQx_u.js";import{C as ue,Z as ie,L as rt,a as st}from"./zap-CDvJ9oJD.js";import{S as ge}from"./search-CPzoxwoE.js";import{P as nt}from"./power-wqc9-u1c.js";import{C as $e}from"./chevron-down-IhuTdfx8.js";import{L as at}from"./layers-DUCYNtcW.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-C18iiJ44.js";import{J as et}from"./JobsBar-BNKB17zh.js";import{C as tt}from"./code-xml-DSUsazUa.js";import{C as ue,Z as ie,L as rt,a as st}from"./zap-GT_FkD2B.js";import{S as ge}from"./search-BnlVCHow.js";import{P as nt}from"./power-DTqUyxxW.js";import{C as $e}from"./chevron-down-NnNHBt9Q.js";import{L as at}from"./layers-DJ-zOHed.js";/**
|
||||
* @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 s,j as e}from"./index-Cx7RCLVH.js";/**
|
||||
import{c as s,j as e}from"./index-C18iiJ44.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
var E=r=>{throw TypeError(r)};var O=(r,e,t)=>e.has(r)||E("Cannot "+t);var a=(r,e,t)=>(O(r,e,"read from private field"),t?t.call(r):e.get(r)),y=(r,e,t)=>e.has(r)?E("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(r):e.set(r,t),v=(r,e,t,n)=>(O(r,e,"write to private field"),n?n.call(r,t):e.set(r,t),t),j=(r,e,t)=>(O(r,e,"access private method"),t);import{a7 as A,a8 as B,a9 as M,aa as F,ab as P,u as J,r as f,ac as I,ad as Q,c as T,ae as U,j as s,J as D,T as H,b as K,L,O as V,q as _,n as R}from"./index-Cx7RCLVH.js";import{J as z}from"./JobsBar-DJ6T2S2l.js";var c,g,o,h,m,w,C,q,G=(q=class extends A{constructor(e,t){super();y(this,m);y(this,c);y(this,g);y(this,o);y(this,h);v(this,c,e),this.setOptions(t),this.bindMethods(),j(this,m,w).call(this)}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(e){var n;const t=this.options;this.options=a(this,c).defaultMutationOptions(e),B(this.options,t)||a(this,c).getMutationCache().notify({type:"observerOptionsUpdated",mutation:a(this,o),observer:this}),t!=null&&t.mutationKey&&this.options.mutationKey&&M(t.mutationKey)!==M(this.options.mutationKey)?this.reset():((n=a(this,o))==null?void 0:n.state.status)==="pending"&&a(this,o).setOptions(this.options)}onUnsubscribe(){var e;this.hasListeners()||(e=a(this,o))==null||e.removeObserver(this)}onMutationUpdate(e){j(this,m,w).call(this),j(this,m,C).call(this,e)}getCurrentResult(){return a(this,g)}reset(){var e;(e=a(this,o))==null||e.removeObserver(this),v(this,o,void 0),j(this,m,w).call(this),j(this,m,C).call(this)}mutate(e,t){var n;return v(this,h,t),(n=a(this,o))==null||n.removeObserver(this),v(this,o,a(this,c).getMutationCache().build(a(this,c),this.options)),a(this,o).addObserver(this),a(this,o).execute(e)}},c=new WeakMap,g=new WeakMap,o=new WeakMap,h=new WeakMap,m=new WeakSet,w=function(){var t;const e=((t=a(this,o))==null?void 0:t.state)??F();v(this,g,{...e,isPending:e.status==="pending",isSuccess:e.status==="success",isError:e.status==="error",isIdle:e.status==="idle",mutate:this.mutate,reset:this.reset})},C=function(e){P.batch(()=>{var t,n,l,x,d,p,k,i;if(a(this,h)&&this.hasListeners()){const u=a(this,g).variables,N=a(this,g).context,S={client:a(this,c),meta:this.options.meta,mutationKey:this.options.mutationKey};if((e==null?void 0:e.type)==="success"){try{(n=(t=a(this,h)).onSuccess)==null||n.call(t,e.data,u,N,S)}catch(b){Promise.reject(b)}try{(x=(l=a(this,h)).onSettled)==null||x.call(l,e.data,null,u,N,S)}catch(b){Promise.reject(b)}}else if((e==null?void 0:e.type)==="error"){try{(p=(d=a(this,h)).onError)==null||p.call(d,e.error,u,N,S)}catch(b){Promise.reject(b)}try{(i=(k=a(this,h)).onSettled)==null||i.call(k,void 0,e.error,u,N,S)}catch(b){Promise.reject(b)}}}this.listeners.forEach(u=>{u(a(this,g))})})},q);function W(r,e){const t=J(),[n]=f.useState(()=>new G(t,r));f.useEffect(()=>{n.setOptions(r)},[n,r]);const l=f.useSyncExternalStore(f.useCallback(d=>n.subscribe(P.batchCalls(d)),[n]),()=>n.getCurrentResult(),()=>n.getCurrentResult()),x=f.useCallback((d,p)=>{n.mutate(d,p).catch(I)},[n]);if(l.error&&Q(n.options.throwOnError,[l.error]))throw l.error;return{...l,mutate:x,mutateAsync:l.mutate}}/**
|
||||
var E=r=>{throw TypeError(r)};var O=(r,e,t)=>e.has(r)||E("Cannot "+t);var a=(r,e,t)=>(O(r,e,"read from private field"),t?t.call(r):e.get(r)),y=(r,e,t)=>e.has(r)?E("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(r):e.set(r,t),v=(r,e,t,n)=>(O(r,e,"write to private field"),n?n.call(r,t):e.set(r,t),t),j=(r,e,t)=>(O(r,e,"access private method"),t);import{a7 as A,a8 as B,a9 as M,aa as F,ab as P,u as J,r as f,ac as I,ad as Q,c as T,ae as U,j as s,J as D,T as H,b as K,L,O as V,q as _,n as R}from"./index-C18iiJ44.js";import{J as z}from"./JobsBar-BNKB17zh.js";var c,g,o,h,m,w,C,q,G=(q=class extends A{constructor(e,t){super();y(this,m);y(this,c);y(this,g);y(this,o);y(this,h);v(this,c,e),this.setOptions(t),this.bindMethods(),j(this,m,w).call(this)}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(e){var n;const t=this.options;this.options=a(this,c).defaultMutationOptions(e),B(this.options,t)||a(this,c).getMutationCache().notify({type:"observerOptionsUpdated",mutation:a(this,o),observer:this}),t!=null&&t.mutationKey&&this.options.mutationKey&&M(t.mutationKey)!==M(this.options.mutationKey)?this.reset():((n=a(this,o))==null?void 0:n.state.status)==="pending"&&a(this,o).setOptions(this.options)}onUnsubscribe(){var e;this.hasListeners()||(e=a(this,o))==null||e.removeObserver(this)}onMutationUpdate(e){j(this,m,w).call(this),j(this,m,C).call(this,e)}getCurrentResult(){return a(this,g)}reset(){var e;(e=a(this,o))==null||e.removeObserver(this),v(this,o,void 0),j(this,m,w).call(this),j(this,m,C).call(this)}mutate(e,t){var n;return v(this,h,t),(n=a(this,o))==null||n.removeObserver(this),v(this,o,a(this,c).getMutationCache().build(a(this,c),this.options)),a(this,o).addObserver(this),a(this,o).execute(e)}},c=new WeakMap,g=new WeakMap,o=new WeakMap,h=new WeakMap,m=new WeakSet,w=function(){var t;const e=((t=a(this,o))==null?void 0:t.state)??F();v(this,g,{...e,isPending:e.status==="pending",isSuccess:e.status==="success",isError:e.status==="error",isIdle:e.status==="idle",mutate:this.mutate,reset:this.reset})},C=function(e){P.batch(()=>{var t,n,l,x,d,p,k,i;if(a(this,h)&&this.hasListeners()){const u=a(this,g).variables,N=a(this,g).context,S={client:a(this,c),meta:this.options.meta,mutationKey:this.options.mutationKey};if((e==null?void 0:e.type)==="success"){try{(n=(t=a(this,h)).onSuccess)==null||n.call(t,e.data,u,N,S)}catch(b){Promise.reject(b)}try{(x=(l=a(this,h)).onSettled)==null||x.call(l,e.data,null,u,N,S)}catch(b){Promise.reject(b)}}else if((e==null?void 0:e.type)==="error"){try{(p=(d=a(this,h)).onError)==null||p.call(d,e.error,u,N,S)}catch(b){Promise.reject(b)}try{(i=(k=a(this,h)).onSettled)==null||i.call(k,void 0,e.error,u,N,S)}catch(b){Promise.reject(b)}}}this.listeners.forEach(u=>{u(a(this,g))})})},q);function W(r,e){const t=J(),[n]=f.useState(()=>new G(t,r));f.useEffect(()=>{n.setOptions(r)},[n,r]);const l=f.useSyncExternalStore(f.useCallback(d=>n.subscribe(P.batchCalls(d)),[n]),()=>n.getCurrentResult(),()=>n.getCurrentResult()),x=f.useCallback((d,p)=>{n.mutate(d,p).catch(I)},[n]);if(l.error&&Q(n.options.throwOnError,[l.error]))throw l.error;return{...l,mutate:x,mutateAsync:l.mutate}}/**
|
||||
* @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 w,j as e,b as c,r as l,T as W,L as ae,E as ve,n as b,O as le,V as oe,X as de,J as Se,u as Ce,k as _e,x as ze,ar as Fe,af as Le,al as Ue,U as P,t as Ae,y as q,q as N}from"./index-Cx7RCLVH.js";import{R as F}from"./refresh-cw-C02wnoZb.js";import{F as Pe}from"./file-text-DCWu7Fnn.js";import{S as Be}from"./search-CPzoxwoE.js";import{S as ce}from"./shield-BEuf-cEX.js";import{A as ne}from"./arrow-right-CBVuzArl.js";import{C as De}from"./clock-DF_nrSqC.js";import{E as Re}from"./external-link-CgE4twbA.js";import{P as $e}from"./power-wqc9-u1c.js";/**
|
||||
import{c as w,j as e,b as c,r as l,T as W,L as ae,E as ve,n as b,O as le,V as oe,X as de,J as Se,u as Ce,k as _e,x as ze,ar as Fe,af as Le,al as Ue,U as P,t as Ae,y as q,q as N}from"./index-C18iiJ44.js";import{R as F}from"./refresh-cw-Cqrl4nkL.js";import{F as Pe}from"./file-text-CSo7S1oD.js";import{S as Be}from"./search-BnlVCHow.js";import{S as ce}from"./shield-BsCXu4e-.js";import{A as ne}from"./arrow-right-BtJkJFRf.js";import{C as De}from"./clock-BHNQaiMJ.js";import{E as Re}from"./external-link-CKfa6ghp.js";import{P as $e}from"./power-DTqUyxxW.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
var to=Object.defineProperty;var eo=(e,n,r)=>n in e?to(e,n,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[n]=r;var Bn=(e,n,r)=>eo(e,typeof n!="symbol"?n+"":n,r);import{c as no,r as W,am as ro,an as io,j as $,ao as oo,ap as ao,n as so,b as Ae,L as Ke,a0 as uo,aq as lo}from"./index-Cx7RCLVH.js";import{i as Ft,c as Xn,a as Yn,b as co,o as fo,m as Zn,d as Kn}from"./string-DoZi9Vij.js";import{R as ho}from"./refresh-cw-C02wnoZb.js";import{S as po}from"./search-CPzoxwoE.js";import{F as go}from"./file-text-DCWu7Fnn.js";/**
|
||||
var to=Object.defineProperty;var eo=(e,n,r)=>n in e?to(e,n,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[n]=r;var Bn=(e,n,r)=>eo(e,typeof n!="symbol"?n+"":n,r);import{c as no,r as W,am as ro,an as io,j as $,ao as oo,ap as ao,n as so,b as Ae,L as Ke,a0 as uo,aq as lo}from"./index-C18iiJ44.js";import{i as Ft,c as Xn,a as Yn,b as co,o as fo,m as Zn,d as Kn}from"./string-DoZi9Vij.js";import{R as ho}from"./refresh-cw-Cqrl4nkL.js";import{S as po}from"./search-BnlVCHow.js";import{F as go}from"./file-text-CSo7S1oD.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}from"./index-Cx7RCLVH.js";/**
|
||||
import{c as r}from"./index-C18iiJ44.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-Cx7RCLVH.js";/**
|
||||
import{c as o}from"./index-C18iiJ44.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-Cx7RCLVH.js";/**
|
||||
import{c}from"./index-C18iiJ44.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-Cx7RCLVH.js";/**
|
||||
import{c as e}from"./index-C18iiJ44.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-Cx7RCLVH.js";/**
|
||||
import{c as a}from"./index-C18iiJ44.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-Cx7RCLVH.js";/**
|
||||
import{c as e}from"./index-C18iiJ44.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
@@ -0,0 +1 @@
|
||||
import{ax as r}from"./index-C18iiJ44.js";var o=r();export{o as r};
|
||||
+12
-12
File diff suppressed because one or more lines are too long
-1
@@ -1 +0,0 @@
|
||||
import{ax as r}from"./index-Cx7RCLVH.js";var o=r();export{o as r};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as a}from"./index-Cx7RCLVH.js";/**
|
||||
import{c as a}from"./index-C18iiJ44.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-Cx7RCLVH.js";/**
|
||||
import{c as o}from"./index-C18iiJ44.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-Cx7RCLVH.js";/**
|
||||
import{c as e}from"./index-C18iiJ44.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-Cx7RCLVH.js";/**
|
||||
import{c}from"./index-C18iiJ44.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-Cx7RCLVH.js";/**
|
||||
import{c as a}from"./index-C18iiJ44.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-Cx7RCLVH.js";/**
|
||||
import{c as a}from"./index-C18iiJ44.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
Vendored
+1
-1
@@ -7,7 +7,7 @@
|
||||
<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-Cx7RCLVH.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-C18iiJ44.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DQUMn05v.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -26,7 +26,7 @@ const AgentView = lazy(() => import("@/views/AgentView").then((m) => ({ default:
|
||||
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 IdeenView = lazy(() => import("@/features/ideen/IdeenAnsicht").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 })))
|
||||
|
||||
@@ -92,12 +92,27 @@ export function Statusleiste() {
|
||||
const durchsatz = tokVerlauf.length ? tokVerlauf[tokVerlauf.length - 1].completion : 0
|
||||
const zeit = betriebszeit(live?.uptime_s ?? sys?.uptime_s)
|
||||
|
||||
const LAGE_STIL: Record<typeof lage, { punkt: string; text: string; titel: string }> = {
|
||||
// Die Anzeige beschreibt, WORAUF man gerade schaut — und muss dafür zwei Dinge
|
||||
// zusammenbringen: den Ereignisstrom UND die Erreichbarkeit der Zentrale.
|
||||
//
|
||||
// Beim Prüfen gegen die noch nicht aktualisierte Box aufgefallen: Dort gibt es
|
||||
// /api/stream nicht, der SPA-Catch-all beantwortet den Pfad mit index.html, und
|
||||
// EventSource scheitert. Die Leiste sagte daraufhin „Getrennt" — obwohl die Poller
|
||||
// sauber lieferten und die Zahlen stimmten. Eine Anzeige, die vor korrekten Daten
|
||||
// warnt, ist genauso falsch wie eine, die veraltete verschweigt.
|
||||
//
|
||||
// Getrennt = die Zentrale antwortet nicht. Die Zahlen sind wirklich alt.
|
||||
// Nachlauf = kein Strom, aber die Zentrale antwortet. Die Zahlen stimmen,
|
||||
// sie kommen nur im Takt statt sofort.
|
||||
// Live = Strom steht, Änderungen erscheinen unmittelbar.
|
||||
const anzeige = zentraleWeg ? "getrennt" : lage === "live" ? "live" : "nachlauf"
|
||||
|
||||
const LAGE_STIL = {
|
||||
live: { punkt: "bg-emerald-500", text: "Live", titel: "Ereignisstrom steht — Änderungen erscheinen sofort." },
|
||||
nachlauf: { punkt: "bg-amber-500", text: "Nachlauf", titel: "Ereignisstrom gerissen — die Ansicht fragt wieder im Takt nach." },
|
||||
getrennt: { punkt: "bg-red-500", text: "Getrennt", titel: "Keine Verbindung zur Zentrale — die Zahlen sind veraltet." },
|
||||
}
|
||||
const lageStil = LAGE_STIL[lage]
|
||||
nachlauf: { punkt: "bg-amber-500", text: "Nachlauf", titel: "Kein Ereignisstrom — die Zahlen stimmen, sie kommen im Takt statt sofort." },
|
||||
getrennt: { punkt: "bg-red-500", text: "Getrennt", titel: "Die Zentrale antwortet nicht — die Zahlen sind veraltet." },
|
||||
} as const
|
||||
const lageStil = LAGE_STIL[anzeige]
|
||||
|
||||
return (
|
||||
<footer
|
||||
@@ -180,7 +195,7 @@ export function Statusleiste() {
|
||||
{/* Ehrlichkeits-Anzeige: sagt, WORAUF man gerade schaut. Ohne sie altert die
|
||||
Oberflaeche still weiter, wenn der Strom reisst (Befund B-15). */}
|
||||
<div className="ml-auto flex shrink-0 items-center gap-1.5 px-3" title={lageStil.titel}>
|
||||
<span className={cn("h-2 w-2 rounded-full", lageStil.punkt, lage === "live" && "animate-pulse")} aria-hidden="true" />
|
||||
<span className={cn("h-2 w-2 rounded-full", lageStil.punkt, anzeige === "live" && "animate-pulse")} aria-hidden="true" />
|
||||
<span className="text-muted-foreground">{lageStil.text}</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// Ergebnis-Panel einer abgeschlossenen Idee: was die Box getan hat, in ihren Worten.
|
||||
//
|
||||
// Herausgeschnitten aus views/IdeenView.tsx (1031 Zeilen) beim v3-Umbau P5, Befund B-06.
|
||||
// Der Inhalt ist UNVERAENDERT — nur die Datei ist neu. So bleibt der Diff pruefbar.
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { ClipboardCheck } from "lucide-react"
|
||||
import { api, type IdeenErgebnis } from "@/lib/api"
|
||||
|
||||
export function ErgebnisPanel({ id }: { id: string }) {
|
||||
const [erg, setErg] = useState<IdeenErgebnis | null>(null)
|
||||
const [fehler, setFehler] = useState(false)
|
||||
// Fehler darf nicht kleben: fällt der erste Versuch in ein Deploy-Fenster
|
||||
// (Zentrale startet gerade neu), muss ein Klick neu laden können (Vorfall 16.07.).
|
||||
const [versuch, setVersuch] = useState(0)
|
||||
useEffect(() => {
|
||||
let weg = false
|
||||
setFehler(false)
|
||||
setErg(null)
|
||||
api<IdeenErgebnis>(`/api/ideen/${id}/ergebnis`)
|
||||
.then((r) => { if (!weg) setErg(r) })
|
||||
.catch(() => { if (!weg) setFehler(true) })
|
||||
return () => { weg = true }
|
||||
}, [id, versuch])
|
||||
const text = erg ? (erg.summary || erg.result) : ""
|
||||
return (
|
||||
<div className="ml-2 -mt-1">
|
||||
<div className="rounded-lg border border-emerald-500/25 bg-emerald-500/5 p-2.5">
|
||||
<p className="mb-1 flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wide text-emerald-300">
|
||||
<ClipboardCheck className="h-3 w-3" /> Ergebnis des Workers
|
||||
</p>
|
||||
{fehler ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setVersuch((v) => v + 1)}
|
||||
className="text-[10px] text-red-400 underline decoration-dotted underline-offset-2 hover:text-red-300"
|
||||
>
|
||||
Ergebnis nicht ladbar — nochmal versuchen
|
||||
</button>
|
||||
) : erg === null ? (
|
||||
<p className="text-[10px] text-muted-foreground animate-pulse">Wird geladen …</p>
|
||||
) : text ? (
|
||||
<p className="max-h-64 overflow-y-auto whitespace-pre-wrap text-xs leading-relaxed text-foreground/85">{text}</p>
|
||||
) : (
|
||||
<p className="text-[10px] text-muted-foreground italic">
|
||||
Der Worker hat keine Abschluss-Zusammenfassung hinterlassen (ältere Karte oder Protokollverstoß).
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Die Seite selbst: Kopf + die Queue. Bewusst duenn — die Logik lebt unveraendert in
|
||||
// IdeenSection, damit dieser Umzug reines Verschieben bleibt und nichts still kippt.
|
||||
@@ -0,0 +1,676 @@
|
||||
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, Check,
|
||||
} from "lucide-react"
|
||||
import { api, type IdeenItem } from "@/lib/api"
|
||||
import { useIdeen, useIdeenLog, useQueryClient, qk } from "@/lib/queries"
|
||||
import { useDialog } from "@/lib/useDialog"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { fmtWhen } from "@/lib/format"
|
||||
import { SectionLabel } from "@/components/ui/SectionLabel"
|
||||
import { AKTIV_RANG, ART_UI, FRAGE_HINT, IDEEN_STATE } from "./zustaende"
|
||||
import { quoteLesen, quoteZaehlen } from "./quote"
|
||||
import { ErgebnisPanel } from "./ErgebnisPanel"
|
||||
import { KonzeptPanel } from "./KonzeptPanel"
|
||||
import { ProjektGruppe } from "./ProjektGruppe"
|
||||
import { Wahl } from "./Wahl"
|
||||
|
||||
// Ideen-Queue (natives Hermes-Kanban) — eigene Seite seit 25.07.2026.
|
||||
// Vorher steckte sie als 703-Zeilen-Abschnitt in der Auftragsbuch-Ansicht und war damit
|
||||
// die MEHRHEIT einer Seite, die nach etwas anderem benannt war. Die beiden laufen in
|
||||
// entgegengesetzte Richtungen: Auftragsbuch = die Box schlaegt DIR vor (Eingang),
|
||||
// Ideen = DU beauftragst die Box (Ausgang). Seit dem Umbau ist dieser Weg der
|
||||
// Haupteingang des Systems (Idee -> Konzept -> Repo -> Agentic IDE) und gehoert nach vorn.
|
||||
//
|
||||
// v3-Umbau P5 (Befund B-06): Die Datei hatte 1031 Zeilen und 26 useState. Fuenf
|
||||
// eigenstaendige Komponenten (Wahl, ProjektGruppe, KonzeptPanel, WeiterLeiste,
|
||||
// ErgebnisPanel) und die Zustandstabellen liegen jetzt daneben — sie hatten ohnehin
|
||||
// klare Props und keine Verflechtung mit dem Rest.
|
||||
//
|
||||
// WAS BEWUSST NOCH HIER STEHT: der Zeilen-Renderer `zeile()`. Er greift auf 16
|
||||
// Werte des umgebenden Zustands zu (busy, openLog, openKonzept, antworten, logData,
|
||||
// steuer, archive, answer, …). Ihn herauszuloesen heisst nicht, Code zu verschieben,
|
||||
// sondern die Zustands-Zugehoerigkeit neu zu entwerfen — eine Props-Liste mit 16
|
||||
// Eintraegen waere nur eine andere Schreibweise desselben Knotens. Das ist eine eigene
|
||||
// Runde wert und keine Beifaenger-Aenderung.
|
||||
|
||||
function IdeenSection() {
|
||||
const { data, isLoading } = useIdeen()
|
||||
const qc = useQueryClient()
|
||||
const { showAlert, showConfirm, dialogElement } = useDialog()
|
||||
const [text, setText] = useState("")
|
||||
// GENAU EINE Auswahl aus drei, in zwei Bereichen (26.07.2026):
|
||||
// denken → du baust selbst, aber erst ein Konzept. Auf „gefällt mir" wird daraus ein Repo.
|
||||
// repo → du baust selbst, Konzept + Gitea-Repo sofort.
|
||||
// box → die Box baut, Ergebnis kommt als Patch-Vorschlag.
|
||||
//
|
||||
// Vorher (25.07.) waren das zwei angeblich unabhängige Dimensionen: „wer baut" plus ein
|
||||
// Haken „nur denken". Das war FALSCH modelliert — `weiterfuehren` macht aus einer
|
||||
// durchdachten Idee genau die IDE-Projekt-Karte, die `repo` direkt anlegt. Es sind also
|
||||
// zwei Stufen EINES Weges, nicht zwei Achsen. Und die Kombination box+denken schob einen
|
||||
// still in die IDE-Welt (submit setzte welt:"ide"), ohne das irgendwo zu sagen.
|
||||
const [modus, setModus] = useState<"denken" | "repo" | "box">("denken")
|
||||
const [tiefe, setTiefe] = useState<"schlank" | "gruendlich">("schlank")
|
||||
// Zugeklappt statt Assistent (26.07.2026): der Entscheidungsbaum EXISTIERT — Wer baut das?,
|
||||
// und nur im IDE-Ast: wie startest du? — aber du läufst ihn nur, wenn die Einordnung
|
||||
// danebenlag. Im Normalfall steht da eine Zeile und du drückst Enter.
|
||||
const [offen, setOffen] = useState(false)
|
||||
// Hat die Box den Text wirklich gelesen? Steuert nur das Etikett „gelesen" in der
|
||||
// zugeklappten Zeile. Ohne Einordnung (Text zu kurz, Aufruf gescheitert) steht dort die
|
||||
// Voreinstellung — und die darf sich nicht als Verständnis ausgeben.
|
||||
const [vorgeschlagen, setVorgeschlagen] = useState(false)
|
||||
const isIde = modus !== "box" // denken UND repo laufen über das projektstart-Profil
|
||||
const isProjekt = modus === "repo" // nur DANN sind Tiefe und Ziel jetzt schon nötig
|
||||
// Ziel-Plattform des Projekts (nur IDE-Welt): läuft es als LXC-Dienst oder in Docker?
|
||||
const [ziel, setZiel] = useState<"lxc" | "docker">("lxc")
|
||||
const [sending, setSending] = useState(false)
|
||||
const [busy, setBusy] = useState<Record<string, boolean>>({})
|
||||
// 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>>({})
|
||||
|
||||
const items = data?.items ?? []
|
||||
const reload = () => qc.invalidateQueries({ queryKey: qk.ideen })
|
||||
|
||||
// Die Box liest mit und wählt vor. Gemessen ~500 ms auf dem dauerwarmen Hirn, deshalb
|
||||
// reicht eine kurze Tipp-Pause. Bewusst ein VORSCHLAG: er setzt nur die Auswahl, die du
|
||||
// ohnehin siehst und mit einem Klick überschreibst. Scheitert er, passiert schlicht nichts
|
||||
// — kein Ladebalken, keine Fehlermeldung, das Tippen wird nie blockiert.
|
||||
useEffect(() => {
|
||||
const t = text.trim()
|
||||
if (t.length < 12) { setVorgeschlagen(false); return }
|
||||
let verworfen = false
|
||||
const timer = setTimeout(async () => {
|
||||
try {
|
||||
const r = await api<{ ok: boolean; wer?: "box" | "ide"; tiefe?: "schlank" | "gruendlich"; nur_denken?: boolean }>(
|
||||
"/api/ideen/einordnen", { method: "POST", body: JSON.stringify({ text: t }) })
|
||||
if (verworfen || !r?.ok) return
|
||||
// Die drei Karten aus der Einordnung ableiten: eine Frage („lohnt sich…?") wird zum
|
||||
// Durchdenken, ein eigenständiges Programm zum Repo, alles am Heimlabor zur Box.
|
||||
setModus(r.nur_denken ? "denken" : r.wer === "ide" ? "repo" : "box")
|
||||
if (r.tiefe) setTiefe(r.tiefe)
|
||||
setVorgeschlagen(true)
|
||||
} catch { /* stumm — die Voreinstellung bleibt einfach stehen */ }
|
||||
}, 700)
|
||||
return () => { verworfen = true; clearTimeout(timer) }
|
||||
}, [text])
|
||||
|
||||
// Polling-Log nur aktivieren, wenn Idee triage/running und geöffnet
|
||||
const logEnabled = openLog !== null && (data?.items.find((i) => i.id === openLog)?.status === "triage" || data?.items.find((i) => i.id === openLog)?.status === "running")
|
||||
const logData = useIdeenLog(openLog ?? "", logEnabled)
|
||||
|
||||
async function submit() {
|
||||
const t = text.trim()
|
||||
if (t.length < 3 || sending) return
|
||||
setSending(true)
|
||||
try {
|
||||
// Lange Eingaben: Kurzfassung als Titel, alles als Notiz — der Specifier der Box
|
||||
// macht daraus ohnehin einen sauberen Auftrag.
|
||||
const titel = t.length > 160 ? t.slice(0, 157) + "…" : t
|
||||
await api("/api/ideen", { method: "POST", body: JSON.stringify({
|
||||
titel, notiz: t.length > 160 ? t : "",
|
||||
// Abbildung auf die unveränderte API. Tiefe und Ziel gehen NUR beim Repo-Weg mit —
|
||||
// beim Durchdenken fragt die Weiter-Leiste sie später beim „gefällt mir", und dann
|
||||
// weiss man mehr als jetzt.
|
||||
welt: isIde ? "ide" : "box",
|
||||
tiefe: isProjekt ? (tiefe === "gruendlich" ? "gruendlich" : "simpel") : "",
|
||||
ziel: isProjekt ? ziel : "",
|
||||
art: modus === "denken" ? "idee" : "projekt",
|
||||
}) })
|
||||
quoteZaehlen("gesendet")
|
||||
setText("")
|
||||
setVorgeschlagen(false)
|
||||
setOffen(false)
|
||||
reload()
|
||||
} catch (e: any) {
|
||||
showAlert("Das hat nicht geklappt", String(e?.message || e))
|
||||
} finally {
|
||||
setSending(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Sammel-Aufräumen: fertige Karten wegräumen, ohne 18-mal auf das X zu klicken.
|
||||
// Fragt vorher nach — und sagt dabei ehrlich, dass nichts gelöscht wird, sondern
|
||||
// ins Kanban-Archiv wandert. Das Backend fasst ausschliesslich `done` an.
|
||||
function archiveAlle(projekt: string, anzahl: number, was: string) {
|
||||
showConfirm(
|
||||
`${anzahl} fertige Karte${anzahl === 1 ? "" : "n"} wegräumen?`,
|
||||
`${was} verschwindet aus der Liste. Gelöscht wird nichts — die Karten wandern ins ` +
|
||||
`Archiv der Queue und bleiben dort lesbar. Laufende, hängende und wartende Karten ` +
|
||||
`bleiben unberührt.`,
|
||||
async () => {
|
||||
const key = "arch:" + (projekt || "*")
|
||||
setBusy((b) => ({ ...b, [key]: true }))
|
||||
try {
|
||||
const r = await api<{ archiviert: number; fehlgeschlagen: { titel: string }[] }>(
|
||||
"/api/ideen/archivieren-alle", { method: "POST", body: JSON.stringify({ projekt }) })
|
||||
if (r.fehlgeschlagen?.length) {
|
||||
showAlert("Teilweise erledigt",
|
||||
`${r.archiviert} weggeräumt, ${r.fehlgeschlagen.length} nicht: ` +
|
||||
r.fehlgeschlagen.map((f) => f.titel).join(", "))
|
||||
}
|
||||
reload()
|
||||
} catch (e: any) {
|
||||
showAlert("Das hat nicht geklappt", String(e?.message || e))
|
||||
} finally {
|
||||
setBusy((b) => ({ ...b, [key]: false }))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
async function archive(id: string) {
|
||||
setBusy((b) => ({ ...b, [id]: true }))
|
||||
try {
|
||||
await api("/api/ideen/archivieren", { method: "POST", body: JSON.stringify({ id }) })
|
||||
reload()
|
||||
} catch (e: any) {
|
||||
showAlert("Das hat nicht geklappt", String(e?.message || e))
|
||||
} finally {
|
||||
setBusy((b) => ({ ...b, [id]: false }))
|
||||
}
|
||||
}
|
||||
|
||||
// Karten-Steuerung (Stopp / Neuer Versuch / Prio) — ein Helfer für alle drei Endpunkte.
|
||||
async function steuer(prefix: string, id: string, pfad: string, body: object) {
|
||||
setBusy((b) => ({ ...b, [prefix + id]: true }))
|
||||
try {
|
||||
await api(pfad, { method: "POST", body: JSON.stringify(body) })
|
||||
reload()
|
||||
} catch (e: any) {
|
||||
showAlert("Das hat nicht geklappt", String(e?.message || e))
|
||||
} finally {
|
||||
setBusy((b) => ({ ...b, [prefix + id]: false }))
|
||||
}
|
||||
}
|
||||
|
||||
// Hängende Aufgabe beantworten: die Antwort geht als Kommentar an die Karte, der Worker
|
||||
// macht damit weiter (Ausweg aus der Sackgasse „fragt, aber niemand kann antworten").
|
||||
async function answer(id: string) {
|
||||
const t = (antworten[id] || "").trim()
|
||||
if (t.length < 1 || busy["ans:" + id]) return
|
||||
setBusy((b) => ({ ...b, ["ans:" + id]: true }))
|
||||
try {
|
||||
await api("/api/ideen/antwort", { method: "POST", body: JSON.stringify({ id, antwort: t }) })
|
||||
setAntworten((a) => ({ ...a, [id]: "" }))
|
||||
reload()
|
||||
} catch (e: any) {
|
||||
showAlert("Das hat nicht geklappt", String(e?.message || e))
|
||||
} finally {
|
||||
setBusy((b) => ({ ...b, ["ans:" + id]: false }))
|
||||
}
|
||||
}
|
||||
|
||||
// EINE Zeilen-Darstellung für alle Sichten (Projekt-Gruppen + Einzelkarten) — damit
|
||||
// Antwortfeld, Live-Log und Ergebnis-Panel überall identisch funktionieren.
|
||||
const zeile = (it: IdeenItem) => {
|
||||
const ui = IDEEN_STATE[it.status] ?? { label: it.status, cls: "text-muted-foreground border-border/60 bg-background/30" }
|
||||
const isOpen = openLog === it.id
|
||||
// Aufklappbar: laufende Karten zeigen das Live-Log, FERTIGE das Ergebnis
|
||||
// (User-Wunsch 15.07.: „Dinge werden getestet, ich sehe das Ergebnis nicht").
|
||||
const canOpen = it.status === "triage" || it.status === "running" || it.status === "done"
|
||||
const blocked = it.status === "blocked"
|
||||
const answering = !!busy["ans:" + it.id]
|
||||
// Hänger-Verdacht: läuft, aber seit >10 min kein Lebenszeichen (Heartbeat) — der
|
||||
// Prozess kann trotzdem „leben" (wartet z. B. auf einen selbst gestarteten Server).
|
||||
const haengt = it.status === "running" && (it.puls_alter ?? 0) > 600
|
||||
const wartend = it.status === "todo" || it.status === "ready" || it.status === "scheduled"
|
||||
return (
|
||||
<li key={it.id} className={cn("flex flex-col gap-2 rounded-xl border bg-background/30 px-3 py-2", blocked ? "border-red-500/30" : haengt ? "border-amber-500/40" : "border-border/40")}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn("flex shrink-0 items-center gap-1.5 rounded-lg border px-2 py-0.5 text-[10px] font-bold", ui.cls)}>
|
||||
{ui.spin && <Loader2 className="h-3 w-3 animate-spin" />}
|
||||
{ui.label}
|
||||
</span>
|
||||
{haengt && (
|
||||
<span className="flex shrink-0 items-center gap-1 rounded-lg border border-amber-500/40 bg-amber-500/10 px-2 py-0.5 text-[10px] font-bold text-amber-300"
|
||||
title="Der Worker lebt, gibt aber kein Lebenszeichen mehr — vermutlich wartet er auf etwas, das nie kommt. „Neuer Versuch“ startet die Karte frisch.">
|
||||
<AlertTriangle className="h-3 w-3" /> {Math.round((it.puls_alter ?? 0) / 60)} min ohne Regung
|
||||
</span>
|
||||
)}
|
||||
{(it.prio ?? 0) !== 0 && (
|
||||
<span className="shrink-0 rounded bg-background/50 px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground"
|
||||
title="Priorität — höher kommt zuerst dran (bei gleicher Prio: Erstellzeit)">P{it.prio}</span>
|
||||
)}
|
||||
{(() => {
|
||||
const a = ART_UI[it.art || (it.welt === "ide" ? "projekt" : "box")]
|
||||
if (!a) return null
|
||||
const Icon = a.icon
|
||||
return (
|
||||
<span className={cn("flex shrink-0 items-center gap-1 rounded-lg border px-2 py-0.5 text-[10px] font-bold", a.cls)} title={a.title}>
|
||||
<Icon className="h-3 w-3" /> {a.label}
|
||||
</span>
|
||||
)
|
||||
})()}
|
||||
{/* Tooltip = der volle TITEL (der ist abgeschnitten), nicht der Auftragstext —
|
||||
der beginnt mit Vorspann-Textbausteinen und half beim Lesen nie. */}
|
||||
<span className="min-w-0 flex-1 truncate text-xs text-foreground" title={it.titel}>{it.titel}</span>
|
||||
<span className="flex shrink-0 items-center gap-1 text-[10px] text-muted-foreground/70"><Clock className="h-3 w-3" />{fmtWhen(it.erstellt)}</span>
|
||||
{wartend && (
|
||||
<span className="flex shrink-0 items-center">
|
||||
<button
|
||||
disabled={!!busy["pr:" + it.id]}
|
||||
onClick={() => steuer("pr:", it.id, "/api/ideen/prio", { id: it.id, richtung: "hoch" })}
|
||||
title="Vorziehen (Prio +1)"
|
||||
className="rounded-md p-1 text-muted-foreground/60 transition-colors hover:text-foreground cursor-pointer disabled:opacity-50">
|
||||
<ArrowUp className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
disabled={!!busy["pr:" + it.id]}
|
||||
onClick={() => steuer("pr:", it.id, "/api/ideen/prio", { id: it.id, richtung: "runter" })}
|
||||
title="Zurückstellen (Prio −1)"
|
||||
className="rounded-md p-1 text-muted-foreground/60 transition-colors hover:text-foreground cursor-pointer disabled:opacity-50">
|
||||
<ArrowDown className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
{(it.status === "running" || blocked) && (
|
||||
<button
|
||||
disabled={!!busy["rt:" + it.id]}
|
||||
onClick={() =>
|
||||
showConfirm("Neuer Versuch?",
|
||||
it.status === "running"
|
||||
? `Der laufende Worker von '${it.titel.slice(0, 60)}' wird beendet (samt gestarteter Test-Prozesse) und die Karte frisch gestartet. Bisheriger Zwischenstand im Workspace bleibt liegen.`
|
||||
: `Die Karte '${it.titel.slice(0, 60)}' wird entsperrt und läuft weiter, sobald ein Platz frei ist.`,
|
||||
() => steuer("rt:", it.id, "/api/ideen/retry", { id: it.id }))}
|
||||
title="Neuer Versuch — Karte frisch starten"
|
||||
className={cn("shrink-0 rounded-md p-1 transition-colors cursor-pointer disabled:opacity-50",
|
||||
haengt ? "text-amber-300 hover:text-amber-200" : "text-muted-foreground/60 hover:text-foreground")}>
|
||||
{busy["rt:" + it.id] ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <RotateCcw className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
)}
|
||||
{(it.status === "running" || wartend || it.status === "triage") && (
|
||||
<button
|
||||
disabled={!!busy["st:" + it.id]}
|
||||
onClick={() =>
|
||||
showConfirm("Karte stoppen?",
|
||||
`'${it.titel.slice(0, 60)}' wird angehalten${it.status === "running" ? " und der laufende Worker beendet" : ""}. Sie bleibt als gestoppt im Board und kann jederzeit per „Neuer Versuch“ weiterlaufen.`,
|
||||
() => steuer("st:", it.id, "/api/ideen/stopp", { id: it.id }))}
|
||||
title="Stoppen — Karte anhalten"
|
||||
className="shrink-0 rounded-md p-1 text-muted-foreground/60 transition-colors hover:text-red-300 cursor-pointer disabled:opacity-50">
|
||||
{busy["st:" + it.id] ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Square className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
)}
|
||||
{(it.status === "done" || blocked) && (
|
||||
<button
|
||||
disabled={!!busy[it.id]}
|
||||
onClick={() => archive(it.id)}
|
||||
title={blocked ? "Aufgeben und aus der Liste räumen (wandert ins Kanban-Archiv)" : "Aus der Liste räumen (wandert ins Kanban-Archiv)"}
|
||||
className="shrink-0 rounded-md p-1 text-muted-foreground/60 transition-colors hover:text-foreground cursor-pointer disabled:opacity-50">
|
||||
{busy[it.id] ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <X className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
)}
|
||||
{it.welt === "ide" && (it.status === "running" || it.status === "done") && (
|
||||
<button
|
||||
onClick={() => setOpenKonzept(openKonzept === it.id ? null : it.id)}
|
||||
title="Konzept ansehen (das ausgearbeitete KONZEPT.md)"
|
||||
className={cn("flex shrink-0 items-center gap-1 rounded-md px-1.5 py-1 text-[10px] font-bold transition-colors cursor-pointer",
|
||||
openKonzept === it.id ? "text-violet-300" : "text-violet-300/70 hover:text-violet-200")}>
|
||||
<FileText className="h-3.5 w-3.5" /> Konzept
|
||||
</button>
|
||||
)}
|
||||
{canOpen && (
|
||||
<button
|
||||
onClick={() => setOpenLog(isOpen ? null : it.id)}
|
||||
title={it.status === "done" ? "Ergebnis anzeigen" : "Worker-Log anzeigen"}
|
||||
className="shrink-0 rounded-md p-1 text-muted-foreground/60 transition-colors hover:text-foreground cursor-pointer">
|
||||
{isOpen ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{openKonzept === it.id && (
|
||||
<KonzeptPanel
|
||||
id={it.id}
|
||||
istIdee={it.art === "idee" && it.status === "done"}
|
||||
onWeiter={reload}
|
||||
/>
|
||||
)}
|
||||
|
||||
{it.status === "running" && it.notiz && (
|
||||
<p className="-mt-1 ml-1 text-[11px] italic leading-relaxed text-sky-200/80" title="Jüngste Zwischenmeldung des Workers (Heartbeat)">
|
||||
„{it.notiz}“
|
||||
</p>
|
||||
)}
|
||||
{(it.status === "todo" || it.status === "scheduled" || it.status === "ready") && (it.wartet_auf?.length ?? 0) > 0 && (
|
||||
<p className="-mt-1 ml-1 text-[10px] text-muted-foreground/70">
|
||||
→ wartet auf: {it.wartet_auf!.join(" · ")}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{blocked && (
|
||||
<div className="space-y-2 border-t border-red-500/20 pt-2">
|
||||
<p className="flex items-start gap-1.5 text-[11px] leading-relaxed text-red-200/90">
|
||||
<HelpCircle className="mt-0.5 h-3.5 w-3.5 shrink-0 text-red-300" />
|
||||
<span>
|
||||
<b>{FRAGE_HINT[it.frage_kind || ""] ?? "Die Aufgabe hängt und wartet auf dich:"}</b>{" "}
|
||||
{it.frage
|
||||
? <span className="text-foreground/90">{it.frage}</span>
|
||||
: <span className="text-muted-foreground italic">kein genauer Grund hinterlegt — antworte einfach mit einer Entscheidung, dann läuft sie weiter.</span>}
|
||||
</span>
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
value={antworten[it.id] || ""}
|
||||
onChange={(e) => setAntworten((a) => ({ ...a, [it.id]: e.target.value }))}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") answer(it.id) }}
|
||||
placeholder="Deine Antwort — die Box macht damit weiter"
|
||||
maxLength={2000}
|
||||
className="h-8 min-w-0 flex-1 rounded-lg border border-border/60 bg-background/60 px-2.5 text-xs text-foreground placeholder:text-muted-foreground/50 focus:border-red-400/50 focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
onClick={() => answer(it.id)}
|
||||
disabled={answering || (antworten[it.id] || "").trim().length < 1}
|
||||
className="flex h-8 shrink-0 items-center gap-1.5 rounded-lg border border-emerald-500/40 bg-emerald-500/10 px-3 text-[10px] font-bold uppercase tracking-wide text-emerald-300 transition-all hover:bg-emerald-500/20 cursor-pointer disabled:opacity-50">
|
||||
{answering ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Send className="h-3.5 w-3.5" />} Antworten & weiter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isOpen && canOpen && it.status !== "done" && (
|
||||
<div className="ml-2 -mt-1">
|
||||
<div className="rounded-lg border border-border/50 bg-background/50 p-2">
|
||||
{logEnabled && logData.isLoading ? (
|
||||
<p className="text-[10px] text-muted-foreground animate-pulse">Log wird geladen …</p>
|
||||
) : logData.error ? (
|
||||
<p className="text-[10px] text-red-400">Log nicht ladbar</p>
|
||||
) : logData.data?.available && logData.data.lines.length > 0 ? (
|
||||
<pre className="max-h-64 overflow-auto font-mono text-[10px] leading-relaxed text-foreground/80 whitespace-pre-wrap">
|
||||
{logData.data.lines.join("\n")}
|
||||
</pre>
|
||||
) : (
|
||||
<p className="text-[10px] text-muted-foreground">Kein Log verfügbar</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isOpen && it.status === "done" && <ErgebnisPanel id={it.id} />}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
// Windows-Dev / Queue nicht verfügbar → Sektion still weglassen (wie der Rest der Seite).
|
||||
if (data && !data.available) return null
|
||||
|
||||
return (
|
||||
<section className="space-y-3">
|
||||
{/* Kopfzeile sagt in einem Blick, was DICH betrifft — vorher stand hier nur eine
|
||||
Gesamtzahl „offen", die nichts darüber verriet, ob etwas auf dich wartet.
|
||||
Rechts daneben das Sammel-Aufräumen, das nur erscheint, wenn es etwas zu räumen gibt. */}
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<SectionLabel icon={Lightbulb}>
|
||||
{(() => {
|
||||
if (!data) return "Ideen-Queue"
|
||||
const n = (s: string[]) => items.filter((i) => s.includes(i.status)).length
|
||||
const teile = [
|
||||
n(["blocked"]) > 0 ? `${n(["blocked"])} braucht dich` : "",
|
||||
n(["running", "triage", "review"]) > 0 ? `${n(["running", "triage", "review"])} in Arbeit` : "",
|
||||
n(["todo", "scheduled", "ready"]) > 0 ? `${n(["todo", "scheduled", "ready"])} wartet` : "",
|
||||
n(["done"]) > 0 ? `${n(["done"])} fertig` : "",
|
||||
].filter(Boolean)
|
||||
// Nur der Zustand, nicht nochmal der Seitenname: „Ideen" steht bereits als
|
||||
// Überschrift darüber und ein drittes Mal in der Kopfzeile der App.
|
||||
return teile.length ? teile.join(" · ") : "noch nichts in der Queue"
|
||||
})()}
|
||||
</SectionLabel>
|
||||
{(() => {
|
||||
const fertig = items.filter((i) => i.status === "done").length
|
||||
if (fertig === 0) return null
|
||||
return (
|
||||
<button
|
||||
disabled={!!busy["arch:*"]}
|
||||
onClick={() => archiveAlle("", fertig, `Alle ${fertig} fertigen Karten`)}
|
||||
title="Räumt ALLE fertigen Karten weg — auch die in abgeschlossenen Projekt-Ketten. Laufende, hängende und wartende bleiben unberührt."
|
||||
className="flex shrink-0 items-center gap-1.5 rounded-lg border border-border/60 px-2.5 py-1 text-[11px] font-semibold text-muted-foreground transition-all hover:border-primary/40 hover:text-foreground disabled:opacity-50 cursor-pointer">
|
||||
{busy["arch:*"] ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <X className="h-3.5 w-3.5" />}
|
||||
{fertig} fertige wegräumen
|
||||
</button>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
<div className="mc-card p-4 space-y-4">
|
||||
{/* Schreiben zuerst. Alles darunter ist eine Entscheidung ZU dem, was hier steht —
|
||||
nicht davor. Der Knopf heisst immer gleich; was passiert, steht bei den Karten. */}
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
value={text}
|
||||
onChange={(e) => { setText(e.target.value); setVorgeschlagen(false) }}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") submit() }}
|
||||
placeholder="Was soll entstehen?"
|
||||
className="h-10 min-w-0 flex-1 rounded-lg border border-border/60 bg-background/50 px-3 text-sm text-foreground placeholder:text-muted-foreground/50 focus:border-primary/50 focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
onClick={submit}
|
||||
disabled={sending || text.trim().length < 3}
|
||||
className="flex h-10 shrink-0 items-center gap-1.5 rounded-lg border border-primary/40 bg-primary/10 px-4 text-xs font-semibold text-primary transition-all hover:bg-primary/20 disabled:opacity-40 cursor-pointer">
|
||||
{sending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
|
||||
Los
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* ZUGEKLAPPT STATT ASSISTENT. Der Entscheidungsbaum ist derselbe, den ein Wizard
|
||||
abfragen wuerde — Wer baut das?, und NUR im IDE-Ast: wie startest du? Nur laeufst
|
||||
du ihn erst, wenn die Einordnung danebenlag. Ein Assistent wuerde dich fragen, was
|
||||
die Box in ~500 ms schon weiss; das hier fragt nur im Zweifelsfall.
|
||||
IMMER sichtbar (26.07., User-Befund): vorher hing der ganze Block an
|
||||
`text.length >= 3` — bei leerem Feld gab es also keinerlei Hinweis, dass es
|
||||
ueberhaupt eine Wahl gibt. Ein Mechanismus, den man nicht sieht, existiert fuer
|
||||
den Nutzer nicht. Ohne Text steht dort die Voreinstellung, klar als solche
|
||||
benannt, und laesst sich schon vorher aendern. */}
|
||||
{(
|
||||
offen ? (
|
||||
<div className="space-y-3 rounded-xl border border-border/40 bg-background/20 p-3">
|
||||
<div>
|
||||
<p className="mb-1.5 text-[11px] text-muted-foreground/70">Wer baut das?</p>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<Wahl aktiv={modus !== "box"}
|
||||
onWahl={() => setModus(modus === "box" ? "denken" : modus)}
|
||||
k={{ id: "ide", icon: Code2, titel: "Ich, in der Agentic IDE",
|
||||
text: "Du bekommst Konzept und Repo — den Code schreibst du selbst." }} />
|
||||
<Wahl aktiv={modus === "box"}
|
||||
onWahl={() => setModus("box")}
|
||||
k={{ id: "box", icon: Hammer, titel: "Die Box, allein",
|
||||
text: "Für dein Heimlabor. Das Ergebnis kommt als Patch-Vorschlag ins Auftragsbuch." }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Die zweite Frage gibt es NUR im IDE-Ast — eingerückt, damit die Verschachtelung
|
||||
sichtbar ist. Bei „die Box" folgt keine Rückfrage: dann geht es direkt los. */}
|
||||
{modus !== "box" && (
|
||||
<div className="border-l-2 border-primary/40 pl-3">
|
||||
<p className="mb-1.5 text-[11px] text-muted-foreground/70">Und wie startest du?</p>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<Wahl aktiv={modus === "denken"} onWahl={() => setModus("denken")}
|
||||
k={{ id: "denken", icon: Lightbulb, titel: "Erst durchdenken",
|
||||
text: "Konzept zuerst — auf dein Ja wird ein Repo daraus." }} />
|
||||
<Wahl aktiv={modus === "repo"} onWahl={() => setModus("repo")}
|
||||
k={{ id: "repo", icon: GitFork, titel: "Direkt ein Repo",
|
||||
text: "Konzept und Gitea-Repo in einem Zug." }} />
|
||||
</div>
|
||||
{modus === "repo" ? (
|
||||
<div className="mt-2.5 flex flex-wrap items-center gap-x-3 gap-y-2">
|
||||
<span className="text-[11px] text-muted-foreground/70">Wie gründlich?</span>
|
||||
<div className="flex rounded-lg border border-border/60 p-0.5">
|
||||
{([["schlank", "schlank"], ["gruendlich", "gründlich"]] as const).map(([id, label]) => (
|
||||
<button key={id} onClick={() => setTiefe(id)}
|
||||
className={cn("rounded px-2.5 py-1 text-[10px] font-bold uppercase tracking-wide transition-all cursor-pointer",
|
||||
tiefe === id ? "bg-primary/15 text-primary" : "text-muted-foreground/60 hover:text-foreground")}>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<span className="ml-auto flex items-center gap-1.5">
|
||||
<span className="text-[11px] text-muted-foreground/70">Läuft als</span>
|
||||
<div className="flex rounded-lg border border-border/40 p-0.5">
|
||||
{([["lxc", "LXC"], ["docker", "Docker"]] as const).map(([id, label]) => (
|
||||
<button key={id} onClick={() => setZiel(id)}
|
||||
title={id === "lxc"
|
||||
? "Dienst in einem eigenen Proxmox-LXC (systemd, kein Docker). Wird vom Backup-Server mitgesichert."
|
||||
: "Natives Docker: Dockerfile + compose, portabel und pipeline-tauglich."}
|
||||
className={cn("rounded px-2 py-0.5 text-[10px] font-semibold transition-all cursor-pointer",
|
||||
ziel === id ? "bg-foreground/10 text-foreground" : "text-muted-foreground/50 hover:text-foreground/80")}>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-2 flex items-start gap-1.5 text-[10px] leading-relaxed text-muted-foreground/60">
|
||||
<HelpCircle className="mt-px h-3 w-3 shrink-0" />
|
||||
Wie gründlich und wo es läuft, fragt die Box erst beim „gefällt mir" — dann weißt du mehr.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Beweisbar statt behauptet: die Quote, an der dieser ganze Entwurf haengt. */}
|
||||
{(() => {
|
||||
const q = quoteLesen()
|
||||
if (q.gesendet < 3) return null
|
||||
return (
|
||||
<p className="border-t border-border/30 pt-2 text-[10px] text-muted-foreground/50">
|
||||
Du hast {q.geaendert} von {q.gesendet} Vorschlägen geändert. Wird das die Regel,
|
||||
bauen wir hier einen richtigen Schritt-für-Schritt-Assistenten.
|
||||
</p>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-wrap items-center gap-2 rounded-xl border border-border/40 bg-background/20 px-3 py-2.5 text-xs">
|
||||
{modus === "box" ? <Hammer className="h-4 w-4 shrink-0 text-primary" />
|
||||
: modus === "repo" ? <GitFork className="h-4 w-4 shrink-0 text-primary" />
|
||||
: <Lightbulb className="h-4 w-4 shrink-0 text-primary" />}
|
||||
{/* Ohne Einordnung ist der Satz eine EINSTELLUNG, keine Aussage über deinen
|
||||
Text — das muss dranstehen, sonst behauptet die Zeile ein Verständnis,
|
||||
das es (noch) nicht gibt. */}
|
||||
{!vorgeschlagen && (
|
||||
<span className="shrink-0 text-[10px] uppercase tracking-wider text-muted-foreground/50">
|
||||
Voreingestellt
|
||||
</span>
|
||||
)}
|
||||
<span className="text-muted-foreground">
|
||||
{modus === "box" ? "Ich baue das selbst und lege dir das Ergebnis als Vorschlag hin."
|
||||
: modus === "repo" ? "Du baust selbst — ich lege dir Konzept und Repo an."
|
||||
: "Du baust selbst — ich arbeite erst ein Konzept aus."}
|
||||
</span>
|
||||
{vorgeschlagen && (
|
||||
<span className="shrink-0 rounded border border-primary/40 bg-primary/10 px-1.5 py-0.5 text-[10px] font-bold text-primary">
|
||||
gelesen
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => { setOffen(true); quoteZaehlen("geaendert") }}
|
||||
className="ml-auto shrink-0 text-[11px] text-muted-foreground/60 underline underline-offset-2 transition-colors hover:text-foreground cursor-pointer">
|
||||
anders
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
{isLoading && (
|
||||
<p className="flex items-center gap-2 text-xs text-muted-foreground"><Loader2 className="h-3.5 w-3.5 animate-spin" /> Queue wird geladen …</p>
|
||||
)}
|
||||
{data?.fehler && (
|
||||
<p className="text-xs text-amber-300">Queue gerade nicht lesbar — neuer Versuch läuft.</p>
|
||||
)}
|
||||
|
||||
{items.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{(data?.projekte ?? []).map((p) => (
|
||||
<ProjektGruppe key={p.key} projekt={p} karten={items.filter((i) => i.projekt === p.key)}
|
||||
zeile={zeile} onArchivAlle={archiveAlle} raeumt={!!busy["arch:" + p.key]} />
|
||||
))}
|
||||
{(() => {
|
||||
// Einzelkarten (keine Kette). Bis 25.07. eine flache Erstellzeit-Liste — bei
|
||||
// 19 Karten, davon 16 fertig, gingen die drei relevanten darin unter. Jetzt
|
||||
// dieselbe Ordnung, die ProjektGruppe für Ketten längst benutzt: aktiv zuerst
|
||||
// nach AKTIV_RANG, Wartende darunter, Fertige eingeklappt.
|
||||
const keys = new Set((data?.projekte ?? []).map((p) => p.key))
|
||||
const einzeln = items.filter((i) => !i.projekt || !keys.has(i.projekt))
|
||||
if (einzeln.length === 0) return null
|
||||
const aktiv = einzeln
|
||||
.filter((i) => AKTIV_RANG[i.status] !== undefined)
|
||||
.sort((a, b) => (AKTIV_RANG[a.status] - AKTIV_RANG[b.status]) || ((a.erstellt ?? 0) - (b.erstellt ?? 0)))
|
||||
const wartend = einzeln.filter((i) => ["todo", "scheduled"].includes(i.status))
|
||||
const fertig = einzeln.filter((i) => i.status === "done")
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{aktiv.length > 0 && <ul className="space-y-1.5">{aktiv.map((it) => zeile(it))}</ul>}
|
||||
{wartend.length > 0 && (
|
||||
<details className="group">
|
||||
<summary className="flex cursor-pointer list-none items-center gap-1.5 py-1 text-[11px] text-muted-foreground/70 hover:text-foreground">
|
||||
<ChevronRight className="h-3 w-3 transition-transform group-open:rotate-90" />
|
||||
<Clock className="h-3 w-3" /> Wartet ({wartend.length})
|
||||
</summary>
|
||||
<ul className="mt-1.5 space-y-1.5">{wartend.map((it) => zeile(it))}</ul>
|
||||
</details>
|
||||
)}
|
||||
{fertig.length > 0 && (
|
||||
<details className="group">
|
||||
<summary className="flex cursor-pointer list-none items-center gap-1.5 py-1 text-[11px] text-muted-foreground/70 hover:text-foreground">
|
||||
<ChevronRight className="h-3 w-3 transition-transform group-open:rotate-90" />
|
||||
<Check className="h-3 w-3 text-emerald-400/70" /> Fertig ({fertig.length})
|
||||
</summary>
|
||||
<ul className="mt-1.5 space-y-1.5">{fertig.map((it) => zeile(it))}</ul>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
{data?.available && !isLoading && items.length === 0 && (
|
||||
// Leerer Zustand als Einladung statt als Feststellung: genau jetzt möchte man
|
||||
// wissen, was man überhaupt hineinschreiben kann. Ein Klick füllt das Feld —
|
||||
// die Box ordnet den Beispieltext dann selbst ein.
|
||||
<div className="space-y-2">
|
||||
<p className="text-[11px] text-muted-foreground/70">Noch nichts hier. Zum Beispiel:</p>
|
||||
{[
|
||||
"Ein kleines Werkzeug, das meine Fotos nach Aufnahmedatum in Ordner sortiert",
|
||||
"Rippy soll die Untertitel-Spur automatisch auswählen statt zu fragen",
|
||||
"Lohnt sich ein Dashboard für die Stromzähler, oder reicht Home Assistant?",
|
||||
].map((b) => (
|
||||
<button key={b} onClick={() => setText(b)}
|
||||
className="flex w-full items-center gap-2 rounded-lg border border-border/40 bg-background/20 px-3 py-2 text-left text-xs text-muted-foreground transition-all hover:border-primary/40 hover:text-foreground cursor-pointer">
|
||||
<Lightbulb className="h-3.5 w-3.5 shrink-0 text-muted-foreground/50" />
|
||||
{b}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{dialogElement}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
// Eine der drei Auswahl-Karten. Der Text steht AN der Option, nicht in einer Legende
|
||||
// darunter — man liest nur, was zu dem gehört, was man gerade anschaut.
|
||||
|
||||
export function IdeenView() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent">
|
||||
Ideen
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Schreib hin, was entstehen soll. Die Box liest mit und schlägt vor, wie es weitergeht —
|
||||
ändern kannst du das immer.
|
||||
</p>
|
||||
</div>
|
||||
<IdeenSection />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Konzept-Panel einer Idee: zeigt den Entwurf, den Lucy geschrieben hat, und bietet
|
||||
// den naechsten Schritt an (nachschaerfen oder als Projekt bauen).
|
||||
//
|
||||
// Herausgeschnitten aus views/IdeenView.tsx (1031 Zeilen) beim v3-Umbau P5, Befund B-06.
|
||||
// Der Inhalt ist UNVERAENDERT — nur die Datei ist neu. So bleibt der Diff pruefbar.
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { FileText, GitFork, RefreshCw } from "lucide-react"
|
||||
import { api, type IdeenKonzept } from "@/lib/api"
|
||||
import { Markdown } from "@/lib/markdown"
|
||||
import { WeiterLeiste } from "./WeiterLeiste"
|
||||
|
||||
export function KonzeptPanel({ id, istIdee, onWeiter }: {
|
||||
id: string
|
||||
istIdee?: boolean
|
||||
onWeiter?: () => void
|
||||
}) {
|
||||
const [data, setData] = useState<IdeenKonzept | null>(null)
|
||||
const [fehler, setFehler] = useState(false)
|
||||
const [versuch, setVersuch] = useState(0)
|
||||
useEffect(() => {
|
||||
let weg = false
|
||||
setData(null); setFehler(false)
|
||||
api<IdeenKonzept>(`/api/ideen/${id}/konzept`)
|
||||
.then((r) => { if (!weg) setData(r) })
|
||||
.catch(() => { if (!weg) setFehler(true) })
|
||||
return () => { weg = true }
|
||||
}, [id, versuch])
|
||||
return (
|
||||
<div className="ml-2 -mt-1">
|
||||
<div className="rounded-lg border border-violet-500/25 bg-violet-500/[0.04] p-3">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<p className="flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wide text-violet-300">
|
||||
<FileText className="h-3 w-3" /> Konzept
|
||||
</p>
|
||||
{data?.repo && (
|
||||
<a href={`https://git.tobisniceshomelab.ddnsfree.com/${data.repo}`}
|
||||
className="flex items-center gap-1 text-[10px] text-violet-300/70 hover:text-violet-200">
|
||||
<GitFork className="h-3 w-3" /> {data.repo}
|
||||
</a>
|
||||
)}
|
||||
{!data?.repo && data?.quelle === "datei" && (
|
||||
<span className="truncate font-mono text-[10px] text-muted-foreground/70"
|
||||
title="Der Worker hat kein Repo angelegt — das Konzept liegt als Datei auf der Box.">
|
||||
{data.pfad}
|
||||
</span>
|
||||
)}
|
||||
<button onClick={() => setVersuch((v) => v + 1)} title="Neu laden"
|
||||
className="ml-auto rounded p-0.5 text-muted-foreground/50 hover:text-foreground cursor-pointer">
|
||||
<RefreshCw className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
{fehler ? (
|
||||
<p className="text-[11px] text-red-400">Konzept nicht ladbar — später nochmal versuchen.</p>
|
||||
) : data === null ? (
|
||||
<p className="text-[10px] text-muted-foreground animate-pulse">Konzept wird geladen …</p>
|
||||
) : data.konzept ? (
|
||||
<div className="max-h-[28rem] overflow-y-auto pr-1">
|
||||
<Markdown text={data.konzept} />
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-[11px] text-muted-foreground italic">
|
||||
{data.error || "Noch kein Konzept — das Projekt wird gerade vorbereitet."}
|
||||
</p>
|
||||
)}
|
||||
{istIdee && data?.konzept && <WeiterLeiste id={id} onFertig={onWeiter} />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Was nach dem Lesen passieren kann. Zwei Wege, bewusst nebeneinander statt hintereinander:
|
||||
// nachschärfen (ein Punkt passt nicht — häufiger Fall, darum oben und offen) oder annehmen.
|
||||
@@ -0,0 +1,86 @@
|
||||
// Karten-Familie (verkettete Karten = ein Projekt) als Block: Fortschritt oben, dann die
|
||||
// AKTIVEN Karten in Ausfuehrungs-Reihenfolge, Wartende und Fertige zusammengeklappt.
|
||||
// Ersetzt fuer Ketten die flache Erstellzeit-Liste (User: „Reihenfolge komplett durcheinander").
|
||||
//
|
||||
// Herausgeschnitten aus views/IdeenView.tsx (1031 Zeilen) beim v3-Umbau P5, Befund B-06.
|
||||
// Der Inhalt ist UNVERAENDERT — nur die Datei ist neu. So bleibt der Diff pruefbar.
|
||||
|
||||
import { useState } from "react"
|
||||
import { ChevronDown, ChevronRight, Layers, Loader2, X } from "lucide-react"
|
||||
import type { IdeenItem, IdeenProjekt } from "@/lib/api"
|
||||
import { AKTIV_RANG } from "./zustaende"
|
||||
|
||||
export function ProjektGruppe({ projekt, karten, zeile, onArchivAlle, raeumt }: {
|
||||
projekt: IdeenProjekt
|
||||
karten: IdeenItem[]
|
||||
zeile: (it: IdeenItem) => React.ReactNode
|
||||
onArchivAlle?: (projekt: string, anzahl: number, was: string) => void
|
||||
raeumt?: boolean
|
||||
}) {
|
||||
const [zeigWartende, setZeigWartende] = useState(false)
|
||||
const [zeigFertige, setZeigFertige] = useState(false)
|
||||
const aktiv = karten
|
||||
.filter((i) => AKTIV_RANG[i.status] !== undefined)
|
||||
.sort((a, b) => (AKTIV_RANG[a.status] - AKTIV_RANG[b.status]) || ((a.erstellt ?? 0) - (b.erstellt ?? 0)))
|
||||
const wartend = karten
|
||||
.filter((i) => i.status === "todo" || i.status === "scheduled")
|
||||
.sort((a, b) => ((a.wartet_auf?.length ? 1 : 0) - (b.wartet_auf?.length ? 1 : 0)) || ((a.erstellt ?? 0) - (b.erstellt ?? 0)))
|
||||
const fertig = karten
|
||||
.filter((i) => i.status === "done")
|
||||
.sort((a, b) => (b.fertig ?? 0) - (a.fertig ?? 0))
|
||||
const anteil = projekt.gesamt > 0 ? Math.round((projekt.fertig / projekt.gesamt) * 100) : 0
|
||||
return (
|
||||
<div className="space-y-2 rounded-xl border border-border/50 bg-background/20 p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Layers className="h-4 w-4 shrink-0 text-primary/70" />
|
||||
<p className="min-w-0 flex-1 truncate text-xs font-semibold text-foreground" title={projekt.titel}>{projekt.titel}</p>
|
||||
<span className="shrink-0 text-[10px] text-muted-foreground">{projekt.fertig} von {projekt.gesamt} fertig</span>
|
||||
</div>
|
||||
<div className="h-1 overflow-hidden rounded-full bg-background/60" title={`${anteil}%`}>
|
||||
<div className="h-1 rounded-full bg-primary/70 transition-all" style={{ width: `${anteil}%` }} />
|
||||
</div>
|
||||
{aktiv.length > 0 && <ul className="space-y-1.5">{aktiv.map((it) => zeile(it))}</ul>}
|
||||
{wartend.length > 0 && (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setZeigWartende((v) => !v)}
|
||||
className="flex items-center gap-1.5 text-[11px] font-semibold text-muted-foreground hover:text-foreground transition-colors cursor-pointer">
|
||||
{zeigWartende ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
|
||||
{wartend.length} Karte{wartend.length === 1 ? "" : "n"} in der Kette — laufen automatisch der Reihe nach
|
||||
</button>
|
||||
{zeigWartende && <ul className="mt-1.5 space-y-1.5">{wartend.map((it) => zeile(it))}</ul>}
|
||||
</div>
|
||||
)}
|
||||
{fertig.length > 0 && (
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setZeigFertige((v) => !v)}
|
||||
className="flex items-center gap-1.5 text-[11px] font-semibold text-emerald-300/70 hover:text-emerald-300 transition-colors cursor-pointer">
|
||||
{zeigFertige ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
|
||||
{fertig.length} fertig
|
||||
</button>
|
||||
{/* Ganze abgeschlossene Kette in einem Zug wegräumen — bei fünf fertigen
|
||||
Projekten waren das sonst ~18 Einzelklicks auf das X. */}
|
||||
{onArchivAlle && (
|
||||
<button
|
||||
disabled={raeumt}
|
||||
onClick={() => onArchivAlle(projekt.key, fertig.length, `„${projekt.titel}"`)}
|
||||
title="Räumt die fertigen Karten dieser Kette weg (Archiv, nichts wird gelöscht)."
|
||||
className="flex items-center gap-1 text-[11px] text-muted-foreground/60 transition-colors hover:text-foreground disabled:opacity-50 cursor-pointer">
|
||||
{raeumt ? <Loader2 className="h-3 w-3 animate-spin" /> : <X className="h-3 w-3" />}
|
||||
wegräumen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{zeigFertige && <ul className="mt-1.5 space-y-1.5">{fertig.map((it) => zeile(it))}</ul>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Konzept einer IDE-Karte: holt es aus dem Gitea-Repo — oder, wenn der Worker keins
|
||||
// angelegt hat, aus seiner Datei in ~/konzepte (Bug-Fund 21.07.: dann stand hier vorher
|
||||
// „noch kein Repo hinterlegt", obwohl das Konzept längst fertig war).
|
||||
// Bei einer fertig durchdachten IDEE hängt unten der Weg nach vorn: „Gefällt mir".
|
||||
@@ -0,0 +1,27 @@
|
||||
// Eine der drei Auswahlkacheln im Ideen-Verfasser (Wer baut das? / Wie startest du?).
|
||||
//
|
||||
// Herausgeschnitten aus views/IdeenView.tsx (1031 Zeilen) beim v3-Umbau P5, Befund B-06.
|
||||
// Der Inhalt ist UNVERAENDERT — nur die Datei ist neu. So bleibt der Diff pruefbar.
|
||||
|
||||
import { Hammer } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export function Wahl({ k, aktiv, onWahl }: {
|
||||
k: { id: string; icon: typeof Hammer; titel: string; text: string }
|
||||
aktiv: boolean
|
||||
onWahl: () => void
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={onWahl}
|
||||
className={cn("rounded-xl border p-3 text-left transition-all cursor-pointer",
|
||||
aktiv ? "border-primary/60 bg-primary/5" : "border-border/50 hover:border-border")}>
|
||||
<div className="flex items-center gap-2">
|
||||
<k.icon className={cn("h-4 w-4 shrink-0", aktiv ? "text-primary" : "text-muted-foreground")} />
|
||||
<span className="text-xs font-semibold text-foreground">{k.titel}</span>
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] leading-relaxed text-muted-foreground">{k.text}</p>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
// Die Leiste unter einem fertigen Konzept: nachschaerfen oder als Projekt weiterfuehren.
|
||||
//
|
||||
// Herausgeschnitten aus views/IdeenView.tsx (1031 Zeilen) beim v3-Umbau P5, Befund B-06.
|
||||
// Der Inhalt ist UNVERAENDERT — nur die Datei ist neu. So bleibt der Diff pruefbar.
|
||||
|
||||
import { useState } from "react"
|
||||
import { Code2, Loader2, RefreshCw } from "lucide-react"
|
||||
import { api } from "@/lib/api"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export function WeiterLeiste({ id, onFertig }: { id: string; onFertig?: () => void }) {
|
||||
const [ziel, setZiel] = useState<"lxc" | "docker">("lxc")
|
||||
const [tiefe, setTiefe] = useState<"simpel" | "gruendlich">("simpel")
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [neu, setNeu] = useState<{ id: string; art: "projekt" | "runde" } | null>(null)
|
||||
const [fehler, setFehler] = useState("")
|
||||
const [hinweis, setHinweis] = useState("")
|
||||
const ruf = async (pfad: string, rumpf: object, art: "projekt" | "runde") => {
|
||||
setBusy(true); setFehler("")
|
||||
try {
|
||||
const r = await api<{ id: string }>(pfad, { method: "POST", body: JSON.stringify(rumpf) })
|
||||
setNeu({ id: r.id, art })
|
||||
onFertig?.()
|
||||
} catch (e: any) {
|
||||
setFehler(String(e?.message || e))
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
if (neu) {
|
||||
return (
|
||||
<p className="mt-3 border-t border-violet-500/20 pt-2 text-[11px] text-emerald-300">
|
||||
{neu.art === "projekt" ? (
|
||||
<>Projekt-Karte <span className="font-mono">{neu.id}</span> ist angelegt — die Box bereitet
|
||||
das Repo vor (Konzept übernimmt sie, sie denkt es nicht neu). Danach öffnest du es in der Agentic IDE.</>
|
||||
) : (
|
||||
<>Überarbeitung <span className="font-mono">{neu.id}</span> läuft — die Box arbeitet deine
|
||||
Rückmeldung ein und schreibt die neue Fassung an dieselbe Stelle. Danach hier wieder
|
||||
„Konzept" öffnen.</>
|
||||
)}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="mt-3 space-y-3 border-t border-violet-500/20 pt-2.5">
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
<b className="text-sky-300/90">Passt etwas nicht?</b> Schreib hin, was anders werden soll —
|
||||
die Box arbeitet es ein und lässt den Rest stehen. Beliebig oft wiederholbar.
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
value={hinweis}
|
||||
onChange={(e) => setHinweis(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === "Enter" && hinweis.trim().length >= 3 && !busy) ruf("/api/ideen/nachschaerfen", { id, hinweis }, "runde") }}
|
||||
placeholder="z. B. „Punkt 3: kein WebSocket, das ist mir zu fragil — nimm Polling“"
|
||||
maxLength={2000}
|
||||
className="h-8 min-w-0 flex-1 rounded-lg border border-border/60 bg-background/60 px-2.5 text-xs text-foreground placeholder:text-muted-foreground/50 focus:border-sky-400/50 focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
onClick={() => ruf("/api/ideen/nachschaerfen", { id, hinweis }, "runde")}
|
||||
disabled={busy || hinweis.trim().length < 3}
|
||||
className="flex h-8 shrink-0 items-center gap-1.5 rounded-lg border border-sky-500/40 bg-sky-500/10 px-3 text-[10px] font-bold uppercase tracking-wide text-sky-300 transition-all hover:bg-sky-500/20 cursor-pointer disabled:opacity-50">
|
||||
{busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <RefreshCw className="h-3.5 w-3.5" />}
|
||||
Nachschärfen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
<b className="text-violet-300/90">Passt es so?</b> Dann macht die Box daraus ein startklares
|
||||
Repo — Konzept wird übernommen (nicht neu gedacht), dazu Roadmap, AGENTS.md & Co.
|
||||
Gebaut wird danach von dir in der Agentic IDE.
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
|
||||
<span className="text-[11px] text-muted-foreground/70">Ziel</span>
|
||||
<div className="flex rounded-lg border border-border/40 p-0.5">
|
||||
{(["lxc", "docker"] as const).map((z) => (
|
||||
<button key={z} onClick={() => setZiel(z)}
|
||||
title={z === "lxc" ? "Dienst in einem eigenen Proxmox-LXC (systemd, kein Docker) — wird automatisch mitgesichert."
|
||||
: "Natives Docker: Dockerfile + compose, portabel und pipeline-tauglich."}
|
||||
className={cn("rounded px-2 py-0.5 text-[10px] font-semibold transition-all cursor-pointer",
|
||||
ziel === z ? "bg-foreground/10 text-foreground" : "text-muted-foreground/50 hover:text-foreground/80")}>
|
||||
{z === "lxc" ? "LXC" : "Docker"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-[11px] text-muted-foreground/70">Aufwand</span>
|
||||
<div className="flex rounded-lg border border-border/40 p-0.5">
|
||||
{(["simpel", "gruendlich"] as const).map((t) => (
|
||||
<button key={t} onClick={() => setTiefe(t)}
|
||||
title={t === "simpel" ? "Konzept ist schon gehärtet — nur kurz auf die Bau-Sicht nachschärfen (empfohlen)."
|
||||
: "Nochmal volle Kaskade über die Bau-Sicht: mehrere Rollen, Härtetests. Dauert entsprechend."}
|
||||
className={cn("rounded px-2 py-0.5 text-[10px] font-semibold transition-all cursor-pointer",
|
||||
tiefe === t ? "bg-foreground/10 text-foreground" : "text-muted-foreground/50 hover:text-foreground/80")}>
|
||||
{t === "simpel" ? "schlank" : "gründlich"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => ruf("/api/ideen/weiterfuehren", { id, ziel, tiefe }, "projekt")}
|
||||
disabled={busy}
|
||||
className="ml-auto flex h-8 shrink-0 items-center gap-1.5 rounded-lg border border-violet-500/40 bg-violet-500/10 px-3 text-[10px] font-bold uppercase tracking-wide text-violet-300 transition-all hover:bg-violet-500/20 cursor-pointer disabled:opacity-50">
|
||||
{busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Code2 className="h-3.5 w-3.5" />}
|
||||
Gefällt mir — Projekt daraus machen
|
||||
</button>
|
||||
</div>
|
||||
{fehler && <p className="text-[11px] text-red-400">{fehler}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Ergebnis einer FERTIGEN Queue-Aufgabe: die Abschluss-Zusammenfassung des Workers
|
||||
// (kanban_complete). Lazy — wird erst beim Aufklappen geladen, ändert sich nie mehr.
|
||||
@@ -0,0 +1,22 @@
|
||||
// Kleine Nutzungs-Quote der Ideen-Eingabe (v3-Umbau P5, unveraendert aus IdeenView).
|
||||
//
|
||||
// Zaehlt im localStorage mit, wie oft abgeschickt und wie oft die Einordnung korrigiert
|
||||
// wurde. Reine Bedienstatistik, kein Geheimnis — der Zaun aus P1 (ESLint-Regel gegen
|
||||
// Geheimnisse im localStorage) greift hier bewusst nicht.
|
||||
|
||||
const QUOTE_KEY = "mc_ideen_quote"
|
||||
|
||||
export function quoteLesen(): { gesendet: number; geaendert: number } {
|
||||
try {
|
||||
const d = JSON.parse(localStorage.getItem(QUOTE_KEY) || "{}")
|
||||
return { gesendet: Number(d.gesendet) || 0, geaendert: Number(d.geaendert) || 0 }
|
||||
} catch { return { gesendet: 0, geaendert: 0 } }
|
||||
}
|
||||
|
||||
export function quoteZaehlen(feld: "gesendet" | "geaendert"): void {
|
||||
try {
|
||||
const q = quoteLesen()
|
||||
q[feld] += 1
|
||||
localStorage.setItem(QUOTE_KEY, JSON.stringify(q))
|
||||
} catch { /* privater Modus o. ä. — die Zählung ist Beiwerk, nie ein Grund zu scheitern */ }
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Zustands- und Art-Tabellen der Ideen-Queue (v3-Umbau P5).
|
||||
//
|
||||
// Herausgeschnitten aus dem 1031-Zeilen-Monolithen views/IdeenView.tsx. Diese Tabellen
|
||||
// sind reine Daten und werden von Liste, Projektgruppe und Panels gebraucht — sie stehen
|
||||
// deshalb fuer sich, statt in einer der Komponenten zu wohnen.
|
||||
|
||||
import { Code2, Hammer, Lightbulb } from "lucide-react"
|
||||
|
||||
export const IDEEN_STATE: Record<string, { label: string; cls: string; spin?: boolean }> = {
|
||||
triage: { label: "wird ausgearbeitet …", cls: "text-violet-300 border-violet-500/40 bg-violet-500/10", spin: true },
|
||||
todo: { label: "wartet", cls: "text-muted-foreground border-border/60 bg-background/30" },
|
||||
scheduled: { label: "geplant", cls: "text-muted-foreground border-border/60 bg-background/30" },
|
||||
ready: { label: "gleich dran", cls: "text-sky-300/80 border-sky-500/30 bg-sky-500/5" },
|
||||
running: { label: "in Arbeit …", cls: "text-sky-300 border-sky-500/40 bg-sky-500/10", spin: true },
|
||||
review: { label: "im Review", cls: "text-amber-300 border-amber-500/40 bg-amber-500/10" },
|
||||
blocked: { label: "hängt — braucht dich", cls: "text-red-300 border-red-500/40 bg-red-500/10" },
|
||||
done: { label: "fertig ✓", cls: "text-emerald-300 border-emerald-500/40 bg-emerald-500/10" },
|
||||
}
|
||||
|
||||
// Welche Welt hat die Karte durchlaufen? Vorher stand auf Idee UND IDE-Projekt dasselbe
|
||||
// „IDE" — man sah der fertigen Karte nicht an, ob sie nur durchdacht oder als Repo
|
||||
// vorbereitet wurde (User 21.07.). Jetzt drei ehrliche Marken.
|
||||
export const ART_UI: Record<string, { label: string; cls: string; icon: typeof Hammer; title: string }> = {
|
||||
box: {
|
||||
label: "BOX", icon: Hammer,
|
||||
cls: "border-border/60 bg-background/40 text-muted-foreground",
|
||||
title: "Box-Welt: die Werkstatt baut das selbst. Das Ergebnis kommt als Patch-Vorschlag zurück — nichts geht ohne deinen Klick live.",
|
||||
},
|
||||
idee: {
|
||||
label: "IDEE", icon: Lightbulb,
|
||||
cls: "border-sky-500/40 bg-sky-500/10 text-sky-300",
|
||||
title: "Nur durchdacht: die Box hat die Produkt-Idee mit voller Denk-Kaskade zu einem Konzept ausgearbeitet. Kein Gerüst, kein Bau — du liest es und entscheidest, ob ein Projekt draus wird.",
|
||||
},
|
||||
projekt: {
|
||||
label: "IDE", icon: Code2,
|
||||
cls: "border-violet-500/40 bg-violet-500/10 text-violet-300",
|
||||
title: "IDE-Welt: die Box bereitet nur vor (Konzept + Repo + Doku, kein Code). Gebaut wird von dir in der Agentic IDE.",
|
||||
},
|
||||
}
|
||||
|
||||
// Wie die Frage einer hängenden Karte eingeleitet wird — je nach Block-Grund des Workers.
|
||||
export const FRAGE_HINT: Record<string, string> = {
|
||||
needs_input: "Die Box hat eine Frage:",
|
||||
capability: "Die Box kam hier nicht weiter:",
|
||||
transient: "Die Aufgabe hakt:",
|
||||
}
|
||||
|
||||
// Wie oft lag die Einordnung daneben? Der ganze zugeklappte Entwurf steht und fällt damit.
|
||||
// Bewusst im Browser statt im Backend: zwei Zahlen brauchen keine Tabelle, keinen Endpunkt
|
||||
// und keine Migration — und es geht niemanden ausser dem Nutzer etwas an. Sichtbar wird die
|
||||
// Quote nur beim Aufklappen, damit sie beweisbar ist statt behauptet.
|
||||
|
||||
// Sortier-Rang der AKTIVEN Karten innerhalb einer Projekt-Gruppe. Steht hier bei den
|
||||
// anderen Zustands-Tabellen, weil ihn Liste UND Projektgruppe brauchen.
|
||||
export const AKTIV_RANG: Record<string, number> = { blocked: 0, running: 1, review: 2, ready: 3, triage: 4 }
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user