Auftragsbuch ausgebaut: Karten-Annahme, Bagatell-Annahme, Lucy-Annahme raus — Gate ist Ampel + User-Merge
Ampel / ampel (push) Failing after 24s

Die Vorschlags-Inbox (routers/services auftragsbuch, AuftragsbuchView, /auftraege, Cockpit-
Kachel + Handlungsbedarf-Eintrag, deploy/auftrag-annehmen.sh, lucy-annahme.sh, bagatell-
annahme.sh, docs/AUFTRAGSBUCH.md) war seit 02.08.2026 ungenutzt: Kanban-Tools seit 21.08. aus,
v3-Umbau lief ueber Branch -> Ampel -> Merge -> deploy.sh, PC-Executor-IP in Box und Config
veraltet. Doku behauptete den Karten-Weg trotzdem als Standard.

Jetzt:
- Cockpit: Kachel "Ideen" (offen/haengend) statt "Auftragsbuch"; Handlungsbedarf zeigt haengende
  Ideen und springt in die Ideen-Ansicht.
- Guide/Schaubild: Kreislauf Idee -> Ideen-Queue -> IDE am PC -> Ampel -> DEIN Merge -> Live.
- events.py: kein Auftragsbuch-Fingerprint mehr; config.py: PC_EXECUTOR_URL-Default auf .22.
- Skills/Skripte: selbst-inventur ohne Karten-Abschnitt, morning-report nennt offene Branches,
  konzept-fliessband verweist auf die Ideen-Ansicht.
- Doku: STACK "Deploy & Pipeline" = Branch -> Ampel -> User-Merge -> deploy.sh (einziger Weg),
  GRENZEN/ARBEITSWEISE/FALLEN/README/BEDIENUNG/RUNBOOK/GEMINI_BRIEFING/gitea-workflow angepasst,
  ZIELBILD Punkt 9, OFFENE-FAEDEN + RAPHAEL.md: erledigt.
- Bewusst geblieben: werkstatt-SOUL/projektstart-SOUL (Werkstatt-Persona, eigener Faden),
  Chronik-Kategorie "auftragsbuch" fuer historische Eintraege.
Auf der Box bereits erledigt (Hand): mc2-bagatell.timer deaktiviert + Units archiviert,
PC-Executor-URL in ~/.hermes/config.yaml auf .22.

