Ideen-Queue: natives Hermes-Kanban wird DIE Sammelstelle (Abloesung S2)

Entscheid nach Probe (Hermes-first): Dispatcher im Gateway, Specifier
(triage -> sauberer Auftrag), Profil-Routing und Artefakt-Tracking sind
nativ E2E bewiesen — kein Eigenbau. Auftragsbuch bleibt das Klick-Gate.

- backend services/ideen.py + routers/ideen.py: MC2-Tuer zur Queue
  (GET /api/ideen, POST /api/ideen -> kanban create --triage, Archiv)
- AuftragsbuchView: Ideen-Queue-Sektion (Eingabefeld + Status-Liste),
  dist frisch gebaut
- mcp_voice.py: idee_notieren (Lucys Sprech-Leitung, Prompt-Diaet: 1 Tool)
- chef-gutachter-feed.sh: Morgenlage bekommt kanban stats + Bewegung
- Bagatell-Pfad: bagatell-annahme.sh + mc2-bagatell.timer (04:10) —
  NUR .md-Anlagen/-AEnderungen, max 2/Nacht, gleicher Annahme-Runner
  mit Health-Gate + Auto-Revert, Chronik/Telegram-Meldung
- werkstatt-SOUL.md: Leitplanken des Kanban-Worker-Profils (propose-only,
  nie main, nie Live-Checkout) — deploy.sh zieht sie kuenftig nach

