Files
Laufzettel/docs/issues/032-anhaenge.md
T
florian.wilde 3ccf3aa2d9
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
feat(anhaenge): Fotos und Dateien an einem Schritt (#52) (#59)
2026-10-05 20:49:26 +00:00

6.3 KiB
Raw Blame History

#032 M19 – Anhänge und Fotos an Schritten

Status: erledigt Im Tracker: #52 Meilenstein: v0.2.0 Labels: phase-2, backend, frontend, infra Geschätzt: 10 h Tatsächlich: 3 h 05 min

Ziel

„Seriennummer fotografiert“, „Typenschild“, „Zustand bei Übergabe“, „Rechnung abgelegt“ — manches lässt sich nicht abhaken, es muss gezeigt werden. Ein Schritt soll Dateien und Fotos aufnehmen können, am Handy direkt aus der Kamera.

Zu entscheiden

  • Dateien im Dateisystem, Metadaten in der Datenbank — Empfehlung. Ein Pfad aus der Umgebung (ANHAENGE_PFAD), darunter nach Kunde und Lauf gegliedert. Gegen bytea in Postgres spricht die Sicherung: ein pg_dump mit Fotos ist nach einem Jahr kein pg_dump mehr, das jemand einspielt. Gegen S3 spricht, dass diese Anwendung selbst gehostet läuft und keinen Objektspeicher voraussetzen soll. Folge, die mitgeplant werden muss: der Compose-Stand und die native Installation brauchen ein Volume, und docs/betrieb/sicherung.md muss beides nennen. Eine Sicherung, die nur die Datenbank mitnimmt, verliert ab hier Daten — das ist genau die Art Zusage, die dieses Projekt sonst teuer bezahlt hat.
  • Keine Verkleinerung auf dem Server im ersten Schritt. Ein Handyfoto hat heute 3–6 MB; eine Obergrenze (etwa 10 MB je Datei, 20 Dateien je Lauf) ist ehrlicher als eine Umwandlung, die Metadaten verliert und eine Bildbibliothek ins Projekt zieht.

Aufgaben

  • Modell Anhang: schrittergebnisId, kundeId, Dateiname, Medientyp, Größe, Prüfsumme, erstelltVonId, erstelltAm; Datei daneben im Dateisystem
  • Hochladen über einen Route Handler mit rechtErforderlich — kein Weg daran vorbei, und jeder Abruf prüft Recht und Mandant, auch der des Bildes selbst
  • Grenzen: Medientypen auf Bilder und PDF, Größe je Datei und Anzahl je Lauf, beides in der Umgebung einstellbar; eine abgewiesene Datei nennt den Grund
  • Am Handy direkt aus der Kamera (capture="environment"), am Rechner aus dem Dateiwähler
  • Der Archiv-Schreibschutz gilt auch hier: an einem archivierten Lauf kommt kein Anhang mehr dazu und keiner weg. laufMussVeraenderbarSein sitzt an genau einer Stelle — dort anschließen
  • Druckansicht: Bilder als Miniaturen mit Dateinamen, PDF nur als Zeile
  • docs/betrieb/sicherung.md, docker-compose.yml, deploy/install.sh: Volume, Rechte, Sicherung und Rückspielung
  • Anleitung Kapitel 02 und 08

Akzeptanzkriterien

  • Test: Anhang an einem Schritt hochladen, in der Abarbeitung und im Druck sichtbar
  • Test: an einem archivierten Lauf wird das Hochladen abgewiesen
  • Test: ein Anhang eines fremden Kunden ist über seine Adresse nicht abrufbar
  • Test: eine zu große Datei und ein falscher Medientyp werden mit Grund abgewiesen
  • Test: docker-compose.yml hat ein Volume für die Anhänge, und sicherung.md nennt es (wie tests/betriebsstand.test.ts es für die Datenbank schon prüft)
  • Sicherung und Rückspielung einmal wirklich gefahren, nicht nur beschrieben

Beim Umsetzen dazugekommen

  • Hochgeladen wird über eine Server Action, nicht über einen eigenen Route Handler. Die Aufgabe oben nannte einen Route Handler; der wäre eine zweite Tür mit eigener Rechteprüfung. Eine Server Action prüft dasselbe Recht an derselben Stelle wie jede andere Schreiboperation am Lauf. Ein Route Handler bleibt für das Ausliefern — dort braucht es eine Adresse, die ein <img> holen kann, und src/app/api/anhaenge/[id]/route.ts ruft selbst rechtErforderlich("lesen"), weil die Hülle unter (app) Route Handler nicht abdeckt.
  • Der Pfad entsteht aus der Kennung, nie aus dem Dateinamen. <ANHAENGE_PFAD>/<kundeId>/<laufId>/<id>.<endung>. Der Name vom Browser wird trotzdem gesäubert, weil er in einer Antwortkopfzeile landet.
  • Kein SVG, obwohl es ein Bild ist: ein SVG ist ein Dokument, das Skripte ausführt. Darum eine Liste erlaubter Medientypen und kein image/*.
  • Reihenfolge beim Anlegen: prüfen, Datei schreiben (flag: "wx"), dann die Zeile — und bei einem Fehler die Datei wieder weg. Eine Datei ohne Zeile findet niemand; eine Zeile ohne Datei ist ein Anhang, der beim Öffnen fehlschlägt. Beim Entfernen umgekehrt: erst die Zeile, dann die Datei.
  • Zwei Felder datei in einem Formular — und FormData.get nahm das falsche. „Foto“ und „Datei“ stehen im selben Formular, abgeschickt wird immer beides, und das leere Feld kam zuerst: 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. Gefunden hat es erst die Klickprobe am gebauten Stand. Die Regel liegt jetzt als reine Funktion gewaehlteDatei() in src/lib, und tests/e2e/anhaenge.spec.ts lädt bewusst über den zweiten Knopf hoch. Gegenprobe gefahren: mit get statt getAll wird der Ablauf rot.
  • docs/betrieb/sicherung.md war nicht nur unvollständig, sondern an einer Stelle falsch — dort stand „Kein Upload-Verzeichnis, keine erzeugten Dateien“. Die Datei ist neu geschrieben: drei Dinge statt zwei, tar für beide Betriebswege, der chown-Fallstrick nach einer Rücksicherung als root, ein Sicherungsskript für den systemd-Timer, das Datenbank und Anhänge im selben Lauf mitnimmt, und die Gegenprobe über die Zahlen.
  • Sicherung und Rücksicherung 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, und die ausgelieferten Bytes gegen die gespeicherte pruefsumme geprüft. Beide stimmten.
  • tests/datenbankregeln.test.ts musste nachziehen. Er zählt die handgeschriebenen Datenbankobjekte namentlich auf; die zwei neuen CHECK-Bedingungen und die beiden zusammengesetzten Fremdschlüssel standen nicht drin, und der Test wurde dadurch rot — genau wie gewollt.
  • /daten/ ist jetzt in .gitignore. Ohne das läge das erste hochgeladene Foto im nächsten Commit.