Stapel-Import und automatischer Titel

Beim Erstbestand sind viele Teile auf einmal zu erfassen. Der Aufwand
steckt nicht im Fotografieren, sondern im Rundlauf pro Stück. Darum:

- Stapel-Import: erst alles mit der Kamera-App fotografieren, dann alle
  Fotos auf einmal hochladen. Pro Foto entsteht ein Entwurf, die Details
  kommen später. Bewusst ohne capture="environment" - das erzwingt ein
  Foto pro Vorgang und schliesst multiple aus.
- Titel wird optional und sonst aus Kategorie und Grösse gebildet
  ("Hose 98/104"). Erzeugt beim Anzeigen, nicht beim Speichern, damit er
  einer späteren Korrektur der Grösse folgt.

Daraus folgt ein neuer Status "draft" im Datenmodell; title und size
müssen NULL erlauben. Entwürfe erscheinen nicht in der Galerie.

Ausserdem HEIC in Phase 2 aufgenommen: iPhones nehmen so auf, und beim
Stapel-Import fiele ein nicht lesbares Bild sonst still hinten runter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GR4bNaj9GtRu57J4Niii8o
This commit is contained in:
2026-08-29 21:45:29 +02:00
co-authored by Claude Opus 5
parent c8f11e0642
commit da4267c369
+66 -5
View File
@@ -76,7 +76,7 @@ getarntes Skript ebenfalls.
## Datenmodell Abweichungen von `projekt.md`
Das Schema wird grundsätzlich übernommen. Vier Ergänzungen:
Das Schema wird grundsätzlich übernommen. Fünf Ergänzungen:
1. **Feste Wertelisten statt freier VARCHAR.** `gender`, `season`, `condition`
und `status` bekommen ein Python-Enum plus `CHECK`-Bedingung in der
@@ -97,12 +97,65 @@ Das Schema wird grundsätzlich übernommen. Vier Ergänzungen:
4. **`updated_at`** wird in der Anwendung gesetzt, nicht per DB-Trigger
bleibt so unabhängig von SQLite-Eigenheiten.
5. **Status `draft` (neu).** `projekt.md` kennt nur `available`, `reserved`
und `given_away`. Der Stapel-Import (siehe unten) legt Einträge an, die
noch keinen Titel und keine Grösse haben die dürfen nicht in der
öffentlichen Galerie erscheinen. `title` und `size` müssen dafür
`NULL` erlauben; gefüllt sein müssen sie erst beim Wechsel auf
`available`.
Ausserdem: beim Löschen eines Eintrags müssen die **Bilddateien mitgelöscht**
werden, sonst wächst das Upload-Verzeichnis unbegrenzt mit verwaisten Dateien.
`ON DELETE CASCADE` räumt nur die Datenbankzeilen weg, nicht die Dateien.
---
## Schnelles Erfassen
Beim Erstbestand sind auf einen Schlag viele Teile zu erfassen. Der Aufwand
steckt dabei nicht im Fotografieren, sondern im Rundlauf pro Stück: Formular
öffnen, fotografieren, Felder ausfüllen, speichern, von vorn. Zwei Massnahmen
dagegen.
### Stapel-Import
Zuerst wird alles mit der gewohnten Kamera-App fotografiert die ist fürs
schnelle Knipsen gebaut, ohne Umweg über den Browser. Danach **einmal** ins
Formular, alle Fotos auf einmal auswählen:
```html
<input type="file" name="fotos" accept="image/*" multiple>
```
Pro Foto entsteht ein Eintrag mit Status `draft`. Die Details werden später
nachgetragen in Ruhe, auch am Rechner mit richtiger Tastatur. Damit ist
Fotografieren vom Erfassen entkoppelt.
Bewusst **ohne** `capture="environment"`: das springt zwar direkt in die
Rückkamera, erzwingt aber ein Foto pro Vorgang und schliesst `multiple` aus.
Für einzelne Nachträge im Alltag ist es passend dann als zweiter, kleiner
Knopf „Direkt fotografieren" neben dem regulären Feld.
Nötig dafür:
- Ansicht „Unfertige Einträge (12)", nur für angemeldete Benutzer
- Entwürfe erscheinen nicht in der Galerie und nicht in `GET /api/v1/items`,
solange dort nicht ausdrücklich `status=draft` angefragt wird
- Ein Entwurf wird erst zu `available`, wenn Grösse und Kategorie gesetzt sind
### Titel ist freiwillig
`title` wird optional. Bleibt das Feld leer, setzt die Anwendung ihn aus
Kategorie und Grösse zusammen „Hose 98/104", „Winterjacke 116". Das ist
genau die Angabe, nach der man in einer Galerie ohnehin sucht, und spart beim
Erfassen das mit Abstand lästigste Feld, weil es als einziges freien Text
verlangt.
Erzeugt wird der Titel **beim Anzeigen**, nicht beim Speichern: sonst bliebe
ein automatisch gesetzter Titel stehen, wenn später die Grösse korrigiert
wird. In der Datenbank bleibt `title` dann schlicht `NULL`.
---
## Absicherung
Die App ist aus dem Internet erreichbar, also gelten hier andere Massstäbe als
@@ -181,9 +234,15 @@ Die Phasen aus `projekt.md`, ergänzt um Tests und die Absicherung.
**Phase 2 Bilder**
- Upload per `multipart/form-data`, mehrere Bilder pro Eintrag
- Prüfung und WebP-Konvertierung (max. 1200 px Breite) nach Abschnitt 1 oben
- **HEIC unterstützen** (`pillow-heif`): iPhones nehmen in diesem Format auf.
Safari wandelt beim Hochladen über ein Datei-Feld meist selbst nach JPEG
um aber nicht zuverlässig, und beim Stapel-Import fällt ein einzelnes
nicht lesbares Bild sonst still hinten runter. Pillow allein kann HEIC
nicht öffnen.
- Ausliefer-Route, `is_primary`-Logik, Aufräumen beim Löschen
- Prüfpunkt: bewusst fehlerhafte Uploads (zu gross, falscher Typ, riesige
Pixelmasse, `.php` als `.jpg` getarnt) werden alle abgewiesen
Pixelmasse, `.php` als `.jpg` getarnt) werden alle abgewiesen; ein
HEIC-Bild wird angenommen
**Phase 3 Absicherung**
- Anmeldung, Sitzung, CSRF, Rate-Limits, Sicherheits-Header
@@ -192,10 +251,12 @@ Die Phasen aus `projekt.md`, ergänzt um Tests und die Absicherung.
**Phase 4 Oberfläche**
- Mobile-First-Layout, Galerie mit Filterleiste (HTMX)
- Detailseite, Erfassungsformular mit Kamera-Aufnahme
(`<input type="file" accept="image/*" capture="environment">`)
- Detailseite und Erfassungsformular
- **Stapel-Import** mit Entwurfsliste zum Nachtragen (siehe „Schnelles
Erfassen"), dazu der automatische Titel aus Kategorie und Grösse
- Reservierungs-Ablauf inklusive Selbst-Freigabe
- Prüfpunkt: der ganze Weg auf einem echten Smartphone
- Prüfpunkt: der ganze Weg auf einem echten Smartphone 20 Fotos in einem
Rutsch hochladen, danach die Entwürfe abarbeiten
**Phase 5 Betrieb**
- `Dockerfile` (Tailwind-Build inbegriffen), `docker-compose.yml`, Volumes