Queue-Steuerung: Stopp/Neuer-Versuch/Prio + Haenger-Warnung (UI+Telegram)
Karten lassen sich gezielt anhalten (generischer Block + Prozessbaum-Kill), frisch starten (Retry beendet Worker samt Kindern - uvicorn-Falle) und per Prio-Pfeilen umsortieren (Dispatcher zieht priority DESC nativ). Laufende Karten ohne Heartbeat >10 min bekommen ein Warn-Badge; der Ketten-Digest alarmiert nach 15 min per Telegram, inkl. Kontext-Verdichtungs-Hinweis. Ausloeser: Scaffold-Worker hing 45 min an selbst gestartetem uvicorn. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -523,6 +523,9 @@ export interface IdeenItem {
|
||||
wartet_auf?: string[] // Titel der offenen Vorgänger (max. 3, für die Anzeige)
|
||||
notiz?: string | null // bei running: jüngste Heartbeat-Notiz des Workers
|
||||
projekt?: string | null // Familien-Schlüssel (verkettete Karten) — null = Einzelkarte
|
||||
prio?: number // Dispatcher-Reihenfolge: priority DESC, created ASC
|
||||
puls_alter?: number | null // bei running: Sekunden seit letztem Lebenszeichen (Hänger-Warnung)
|
||||
gestartet?: number | null // bei running: started_at (Epoch)
|
||||
}
|
||||
// Karten-Familie (verkettete Karten = ein Projekt) für die gruppierte Queue-Sicht
|
||||
export interface IdeenProjekt {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react"
|
||||
import {
|
||||
Inbox, Check, X, GitBranch, FileDiff, Loader2, AlertTriangle, Sparkles,
|
||||
ChevronDown, ChevronRight, Clock, ClipboardCheck, Hammer, RefreshCw, Layers, Lightbulb, Send, HelpCircle,
|
||||
RotateCcw, Square, ArrowUp, ArrowDown,
|
||||
} from "lucide-react"
|
||||
import { api, type AuftragItem, type IdeenErgebnis, type IdeenItem, type IdeenProjekt, type SkillKandidat } from "@/lib/api"
|
||||
import { useAuftragsbuch, useIdeen, useIdeenLog, useQueryClient, qk } from "@/lib/queries"
|
||||
@@ -160,7 +161,7 @@ const FRAGE_HINT: Record<string, string> = {
|
||||
function IdeenSection() {
|
||||
const { data, isLoading } = useIdeen()
|
||||
const qc = useQueryClient()
|
||||
const { showAlert, dialogElement } = useDialog()
|
||||
const { showAlert, showConfirm, dialogElement } = useDialog()
|
||||
const [text, setText] = useState("")
|
||||
const [sending, setSending] = useState(false)
|
||||
const [busy, setBusy] = useState<Record<string, boolean>>({})
|
||||
@@ -204,6 +205,19 @@ function IdeenSection() {
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -231,15 +245,74 @@ function IdeenSection() {
|
||||
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" : "border-border/40")}>
|
||||
<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>
|
||||
)}
|
||||
<span className="min-w-0 flex-1 truncate text-xs text-foreground" title={it.body || 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]}
|
||||
|
||||
Reference in New Issue
Block a user