feat: Verlauf als CSV, Meldung bei Upload mit Drossel, Deckel auf dem Papierkorb

Abschnitt 3 aus docs/ideen.md, bis auf die Sicherungsziele.

VERLAUF ALS CSV. Der Export konnte nur den Bestand; wer nachrechnen wollte,
was ueber einen Zeitraum passiert ist, sass vor einer Tabelle, die nach 500
Zeilen umblaettert. Der Knopf sitzt in der Reiterleiste und holt den Verlauf,
DER GERADE OFFEN IST -- zwei Knoepfe nebeneinander waeren die Frage "welchen
von beiden?" an einer Stelle, wo die Antwort schon auf dem Bildschirm steht.

Semikolon und CRLF wie beim Bestands-Export; das war dort nachgemessen worden.
Gemessen an der ausgelieferten Datei: 5371 Zeilen, kein einziges nacktes LF.
Ohne Anmeldung 401 -- die Route liegt hinter dem Waechter, was nach dem
Sitzungs-Fehler von vorgestern ausdruecklich nachgeprueft wurde.

MELDUNG BEI UPLOAD -- die Drossel ist der eigentliche Inhalt. Jeder Upload
schiebt eine Frist von zwei Minuten nach hinten; erst nach der Ruhe geht EINE
Nachricht raus. Ohne das waere die Funktion ein Schaden: 900 Fahrzeugbilder
ergaeben 900 Nachrichten, Discord drosselt Webhooks, und wer danach eine echte
Meldung bekommt, sieht sie nicht mehr.

Gemessen: drei Uploads mit zehn Sekunden Abstand -> eine Meldung ueber drei
Dateien. Ein einzelner Upload -> eine Meldung ueber eine Datei.

Nur Token-Uploads, absichtlich. Was ueber das Dashboard hereinkommt, hat gerade
jemand selbst hochgeladen und bestaetigt bekommen. Zurueckholen aus dem
Papierkorb meldet gar nichts: das ist kein Neues, das ist ein Wiedergefundenes.

Und derselbe Fehler wie bei 'platte' lauerte schon wieder: die Liste der
Anlaesse steht im Server UND in der Oberflaeche, und der Server verwirft beim
Speichern alles, was in seiner Liste fehlt -- ein Kreuzchen, das sich setzen
laesst und beim naechsten Laden weg ist. Diesmal beide angefasst, in beiden
steht jetzt ein Verweis auf die andere. Nachgemessen: vier geschickt, vier
gespeichert.

DECKEL AUF DEM PAPIERKORB. 30 Tage UND 20 GB, was zuerst greift. Wer 200 GB
loescht, haelt sie sonst 30 Tage doppelt und merkt es erst, wenn die Platte
voll ist. Geraeumt wird das AELTESTE zuerst -- dessen Versehen waere am
ehesten schon aufgefallen.

Ueber PAPIERKORB_MAX_MB verstellbar, und das nicht aus Bequemlichkeit: ein
Deckel von 20 GB laesst sich nicht pruefen, ohne 20 GB zu loeschen. Mit
0.0002 MB nachgemessen -- vier Eintraege zu 422 Bytes, drei entfernt, uebrig
blieb der NEUESTE. Danach zurueck auf 20 GB, gemessen 21474836480.

NEBENBEI, DREIMAL DIESELBE SORTE FEHLER: "0.0 MB" fuer 422 Bytes, "0 KB" fuer
106 Bytes, "0.0 MB von 0.0 MB" beim Deckel. Es gab drei Byte-Formatierer, jeder
mit anderer Untergrenze. Jetzt einer in meldung.ts, von Bytes bis GB, exportiert
und von dash.ts mitbenutzt. Nachgemessen ueber alle Stufen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 08:25:50 +02:00
co-authored by Claude Opus 5
parent 6a49713821
commit bbb74138c8
10 changed files with 465 additions and 28 deletions
+36 -7
View File
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react'
import {
ChevronLeft,
ChevronRight,
Download,
History,
Loader2,
PenLine,
@@ -371,6 +372,10 @@ function Reiter({
reiter: 'dateien' | 'verwaltung'
setReiter: (r: 'dateien' | 'verwaltung') => void
}) {
// Jeder Knopf gibt Rueckmeldung, auch der erfolgreiche: ein Download, der
// nur im Browser-Balken auftaucht, sieht aus wie ein Knopf, der nichts tut.
const note = useStore((s) => s.note)
const stil = (an: boolean) =>
`rounded-md px-3 py-1.5 text-sm ${
an
@@ -378,15 +383,39 @@ function Reiter({
: 'text-schrift-leise hover:bg-flaeche-hoch/50 hover:text-schrift'
}`
return (
<div className="flex gap-1 self-start rounded-lg border border-rand bg-flaeche p-1">
<button className={stil(reiter === 'dateien')} onClick={() => setReiter('dateien')}>
Dateien
</button>
<div className="flex flex-wrap items-center gap-2">
<div className="flex gap-1 rounded-lg border border-rand bg-flaeche p-1">
<button className={stil(reiter === 'dateien')} onClick={() => setReiter('dateien')}>
Dateien
</button>
<button
className={stil(reiter === 'verwaltung')}
onClick={() => setReiter('verwaltung')}
>
Verwaltung
</button>
</div>
{/* Der Knopf holt IMMER den Verlauf, der gerade offen ist. Zwei
Knoepfe nebeneinander waeren die Frage „welchen von beiden?" an
einer Stelle, wo die Antwort schon auf dem Bildschirm steht. */}
<button
className={stil(reiter === 'verwaltung')}
onClick={() => setReiter('verwaltung')}
onClick={() => {
// Ueber einen Link und nicht ueber fetch: so bekommt die
// Datei den Namen aus dem Content-Disposition und landet im
// Download-Ordner statt im Speicher des Browsers.
const a = document.createElement('a')
a.href = `/api/dash/verlauf/export?was=${reiter}`
a.rel = 'noreferrer'
a.click()
note('ok', `Der ${reiter === 'dateien' ? 'Datei' : 'Verwaltungs'}-Verlauf wird heruntergeladen.`)
}}
title="Als CSV herunterladen — Semikolon und CRLF, damit Excel es direkt aufmacht"
className="flex items-center gap-1.5 rounded-md border border-rand px-3 py-1.5
text-sm text-schrift-leise hover:bg-flaeche-hoch hover:text-schrift"
>
Verwaltung
<Download size={15} />
CSV
</button>
</div>
)