import { useCallback, useEffect, useState } from 'react' import { ChevronLeft, ChevronRight, History, Loader2, PenLine, RefreshCw, Search, FolderInput, Trash2, Upload, X, } from 'lucide-react' import { api, type EventKind, type EventsSeite } from '../api' import { useStore } from '../store' import { bytes, zeit } from '../format' import { Karte, Knopf, Leer } from '../components/Bausteine' import { useAuffrischen } from '../auffrischen' import { useMerker } from '../merker' import { Avatar } from '../components/Avatar' import { VerwaltungListe } from '../components/VerwaltungListe' const SEITENGROESSEN = [25, 50, 100, 200] as const const arten: Record = { upload: { text: 'hochgeladen', farbe: 'text-gut', kind: Upload }, replace: { text: 'ersetzt', farbe: 'text-warn', kind: RefreshCw }, delete: { text: 'gelöscht', farbe: 'text-schlecht', kind: Trash2 }, move: { text: 'verschoben', farbe: 'text-akzent', kind: FolderInput }, // Eigene Art, obwohl es technisch ein Verschieben ist: verschoben heisst // „liegt woanders", umbenannt heisst „heisst anders, liegt noch da". Wer // den Verlauf aufschlägt, will genau diesen Unterschied wissen. rename: { text: 'umbenannt', farbe: 'text-akzent', kind: PenLine }, } export function Verlauf() { const note = useStore((s) => s.note) const [daten, setDaten] = useState(null) const [laedt, setLaedt] = useState(true) const [seite, setSeite] = useState(0) const [art, setArt] = useState('') const [wer, setWer] = useState('') const [proSeite, setProSeite] = useMerker('verlauf.proSeite', 50) const [suche, setSuche] = useState('') // Zwei Verläufe, eine Seite. Der eine beantwortet „was ist mit den Dateien // passiert", der andere „wer hat daran gedreht" — dieselbe Frage aus zwei // Richtungen, und beide gehören dorthin, wo man Verlauf sucht. const [reiter, setReiter] = useState<'dateien' | 'verwaltung'>('dateien') // Wie in der Galerie: erst tippen lassen, dann fragen. Sonst geht bei // "vehicles" eine Anfrage je Buchstabe raus. const [sucheAktiv, setSucheAktiv] = useState('') useEffect(() => { const t = setTimeout(() => { setSucheAktiv(suche) setSeite(0) }, 250) return () => clearTimeout(t) }, [suche]) const laden = useCallback(async () => { setLaedt(true) try { setDaten(await api.events(art, wer, sucheAktiv, proSeite, seite * proSeite)) } catch (err) { note('fehler', err instanceof Error ? err.message : 'Unbekannter Fehler') } finally { setLaedt(false) } }, [art, wer, sucheAktiv, proSeite, seite, note]) useEffect(() => { void laden() }, [laden]) // Im Verlauf ist das Mitlesen der eigentliche Zweck: waehrend ein // Serienlauf laeuft, sieht man hier zu. Blockiert wird nur, wenn man // geblaettert hat -- eine Seite drei, die unter den Haenden zu Seite eins // wird, ist aergerlich. const frisch = useAuffrischen({ laden: () => void laden(), blockiert: seite > 0 }) const items = daten?.items ?? [] // Die Bilder stehen bei den Akteuren und nicht an jedem Eintrag — sonst // schleppte jede der 200 Zeilen dieselbe Adresse mit. const bilder = new Map((daten?.akteure ?? []).map((a) => [a.actor, a.avatar])) const seiten = Math.max(1, Math.ceil((daten?.total ?? 0) / proSeite)) if (reiter === 'verwaltung') { return (
) } return (
{ setArt(''); setSeite(0) }} text="Alles" /> {(Object.keys(arten) as EventKind[]).map((k) => ( { setArt(k); setSeite(0) }} text={arten[k].text} /> ))} { setWer(''); setSeite(0) }} text="alle" /> {daten?.akteure.map((a) => ( { setWer(a.actor); setSeite(0) }} text={`${a.actor} (${a.n})`} kind={ a.actor_kind === 'unbekannt' ? null : ( ) } /> ))} {frisch.wartet && ( Neues da — laden } /> )} {laedt ? ( ) : ( `${daten?.total ?? 0} Einträge` )}
setSuche(e.target.value)} placeholder="Pfad durchsuchen, z. B. vehicles/adder" className="w-full rounded-md border border-rand bg-flaeche py-1.5 pr-8 pl-9 text-sm placeholder:text-schrift-leise/60 focus:border-akzent focus:outline-none" /> {suche && ( )}
{items.length === 0 && !laedt ? ( } text="Für diese Auswahl gibt es nichts." /> ) : ( {/* Die Spaltenbreiten stehen HIER und nicht verteilt an den Zellen: vorher hatten Kopf und Koerper verschiedene Abstaende, und der Pfad wurde abgeschnitten, obwohl daneben Platz frei war. */} {items.map((e) => { const a = arten[e.kind] const Kind = a.kind return ( ) })}
Wann Was Pfad Größe Von wem
{zeit(e.at)} {a.text} {e.von && ( {e.von} →{' '} )} {e.path} {e.size === null ? '—' : bytes(e.size)} {e.actor_kind !== 'unbekannt' && ( )} {e.actor}
)}
{seiten > 1 && (
setSeite((s) => s - 1)} kind={} /> Seite {seite + 1} von {seiten} = seiten} onClick={() => setSeite((s) => s + 1)} kind={} />
)} {/* Was der Verlauf NICHT weiß, gehört dazugesagt. Ein Tab, der so tut, als wüsste er alles, ist schlimmer als einer, der seine Grenze nennt. */}

Aufgezeichnet wird seit {daten?.seit ? zeit(daten.seit) : '—'}. Einträge mit „vor der Aufzeichnung" wurden nachträglich aus dem Bestand erschlossen — ihr Zeitpunkt stimmt, aber wer sie abgelegt hat, weiß niemand mehr. Der Verlauf behält ein halbes Jahr.

) } function Filter({ an, onClick, text, kind, }: { an: boolean onClick: () => void text: string kind?: React.ReactNode }) { return ( ) } /** Die zwei Verläufe nebeneinander. * * Als Reiter und nicht als zweite Seite im Menü: es ist beides ein Verlauf, * und wer einen sucht, sucht nicht zwei Menüeinträge ab. */ function Reiter({ reiter, setReiter, }: { reiter: 'dateien' | 'verwaltung' setReiter: (r: 'dateien' | 'verwaltung') => void }) { const stil = (an: boolean) => `rounded-md px-3 py-1.5 text-sm ${ an ? 'bg-flaeche-hoch text-schrift' : 'text-schrift-leise hover:bg-flaeche-hoch/50 hover:text-schrift' }` return (
) }