Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BNhLunz586g6DeYMa7Qsd1
121 lines
9.1 KiB
Markdown
121 lines
9.1 KiB
Markdown
# Klarbild
|
||
|
||
Verwandelt Screenshots und Fundstücke in saubere, druckfertige Bilder — Rahmen und
|
||
Shop-Oberflächen entfernen, exakte Fotoformate (30×40 cm etc.), The-Frame-Querformat,
|
||
für den Cricut freistellen, und automatisch nach Picdrop ausliefern.
|
||
|
||
**Claim:** „Screenshot rein, sauberes Bild raus."
|
||
|
||
## Stack
|
||
Astro 5 (SSR, Node standalone) + React-Inseln · Postgres · S3 (MinIO) · pg-boss ·
|
||
sharp · pdf-lib (Druckbogen) · grammY (Telegram) · OKLCH-Tokens, kein Tailwind.
|
||
Deploy: Gitea → Coolify → Hetzner.
|
||
|
||
## Entwicklung
|
||
```bash
|
||
npm install
|
||
cp .env.example .env # Werte eintragen
|
||
npm run dev
|
||
```
|
||
Migrationen und Seeds laufen beim Serverstart automatisch. Erststart-Passwörter über
|
||
`SEED_TILL_PASSWORD` / `SEED_LEA_PASSWORD`.
|
||
|
||
## Funktionen (Kurzüberblick)
|
||
- **Modi:** Bereinigen (Screenshot säubern/freistellen/formatieren), Umwandeln (ein Bild + Text,
|
||
z. B. Foto → Ölgemälde), Kombinieren (mehrere Bilder + Text), Neu erzeugen (nur Text).
|
||
- **Formate:** feste cm-/DIN-/Screen-Formate **und frei definierbare** — Schlüssel `WxH` (z. B.
|
||
`25x35`) oder `sticker<N>` (`sticker5` = 5×5 cm). Parsing in `src/lib/format.ts`, keine Migration nötig.
|
||
- **Dateiformat:** PNG (verlustfrei, Transparenz) oder JPG (klein, Picdrop-freundlich). Global über
|
||
`settings.output_ext`, pro Rezept via `recipes.output_ext`, pro Umwandlung im Studio. JPG nur ohne Alpha.
|
||
- **Auslieferung:** Picdrop (SFTP/FTPS) + beliebige Zusatzziele; NAS/Backup-Ziele; optionaler
|
||
`.md`-Metadaten-Beileger **pro Ziel** schaltbar. Nicht-transparente Ergebnisse gehen als JPG an Picdrop.
|
||
- **Bibliothek:** Ordner, Farbmarkierungen, Prompt-Anzeige, Vorher/Nachher, Vollbild + „In Fotos sichern".
|
||
- **Rechte/Datenschutz:** Sichtbarkeit (eigene/alle), anonyme Generierungen, private Sessions, NSFW-Gate.
|
||
- **Telegram-Bot** (grammY-Webhook) als vollwertiger Website-Ersatz. **API-Token** (`Bearer`) für `/api/*`.
|
||
- **Drucken (`/drucken`, ohne KI):** Bilder exakt auf physische Maße bringen (interaktiver Zuschnitt mit
|
||
Zoom/Verschieben) und mehrere davon in 100-%-Größe mit **Schnittmarken** auf einen Bogen setzen —
|
||
Passbildsatz, Kita-Satz, Sticker. Papier DIN A6–A2 inkl. **A3+**, Fotopapier 9×13–20×30, Letter/Legal
|
||
oder freies Maß. Marken: Eckmarken (0,25 pt, außerhalb des Endformats) oder durchgehende Linien.
|
||
Beschnittzugabe 0–10 mm. Ausgabe: PDF in 1:1 (`pdf-lib`) bzw. Einzelbild mit dpi-Metadaten.
|
||
Layout: gleiche Größen → exaktes Raster, gemischte Größen → MaxRects-Packer.
|
||
Alle Formate der KI-Generierung sind auch hier wählbar; passt das Bild nicht zum Format,
|
||
entscheidet man je Bild zwischen **Zuschneiden** und **Rand lassen** (mit Randfarbe) — nie verzerren.
|
||
86 Bildmaße in 10 Gruppen (Passbild bis 100 × 140 cm), 19 Papierformate.
|
||
**Größer als der Bogen** (20 × 30 cm auf A4): je Zelle `oversize` = `fit` (melden) ·
|
||
`overflow` (mittig aufs ganze Blatt, Verlust wird vorab beziffert — hier genau 3 mm) ·
|
||
`tile` (Posterdruck auf mehrere Blätter mit gestricheltem Klebefalz und Blattnummer).
|
||
**Beschriftung** frei: aus, automatisch oder eigener Text an einer von sechs Stellen;
|
||
sie entfällt automatisch, wenn an der Kante kein Platz frei ist.
|
||
Dasselbe Bild darf mehrfach als Zelle vorkommen („Duplizieren"), um es in zwei Maßen zu drucken.
|
||
Kern: `src/lib/paper.ts` (Formate), `src/lib/printlayout.ts` (reine Mathematik, getestet),
|
||
`src/lib/printoversize.ts` (Überstand & Kachelung, getestet), `src/lib/printrender.ts`
|
||
(sharp + pdf-lib). Browser-Vorschau und Server-PDF benutzen denselben Rechenkern.
|
||
Auch per Telegram („🖨 Drucken") und MCP (`exact_size`, `print_sheet`).
|
||
Öffentliche Vorstellungsseite ohne Login: `/vorstellung`.
|
||
- **Dateinamen (`src/lib/naming.ts`, abhängigkeitsfrei):** `JJJJ-MM-TT_HHMMSS_<motiv>_<format>_<kürzel>.<ext>`,
|
||
z. B. `2026-08-20_143207_lea-badge_the-frame_a3f9.jpg`. Zeitstempel vorn → jede Dateiliste sortiert
|
||
chronologisch; Kürzel aus der Item-ID → zwei Bilder können sich beim Ausliefern **nicht mehr überschreiben**.
|
||
Zeitzone `KLARBILD_TZ` (Standard `Europe/Berlin`). Nachbenennung der Bibliothek und erneute Auslieferung
|
||
im Admin unter „Dateinamen" (`src/lib/renames.ts`), idempotent.
|
||
- **Teilen (`/s/<slug>`, öffentlich):** ein Bild oder eine Sammlung ohne Konto zugänglich machen —
|
||
ansehen und in voller Auflösung herunterladen. Voreinstellungen bewusst vorsichtig: **Ablauf**
|
||
(Standard 30 Tage), **widerrufbar**, optionales **Zugangswort** (argon2), **Download abschaltbar**,
|
||
`noindex`. Zugriff hängt am **Link**, nicht am Bild (fremde `item`-Kennung ⇒ 404); private Sitzungen
|
||
lassen sich nie teilen. Verwaltung unter `/teilen` mit Aufrufen und Downloads. Gezählt wird *wann*
|
||
und *womit*, nicht *wer*: IP nur gehasht, gesalzen mit Server-Geheimnis **und** Link-Kennung, eine
|
||
Zeile je Besucher und Stunde, Löschung nach 90 Tagen. **Alle Bilder als ZIP** über
|
||
`GET /api/s/<slug>/zip` — strömend, eine Datei nach der anderen (440 MB Bilder → 196 MB
|
||
Spitzenspeicher gemessen), gedeckelt bei 200 Dateien und 500 MB. Kern: `src/lib/shares.ts`.
|
||
- **Weitergeben:** per E-Mail aus Klarbild heraus (mehrere Empfänger, **kein Anhang** — ein Link lässt
|
||
sich zurückziehen), über das eigene Mailprogramm oder per `wa.me`-Link, der WhatsApp mit fertigem
|
||
Text öffnet. (Server-seitiger WhatsApp-Versand bräuchte die Business-API: Meta-Konto, genehmigte
|
||
Vorlagen, Gebühren je Konversation.)
|
||
- **E-Mail (`src/lib/mail.ts`):** SMTP über das **eigene Postfach** — SPF/DKIM stimmen dadurch von
|
||
selbst und kein Versanddienst sieht mit. Zugang AES-256-GCM in `settings`. Trägt Passwort-Reset
|
||
(Einmal-Token, nur Hash gespeichert, 60 min, gleiche Antwort für unbekannte Adressen),
|
||
Auftragshinweise (je Nutzer, `/konto`) und den Versand von Teilen-Links.
|
||
- **Sitzungen:** signiertes Cookie **plus** serverseitige Prüfung (`session_epoch`). Passwortwechsel und
|
||
Rollenänderung entwerten alle offenen Anmeldungen; die Rolle kommt aus der Datenbank, nicht aus dem
|
||
Cookie.
|
||
- **KI-Kennzeichnung (`src/lib/provenance.ts`):** IPTC `DigitalSourceType` als XMP direkt in der Datei
|
||
(`trainedAlgorithmicMedia` bzw. `compositeWithTrainedAlgorithmicMedia`) samt Modell — von Hand in
|
||
PNG-`iTXt` und JPEG-`APP1` geschrieben, ohne neue native Abhängigkeit. Mit exiftool prüfbar.
|
||
- **Sicherheits-Header:** CSP mit echten Hashes für jedes inline-Skript (`experimental.csp`), dazu
|
||
`nosniff`, `referrer-policy`, `X-Frame-Options`, `permissions-policy`. `style-src` bleibt offen, weil
|
||
React Stile als Attribute setzt und Attribute sich nicht hashen lassen.
|
||
- **Admin:** Key/Picdrop/Modelle/Nutzer/Kostendeckel/Presets/Speicherverwaltung, E-Mail, Teilen-Links,
|
||
Dateinamen, Picdrop-Diagnose.
|
||
|
||
## Datenmodell / Migrationen
|
||
Nummerierte SQL in `migrations/`, beim Start automatisch (kein ORM-Push). Wichtige Spalten:
|
||
`items.has_alpha`, `jobs.status` (inkl. `failed`), `jobs.finished_at`,
|
||
`settings.output_ext` / `recipes.output_ext`, `settings.*_metadata_sidecar`, `delivery_targets.*`,
|
||
`print_presets` (Bogen-Vorlagen, `013`), `shares`/`share_items`/`share_views` + `settings.smtp_*` +
|
||
`users.email`/`notify_jobs` (`014`), `users.session_epoch` (`015`).
|
||
|
||
## Tests
|
||
`npm test` — Node-Testrunner mit nativem Type-Stripping (`node --experimental-strip-types`).
|
||
**Kein `tsx`, kein `playwright` in `package.json`** — der Docker-Build installiert devDependencies,
|
||
und deren native postinstall-Schritte haben den Deploy schon zweimal zerlegt. Testwerkzeug gehört
|
||
außerhalb des Projekts installiert. Deckt die Druck-Layoutmathematik ab (Maß-Parsing,
|
||
mm → px exakt, Überlappungsfreiheit, Ränder, Beschnittzugabe, Markengeometrie, Mehrseitigkeit).
|
||
|
||
## Stolpersteine & Lösungen (Learnings)
|
||
- **„Bild bei Picdrop ✓, aber nicht in der Galerie":** Fehldiagnose vermeiden — die Dateien lagen die ganze
|
||
Zeit **korrekt** per SFTP in `/POSTER LEA` (per Admin → Picdrop → „Ordner anzeigen" verifizierbar). Der
|
||
eigentliche Grund war die **Sortierung in der Picdrop-Weboberfläche**, nicht das Format. Unabhängig davon
|
||
liefern wir nicht-transparente Ergebnisse jetzt als JPG aus (`src/lib/delivery.ts`, `deliverableBuffer`):
|
||
~25 MB PNG → ~2 MB JPG, schneller und handlicher — plus wählbares Ausgabeformat (`output_ext`).
|
||
- **„Unbekanntes Format: sticker5":** Seed legte ein Rezept mit Formatschlüssel `sticker5` an, den der
|
||
Resolver nicht kannte → jeder Lauf schlug fehl. Lösung: generisches Parsen von `sticker<N>`/`WxH`.
|
||
- **Auftrag trotz Fehler grün:** `jobs.status` kannte kein `failed`; alle Positionen fehlgeschlagen wurde
|
||
trotzdem `done`. Lösung: Status `failed`, wenn `done=0 && failed>0` (`src/worker.ts` `maybeComplete`).
|
||
- **Doppelte Datums-/UUID-Namen:** `\b`-Wortgrenzen greifen nicht an Unterstrichen. Lösung: `\b` entfernt,
|
||
Datum/UUID/Hex global ersetzt (`src/lib/pipeline.ts` `cleanMotif`).
|
||
- **Content-Type bei gemischten PNG/JPG-Ergebnissen:** `file.ts` snifft jetzt die Magic Bytes.
|
||
- **`.tmp-`-Reste auf Picdrop:** früher Temp-Rename-Bug; `cleanupTmp` + Admin-Knopf „Reste aufräumen".
|
||
|
||
## Status
|
||
Reihenfolge und Nachweise siehe `CLAUDE.md`. Anforderungen im Handover-Paket
|
||
(`Imagetool-Lea/00..07`). Führend: `01-anforderungen.md`.
|