UI: Auftragsbuch, Chronik+Zeitmaschine, Wissens-Wiki, Morgenlage, Erinnerungen
Neue Views: Auftragsbuch (Karten mit Diff/Kritiker-Kontext, Annehmen/Ablehnen mit Fangnetz-Dialog), Chronik (Tages-Timeline aus dem Briefkasten + Zeitmaschine mit Ein-Klick-Restore), Wissen (Vault als klickbares Wiki, [[Links]] springen). Cockpit: Auftragsbuch-Kachel + Braucht-dich-Eintrag, Morgenlage-Karte (juengstes Chef-Verdikt), Anstehendes (Erinnerungen anlegen/loeschen), Wissen/Chronik-Kacheln. Gedaechtnis: Dubletten-Dialog zeigt jetzt konkret, was bleibt und was wegfaellt. dist mitgebaut (tsc+vite gruen). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,17 +2,20 @@ import { useState } from "react"
|
||||
import {
|
||||
Boxes, Brain, Server, ShieldAlert, Plug, Bot, TerminalSquare, ScrollText,
|
||||
HeartPulse, AlertTriangle, Loader2, Wrench, Check, HardDrive, ChevronRight,
|
||||
Inbox, History, Library,
|
||||
} from "lucide-react"
|
||||
import { api } from "@/lib/api"
|
||||
import {
|
||||
useModels, useServices, useUpdates, useMemory, useAgentStatus, useConnectHealth,
|
||||
useQueryClient, qk,
|
||||
useAuftragsbuch, useQueryClient, qk,
|
||||
} from "@/lib/queries"
|
||||
import { SquareTerminal } from "lucide-react"
|
||||
import { useLucyHealth, type Repair } from "@/lib/useLucyHealth"
|
||||
import { useDialog } from "@/lib/useDialog"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CockpitTile } from "./CockpitTile"
|
||||
import { MorgenlageCard } from "./MorgenlageCard"
|
||||
import { ErinnerungenCard } from "./ErinnerungenCard"
|
||||
import { SystemStatusCard } from "@/components/dashboard/SystemStatusCard"
|
||||
import { TokenPerformanceCard } from "@/components/dashboard/TokenPerformanceCard"
|
||||
import { LatencyCard } from "@/components/dashboard/LatencyCard"
|
||||
@@ -31,6 +34,7 @@ export function CockpitView({ onNavigate }: { onNavigate: (v: string) => void })
|
||||
const { data: memory } = useMemory()
|
||||
const { data: agent } = useAgentStatus()
|
||||
const { data: connect } = useConnectHealth()
|
||||
const { data: auftraege } = useAuftragsbuch(20_000)
|
||||
const health = useLucyHealth()
|
||||
|
||||
const { showAlert, dialogElement } = useDialog()
|
||||
@@ -69,6 +73,7 @@ export function CockpitView({ onNavigate }: { onNavigate: (v: string) => void })
|
||||
(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 connectOk = connect?.gateway.ok && connect?.memory.ok
|
||||
const openAuftraege = auftraege?.available ? auftraege.open_count : 0
|
||||
|
||||
return (
|
||||
<div className="space-y-7">
|
||||
@@ -83,18 +88,30 @@ export function CockpitView({ onNavigate }: { onNavigate: (v: string) => void })
|
||||
{/* Ehrliche Status-Zeile */}
|
||||
<StatusLine verdict={health.verdict} memory={health.memory} warns={health.warns} />
|
||||
|
||||
{/* Morgenlage — das jüngste Nacht-Gutachten (verschwindet, wenn keines da ist) */}
|
||||
<MorgenlageCard />
|
||||
|
||||
{/* „Braucht dich" — offene Aktionen, sonst Ruhe */}
|
||||
<NeedsYou
|
||||
problems={health.problems}
|
||||
pendingCount={pendingCount}
|
||||
openAuftraege={openAuftraege}
|
||||
busy={busy}
|
||||
onRepair={runRepair}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
|
||||
{/* Bereichs-Kacheln */}
|
||||
<section>
|
||||
<ZoneLabel>Bereiche</ZoneLabel>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<CockpitTile
|
||||
icon={Inbox} title="Auftragsbuch" value={auftraege?.available ? openAuftraege : "—"}
|
||||
unit={openAuftraege === 1 ? "Vorschlag" : "Vorschläge"}
|
||||
tone={!auftraege ? "loading" : openAuftraege > 0 ? "warn" : "ok"}
|
||||
hint="Annehmen oder ablehnen"
|
||||
onClick={() => onNavigate("auftraege")}
|
||||
/>
|
||||
<CockpitTile
|
||||
icon={Boxes} title="Modelle" value={running} unit="warm"
|
||||
tone={running > 0 ? "ok" : "muted"} hint="Speicher, laden & Rollen"
|
||||
@@ -129,6 +146,12 @@ export function CockpitView({ onNavigate }: { onNavigate: (v: string) => void })
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Anstehendes — Erinnerungen & Routinen (GUI-Hälfte der Wecker-API) */}
|
||||
<section>
|
||||
<ZoneLabel>Anstehendes</ZoneLabel>
|
||||
<ErinnerungenCard />
|
||||
</section>
|
||||
|
||||
{/* Leistung — die Live-Graphen zurück auf der Startseite (User-Wunsch 07.07.:
|
||||
System-Verlauf, tok/s und Turn-Latenz waren nach dem UI-v3-Umbau nur noch in
|
||||
der alten Zentrale erreichbar). Karten sind autark (eigene Hooks/Polling). */}
|
||||
@@ -162,6 +185,16 @@ export function CockpitView({ onNavigate }: { onNavigate: (v: string) => void })
|
||||
tone={!agent ? "loading" : agent.box_console_reachable ? "ok" : "warn"} hint="Direkte Box-Shell (SSH)"
|
||||
onClick={() => onNavigate("konsole")}
|
||||
/>
|
||||
<CockpitTile
|
||||
icon={Library} title="Wissen" value="öffnen"
|
||||
tone="muted" hint="Lucys Wissens-Vault (Traum-Notizen)"
|
||||
onClick={() => onNavigate("wissen")}
|
||||
/>
|
||||
<CockpitTile
|
||||
icon={History} title="Chronik" value="öffnen"
|
||||
tone="muted" hint="Autonome Taten + Zeitmaschine"
|
||||
onClick={() => onNavigate("chronik")}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -225,18 +258,21 @@ function StatusLine({ verdict, memory, warns }: {
|
||||
)
|
||||
}
|
||||
|
||||
// „Braucht dich": kritische Probleme (mit 1-Klick-Reparatur) + bereitliegende Updates.
|
||||
// Nichts offen → ruhige Bestätigung statt leerer Fläche.
|
||||
// „Braucht dich": kritische Probleme (mit 1-Klick-Reparatur) + bereitliegende Updates
|
||||
// + offene Vorschläge im Auftragsbuch. Nichts offen → ruhige Bestätigung statt leerer Fläche.
|
||||
function NeedsYou({
|
||||
problems, pendingCount, busy, onRepair,
|
||||
problems, pendingCount, openAuftraege, busy, onRepair, onNavigate,
|
||||
}: {
|
||||
problems: ReturnType<typeof useLucyHealth>["problems"]
|
||||
pendingCount: number
|
||||
openAuftraege: number
|
||||
busy: Record<string, boolean>
|
||||
onRepair: (id: string, r: Repair) => void
|
||||
onNavigate: (v: string) => void
|
||||
}) {
|
||||
const hasUpdates = pendingCount > 0
|
||||
const nothing = problems.length === 0 && !hasUpdates
|
||||
const hasAuftraege = openAuftraege > 0
|
||||
const nothing = problems.length === 0 && !hasUpdates && !hasAuftraege
|
||||
|
||||
return (
|
||||
<section>
|
||||
@@ -274,6 +310,23 @@ function NeedsYou({
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{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">Die Box hat etwas gebaut oder sich ausgedacht — annehmen oder ablehnen.</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight className="h-4 w-4 shrink-0 text-primary/70" />
|
||||
</button>
|
||||
)}
|
||||
{hasUpdates && (
|
||||
<button
|
||||
onClick={() => openDrawer("maintenance")}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { useState } from "react"
|
||||
import { AlarmClock, Plus, Trash2, Loader2, X, Repeat } from "lucide-react"
|
||||
import { api } from "@/lib/api"
|
||||
import { useReminders, useQueryClient, qk } from "@/lib/queries"
|
||||
import { useDialog } from "@/lib/useDialog"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
// „Anstehendes": Erinnerungen & Routinen sichtbar und klickbar machen. Die API kann
|
||||
// längst alles (Lucy legt per Stimme an) — hier kommt nur die GUI-Hälfte dazu.
|
||||
|
||||
const REPEAT_LABEL: Record<string, string> = {
|
||||
"": "einmalig", daily: "täglich", weekdays: "werktags", weekly: "wöchentlich",
|
||||
}
|
||||
|
||||
export function ErinnerungenCard() {
|
||||
const { data: items = [], isLoading } = useReminders()
|
||||
const qc = useQueryClient()
|
||||
const { showAlert, showConfirm, dialogElement } = useDialog()
|
||||
const [showAdd, setShowAdd] = useState(false)
|
||||
const [text, setText] = useState("")
|
||||
const [when, setWhen] = useState("")
|
||||
const [repeat, setRepeat] = useState("")
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
const reload = () => qc.invalidateQueries({ queryKey: qk.reminders })
|
||||
|
||||
async function add() {
|
||||
if (!text.trim() || !when) return
|
||||
setSaving(true)
|
||||
try {
|
||||
await api("/api/reminders", { method: "POST", body: JSON.stringify({ text, when, repeat }) })
|
||||
setText(""); setWhen(""); setRepeat(""); setShowAdd(false)
|
||||
reload()
|
||||
} catch (e: any) {
|
||||
showAlert("Anlegen fehlgeschlagen", String(e?.message || e))
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
function del(id: number, t: string) {
|
||||
showConfirm("Erinnerung löschen?", `„${t}" wird entfernt.`, async () => {
|
||||
try { await api(`/api/reminders/${id}`, { method: "DELETE" }); reload() }
|
||||
catch (e: any) { showAlert("Fehler", String(e?.message || e)) }
|
||||
})
|
||||
}
|
||||
|
||||
const sorted = [...items].sort((a, b) => a.next_ts - b.next_ts)
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-border/60 bg-card/45 p-4 shadow-lg shadow-black/15 backdrop-blur-md">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<span className="flex items-center gap-2 text-sm font-bold text-foreground">
|
||||
<span className="flex h-8 w-8 items-center justify-center rounded-lg border border-border/40 bg-background/30 text-amber-300">
|
||||
<AlarmClock className="h-4 w-4" />
|
||||
</span>
|
||||
Anstehendes
|
||||
</span>
|
||||
<button onClick={() => setShowAdd((s) => !s)}
|
||||
className="flex h-8 items-center gap-1 rounded-lg border border-border/60 bg-background/30 px-2.5 text-[11px] font-semibold text-muted-foreground hover:text-foreground hover:border-primary/40 transition-all cursor-pointer">
|
||||
{showAdd ? <X className="h-3.5 w-3.5" /> : <Plus className="h-3.5 w-3.5" />} {showAdd ? "Abbrechen" : "Neu"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showAdd && (
|
||||
<div className="mb-3 space-y-2 rounded-xl border border-border/50 bg-background/30 p-3">
|
||||
<input value={text} onChange={(e) => setText(e.target.value)} placeholder="Woran soll Lucy erinnern?"
|
||||
aria-label="Erinnerungstext"
|
||||
className="h-9 w-full rounded-lg border border-border/60 bg-card/45 px-3 text-xs text-foreground outline-none focus:ring-1 focus:ring-primary/50" />
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<input type="datetime-local" value={when} onChange={(e) => setWhen(e.target.value)}
|
||||
aria-label="Zeitpunkt"
|
||||
className="h-9 rounded-lg border border-border/60 bg-card/45 px-2 text-xs text-foreground outline-none focus:ring-1 focus:ring-primary/50 [color-scheme:dark]" />
|
||||
<select value={repeat} onChange={(e) => setRepeat(e.target.value)} aria-label="Wiederholung"
|
||||
className="h-9 cursor-pointer rounded-lg border border-border/60 bg-card/45 px-2 text-xs font-semibold text-foreground outline-none">
|
||||
{Object.entries(REPEAT_LABEL).map(([v, l]) => <option key={v} value={v} className="bg-popover">{l}</option>)}
|
||||
</select>
|
||||
<button onClick={add} disabled={saving || !text.trim() || !when}
|
||||
className="ml-auto flex h-9 items-center gap-1.5 rounded-lg bg-primary px-3 text-xs font-semibold text-primary-foreground hover:opacity-90 transition-all cursor-pointer disabled:opacity-50">
|
||||
{saving ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Plus className="h-3.5 w-3.5" />} Anlegen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<p className="text-xs text-muted-foreground">Wird geladen …</p>
|
||||
) : sorted.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">Keine Erinnerungen. Lucy sagt dir hier (und per Stimme) Bescheid, wenn etwas ansteht.</p>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
{sorted.slice(0, 6).map((r) => {
|
||||
const overdue = r.next_ts * 1000 < Date.now() && !r.repeat
|
||||
return (
|
||||
<div key={r.id} className="group flex items-center justify-between gap-2 rounded-lg border border-border/40 bg-background/20 px-2.5 py-2">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-xs text-foreground">{r.text}</p>
|
||||
<p className={cn("flex items-center gap-1.5 text-[10px]", overdue ? "text-amber-300" : "text-muted-foreground/70")}>
|
||||
{new Date(r.next_ts * 1000).toLocaleString("de-DE", { weekday: "short", day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" })}
|
||||
{r.repeat && <span className="flex items-center gap-0.5"><Repeat className="h-2.5 w-2.5" />{REPEAT_LABEL[r.repeat] ?? r.repeat}</span>}
|
||||
{overdue && "· überfällig"}
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={() => del(r.id, r.text)} aria-label="Erinnerung löschen"
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg border border-border/40 text-muted-foreground opacity-0 transition-all hover:bg-red-500/5 hover:text-red-400 group-hover:opacity-100 cursor-pointer">
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{sorted.length > 6 && <p className="text-[10px] text-muted-foreground/60">+{sorted.length - 6} weitere</p>}
|
||||
</div>
|
||||
)}
|
||||
{dialogElement}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useState } from "react"
|
||||
import { Moon, ChevronDown, ChevronRight } from "lucide-react"
|
||||
import { useChronik } from "@/lib/queries"
|
||||
|
||||
// Morgenlage: das jüngste Verdikt des nächtlichen Chef-Gutachters (gpt-oss urteilt im
|
||||
// Leerlauf über die autonome Arbeit; der Feed spiegelt es still in den Briefkasten).
|
||||
// Keine Lage vorhanden → Karte verschwindet ganz (Ruhe statt leerer Fläche).
|
||||
export function MorgenlageCard() {
|
||||
const { data: items = [] } = useChronik(150, 60_000)
|
||||
const [open, setOpen] = useState(false)
|
||||
const lage = items.find((i) => i.source === "chef-gutachter")
|
||||
if (!lage) return null
|
||||
|
||||
const when = new Date(lage.ts * 1000).toLocaleString("de-DE", {
|
||||
weekday: "long", day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit",
|
||||
})
|
||||
// VERDIKT-Zeile als Kurzfassung herausziehen (Raster des Chef-Gutachters).
|
||||
const verdikt = lage.text.split("\n").find((l) => l.trim().startsWith("VERDIKT:"))?.replace(/^\s*VERDIKT:\s*/, "")
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-violet-500/25 bg-violet-500/[0.05] p-4 shadow-lg shadow-black/15 backdrop-blur-md">
|
||||
<button onClick={() => setOpen((o) => !o)} className="flex w-full items-center gap-3 text-left cursor-pointer">
|
||||
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl border border-violet-500/30 bg-background/30 text-violet-300">
|
||||
<Moon className="h-4.5 w-4.5" />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block text-sm font-bold text-foreground">Morgenlage</span>
|
||||
<span className="block truncate text-xs text-muted-foreground">
|
||||
{verdikt || lage.subject || "Nächtliches Gutachten über die autonome Arbeit"}
|
||||
</span>
|
||||
</span>
|
||||
<span className="shrink-0 text-[10px] text-muted-foreground/60">{when}</span>
|
||||
{open ? <ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground" /> : <ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground" />}
|
||||
</button>
|
||||
{open && (
|
||||
<div className="mt-3 border-t border-violet-500/15 pt-3">
|
||||
{lage.subject && <p className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-violet-300/80">{lage.subject}</p>}
|
||||
<p className="whitespace-pre-wrap text-xs leading-relaxed text-foreground/85">{lage.text}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user