refactor(ideen): v3-Umbau P5 — der Monolith bekommt einen eigenen Bereich
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:
Hitonabi
2026-08-28 09:22:45 +02:00
co-authored by Claude Opus 5
parent b74e98ffbb
commit dcee0096fe
43 changed files with 1208 additions and 1113 deletions
@@ -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,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.
@@ -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.
File diff suppressed because one or more lines are too long
@@ -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,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.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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};
@@ -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};
File diff suppressed because one or more lines are too long
@@ -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.
@@ -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,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.
@@ -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,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.
@@ -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.
@@ -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,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,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,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,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
View File
@@ -0,0 +1 @@
import{ax as r}from"./index-C18iiJ44.js";var o=r();export{o as r};
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
import{ax as r}from"./index-Cx7RCLVH.js";var o=r();export{o as r};
@@ -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,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,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,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,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,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
View File
@@ -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>
+1 -1
View File
@@ -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 })))
+21 -6
View File
@@ -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 &amp; 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".
+27
View File
@@ -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 &amp; 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.
+22
View File
@@ -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 */ }
}
+55
View File
@@ -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