import { useMemo, useState } from "react" import { AlarmClock, Archive, Bell, HeartPulse, History, Inbox, Loader2, MessageCircle, Moon, Radar, RotateCcw, Search, ShieldAlert, Sparkles, type LucideIcon } from "lucide-react" import { api, type ChronikItem, type Snapshot } from "@/lib/api" import { useNavigate, useSearch } from "@tanstack/react-router" import { Dichteleiste } from "@/features/chronik/Dichteleiste" import { useChronik, useZeitmaschine, useQueryClient, qk } from "@/lib/queries" import { useDialog } from "@/lib/useDialog" import { cn } from "@/lib/utils" // Chronik = die lesbare Geschichte dessen, was die Box von allein getan und gemeldet hat // (Quelle: Melde-Briefkasten). Daneben die Zeitmaschine: Snapshots ansehen und per Klick // zu einem Stand zurückkehren — mit hartem Bestätigungsdialog und Pre-Restore-Sicherung. const SOURCE_UI: Record = { sentry: { icon: HeartPulse, label: "Health-Wächter", cls: "text-red-300 bg-red-500/10 border-red-500/25" }, alarm: { icon: ShieldAlert, label: "Alarm", cls: "text-red-200 bg-red-500/15 border-red-500/40" }, notify: { icon: Bell, label: "Meldung", cls: "text-sky-300 bg-sky-500/10 border-sky-500/25" }, "chef-gutachter": { icon: Moon, label: "Morgenlage", cls: "text-violet-300 bg-violet-500/10 border-violet-500/25" }, radar: { icon: Radar, label: "Radar", cls: "text-teal-300 bg-teal-500/10 border-teal-500/25" }, auftragsbuch: { icon: Inbox, label: "Auftragsbuch", cls: "text-primary bg-primary/10 border-primary/25" }, zeitmaschine: { icon: RotateCcw, label: "Zeitmaschine", cls: "text-amber-300 bg-amber-500/10 border-amber-500/25" }, reminder: { icon: AlarmClock, label: "Erinnerung", cls: "text-amber-300 bg-amber-500/10 border-amber-500/25" }, traum: { icon: Sparkles, label: "Traum", cls: "text-violet-300 bg-violet-500/10 border-violet-500/25" }, } const SOURCE_FALLBACK = { icon: MessageCircle, label: "Box", cls: "text-muted-foreground bg-background/40 border-border/50" } function dayKey(ts: number): string { return new Date(ts * 1000).toLocaleDateString("de-DE", { weekday: "long", day: "2-digit", month: "long" }) } export function ChronikView() { const { data: alle = [], isLoading } = useChronik() const [limitPerDay, setLimitPerDay] = useState(true) // v3-Umbau P6: Das Suchwort steht in der URL (/chronik?q=…). So landet der // „Finden"-Eintrag der Befehlspalette hier — und ein Treffer ist teilbar. const navigate = useNavigate() const { q = "" } = useSearch({ from: "/chronik" }) const suchen = (wert: string) => navigate({ to: "/chronik", search: wert ? { q: wert } : {}, replace: true }) const items = useMemo(() => { const ql = q.trim().toLowerCase() if (!ql) return alle return alle.filter((it) => it.subject.toLowerCase().includes(ql) || it.text.toLowerCase().includes(ql) || it.source.toLowerCase().includes(ql)) }, [alle, q]) const grouped = useMemo(() => { const g: { day: string; items: ChronikItem[] }[] = [] for (const it of items) { const day = dayKey(it.ts) const last = g[g.length - 1] if (last && last.day === day) last.items.push(it) else g.push({ day, items: [it] }) } return g }, [items]) return (

Chronik

Was deine Box von allein getan und gemeldet hat — und die Zeitmaschine, falls du zu einem früheren Stand zurück willst.

Verlauf {q && · {items.length} von {alle.length}}

{/* Zeigt, WANN die Box aktiv war — bevor man liest, WAS sie getan hat (§4.5). */} {!isLoading && items.length > 0 && } {isLoading && (
Chronik wird geladen …
)} {!isLoading && items.length === 0 && (
{q ? <>Nichts zu „{q}" im Verlauf. : "Noch keine Einträge. Sobald die Box etwas von allein tut oder meldet (Updates, Wächter, Träume), steht es hier."}
)} {grouped.map(({ day, items: dayItems }) => (

{day}

{(limitPerDay ? dayItems.slice(0, 8) : dayItems).map((it) => { const ui = SOURCE_UI[it.source] ?? SOURCE_FALLBACK const Icon = ui.icon return (
{ui.label} {it.subject && {it.subject}} {new Date(it.ts * 1000).toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" })}

{it.text}

) })} {limitPerDay && dayItems.length > 8 && ( )}
))}
) } // ── Zeitmaschine: Snapshots + Ein-Klick-Restore ─────────────────────────────── function Zeitmaschine() { const { data } = useZeitmaschine() const qc = useQueryClient() const { showAlert, showConfirm, dialogElement } = useDialog() const [busy, setBusy] = useState(null) const [backupBusy, setBackupBusy] = useState(false) const [showAll, setShowAll] = useState(false) const backups = data?.backups ?? [] const shown = showAll ? backups : backups.slice(0, 4) function fmtSnap(s: Snapshot): string { // Zeitstempel aus backup.sh ist kompakt (20260708-181136) — lesbar machen; // tolerant auch für Varianten mit Trennzeichen (2026-07-08T18-11). const m = s.snapshot.match(/^(\d{4})-?(\d{2})-?(\d{2})[T_-](\d{2})[-:]?(\d{2})/) if (!m) return s.snapshot return `${m[3]}.${m[2]}.${m[1]}, ${m[4]}:${m[5]} Uhr` } async function restore(s: Snapshot) { setBusy(s.file) try { await api("/api/zeitmaschine/restore", { method: "POST", body: JSON.stringify({ file: s.file }) }) showAlert("Zeitmaschine läuft", "Die Wiederherstellung wurde gestartet. Die Dienste starten gleich neu — die Zentrale ist kurz weg und meldet sich von selbst zurück. Vorher wurde automatisch ein Sicherheits-Backup des jetzigen Zustands gemacht.") } catch (e: any) { showAlert("Start fehlgeschlagen", String(e?.message || e)) } finally { setBusy(null) } } async function backupNow() { setBackupBusy(true) try { const r = await api<{ ok: boolean; snapshot?: string; error?: string }>("/api/system/backup", { method: "POST" }) if (r.ok) showAlert("Gesichert", `Snapshot ${r.snapshot} wurde angelegt.`) else showAlert("Backup fehlgeschlagen", r.error || "Unbekannter Fehler") qc.invalidateQueries({ queryKey: qk.zeitmaschine }) } catch (e: any) { showAlert("Backup fehlgeschlagen", String(e?.message || e)) } finally { setBackupBusy(false) } } return (

Zeitmaschine — gesicherte Stände

{data && !data.available && (
Die Snapshots liegen auf der Box — im lokalen Dev-Modus nicht verfügbar.
)} {data?.available && backups.length === 0 && (
Noch keine Snapshots. Das nächtliche Backup (03:30) legt sie automatisch an — oder „Jetzt sichern".
)} {shown.length > 0 && (
{shown.map((s, idx) => (

{fmtSnap(s)} {idx === 0 && neuester}

{s.size_mb.toLocaleString("de-DE")} MB · {s.file}

))}
)} {backups.length > 4 && !showAll && ( )} {dialogElement}
) }