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
@@ -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 }
|
||||
Reference in New Issue
Block a user