Baut auf wartung/lucy-stimme-proxy-raphael auf (Proxy /api/lucy/stimme/*).
Gates: eslint 0 Fehler, vitest 59/59, tsc + vite build gruen, frontend/dist committet,
py_compile gruen.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-04 16:25:05 +02:00
co-authored by Claude Fable 5.1
parent df8d088fac
commit 479fecd2c6
72 changed files with 569 additions and 2323 deletions
-3
View File
@@ -25,7 +25,6 @@ const ConnectView = lazy(() => import("@/views/ConnectView").then((m) => ({ defa
const AgentView = lazy(() => import("@/views/AgentView").then((m) => ({ default: m.AgentView })))
const KonsoleView = lazy(() => import("@/views/KonsoleView").then((m) => ({ default: m.KonsoleView })))
const GuideView = lazy(() => import("@/views/GuideView").then((m) => ({ default: m.GuideView })))
const AuftragsbuchView = lazy(() => import("@/views/AuftragsbuchView").then((m) => ({ default: m.AuftragsbuchView })))
const IdeenView = lazy(() => import("@/features/ideen/IdeenAnsicht").then((m) => ({ default: m.IdeenView })))
const SkillsView = lazy(() => import("@/views/SkillsView").then((m) => ({ default: m.SkillsView })))
const ChronikView = lazy(() => import("@/views/ChronikView").then((m) => ({ default: m.ChronikView })))
@@ -99,7 +98,6 @@ function CockpitSeite() {
}
const cockpitRoute = ansicht("/cockpit", () => <CockpitSeite />)
const auftraegeRoute = ansicht("/auftraege", () => <AuftragsbuchView />)
const skillsRoute = ansicht("/skills", () => <SkillsView />)
/** Chronik: `?q=` filtert den Verlauf — Ziel der „Finden"-Einträge der Palette (P6). */
export interface ChronikSuche {
@@ -178,7 +176,6 @@ export const routeTree = rootRoute.addChildren([
indexRoute,
cockpitRoute,
ideenRoute,
auftraegeRoute,
skillsRoute,
chronikRoute,
wissenRoute,
+1 -1
View File
@@ -481,7 +481,7 @@ function IdeenSection() {
<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." }} />
text: "Für dein Heimlabor. Das Ergebnis kommt als Branch nach Gitea — mergen tust du." }} />
</div>
</div>
-40
View File
@@ -490,46 +490,6 @@ export interface ModelsResp {
running?: string[]
}
// ── Auftragsbuch (Vorschlags-Inbox) ──────────────────────────────────────────
export interface AuftragStatus {
state: "laeuft" | "eingespielt" | "fehlgeschlagen" | "rollback" | "zurueckgerollt" | "kritisch" | "abgelehnt"
detail: string
ts: number
}
export interface AuftragItem {
repo?: "mc2" | "lucy" // Quelle: Box-Stack (mc2) oder Lucy-Desktop (Annahme baut am PC)
branch: string
kind: string // wartung | orchestrator | doku | feature
subject: string
body: string
author: string
ts: number | null
ahead: number
behind: number
shortstat: string
files: { status: string; path: string }[]
files_truncated: boolean
frontend_ohne_build: boolean // Frontend-Quelltext ohne dist → Box kann nicht bauen
verwaist?: boolean // kein gemeinsamer Ursprung mit main (kaputt aufgesetzt) → Merge unmöglich
leer?: boolean // Diff gegen main ist leer → bringt nichts (schon in main / am Repo vorbei)
status: AuftragStatus | null
ci_status?: "success" | "failure" | "pending" | "skipped" | null
// Empfehlungs-Stempel des nächtlichen Karten-Gutachters (Meinung, kein Gate).
empfehlung: { empfehlung: "ANNEHMEN" | "ABLEHNEN" | "UNKLAR"; satz: string; richter: string } | null
}
export interface SkillKandidat {
file: string
title: string
preview: string
mtime: number
}
export interface AuftragsbuchResp {
available: boolean
items: AuftragItem[]
skill_kandidaten: SkillKandidat[]
open_count: number
}
// ── Ideen-Queue (natives Hermes-Kanban) ──────────────────────────────────────
export interface IdeenItem {
id: string
+1 -12
View File
@@ -8,7 +8,6 @@ import {
api,
type AgentStatus,
type AktivitaetResp,
type AuftragsbuchResp,
type ChronikResp,
type ConnectResp,
type ConnectHealth,
@@ -56,7 +55,6 @@ export const qk = {
connectHealth: ["connect-health"] as const,
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,
@@ -71,7 +69,7 @@ export const qk = {
// Intervalle mehr (mehrere Beobachter = kleinstes Intervall gewinnt).
const TAKT = {
graph: 3_000, // System + Token: seit P4 nur noch Rueckfall/Randdaten (siehe unten)
schnell: 8_000, // „fühlt sich live an": Modelle, Voice-Trace, Auftragsbuch
schnell: 8_000, // „fühlt sich live an": Modelle, Voice-Trace
normal: 10_000, // Dienste, Routing, Agent-Status, Health
gemuetlich: 30_000, // Chronik, Ideen
traege: 60_000, // Updates-Check, Reminders, Zeitmaschine, HF-Abfragen
@@ -96,15 +94,6 @@ const relax = (ms: number) => () => (sseVerbunden() ? ms * 5 : ms)
// 3 s → 60 s ist eine Zwanzigstel-Last; abschalten hätte diese Felder einfrieren lassen.
const langsamWennStrom = (ms: number) => () => (sseVerbunden() ? 60_000 : ms)
// Auftragsbuch: Vorschlags-Inbox (Branches + Skill-Kandidaten). Pollt, damit laufende
// Annahme-Läufe (Status aus der JSON-Datei des Runners) live sichtbar werden.
export const useAuftragsbuch = (refetchInterval: number = TAKT.schnell) =>
useQuery({
queryKey: qk.auftragsbuch,
queryFn: () => api<AuftragsbuchResp>("/api/auftragsbuch"),
refetchInterval: relax(refetchInterval),
})
// Ideen-Queue (natives Hermes-Kanban): das Backend cached ~15 s, öfter pollen lohnt nicht.
export const useIdeen = (refetchInterval: number = TAKT.gemuetlich) =>
useQuery({
+1 -3
View File
@@ -1,7 +1,6 @@
import {
LayoutDashboard,
Boxes,
Inbox,
History,
Library,
Plug,
@@ -12,7 +11,7 @@ import {
type LucideIcon,
} from "lucide-react"
export type ViewId = "dashboard" | "ideen" | "auftraege" | "skills" | "models" | "wissen" | "chronik" | "connect" | "agent" | "konsole" | "guide"
export type ViewId = "dashboard" | "ideen" | "skills" | "models" | "wissen" | "chronik" | "connect" | "agent" | "konsole" | "guide"
export type NavGroup = "Operativ" | "Wissen" | "Werkzeuge" | "Wartung"
@@ -35,7 +34,6 @@ export const NAV: NavItem[] = [
// Hinweis bewusst kurz: die App zeigt ihn oben in Großbuchstaben, direkt über der
// Seitenüberschrift „Ideen" — ein langer Satz stand dort dreifach gedoppelt.
{ id: "ideen", pfad: "/ideen", label: "Ideen", hint: "Schreib hin, was entstehen soll", icon: Lightbulb, group: "Operativ" },
{ id: "auftraege", pfad: "/auftraege", label: "Auftragsbuch", hint: "Vorschläge der Box — annehmen oder ablehnen", icon: Inbox, group: "Operativ" },
{ id: "skills", pfad: "/skills", label: "Skills & Jobs", hint: "Autonome Skills manuell auslösen", icon: Bot, group: "Operativ" },
{ id: "chronik", pfad: "/chronik", label: "Chronik", hint: "Was die Box von allein getan hat + Zeitmaschine", icon: History, group: "Operativ" },
{ id: "wissen", pfad: "/wissen", label: "Wissen", hint: "Lucys Wissens-Vault (Traum-Notizen)", icon: Library, group: "Wissen" },
-425
View File
@@ -1,425 +0,0 @@
import { useState } from "react"
import {
Inbox, Check, X, GitBranch, FileDiff, Loader2, AlertTriangle, Sparkles,
ChevronDown, ChevronRight, Clock, Hammer, RefreshCw, Lightbulb, ArrowRight,
} from "lucide-react"
import { useNavigate } from "@tanstack/react-router"
import { api, type AuftragItem, type SkillKandidat } from "@/lib/api"
import { useAuftragsbuch, useIdeen, 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"
// Auftragsbuch = die Vorschlags-Inbox der Box. Werkstatt/Orchestrator liefern propose-only-
// Branches, der Traum liefert Skill-Kandidaten — hier fällt die Entscheidung per Klick.
// Annehmen (Branch) = Merge→Push→Deploy→Health mit Auto-Rollback; alles detached auf der Box.
const KIND_LABEL: Record<string, string> = {
wartung: "Werkstatt",
orchestrator: "Orchestrator",
doku: "Doku",
feature: "Feature",
}
const STATE_UI: Record<string, { label: string; cls: string; spin?: boolean }> = {
laeuft: { label: "wird eingespielt …", cls: "text-sky-300 border-sky-500/40 bg-sky-500/10", spin: true },
rollback: { label: "Rollback läuft …", cls: "text-amber-300 border-amber-500/40 bg-amber-500/10", spin: true },
eingespielt: { label: "eingespielt ✓", cls: "text-emerald-300 border-emerald-500/40 bg-emerald-500/10" },
fehlgeschlagen: { label: "fehlgeschlagen", cls: "text-red-300 border-red-500/40 bg-red-500/10" },
zurueckgerollt: { label: "zurückgerollt", cls: "text-amber-300 border-amber-500/40 bg-amber-500/10" },
kritisch: { label: "KRITISCH — Box prüfen", cls: "text-red-200 border-red-500/60 bg-red-500/20" },
abgelehnt: { label: "abgelehnt", cls: "text-muted-foreground border-border/60 bg-background/30" },
}
export function AuftragsbuchView() {
const navigate = useNavigate()
const { data, isLoading, refetch, isFetching } = useAuftragsbuch()
// Geteilter Cache mit der IdeenSection — hier nur fürs ehrliche "Nichts zu entscheiden"-
// Banner (eine hängende Karte direkt darunter widerspräche dem grünen Häkchen).
const { data: ideenData } = useIdeen()
const qc = useQueryClient()
const { showAlert, showConfirm, dialogElement } = useDialog()
const [busy, setBusy] = useState<Record<string, boolean>>({})
const reload = () => qc.invalidateQueries({ queryKey: qk.auftragsbuch })
async function act(key: string, path: string, body: object, done?: string) {
setBusy((b) => ({ ...b, [key]: true }))
try {
await api(path, { method: "POST", body: JSON.stringify(body) })
if (done) showAlert("Erledigt", done)
reload()
} catch (e: any) {
showAlert("Das hat nicht geklappt", String(e?.message || e))
} finally {
setBusy((b) => ({ ...b, [key]: false }))
}
}
const items = data?.items ?? []
const kandidaten = data?.skill_kandidaten ?? []
// Hängende Ideen leben seit 25.07. auf der eigenen Ideen-Seite. Das grüne „Nichts zu
// entscheiden" darf trotzdem nicht behaupten, alles sei erledigt — es zeigt jetzt einen
// Querverweis statt die Karte selbst (vorher stand sie direkt darunter).
const blockedIdeen = ideenData?.available ? ideenData.items.filter((i) => i.status === "blocked").length : 0
const empty = items.length === 0 && kandidaten.length === 0
return (
<div className="space-y-6">
<div className="flex items-center justify-between gap-3">
<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">
Auftragsbuch
</h1>
<p className="text-sm text-muted-foreground">
Was die Box gebaut oder sich ausgedacht hat du entscheidest. Annehmen spielt es mit Fangnetz ein (Health-Check, automatischer Rollback).
</p>
</div>
<button onClick={() => refetch()}
className="flex h-9 items-center gap-1.5 rounded-lg border border-border/60 bg-card/45 px-3 text-xs font-semibold text-muted-foreground hover:text-foreground hover:border-primary/40 transition-all cursor-pointer">
<RefreshCw className={cn("h-3.5 w-3.5", isFetching && "animate-spin")} /> Aktualisieren
</button>
</div>
{data && !data.available && (
<div className="rounded-2xl border border-amber-500/30 bg-amber-500/5 p-4 text-xs text-amber-300">
Das Auftragsbuch lebt auf der Box (liest die Gitea-Branches dort). Im lokalen Dev-Modus ist hier nichts zu sehen.
</div>
)}
{isLoading && (
<div className="flex items-center gap-2 rounded-2xl border border-border/60 bg-card/30 p-6 text-xs text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" /> Vorschläge werden geladen
</div>
)}
{data?.available && empty && (
<div className="flex items-center gap-3 rounded-2xl border border-emerald-500/25 bg-emerald-500/5 p-5">
<Check className="h-5 w-5 shrink-0 text-emerald-400" />
<div>
<p className="text-sm font-semibold text-foreground">Nichts zu entscheiden</p>
<p className="text-xs text-muted-foreground">Keine offenen Vorschläge. Werkstatt, Orchestrator und Traum melden sich hier, sobald es etwas gibt.</p>
</div>
</div>
)}
{blockedIdeen > 0 && (
<button
onClick={() => navigate({ to: "/ideen" })}
className="flex w-full items-center gap-3 rounded-2xl border border-red-500/30 bg-red-500/5 p-4 text-left transition-all hover:border-red-500/50 cursor-pointer">
<Lightbulb className="h-5 w-5 shrink-0 text-red-300" />
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold text-foreground">
{blockedIdeen === 1 ? "Eine Idee hängt und braucht dich" : `${blockedIdeen} Ideen hängen und brauchen dich`}
</p>
<p className="text-xs text-muted-foreground">Hier ist nichts zu tun aber drüben in den Ideen wartet eine Frage auf deine Antwort.</p>
</div>
<ArrowRight className="h-4 w-4 shrink-0 text-red-300/70" />
</button>
)}
{items.length > 0 && (
<section className="space-y-3">
<SectionLabel icon={GitBranch}>Patch-Vorschläge ({items.length})</SectionLabel>
{items.map((it) => (
<ProposalCard key={`${it.repo ?? "mc2"}:${it.branch}`} item={it} busy={busy} onAct={act} showConfirm={showConfirm} />
))}
</section>
)}
{kandidaten.length > 0 && (
<section className="space-y-3">
<SectionLabel icon={Sparkles}>Skill-Kandidaten aus dem Traum ({kandidaten.length})</SectionLabel>
{kandidaten.map((k) => (
<KandidatCard key={k.file} k={k} busy={busy} onAct={act} showConfirm={showConfirm} />
))}
</section>
)}
{dialogElement}
</div>
)
}
function StateBadge({ item }: { item: AuftragItem }) {
const st = item.status
if (!st) return null
const ui = STATE_UI[st.state]
if (!ui) return null
return (
<span className={cn("flex items-center gap-1.5 rounded-lg border px-2 py-0.5 text-[10px] font-bold", ui.cls)} title={st.detail}>
{ui.spin && <Loader2 className="h-3 w-3 animate-spin" />}
{ui.label}
</span>
)
}
function CIAmpelBadge({ item }: { item: AuftragItem }) {
const ci = item.ci_status
if (!ci) return null
if (ci === "success") {
return <span className="flex items-center gap-1 rounded-lg border border-emerald-500/30 bg-emerald-500/10 px-1.5 py-0.5 text-[10px] font-bold text-emerald-400" title="CI-Ampel: Grün (Baut & läuft)"><Check className="h-3 w-3" /> CI Grün</span>
}
if (ci === "failure") {
return <span className="flex items-center gap-1 rounded-lg border border-red-500/30 bg-red-500/10 px-1.5 py-0.5 text-[10px] font-bold text-red-400" title="CI-Ampel: Rot (Fehler)"><X className="h-3 w-3" /> CI Rot</span>
}
if (ci === "pending") {
return <span className="flex items-center gap-1 rounded-lg border border-amber-500/30 bg-amber-500/10 px-1.5 py-0.5 text-[10px] font-bold text-amber-400" title="CI-Ampel: Läuft"><Loader2 className="h-3 w-3 animate-spin" /> CI Läuft</span>
}
return null
}
function ProposalCard({
item, busy, onAct, showConfirm,
}: {
item: AuftragItem
busy: Record<string, boolean>
onAct: (key: string, path: string, body: object, done?: string) => void
showConfirm: (title: string, msg: string, ok: () => void) => void
}) {
const [open, setOpen] = useState(false)
const [diff, setDiff] = useState<string | null>(null)
const [diffLoading, setDiffLoading] = useState(false)
// Ablehnen mit Grund: der Grund ist das Lern-Gedächtnis des Kreislaufs (Radar/Specifier
// bekommen ihn als „nie wieder vorschlagen"-Material) — deshalb ein kleines Inline-Feld.
const [rejecting, setRejecting] = useState(false)
const [grund, setGrund] = useState("")
const running = item.status?.state === "laeuft" || item.status?.state === "rollback"
const decided = item.status?.state === "eingespielt"
// Lucy-Karten (Desktop-App): Annehmen baut am PC (dist) statt auf der Box zu deployen.
const repo = item.repo ?? "mc2"
const isLucy = repo === "lucy"
const k = `${repo}:${item.branch}`
async function loadDiff() {
const next = !open
setOpen(next)
if (next && diff === null) {
setDiffLoading(true)
try {
const r = await api<{ diff: string; truncated: boolean }>(`/api/auftragsbuch/diff?branch=${encodeURIComponent(item.branch)}&repo=${repo}`)
setDiff(r.diff + (r.truncated ? "\n… (gekürzt)" : ""))
} catch (e: any) {
setDiff(`(Diff nicht ladbar: ${e?.message || e})`)
} finally {
setDiffLoading(false)
}
}
}
return (
<div className="mc-card p-4 space-y-3">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<span className="rounded-md border border-primary/30 bg-primary/10 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-primary">
{KIND_LABEL[item.kind] ?? item.kind}
</span>
{isLucy && (
<span className="rounded-md border border-pink-500/40 bg-pink-500/10 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-pink-300"
title="Vorschlag fürs Lucy-Repo — Annehmen baut die App am PC neu und startet sie neu, wenn sie läuft.">
Lucy
</span>
)}
<span className="font-mono text-[11px] text-muted-foreground truncate" title={item.branch}>{item.branch}</span>
<span className="flex items-center gap-1 text-[10px] text-muted-foreground/70"><Clock className="h-3 w-3" />{fmtWhen(item.ts)}</span>
<CIAmpelBadge item={item} />
<StateBadge item={item} />
</div>
<p className="mt-1.5 text-sm font-semibold text-foreground">{item.subject || "(ohne Titel)"}</p>
{item.body && <p className="mt-1 whitespace-pre-wrap text-xs leading-relaxed text-muted-foreground line-clamp-6">{item.body}</p>}
{item.empfehlung && (
<p className={cn("mt-1.5 flex items-start gap-1.5 rounded-lg border px-2 py-1 text-[11px]",
item.empfehlung.empfehlung === "ANNEHMEN" && "border-emerald-500/30 bg-emerald-500/5 text-emerald-300",
item.empfehlung.empfehlung === "ABLEHNEN" && "border-red-500/30 bg-red-500/5 text-red-300",
item.empfehlung.empfehlung === "UNKLAR" && "border-amber-500/30 bg-amber-500/5 text-amber-300")}
title={`Nächtliches Kurz-Gutachten (${item.empfehlung.richter}) — eine Meinung, kein Muss. Die Entscheidung bleibt bei dir.`}>
<Sparkles className="mt-0.5 h-3 w-3 shrink-0" />
<span><b>Empfehlung: {item.empfehlung.empfehlung === "ANNEHMEN" ? "Annehmen" : item.empfehlung.empfehlung === "ABLEHNEN" ? "Ablehnen" : "Unklar"}.</b> {item.empfehlung.satz}</span>
</p>
)}
</div>
{!running && !decided && (
<div className="flex shrink-0 items-center gap-2">
{!item.verwaist && (
<button
disabled={!!busy[k]}
onClick={() =>
showConfirm("Vorschlag annehmen?",
isLucy
? `'${item.branch}' wird im Lucy-Repo auf main gemergt, dann baut dein PC die App neu (dist) und startet Lucy neu, falls sie gerade läuft. Der PC muss dafür an sein. Schlägt Build oder Start fehl, dreht die Box den Merge automatisch zurück — die laufende Lucy bleibt die alte.`
: `'${item.branch}' wird auf main gemergt und live eingespielt. Die Zentrale startet dabei kurz neu. Geht der Health-Check danach rot, rollt die Box automatisch zurück.${item.frontend_ohne_build ? "\n\nAchtung: Enthält Frontend-Quelltext ohne gebautes Bundle — die Oberfläche bleibt alt, bis am PC gebaut wird." : ""}`,
() => onAct(k, "/api/auftragsbuch/annehmen", { branch: item.branch, repo }))}
className="flex h-9 items-center gap-1.5 rounded-lg border border-emerald-500/40 bg-emerald-500/10 px-3 text-[11px] font-bold uppercase tracking-wide text-emerald-300 transition-all hover:bg-emerald-500/20 cursor-pointer disabled:opacity-50">
{busy[k] ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Check className="h-3.5 w-3.5" />} Annehmen
</button>
)}
<button
disabled={!!busy["ab:" + k]}
onClick={() => setRejecting((v) => !v)}
className="flex h-9 items-center gap-1.5 rounded-lg border border-red-500/30 bg-red-500/5 px-3 text-[11px] font-bold uppercase tracking-wide text-red-300/90 transition-all hover:bg-red-500/15 cursor-pointer disabled:opacity-50">
{busy["ab:" + k] ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <X className="h-3.5 w-3.5" />} Ablehnen
</button>
</div>
)}
</div>
{rejecting && !running && !decided && (
<div className="space-y-2 rounded-xl border border-red-500/25 bg-red-500/5 p-3">
<p className="text-[11px] text-muted-foreground">
Der Branch '{item.branch}' wird auf Gitea gelöscht (die Arbeit bleibt in der Historie).
Sag der Box kurz <b>warum</b> dann schlägt sie dir so etwas nicht wieder vor.
</p>
<input
value={grund}
onChange={(e) => setGrund(e.target.value)}
placeholder={"Grund (optional) — z. B. „gibt es schon“ oder „will ich nicht“"}
maxLength={400}
className="w-full rounded-lg border border-border/60 bg-background/60 px-2.5 py-1.5 text-xs text-foreground placeholder:text-muted-foreground/50 focus:border-red-400/50 focus:outline-none"
/>
<div className="flex items-center gap-2">
<button
disabled={!!busy["ab:" + k]}
onClick={() => {
setRejecting(false)
onAct("ab:" + k, "/api/auftragsbuch/ablehnen", { branch: item.branch, repo, grund: grund.trim() })
}}
className="flex h-8 items-center gap-1.5 rounded-lg border border-red-500/40 bg-red-500/10 px-3 text-[11px] font-bold uppercase tracking-wide text-red-300 transition-all hover:bg-red-500/20 cursor-pointer disabled:opacity-50">
<X className="h-3.5 w-3.5" /> Wirklich ablehnen
</button>
<button
onClick={() => setRejecting(false)}
className="flex h-8 items-center rounded-lg border border-border/60 bg-background/40 px-3 text-[11px] font-semibold text-muted-foreground transition-all hover:text-foreground cursor-pointer">
Abbrechen
</button>
</div>
</div>
)}
{item.status && (running || !decided) && item.status.detail && (
<p className="text-[11px] text-muted-foreground">{item.status.detail}</p>
)}
{item.verwaist && (
<p className="rounded-lg border border-red-500/30 bg-red-500/5 px-2 py-1.5 text-[11px] text-red-300/90">
<b>Kaputt aufgesetzt:</b> Dieser Branch hat keinen gemeinsamen Ursprung mit main (der Werkstatt-Worker
hat nicht richtig geklont) Annehmen ist technisch unmöglich, deshalb fehlt der Knopf.
Am besten <b>ablehnen mit Grund</b>; die Idee kann danach frisch in die Queue.
</p>
)}
<div className="flex flex-wrap items-center gap-2 text-[11px] text-muted-foreground">
<span className="rounded bg-background/40 px-1.5 py-0.5 font-mono">{item.ahead} Commit{item.ahead === 1 ? "" : "s"}</span>
{item.shortstat && <span className="rounded bg-background/40 px-1.5 py-0.5 font-mono">{item.shortstat.trim()}</span>}
{item.verwaist && (
<span className="flex items-center gap-1 rounded bg-red-500/10 px-1.5 py-0.5 text-red-300" title="Kein gemeinsamer Vorfahre mit main — git kann diesen Branch niemals mergen.">
<AlertTriangle className="h-3 w-3" /> kein gemeinsamer Ursprung
</span>
)}
{item.leer && (
<span className="flex items-center gap-1 rounded bg-amber-500/10 px-1.5 py-0.5 text-amber-300" title="Der Diff gegen main ist leer — der Branch würde nichts verändern (Inhalt schon in main oder am Repo vorbei gearbeitet).">
<AlertTriangle className="h-3 w-3" /> Diff leer bringt nichts
</span>
)}
{item.behind > 0 && !item.verwaist && (
<span className="flex items-center gap-1 rounded bg-amber-500/10 px-1.5 py-0.5 text-amber-300" title="main ist seit diesem Vorschlag weitergelaufen — bei Konflikten versucht die Annahme automatisch einen Rebase.">
<AlertTriangle className="h-3 w-3" /> {item.behind} hinter main
</span>
)}
{item.frontend_ohne_build && (
<span className="flex items-center gap-1 rounded bg-amber-500/10 px-1.5 py-0.5 text-amber-300" title="Die Box kann kein Frontend bauen (kein Node) — nach dem Annehmen bleibt die Oberfläche alt, bis am PC gebaut wird.">
<AlertTriangle className="h-3 w-3" /> Frontend ohne Build
</span>
)}
</div>
<div>
<button onClick={loadDiff}
className="flex items-center gap-1.5 text-[11px] font-semibold text-muted-foreground hover:text-foreground transition-colors cursor-pointer">
{open ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
<FileDiff className="h-3.5 w-3.5" /> {item.files.length} Datei{item.files.length === 1 ? "" : "en"} Änderungen ansehen
</button>
{open && (
<div className="mt-2 space-y-2">
<div className="flex flex-wrap gap-1.5">
{item.files.map((f) => (
<span key={f.path} className="rounded bg-background/40 px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground">
<b className={cn(f.status.startsWith("A") && "text-emerald-400", f.status.startsWith("D") && "text-red-400", f.status.startsWith("M") && "text-sky-400")}>{f.status}</b> {f.path}
</span>
))}
{item.files_truncated && <span className="text-[10px] text-muted-foreground/60"></span>}
</div>
<div className="max-h-96 overflow-auto rounded-xl border border-border/50 bg-background/50 p-3">
{diffLoading
? <span className="text-[11px] text-muted-foreground">Diff wird geladen </span>
: <pre className="text-[10px] leading-relaxed text-foreground/80 whitespace-pre">{
(diff ?? "").split("\n").map((ln, i) => (
<span key={i} className={cn("block", ln.startsWith("+") && !ln.startsWith("+++") && "text-emerald-300/90 bg-emerald-500/5", ln.startsWith("-") && !ln.startsWith("---") && "text-red-300/90 bg-red-500/5", (ln.startsWith("@@") || ln.startsWith("diff ")) && "text-sky-300/90")}>{ln || " "}</span>
))
}</pre>}
</div>
</div>
)}
</div>
</div>
)
}
function KandidatCard({
k, busy, onAct, showConfirm,
}: {
k: SkillKandidat
busy: Record<string, boolean>
onAct: (key: string, path: string, body: object, done?: string) => void
showConfirm: (title: string, msg: string, ok: () => void) => void
}) {
const [open, setOpen] = useState(false)
return (
<div className="rounded-2xl border border-violet-500/25 bg-violet-500/[0.04] p-4 shadow-lg shadow-black/15 backdrop-blur-md space-y-3">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<span className="rounded-md border border-violet-500/30 bg-violet-500/10 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-violet-300">Traum-Idee</span>
<span className="font-mono text-[11px] text-muted-foreground truncate">{k.file}</span>
<span className="flex items-center gap-1 text-[10px] text-muted-foreground/70"><Clock className="h-3 w-3" />{fmtWhen(k.mtime)}</span>
</div>
<p className="mt-1.5 text-sm font-semibold text-foreground">{k.title}</p>
</div>
<div className="flex shrink-0 items-center gap-2">
<button
disabled={!!busy["sk:" + k.file]}
onClick={() =>
showConfirm("Werkstatt beauftragen?",
`Der Kandidat '${k.title}' geht als Auftrag an die Werkstatt (Orchestrator, propose-only). Das Ergebnis erscheint später als neuer Patch-Vorschlag hier im Auftragsbuch — nichts geht ungefragt live.`,
() => onAct("sk:" + k.file, "/api/auftragsbuch/skill/annehmen", { file: k.file },
"Die Werkstatt arbeitet. Das Ergebnis taucht als neuer Vorschlag im Auftragsbuch auf (das kann einige Minuten dauern)."))}
className="flex h-9 items-center gap-1.5 rounded-lg border border-violet-500/40 bg-violet-500/10 px-3 text-[11px] font-bold uppercase tracking-wide text-violet-300 transition-all hover:bg-violet-500/20 cursor-pointer disabled:opacity-50">
{busy["sk:" + k.file] ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Hammer className="h-3.5 w-3.5" />} Beauftragen
</button>
<button
disabled={!!busy["skab:" + k.file]}
onClick={() =>
showConfirm("Kandidat verwerfen?",
`'${k.title}' wandert ins Archiv des Wissens-Vaults (verworfen/). Der Traum schlägt ihn nicht erneut vor.`,
() => onAct("skab:" + k.file, "/api/auftragsbuch/skill/ablehnen", { file: k.file }))}
className="flex h-9 items-center gap-1.5 rounded-lg border border-red-500/30 bg-red-500/5 px-3 text-[11px] font-bold uppercase tracking-wide text-red-300/90 transition-all hover:bg-red-500/15 cursor-pointer disabled:opacity-50">
{busy["skab:" + k.file] ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <X className="h-3.5 w-3.5" />} Verwerfen
</button>
</div>
</div>
<button onClick={() => setOpen((o) => !o)}
className="flex items-center gap-1.5 text-[11px] font-semibold text-muted-foreground hover:text-foreground transition-colors cursor-pointer">
{open ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
<Inbox className="h-3.5 w-3.5" /> Vorschlag lesen
</button>
{open && (
<pre className="max-h-72 overflow-auto rounded-xl border border-border/50 bg-background/50 p-3 text-[11px] leading-relaxed text-foreground/85 whitespace-pre-wrap">{k.preview}</pre>
)}
</div>
)
}
+6 -6
View File
@@ -168,9 +168,9 @@ export function GuideView() {
<strong className="text-foreground"> Hermes Desktop</strong> für richtige Projekte und
<strong className="text-foreground"> Telegram</strong> für unterwegs. In der Box denken die Modelle, das
Gedächtnis lernt automatisch mit — und nachts arbeitet sie allein: Sie träumt, inventarisiert sich selbst,
entwickelt Ideen zu fertigen Vorschlägen und legt sie dir als Karten ins Auftragsbuch.
<strong className="text-primary"> Nichts geht live ohne deinen Klick</strong> — und wenn nach einer Annahme
etwas rot wird, baut die Box es selbst zurück.
entwickelt Ideen zu Konzepten. Gebaut wird am PC in der IDE, jede Änderung geht als Branch nach Gitea.
<strong className="text-primary"> Nichts geht live ohne deinen Merge</strong> — die Ampel-CI muss grün sein,
danach spielt <code>deploy.sh</code> den Stand ein.
</p>
<SystemSchaubild />
</section>
@@ -379,8 +379,8 @@ description: Treibt Entwicklung mit strikter TDD-Praxis
Reden tust du mit ihm über <strong>Hermes Desktop</strong> (am PC, remote via <Pill>/hermes-ui</Pill>),
die <strong>Konsole</strong> (Box-Shell im Browser), <strong>Telegram</strong> — oder per Stimme über
<strong> Lucy</strong>. Er hat MCP-Server für Gedächtnis, Stack-Steuerung, Web-Fetch und
<strong> PC-Steuerung</strong> (Executor auf deinem Windows-PC). Nachts betreibt er Werkstatt &amp;
Radar-Crons — alles Gebaute landet als Karte im <strong>Auftragsbuch</strong>.
<strong> PC-Steuerung</strong> (Executor auf deinem Windows-PC). Nachts betreibt er Radar-Crons
und meldet Funde in Chronik, Briefkasten und Telegram.
Status &amp; Verdrahtung im <strong>Hermes</strong>-Tab.
</Stack>
</Card>
@@ -445,7 +445,7 @@ description: Treibt Entwicklung mit strikter TDD-Praxis
<ul className="list-disc pl-4 space-y-1">
<li><strong>Backup:</strong> PBS-Backup-Zentrum (Sicherung auf den Proxmox-Server) + tägliche Tarball-Sicherung; Restore echt durchgespielt (<Pill>restore.sh</Pill>, Doku in <Pill>docs/BACKUP.md</Pill>).</li>
<li><strong>Updates:</strong> git-basierter Update-Check; <strong>Update-Buttons</strong> mit anschließendem <strong>Gehirn-Check</strong> (rot, falls ein Update das Gedächtnis zerschießt).</li>
<li><strong>Annahme-Sicherheit:</strong> jede Auftragsbuch-Annahme läuft mit Health-Check wird's rot, baut die Box automatisch zurück (Auto-Revert).</li>
<li><strong>Deploy-Sicherheit:</strong> <code>deploy.sh</code> sichert vorher und prüft danach die Gesundheit; die Zeitmaschine in der Chronik ist der Rückweg.</li>
<li><strong>Dienste &amp; Gefahrenzone:</strong> Restart/Logs pro Dienst, kritische Eingriffe getrennt.</li>
</ul>
<p className="text-[10px] italic">Erreichbar über das System-Icon Tab Wartung".</p>
+14 -36
View File
@@ -2,13 +2,13 @@ import { useState } from "react"
import {
Boxes, Server, ShieldAlert, Plug,
HeartPulse, AlertTriangle, Loader2, Wrench, Check, HardDrive, ChevronRight,
Inbox, HelpCircle,
Lightbulb, HelpCircle,
} from "lucide-react"
import { useNavigate } from "@tanstack/react-router"
import { api } from "@/lib/api"
import {
useModels, useServices, useUpdates, useAgentStatus, useConnectHealth,
useAuftragsbuch, useIdeen, useQueryClient, qk,
useIdeen, useQueryClient, qk,
} from "@/lib/queries"
import { useLucyHealth, type Repair } from "@/lib/useLucyHealth"
import { useDialog } from "@/lib/useDialog"
@@ -37,7 +37,6 @@ export function CockpitView({ onNavigate }: { onNavigate: (v: string) => void })
const { data: updates } = useUpdates()
const { data: agent } = useAgentStatus()
const { data: connect } = useConnectHealth()
const { data: auftraege } = useAuftragsbuch()
const { data: ideen } = useIdeen()
const health = useLucyHealth()
@@ -75,9 +74,8 @@ export function CockpitView({ onNavigate }: { onNavigate: (v: string) => void })
const pendingCount =
(updates ? (updates.os > 0 ? 1 : 0) + (updates.engine > 0 ? 1 : 0) + (updates.swap > 0 ? 1 : 0) + (updates.models > 0 ? 1 : 0) : 0) +
(updates?.components?.filter((c) => c.update === true).length ?? 0)
const openAuftraege = auftraege?.available ? auftraege.open_count : 0
// Hängende Karten warten auf DEINE Antwort — bisher sah das Cockpit sie gar nicht
// („Nichts zu tun" trotz roter Karte im Auftragsbuch, Review 15.07.).
// Hängende Ideen warten auf DEINE Antwort — das Cockpit muss sie zeigen (Review 15.07.).
const openIdeen = ideen?.available ? ideen.items.filter((i) => i.status !== "done").length : 0
const blockedIdeen = ideen?.available ? ideen.items.filter((i) => i.status === "blocked").length : 0
// Verbinden: die drei Leitungen (Modell-Gateway · Gedächtnis · Desktop-Gateway).
@@ -136,13 +134,13 @@ export function CockpitView({ onNavigate }: { onNavigate: (v: string) => void })
<ZoneLabel>Bereiche</ZoneLabel>
<div className="grid gap-4 grid-cols-2 md:grid-cols-3">
<CockpitTile
icon={Inbox} title="Auftragsbuch" value={auftraege?.available ? openAuftraege : "—"}
unit={openAuftraege === 1 ? "Vorschlag" : "Vorschläge"}
tone={!auftraege ? "loading" : blockedIdeen > 0 || openAuftraege > 0 ? "warn" : "ok"}
icon={Lightbulb} title="Ideen" value={ideen?.available ? openIdeen : "—"}
unit="offen"
tone={!ideen ? "loading" : blockedIdeen > 0 ? "warn" : "ok"}
hint={blockedIdeen > 0
? `${blockedIdeen} Karte${blockedIdeen === 1 ? "" : "n"} wartet auf deine Antwort`
: openAuftraege > 0 ? `${openAuftraege} offene Patches` : "Keine ausstehenden Vorschläge"}
onClick={() => onNavigate("auftraege")}
? `${blockedIdeen} Idee${blockedIdeen === 1 ? "" : "n"} wartet auf deine Antwort`
: openIdeen > 0 ? `${openIdeen} in Arbeit oder wartend` : "Nichts offen"}
onClick={() => onNavigate("ideen")}
/>
<CockpitTile
icon={Boxes} title="Modelle" value={running} unit="warm"
@@ -180,7 +178,6 @@ export function CockpitView({ onNavigate }: { onNavigate: (v: string) => void })
<NeedsYou
problems={health.problems}
pendingCount={pendingCount}
openAuftraege={openAuftraege}
blockedIdeen={blockedIdeen}
busy={busy}
onRepair={runRepair}
@@ -258,13 +255,12 @@ function StatusLine({ verdict, memory, warns, problems }: {
}
// „Braucht dich": kritische Probleme (mit 1-Klick-Reparatur) + bereitliegende Updates
// + offene Vorschläge im Auftragsbuch.
// + hängende Ideen.
function NeedsYou({
problems, pendingCount, openAuftraege, blockedIdeen, busy, onRepair, onNavigate,
problems, pendingCount, blockedIdeen, busy, onRepair, onNavigate,
}: {
problems: ReturnType<typeof useLucyHealth>["problems"]
pendingCount: number
openAuftraege: number
blockedIdeen: number
busy: Record<string, boolean>
onRepair: (id: string, r: Repair) => void
@@ -272,9 +268,8 @@ function NeedsYou({
}) {
const openDrawer = useSchublade()
const hasUpdates = pendingCount > 0
const hasAuftraege = openAuftraege > 0
const hasBlocked = blockedIdeen > 0
const nothing = problems.length === 0 && !hasUpdates && !hasAuftraege && !hasBlocked
const nothing = problems.length === 0 && !hasUpdates && !hasBlocked
return (
<div className="h-full flex flex-col justify-between">
@@ -313,7 +308,7 @@ function NeedsYou({
))}
{hasBlocked && (
<button
onClick={() => onNavigate("auftraege")}
onClick={() => onNavigate("ideen")}
className="flex w-full items-center justify-between gap-3 rounded-2xl border border-red-500/30 bg-red-500/5 p-4 text-left transition-all hover:bg-red-500/10 cursor-pointer"
>
<div className="flex items-center gap-3">
@@ -328,23 +323,6 @@ function NeedsYou({
<ChevronRight className="h-4 w-4 shrink-0 text-red-300/70" />
</button>
)}
{hasAuftraege && (
<button
onClick={() => onNavigate("auftraege")}
className="flex w-full items-center justify-between gap-3 rounded-2xl border border-primary/30 bg-primary/5 p-4 text-left transition-all hover:bg-primary/10 cursor-pointer"
>
<div className="flex items-center gap-3">
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-primary/30 bg-background/30 text-primary">
<Inbox className="h-4.5 w-4.5" />
</span>
<div>
<p className="text-sm font-bold text-foreground">{openAuftraege} Vorschl{openAuftraege === 1 ? "ag" : "äge"} im Auftragsbuch</p>
<p className="text-xs text-muted-foreground">Annehmen oder ablehnen.</p>
</div>
</div>
<ChevronRight className="h-4 w-4 shrink-0 text-primary/70" />
</button>
)}
{hasUpdates && (
<button
onClick={() => openDrawer("wartung")}
+6 -6
View File
@@ -94,7 +94,7 @@ function Landkarte() {
</text>
<NodeBox x={444} y={54} w={396} h={62} color={T.teal} title="Mission Control (:9001)"
sub="Cockpit · Auftragsbuch (Klick-Gate) · Chronik · Wissen" />
sub="Cockpit · Ideen · Chronik · Wissen" />
<NodeBox x={444} y={126} w={396} h={62} color={T.rose} title="Hermes-Agent"
sub="denkt, nutzt Werkzeuge, betreibt die Werkstatt & Nacht-Crons" />
<NodeBox x={444} y={198} w={396} h={78} color={T.sky} title="Modell-Stall (llama-swap)"
@@ -125,14 +125,14 @@ function Kreislauf() {
const stationen = [
{ x: 14, color: T.amber, title: "Idee", sub: "von dir oder vom", sub2: "Nacht-Radar" },
{ x: 158, color: T.violet, title: "Ideen-Queue", sub: "Kanban sammelt,", sub2: "Specifier plant" },
{ x: 302, color: T.rose, title: "Werkstatt", sub: "Agent baut den", sub2: "Vorschlag (Branch)" },
{ x: 446, color: T.sky, title: "Auftragsbuch", sub: "Karte + Stempel", sub2: "vom Gutachter" },
{ x: 590, color: T.primary, title: "DEIN Klick", sub: "annehmen oder", sub2: "ablehnen (mit Grund)", strong: true },
{ x: 734, color: T.emerald, title: "Live", sub: "Merge + Deploy,", sub2: "Rot = Auto-Rückbau" },
{ x: 302, color: T.rose, title: "IDE am PC", sub: "Agent baut den", sub2: "Branch (Gitea)" },
{ x: 446, color: T.sky, title: "Ampel", sub: "CI prüft Build", sub2: "und Tests" },
{ x: 590, color: T.primary, title: "DEIN Merge", sub: "nur du bringst", sub2: "es auf main", strong: true },
{ x: 734, color: T.emerald, title: "Live", sub: "deploy.sh,", sub2: "Zeitmaschine = Rückweg" },
]
return (
<svg viewBox="0 0 880 246" role="img"
aria-label="Kreislauf: Idee, Ideen-Queue, Werkstatt, Auftragsbuch, dein Klick, live — Chronik und Morgenlage berichten zurück."
aria-label="Kreislauf: Idee, Ideen-Queue, IDE am PC, Ampel, dein Merge, live — Chronik und Morgenlage berichten zurück."
style={{ width: "100%", height: "auto", fontFamily: "var(--font-sans)" }}>
{stationen.map((s) => (