POST /api/dash/upload, eigener Weg neben /api/upload. Der dortige haengt an
einem Bearer-Token; wer im Dashboard sitzt, hat eine SITZUNG und keinen Token,
und einen Token nur zum Hochladen anzulegen waere ein Geheimnis mehr, das
jemand verlieren kann.
Mehrere Dateien auf einmal, und JEDE bekommt ihr eigenes Ergebnis mit Pfad,
Groesse und Grund im Fehlerfall. Ein "3 von 5 hochgeladen" ohne die Namen der
zwei anderen ist genau die Sorte Rueckmeldung, wegen der wir hier neu bauen.
Eine kaputte Datei bricht die anderen nicht ab.
parseBody({ all: true }) ist noetig: ohne das liefert Hono bei mehreren Feldern
gleichen Namens nur das LETZTE, und aus fuenf Dateien wuerde eine.
DATEINAMEN WERDEN ZURECHTGEBOGEN, und zwar sichtbar. checkPath laesst
"Mein Video (final) 2.MP4" mit Recht nicht durch, also macht safeFilename
daraus "mein-video-final-2.mp4" -- Umlaute werden umschrieben und nicht
weggeworfen ("Grüße" -> "gruesse", nicht "gr-e"). Die Ergebnisliste zeigt den
neuen Pfad neben dem alten Namen.
BEIM MESSEN GEFUNDEN: bleibt vom Namen nichts uebrig -- "ЖУРНАЛ.txt",
" .png", "★★★.txt" --, hiessen bisher ALLE "datei.txt", und die zweite
Datei ueberschrieb die erste stillschweigend. Jetzt haengen die ersten acht
Stellen des SHA-256 daran: gleicher Inhalt ergibt denselben Namen,
verschiedener Inhalt verschiedene. Nachgemessen mit drei solchen Namen.
Der Fortschrittsbalken laeuft ueber XMLHttpRequest und nicht fetch: nur der
kennt upload.onprogress, und bei einem 40-MB-Video ist ein Balken der
Unterschied zwischen "es tut sich was" und "ist es abgestuerzt?".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ORDNERUEBERSICHT. Eine Spalte neben der Galerie mit allen Ordnern, auf allen
Ebenen, mit Anzahl und Groesse; ein Klick filtert. Neu dafuer:
- GET /api/dash/folders -- die Ordner werden in JavaScript zusammengezaehlt
und nicht in SQL: fuer eine Baumansicht braucht es JEDE Ebene, und das waere
in SQLite eine rekursive Abfrage, die niemand mehr liest.
- GET /api/dash/media?folder=... als EIGENER Filter neben der Suche. Die Suche
findet mit LIKE %...% auch "alt/vehicles/adder.webp"; ein Ordner, der Dinge
zeigt, die nicht darin liegen, ist keiner. Hier ist der Anfang verankert.
- Die LIKE-Sonderzeichen werden maskiert: ein Ordner "fotos_alt" wuerde sonst
auch "fotosXalt" treffen, weil _ in LIKE ein beliebiges Zeichen ist.
Die Zahlen sind kumulativ -- "vehicles" enthaelt, was in "vehicles/showroom"
liegt. Alles andere ueberrascht in einer Baumansicht. Nachgemessen ueber drei
Ebenen: vehicles (24) > showroom (2) > tief (1), Filter und Zaehlung stimmen,
und nach dem Aufraeumen steht wieder vehicles (22).
Flache Liste mit Einrueckung statt aufklappbarem Baum: bei einer Handvoll
Ordner ist ein Aufklappmechanismus mehr Bedienung als Nutzen -- und was man
nicht aufklappen muss, kann man auch nicht zugeklappt uebersehen.
Dazu: der NPM-Zwischenspeicher ist zu. Der Schnipsel steht im Feld "Custom
Nginx Configuration"; NPM setzt ihn innerhalb des server-Blocks ein, eine
location ist dort also gueltig. Nachgemessen: cache-control kommt wieder von
uns, und ein ueberschriebenes Bild ist sofort das neue.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SICHERUNG (tools/sichern.ps1, taeglich 04:30 als geplante Aufgabe).
Die Datenbank wird nicht kopiert, sondern ueber SQLites eigene
Sicherungsschnittstelle herausgeholt (server/src/backup.ts): im WAL-Modus
liegt das Zuletzte noch nicht in media.db, und selbst alle drei Dateien zu
kopieren ist nicht sicher, wenn waehrenddessen geschrieben wird. Die Bilder
kommen aus einem NUR LESEND eingehaengten Volume dazu.
Und sie prueft sich selbst: auspacken, Datenbank oeffnen, Medien/Token/
Benutzer zaehlen, mit dem laufenden Dienst vergleichen, sonst Fehler. Einmal
wirklich zurueckgespielt -- leeres Volume, zweiter Dienst, Anmeldung mit dem
echten Passwort, Bild abgerufen, Byte fuer Byte identisch. Eine Sicherung, die
nie zurueckgespielt wurde, ist eine Hoffnung.
ANMELDEBREMSE. Das Formular steht oeffentlich, und scrypt macht einen Versuch
teuer -- aber teuer ist nicht selten. Fuenf freie Versuche je Adresse, dann
Sperre ab 30 s mit Verdopplung bis 15 min, 429 samt Retry-After und einem Text,
der sagt wie lange. Erfolg setzt zurueck. Nur im Speicher: wer sich aussperrt,
startet den Container neu. Durchgemessen bis zur Erholung.
VORSCHAUBILDER. sharp erzeugt beim Upload eine 320er WebP-Fassung unter
/data/thumbs, ausgeliefert unter /t/<pfad>. Gemessen: 131502 -> 13078 Bytes,
Faktor 10; eine Galerieseite faellt von 7,5 MB auf 766 KB. KEINE Spalte in der
Datenbank -- ob es eine Vorschau gibt, sagt das Dateisystem, und die Galerie
faellt bei 404 aufs Vollbild zurueck. Eine zweite Wahrheit, die auseinander-
laufen kann, gibt es damit gar nicht erst. Ein Knopf zieht Fehlendes nach und
nennt Zahlen statt "fertig".
SCHRITT 5. Die Fivemanage-Dateien liegen in legacy/ mit Erklaerung; unsere
docker-compose.media.yml heisst jetzt docker-compose.yml. Drei Volumes und drei
Abbilder geloescht, rund 670 MB -- nachgezaehlt war vorher, dass kein Bild
darin lag.
Nebenbefund: `npm i sharp` scheitert auf diesem Rechner nicht an sharp, sondern
an better-sqlite3 -- der Host laeuft auf Node 24 (ABI 137), node-gyp uebernimmt
und findet keine Bauwerkzeuge. Genau die Falle aus dem Dockerfile. Eingetragen
wurde mit --package-lock-only, gebaut wird im Abbild auf Node 22.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Schritt 4. React 18, TypeScript, Vite 5, Tailwind 4 (CSS-first), Lucide,
Zustand; kein Konsta. Quelle in server/ui, Build nach server/web (gitignored),
im Abbild von einer eigenen Docker-Stufe gebaut.
JEDER KNOPF GIBT RUECKMELDUNG -- und zwar erzwungen, nicht vorgenommen:
- api.ts haelt den EINZIGEN fetch und GENAU EINE Fehlerklasse. Der
Fivemanage-Fehler war, zwei zu haben: geworfen wurde `new Error`, geprueft
auf `instanceof ApiError`, und damit verschwand jeder Fehlschlag lautlos.
- store.ts haelt run(): Aktion rein, Erfolgstext rein, und es meldet bei
Erfolg diesen und bei Fehlschlag den Text DES DIENSTES. Wer darueber geht,
kann keinen stillen Knopf bauen.
- Jeder wartende Knopf ist gesperrt und zeigt einen Kreisel. Der zweite Klick
eine Sekunde spaeter war der Weg zu neunzehn gleichnamigen Organisationen.
- Fehlermeldungen bleiben stehen, bis jemand sie wegklickt. Ein Fehler, der
von allein verschwindet, ist einer, den niemand gelesen hat.
@vitejs/plugin-react ist auf ^4 festgenagelt: die 6 verlangt vite ^8. In den
peerDependencies nachgesehen -- Vite 5.4.21 liegt in der Schnittmenge von
plugin-react@4 und @tailwindcss/vite@4.
Am laufenden Container nachgemessen: / kommt als text/html, assets/*.js als
text/javascript, *.css als text/css (die MIME-Falle aus Schritt 1 traegt), und
/tokens, /speicher, /konto liefern dieselbe index.html -- die SPA-Rueckfall-
route tut, wofuer sie gebaut wurde.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>