Two badges of the same person, same format, same day produced the same filename -- `2026-07-23_lea_the-frame.png` -- and the second upload silently overwrote the first at Picdrop. The name now carries the time down to the second and a short tag derived from the item id: 2026-08-20_143207_lea-badge_the-frame_a3f9.jpg The timestamp leads, so any file listing sorts chronologically on its own. The tag is derived, not random, so re-running the backfill is idempotent. Timestamps use Europe/Berlin (KLARBILD_TZ) rather than UTC -- a picture taken at 00:30 should not be dated to the previous day. Naming moved into its own dependency-free module so the rules are testable and identical everywhere. The generator strips its own stamp when a result is fed back in as a source, so names cannot accumulate. Backfill for the existing library in Admin -> Dateinamen: preview first, then apply. It re-stamps from `created_at` and the item id, keeping the motif and format already chosen, and leaves the stored files alone -- they live under their id, not their name. A separate button re-delivers everything under the new name; the old copies stay put at the destination, since deleting a customer's files automatically is the wrong reflex. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BNhLunz586g6DeYMa7Qsd1
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
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) odersticker<N>(sticker5= 5×5 cm). Parsing insrc/lib/format.ts, keine Migration nötig. - Dateiformat: PNG (verlustfrei, Transparenz) oder JPG (klein, Picdrop-freundlich). Global über
settings.output_ext, pro Rezept viarecipes.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 Zelleoversize=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. - Admin: Key/Picdrop/Modelle/Nutzer/Kostendeckel/Presets/Speicherverwaltung, 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).
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
sticker5an, den der Resolver nicht kannte → jeder Lauf schlug fehl. Lösung: generisches Parsen vonsticker<N>/WxH. - Auftrag trotz Fehler grün:
jobs.statuskannte keinfailed; alle Positionen fehlgeschlagen wurde trotzdemdone. Lösung: Statusfailed, wenndone=0 && failed>0(src/worker.tsmaybeComplete). - Doppelte Datums-/UUID-Namen:
\b-Wortgrenzen greifen nicht an Unterstrichen. Lösung:\bentfernt, Datum/UUID/Hex global ersetzt (src/lib/pipeline.tscleanMotif). - Content-Type bei gemischten PNG/JPG-Ergebnissen:
file.tssnifft 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.