User-Befund, und er trifft einen echten Modellierungsfehler von gestern: "Eine durchgedachte Idee wird ja sowieso mit meinem Ja zu einem IDE-Repo." Stimmt — /ideen/weiterfuehren macht aus der durchdachten Idee GENAU die IDE-Projekt-Karte, die der Repo-Weg direkt anlegt. Es sind zwei Stufen EINES Weges, keine zwei Achsen. Der Haken von gestern war deshalb falsch: "Die Box, allein" + "nur denken" war anklickbar, submit() setzte dann aber still welt:"ide" und schob einen in die Zed-Welt, ohne das zu sagen. Genau die versteckte Umschaltung, die anderswo in diesem Repo als Fassade gilt. Jetzt GENAU EINE Auswahl aus drei, in zwei Bereichen: Du baust selbst, in Zed [Erst durchdenken] [Direkt ein Repo] Oder die Box macht es [Die Box baut allein] Die falsche Kombination ist damit strukturell unmoeglich, nicht nur unwahrscheinlich. Nebengewinn: beim Durchdenken entfallen Tiefe und Ziel am Eingang. Die Weiter-Leiste fragt beides ohnehin beim "gefaellt mir" — und dann weiss man, was das Ding ueberhaupt wird. Vorher entschied man LXC-oder-Docker, bevor ein Konzept existierte. Ausgewaehlt ist immer eine KARTE, nie ein Bereich — der Bereich gruppiert nur, was denselben Weg geht. Voreinstellung ist "Erst durchdenken": die Wahl, die am wenigsten anlegt. Der Einordner ueberschreibt sie ohnehin in ~500 ms. API unveraendert; alle drei Abbildungen gegen den alten Vertrag geprueft. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+129
-101
@@ -67,20 +67,23 @@ function IdeenSection() {
|
||||
const qc = useQueryClient()
|
||||
const { showAlert, showConfirm, dialogElement } = useDialog()
|
||||
const [text, setText] = useState("")
|
||||
// Zwei UNABHÄNGIGE Entscheidungen statt vier sich ausschliessender Modi (25.07.2026):
|
||||
// wer — baut die Box selbst, oder bekommst du ein Repo für Zed?
|
||||
// nurDenken — soll erst nur ein Konzept entstehen? (passt zu beidem)
|
||||
// Vorher hiessen die Knöpfe BOX/IDEE/IDE:SIMPEL/IDE:GRÜNDLICH und pressten beides in eine
|
||||
// Reihe — das brauchte 116 Wörter Beipackzettel darunter. Die Abbildung auf die
|
||||
// unveränderte API passiert in submit().
|
||||
const [wer, setWer] = useState<"box" | "zed">("box")
|
||||
// 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")
|
||||
const [nurDenken, setNurDenken] = useState(false)
|
||||
// Kam die aktuelle Auswahl von der Box? Nur fürs Etikett „Vorschlag" — sobald du selbst
|
||||
// klickst, verschwindet es, damit nie unklar ist, wessen Entscheidung da steht.
|
||||
const [vorgeschlagen, setVorgeschlagen] = useState(false)
|
||||
const isIde = wer === "zed" || nurDenken // beides läuft über das projektstart-Profil
|
||||
const isProjekt = wer === "zed" && !nurDenken // nur DANN sind Tiefe und Ziel wirksam
|
||||
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)
|
||||
@@ -105,9 +108,10 @@ function IdeenSection() {
|
||||
const r = await api<{ ok: boolean; wer?: "box" | "zed"; tiefe?: "schlank" | "gruendlich"; nur_denken?: boolean }>(
|
||||
"/api/ideen/einordnen", { method: "POST", body: JSON.stringify({ text: t }) })
|
||||
if (verworfen || !r?.ok) return
|
||||
if (r.wer) setWer(r.wer)
|
||||
// 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 === "zed" ? "repo" : "box")
|
||||
if (r.tiefe) setTiefe(r.tiefe)
|
||||
setNurDenken(!!r.nur_denken)
|
||||
setVorgeschlagen(true)
|
||||
} catch { /* stumm — die Voreinstellung bleibt einfach stehen */ }
|
||||
}, 700)
|
||||
@@ -128,12 +132,13 @@ function IdeenSection() {
|
||||
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: „nur denken" läuft immer über die IDE-Welt,
|
||||
// weil dort das Profil sitzt, das denkt statt baut.
|
||||
// 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 : "", // eine Idee braucht noch keine Plattform
|
||||
art: nurDenken ? "idee" : "projekt",
|
||||
ziel: isProjekt ? ziel : "",
|
||||
art: modus === "denken" ? "idee" : "projekt",
|
||||
}) })
|
||||
setText("")
|
||||
setVorgeschlagen(false)
|
||||
@@ -466,96 +471,90 @@ function IdeenSection() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Wer baut das? Zwei Karten statt vier Kürzel — jede sagt, was DU danach bekommst. */}
|
||||
{/* ZWEI BEREICHE, drei Karten — ausgewaehlt ist immer genau EINE Karte, nie ein
|
||||
ganzer Bereich. Der Bereich gruppiert nur, was denselben Weg geht: „durchdenken"
|
||||
und „Repo" sind zwei Stufen desselben Wegs (weiterfuehren macht aus der einen die
|
||||
andere), waehrend die Box-Welt etwas voellig anderes tut. */}
|
||||
<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">
|
||||
{([
|
||||
{ id: "box" as const, icon: Hammer, titel: "Die Box, allein",
|
||||
text: "Sie baut es und legt dir das Ergebnis als Vorschlag hin — nichts geht ohne deinen Klick live." },
|
||||
{ id: "zed" as const, icon: Code2, titel: "Ich, in Zed",
|
||||
text: "Du bekommst ein fertiges Repo mit Konzept und Doku. Den Code schreibst du selbst." },
|
||||
]).map((k) => {
|
||||
const aktiv = wer === k.id
|
||||
return (
|
||||
<button key={k.id}
|
||||
onClick={() => { setWer(k.id); setVorgeschlagen(false) }}
|
||||
disabled={nurDenken}
|
||||
className={cn("rounded-xl border p-3 text-left transition-all cursor-pointer disabled:cursor-default",
|
||||
nurDenken ? "border-border/30 opacity-40"
|
||||
: 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 && !nurDenken ? "text-primary" : "text-muted-foreground")} />
|
||||
<span className="text-xs font-semibold text-foreground">{k.titel}</span>
|
||||
{aktiv && vorgeschlagen && !nurDenken && (
|
||||
<span className="ml-auto shrink-0 rounded border border-primary/40 bg-primary/10 px-1.5 py-0.5 text-[10px] font-bold text-primary">
|
||||
Vorschlag
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] leading-relaxed text-muted-foreground">{k.text}</p>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{nurDenken && (
|
||||
<p className="mt-1.5 text-[10px] text-muted-foreground/60">
|
||||
Wer baut, entscheidest du, wenn das Konzept da ist.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<p className="mb-2 text-[11px] text-muted-foreground/70">Was soll damit passieren?</p>
|
||||
|
||||
{/* Tiefe und Ziel wirken NUR beim Repo-Weg — deshalb erscheinen sie auch nur dort.
|
||||
Ein Schalter, der nichts tut, ist schlimmer als keiner. */}
|
||||
{isProjekt && (
|
||||
<div className="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); setVorgeschlagen(false) }}
|
||||
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 className="rounded-xl border border-border/40 bg-background/20 p-3">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<Code2 className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
Du baust selbst, in Zed
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
{([
|
||||
{ id: "denken" as const, icon: Lightbulb, titel: "Erst durchdenken",
|
||||
text: "Du bekommst ein Konzept. Gefällt es dir, wird auf Klick ein Repo daraus." },
|
||||
{ id: "repo" as const, icon: GitFork, titel: "Direkt ein Repo",
|
||||
text: "Konzept und Gitea-Repo in einem Zug — wenn du es ohnehin bauen willst." },
|
||||
]).map((k) => (
|
||||
<Wahl key={k.id} k={k} aktiv={modus === k.id} vorgeschlagen={vorgeschlagen}
|
||||
onWahl={() => { setModus(k.id); setVorgeschlagen(false) }} />
|
||||
))}
|
||||
</div>
|
||||
<span className="text-[10px] text-muted-foreground/60">
|
||||
{tiefe === "gruendlich"
|
||||
? "mehrere Fach-Rollen, bis zu 3 Denk-Runden, mehrfacher Härtetest — dauert entsprechend"
|
||||
: "2 Rollen, eine Denk-Runde — ein paar Minuten"}
|
||||
</span>
|
||||
<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 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 === id ? "bg-foreground/10 text-foreground" : "text-muted-foreground/50 hover:text-foreground/80")}>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Orthogonal zu allem darüber: erst denken, bauen später entscheiden. */}
|
||||
<label className="flex cursor-pointer items-start gap-2 border-t border-border/30 pt-3">
|
||||
<input type="checkbox" checked={nurDenken}
|
||||
onChange={(e) => { setNurDenken(e.target.checked); setVorgeschlagen(false) }}
|
||||
className="mt-0.5 h-3.5 w-3.5 shrink-0 accent-sky-400 cursor-pointer" />
|
||||
<span className="min-w-0">
|
||||
<span className="text-xs font-semibold text-foreground">Erst nur durchdenken, noch nichts anlegen</span>
|
||||
<span className="mt-0.5 block text-[11px] leading-relaxed text-muted-foreground">
|
||||
Die Box arbeitet die Idee mit Produkt-Blick aus — was ist es, für wen, was gibt es
|
||||
schon, was wäre der kleinste sinnvolle Wurf, was spricht dagegen. Ergebnis ist ein
|
||||
Konzept: kein Repo, kein Code, kein Bau-Druck.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
{/* Tiefe und Ziel nur beim Repo-Weg: beim Durchdenken fragt die Weiter-Leiste
|
||||
sie spaeter — dann weisst du, was das Ding ueberhaupt wird. */}
|
||||
{modus === "repo" && (
|
||||
<div className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-2 border-t border-border/30 pt-3">
|
||||
<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); setVorgeschlagen(false) }}
|
||||
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="text-[10px] text-muted-foreground/60">
|
||||
{tiefe === "gruendlich"
|
||||
? "mehrere Fach-Rollen, bis zu 3 Denk-Runden, mehrfacher Härtetest"
|
||||
: "2 Rollen, eine Denk-Runde — ein paar Minuten"}
|
||||
</span>
|
||||
<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 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 === id ? "bg-foreground/10 text-foreground" : "text-muted-foreground/50 hover:text-foreground/80")}>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{modus === "denken" && (
|
||||
<p className="mt-2.5 flex items-start gap-1.5 border-t border-border/30 pt-2.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>
|
||||
|
||||
<div className="mt-2 rounded-xl border border-border/40 bg-background/20 p-3">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<Hammer className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
Oder die Box macht es
|
||||
</span>
|
||||
</div>
|
||||
<Wahl
|
||||
k={{ id: "box", icon: Hammer, titel: "Die Box baut allein",
|
||||
text: "Für dein Heimlabor — Mission Control, Lucy, Rippy, Wartung, Doku. Das Ergebnis kommt als Patch-Vorschlag ins Auftragsbuch; nichts geht ohne deinen Klick live." }}
|
||||
aktiv={modus === "box"} vorgeschlagen={vorgeschlagen}
|
||||
onWahl={() => { setModus("box"); setVorgeschlagen(false) }} />
|
||||
</div>
|
||||
</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>
|
||||
)}
|
||||
@@ -633,6 +632,35 @@ function IdeenSection() {
|
||||
)
|
||||
}
|
||||
|
||||
// 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.
|
||||
function Wahl({ k, aktiv, vorgeschlagen, onWahl }: {
|
||||
k: { id: string; icon: typeof Hammer; titel: string; text: string }
|
||||
aktiv: boolean
|
||||
vorgeschlagen: 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>
|
||||
{/* Etikett nur an der AUSGEWÄHLTEN Karte und nur solange der Vorschlag unberührt
|
||||
ist — sonst wäre nie klar, wessen Entscheidung gerade dasteht. */}
|
||||
{aktiv && vorgeschlagen && (
|
||||
<span className="ml-auto shrink-0 rounded border border-primary/40 bg-primary/10 px-1.5 py-0.5 text-[10px] font-bold text-primary">
|
||||
Vorschlag
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] leading-relaxed text-muted-foreground">{k.text}</p>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// Karten-Familie (verkettete Karten = ein Projekt) als Block: Fortschritt oben, dann die
|
||||
// AKTIVEN Karten in Ausführungs-Reihenfolge, Wartende und Fertige zusammengeklappt.
|
||||
// Ersetzt für Ketten die flache Erstellzeit-Liste (User: „Reihenfolge komplett durcheinander").
|
||||
|
||||
Reference in New Issue
Block a user