Box-live heute schon (ausserhalb Repo, mit Backups): toolsets+kanban,
platform_toolsets.telegram+kanban, SOUL.md-Ideen-Regel, Profil werkstatt
(model.default=coder), Gateway neu gestartet + E2E-Tuer-Test gruen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Claude (Werkstatt PC)
2026-07-10 12:56:14 +02:00
committed by Hitonabi
parent ae4b0f229e
commit e1ca7f6a5e
17 changed files with 700 additions and 174 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+15 -15
View File
@@ -1,16 +1,16 @@
<!doctype html>
<html lang="de" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0d1117" />
<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-BfahO8B0.js"></script>
<!doctype html>
<html lang="de" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0d1117" />
<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-BRolD2Rb.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BSNo2FSN.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
</head>
<body>
<div id="root"></div>
</body>
+18
View File
@@ -475,6 +475,24 @@ export interface AuftragsbuchResp {
open_count: number
}
// ── Ideen-Queue (natives Hermes-Kanban) ──────────────────────────────────────
export interface IdeenItem {
id: string
titel: string
body: string
status: string // triage | todo | ready | running | blocked | review | done
assignee: string | null
erstellt: number | null
fertig: number | null
von: string | null
}
export interface IdeenResp {
available: boolean
items: IdeenItem[]
offen: number
fehler?: string
}
// ── Chronik (Timeline aus dem Melde-Briefkasten) ─────────────────────────────
export interface ChronikItem {
id: number
+10
View File
@@ -15,6 +15,7 @@ import {
type GroupsResp,
type HermesBrainResp,
type Health,
type IdeenResp,
type Job,
type Memory,
type MemoryGraph,
@@ -53,6 +54,7 @@ export const qk = {
memoryGraph: ["memory-graph"] as const,
voiceTrace: ["voice-trace"] as const,
auftragsbuch: ["auftragsbuch"] as const,
ideen: ["ideen"] as const,
chronik: ["chronik"] as const,
wissen: ["wissen"] as const,
zeitmaschine: ["zeitmaschine"] as const,
@@ -68,6 +70,14 @@ export const useAuftragsbuch = (refetchInterval = 8_000) =>
refetchInterval,
})
// Ideen-Queue (natives Hermes-Kanban): das Backend cached ~15 s, öfter pollen lohnt nicht.
export const useIdeen = (refetchInterval = 20_000) =>
useQuery({
queryKey: qk.ideen,
queryFn: () => api<IdeenResp>("/api/ideen"),
refetchInterval,
})
export const useChronik = (limit = 150, refetchInterval = 15_000) =>
useQuery({
queryKey: qk.chronik,
+133 -3
View File
@@ -1,10 +1,10 @@
import { useState } from "react"
import {
Inbox, Check, X, GitBranch, FileDiff, Loader2, AlertTriangle, Sparkles,
ChevronDown, ChevronRight, Clock, Hammer, RefreshCw,
ChevronDown, ChevronRight, Clock, Hammer, RefreshCw, Lightbulb, Send,
} from "lucide-react"
import { api, type AuftragItem, type SkillKandidat } from "@/lib/api"
import { useAuftragsbuch, useQueryClient, qk } from "@/lib/queries"
import { api, type AuftragItem, type IdeenItem, type SkillKandidat } from "@/lib/api"
import { useAuftragsbuch, useIdeen, useQueryClient, qk } from "@/lib/queries"
import { useDialog } from "@/lib/useDialog"
import { cn } from "@/lib/utils"
@@ -98,6 +98,8 @@ export function AuftragsbuchView() {
</div>
)}
<IdeenSection />
{items.length > 0 && (
<section className="space-y-3">
<SectionLabel icon={GitBranch}>Fertige Patches ({items.length})</SectionLabel>
@@ -129,6 +131,134 @@ function SectionLabel({ icon: Icon, children }: { icon: any; children: React.Rea
)
}
// ── Ideen-Queue (natives Hermes-Kanban) ──────────────────────────────────────
// Eine Queue, drei Türen (Zentrale/Telegram/Lucy). Idee rein → die Box arbeitet sie
// selbst aus (Specifier) und baut (Werkstatt-Profil) → Code kommt als Karte zurück.
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: "wartet", cls: "text-muted-foreground border-border/60 bg-background/30" },
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" },
}
function IdeenSection() {
const { data, isLoading } = useIdeen()
const qc = useQueryClient()
const { showAlert, dialogElement } = useDialog()
const [text, setText] = useState("")
const [sending, setSending] = useState(false)
const [busy, setBusy] = useState<Record<string, boolean>>({})
const items = data?.items ?? []
const reload = () => qc.invalidateQueries({ queryKey: qk.ideen })
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 : "" }) })
setText("")
reload()
} catch (e: any) {
showAlert("Das hat nicht geklappt", String(e?.message || e))
} finally {
setSending(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 }))
}
}
// 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">
<SectionLabel icon={Lightbulb}>
Ideen-Queue{data ? `${data.offen} offen` : ""}
</SectionLabel>
<div className="rounded-2xl border border-border/60 bg-card/45 p-4 shadow-lg shadow-black/15 backdrop-blur-md space-y-3">
<div className="flex gap-2">
<input
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") submit() }}
placeholder="Neue Idee — einfach hinschreiben, die Box macht den Rest"
className="h-9 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-9 shrink-0 items-center gap-1.5 rounded-lg border border-primary/40 bg-primary/10 px-3 text-[11px] font-bold uppercase tracking-wide text-primary transition-all hover:bg-primary/20 cursor-pointer disabled:opacity-50">
{sending ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Send className="h-3.5 w-3.5" />} In die Queue
</button>
</div>
<p className="text-[11px] text-muted-foreground">
Eine Queue, alle Türen: hier, per Telegram oder im Gespräch mit Lucy. Die Box arbeitet Ideen selbst
aus und baut Code-Ergebnisse erscheinen als Karte unter Fertige Patches", nichts davon geht ohne
deinen Klick live. Nur reine Doku-Bagatellen zieht die Box nachts selbst ein (mit Fangnetz und Meldung).
</p>
{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 && (
<ul className="space-y-1.5">
{items.map((it: IdeenItem) => {
const ui = IDEEN_STATE[it.status] ?? { label: it.status, cls: "text-muted-foreground border-border/60 bg-background/30" }
return (
<li key={it.id} className="flex items-center gap-2 rounded-xl border border-border/40 bg-background/30 px-3 py-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>
<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>
{(it.status === "done" || it.status === "blocked") && (
<button
disabled={!!busy[it.id]}
onClick={() => archive(it.id)}
title="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>
)}
</li>
)
})}
</ul>
)}
{data?.available && !isLoading && items.length === 0 && (
<p className="text-xs text-muted-foreground/70">Queue ist leer — wirf der Box eine Idee zu.</p>
)}
</div>
{dialogElement}
</section>
)
}
function StateBadge({ item }: { item: AuftragItem }) {
const st = item.status
if (!st) return null