feat(anhaenge): Fotos und Dateien an einem Schritt (#52) #59

Merged
florian.wilde merged 1 commits from feat/52-anhaenge into main 2026-10-05 20:49:27 +00:00
Owner

M19 aus Phase 2. Ein Schritt nimmt Bilder (JPEG, PNG, WebP, HEIC) und PDF auf, am Handy direkt aus der Kamera.

Wie es gebaut ist

  • Dateien im Dateisystem, Metadaten in der Datenbank. Unter ANHAENGE_PFAD, nach Kunde und Lauf gegliedert. Der Pfad entsteht aus der Kennung, nie aus dem Dateinamen vom Browser. Gegen bytea sprach die Sicherung: ein pg_dump mit Fotos ist nach einem Jahr kein Abzug mehr, den jemand einspielt.
  • Jeder Abruf prüft Recht und Mandantengrenze — auch der des Bildes selbst. src/app/api/anhaenge/[id]/route.ts ruft rechtErforderlich("lesen") selbst, weil die Hülle unter (app) Route Handler nicht abdeckt.
  • Hochgeladen wird über eine Server Action, nicht über einen zweiten Endpunkt. Der Archiv-Schreibschutz hängt an derselben einen Stelle wie alles andere am Lauf.
  • Kein SVG. Ein SVG ist ein Dokument, das Skripte ausführt; darum eine Liste erlaubter Medientypen und kein image/*.
  • Die Regeln stehen auch in der Datenbank: zwei CHECK-Bedingungen und zwei zusammengesetzte Fremdschlüssel. Ein Anhang kann strukturell nicht an einem Schritt eines anderen Laufs oder bei einem fremden Kunden hängen.

Was die Klickprobe gefunden hat

Zwei Felder datei in einem Formular — „Foto“ und „Datei“ —, und FormData.get nahm das erste, also das leere. Wer „Datei“ wählte, bekam „Keine Datei gewählt“, während „Foto“ ging. Neunzehn grüne Dienst-Tests sahen davon nichts, weil kein Dienst-Test zwei Felder schickt. Dieselbe Lehre wie in #37: Fehler im Weg, den ein Dritter geht, sind für eine grüne Suite unsichtbar.

Die Regel liegt jetzt als reine Funktion gewaehlteDatei() in src/lib/anhaenge.ts, und tests/e2e/anhaenge.spec.ts lädt bewusst über den zweiten Knopf hoch. Gegenprobe gefahren: mit get statt getAll wird der Ablauf rot.

Sicherung

docs/betrieb/sicherung.md enthielt einen falschen Satz: „Kein Upload-Verzeichnis, keine erzeugten Dateien.“ Eine Sicherung nach der alten Anleitung hätte ab hier still Daten verloren. Die Datei ist neu geschrieben — drei Dinge statt zwei, tar für beide Betriebswege, der chown-Fallstrick nach einer Rücksicherung als root, ein Skript, das Datenbank und Anhänge im selben Lauf mitnimmt.

Einmal wirklich gefahren, nicht beschrieben: Abzug gezogen, Datenbank verworfen und neu angelegt, Verzeichnis gelöscht, erst nur die Datenbank zurückgespielt — und da meldete der Abruf genau den dokumentierten Satz „Die Datei zu … fehlt im Dateisystem“ —, dann die Anhänge dazu. Die ausgelieferten Bytes gegen die gespeicherte pruefsumme: beide stimmten.

Geprüft

  • pnpm lint, typecheck, format:pruefen, pruefe:doku grün
  • 1247 Vitest-Tests grün (vorher 1225), davon 20 neue in tests/anhaenge.test.ts
  • 4 Playwright-Abläufe in tests/e2e/anhaenge.spec.ts, Desktop und Handy
  • Klickprobe am gebauten Stand: hochladen, Miniatur, neu laden, abrufen mit image/png und nosniff, SVG abgewiesen, 11 MB abgewiesen, entfernen, erneut hochladen, Druckansicht, archivierter Lauf nur lesend, 390 px ohne waagerechtes Scrollen

Closes #52

🤖 Generated with Claude Code

M19 aus Phase 2. Ein Schritt nimmt Bilder (JPEG, PNG, WebP, HEIC) und PDF auf, am Handy direkt aus der Kamera. ## Wie es gebaut ist - **Dateien im Dateisystem, Metadaten in der Datenbank.** Unter `ANHAENGE_PFAD`, nach Kunde und Lauf gegliedert. Der Pfad entsteht aus der Kennung, nie aus dem Dateinamen vom Browser. Gegen `bytea` sprach die Sicherung: ein `pg_dump` mit Fotos ist nach einem Jahr kein Abzug mehr, den jemand einspielt. - **Jeder Abruf prüft Recht und Mandantengrenze** — auch der des Bildes selbst. `src/app/api/anhaenge/[id]/route.ts` ruft `rechtErforderlich("lesen")` selbst, weil die Hülle unter `(app)` Route Handler nicht abdeckt. - **Hochgeladen wird über eine Server Action**, nicht über einen zweiten Endpunkt. Der Archiv-Schreibschutz hängt an derselben einen Stelle wie alles andere am Lauf. - **Kein SVG.** Ein SVG ist ein Dokument, das Skripte ausführt; darum eine Liste erlaubter Medientypen und kein `image/*`. - **Die Regeln stehen auch in der Datenbank:** zwei CHECK-Bedingungen und zwei zusammengesetzte Fremdschlüssel. Ein Anhang kann strukturell nicht an einem Schritt eines anderen Laufs oder bei einem fremden Kunden hängen. ## Was die Klickprobe gefunden hat Zwei Felder `datei` in einem Formular — „Foto“ und „Datei“ —, und `FormData.get` nahm das erste, also das leere. Wer „Datei“ wählte, bekam „Keine Datei gewählt“, während „Foto“ ging. **Neunzehn grüne Dienst-Tests sahen davon nichts**, weil kein Dienst-Test zwei Felder schickt. Dieselbe Lehre wie in #37: Fehler im Weg, den ein Dritter geht, sind für eine grüne Suite unsichtbar. Die Regel liegt jetzt als reine Funktion `gewaehlteDatei()` in `src/lib/anhaenge.ts`, und `tests/e2e/anhaenge.spec.ts` lädt bewusst **über den zweiten Knopf** hoch. Gegenprobe gefahren: mit `get` statt `getAll` wird der Ablauf rot. ## Sicherung `docs/betrieb/sicherung.md` enthielt einen **falschen** Satz: „Kein Upload-Verzeichnis, keine erzeugten Dateien.“ Eine Sicherung nach der alten Anleitung hätte ab hier still Daten verloren. Die Datei ist neu geschrieben — drei Dinge statt zwei, `tar` für beide Betriebswege, der `chown`-Fallstrick nach einer Rücksicherung als `root`, ein Skript, das Datenbank und Anhänge im **selben** Lauf mitnimmt. **Einmal wirklich gefahren**, nicht beschrieben: Abzug gezogen, Datenbank verworfen und neu angelegt, Verzeichnis gelöscht, erst nur die Datenbank zurückgespielt — und da meldete der Abruf genau den dokumentierten Satz „Die Datei zu … fehlt im Dateisystem“ —, dann die Anhänge dazu. Die ausgelieferten Bytes gegen die gespeicherte `pruefsumme`: beide stimmten. ## Geprüft - `pnpm lint`, `typecheck`, `format:pruefen`, `pruefe:doku` grün - **1247 Vitest-Tests** grün (vorher 1225), davon 20 neue in `tests/anhaenge.test.ts` - **4 Playwright-Abläufe** in `tests/e2e/anhaenge.spec.ts`, Desktop und Handy - Klickprobe am **gebauten** Stand: hochladen, Miniatur, neu laden, abrufen mit `image/png` und `nosniff`, SVG abgewiesen, 11 MB abgewiesen, entfernen, erneut hochladen, Druckansicht, archivierter Lauf nur lesend, 390 px ohne waagerechtes Scrollen Closes #52 🤖 Generated with [Claude Code](https://claude.com/claude-code)
florian.wilde added 1 commit 2026-10-04 21:05:38 +00:00
feat(anhaenge): Fotos und Dateien an einem Schritt (#52)
Prüfung / pruefung (push) Canceled after 0s
Prüfung / pruefung (pull_request) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
Prüfung / e2e (pull_request) Canceled after 0s
3cbd5026f8
Ein Schritt nimmt jetzt Bilder (JPEG, PNG, WebP, HEIC) und PDF auf, am
Handy direkt aus der Kamera. Die Dateien liegen im Dateisystem unter
ANHAENGE_PFAD, nach Kunde und Lauf gegliedert; in der Datenbank stehen
nur Name, Typ, Groesse und Pruefsumme. Der Pfad entsteht aus der
Kennung, nie aus dem Dateinamen vom Browser.

Jeder Abruf prueft Recht und Mandantengrenze — auch der des Bildes
selbst: der Route Handler ruft rechtErforderlich() selbst, weil die
Huelle unter (app) Route Handler nicht abdeckt. Hochgeladen wird ueber
eine Server Action statt ueber einen zweiten Endpunkt; der
Archiv-Schreibschutz haengt an derselben einen Stelle wie alles andere
am Lauf. Kein SVG: ein SVG ist ein Dokument, das Skripte ausfuehrt.

In der Datenbank halten zwei CHECK-Bedingungen und zwei
zusammengesetzte Fremdschluessel fest, was auch gilt, wenn jemand an
der Service-Schicht vorbei schreibt — ein Anhang kann strukturell nicht
an einem Schritt eines anderen Laufs oder bei einem fremden Kunden
haengen.

docs/betrieb/sicherung.md ist neu geschrieben: dort stand "Kein
Upload-Verzeichnis, keine erzeugten Dateien", und eine Sicherung nach
der alten Anleitung haette ab hier still Daten verloren. Sicherung und
Ruecksicherung sind einmal wirklich gefahren, mit Pruefsummenvergleich.

Gefunden in der Klickprobe am gebauten Stand und nur dort: zwei Felder
"datei" in einem Formular, und FormData.get nahm das leere. Wer "Datei"
waehlte, bekam "Keine Datei gewaehlt", waehrend "Foto" ging. Die Regel
liegt jetzt als reine Funktion in src/lib, und tests/e2e/anhaenge.spec.ts
laedt bewusst ueber den zweiten Knopf hoch.

Closes #52

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
florian.wilde merged commit 3ccf3aa2d9 into main 2026-10-05 20:49:27 +00:00
florian.wilde deleted branch feat/52-anhaenge 2026-10-05 20:49:27 +00:00
Sign in to join this conversation.