feat(anhaenge): Fotos und Dateien an einem Schritt (#52)
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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
855164066c
commit
3cbd5026f8
+56
-18
@@ -1,10 +1,10 @@
|
||||
# #032 M19 – Anhänge und Fotos an Schritten
|
||||
|
||||
**Status:** offen
|
||||
**Status:** erledigt
|
||||
**Im Tracker:** [#52](https://git.wilde-server.de/florian.wilde/Laufzettel/issues/52)
|
||||
**Meilenstein:** v0.2.0
|
||||
**Labels:** phase-2, backend, frontend, infra
|
||||
**Geschätzt:** 10 h **Tatsächlich:** –
|
||||
**Geschätzt:** 10 h **Tatsächlich:** 3 h 05 min
|
||||
|
||||
## Ziel
|
||||
|
||||
@@ -14,7 +14,7 @@ können, am Handy direkt aus der Kamera.
|
||||
|
||||
## Zu entscheiden
|
||||
|
||||
- [ ] **Dateien im Dateisystem, Metadaten in der Datenbank** — Empfehlung. Ein Pfad aus der
|
||||
- [x] **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
|
||||
@@ -23,32 +23,70 @@ können, am Handy direkt aus der Kamera.
|
||||
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
|
||||
- [x] **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,
|
||||
- [x] 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
|
||||
- [x] 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
|
||||
- [x] 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
|
||||
- [x] Am Handy direkt aus der Kamera (`capture="environment"`), am Rechner aus dem Dateiwähler
|
||||
- [x] **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,
|
||||
- [x] Druckansicht: Bilder als Miniaturen mit Dateinamen, PDF nur als Zeile
|
||||
- [x] `docs/betrieb/sicherung.md`, `docker-compose.yml`, `deploy/install.sh`: Volume, Rechte,
|
||||
Sicherung **und** Rückspielung
|
||||
- [ ] Anleitung Kapitel 02 und 08
|
||||
- [x] 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
|
||||
- [x] Test: Anhang an einem Schritt hochladen, in der Abarbeitung und im Druck sichtbar
|
||||
- [x] Test: an einem archivierten Lauf wird das Hochladen abgewiesen
|
||||
- [x] Test: ein Anhang eines fremden Kunden ist über seine Adresse **nicht** abrufbar
|
||||
- [x] Test: eine zu große Datei und ein falscher Medientyp werden mit Grund abgewiesen
|
||||
- [x] 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
|
||||
- [x] 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.
|
||||
|
||||
Reference in New Issue
Block a user