Drei Dinge, die im Alltag gefehlt haben. **Bilder aus der Bibliothek in den Druckbereich.** Bisher musste man eine Datei hochladen, auch wenn Klarbild sie längst hatte. Jetzt gibt es einen Wähler mit Suche, Ordnerfilter und Mehrfachauswahl — erreichbar aus der leeren Ablage, aus der Kopfzeile der ersten Karte und an jeder Zellengruppe. **Verlauf der Drucke.** Gespeichert wird bewusst nur die Anleitung, nicht das Ergebnis: ein A3-Bogen ist schnell 20 bis 80 MB, ein Verlaufseintrag rund ein bis drei Kilobyte. Weil das Druckmodul ohne KI rechnet, ergibt derselbe Auftrag wieder exakt dasselbe Blatt — nur die Zeitstempel im PDF unterscheiden sich, der Bildinhalt ist Byte für Byte gleich. „Nochmal drucken" gibt es deshalb nur, wenn alle Zellen aus der Bibliothek kamen und alle Bilder noch da sind; sonst steht am Eintrag, woran es liegt. Je Konto bleiben die letzten 100 Drucke, ältere räumt der Eintrag selbst weg. Sichtbar sind nur die eigenen Läufe, Admins sehen alle. **Mailzugang aus der Umgebung.** Der Versand nimmt jetzt die Server- Einstellung, solange unter Admin → E-Mail nichts eingetragen ist. Die Zugangsdaten liegen als Umgebungsvariablen in Coolify, nicht im Repo. Der Admin zeigt an, welche Quelle gerade wirklich greift. Unterwegs mitgenommen: - Auf schmalen Geräten stand die rechte Spalte oben (order:-1, damit die Vorschau gleich sichtbar ist). Mit der Verlaufskarte hätte man an einem ganzen Block vorbeigescrollt, ehe man ein Bild hinzufügen kann — die Karten laufen jetzt in ihrer eigenen Nummerierung. - Die Kurzfassung listete jede Zelle einzeln: „10 × 15 cm, 10 × 15 cm". Gleiche Maße werden zusammengezogen, vorhandene Stückzahlen addiert. Der erste Ansatz machte dabei aus „13 × 18 cm" prompt „13 cm ×18" — das × im Maß sah aus wie eine Stückzahl. Sieben Tests halten das jetzt fest. - „das PDF war 0.0 MB groß" für einen 20-KB-Bogen: unter einem Megabyte wird in KB gerechnet. - Fehlt eine Datei auf der Platte, zeigte der Browser ein kaputtes Bildsymbol. Jetzt bleibt die Fläche ruhig. 111 Tests grün. 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. - 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. ZeitzoneKLARBILD_TZ(StandardEurope/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 (fremdeitem-Kennung ⇒ 404); private Sitzungen lassen sich nie teilen. Verwaltung unter/teilenmit 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 überGET /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 insettings. 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): IPTCDigitalSourceTypeals XMP direkt in der Datei (trainedAlgorithmicMediabzw.compositeWithTrainedAlgorithmicMedia) samt Modell — von Hand in PNG-iTXtund JPEG-APP1geschrieben, ohne neue native Abhängigkeit. Mit exiftool prüfbar. - Sicherheits-Header: CSP mit echten Hashes für jedes inline-Skript (
experimental.csp), dazunosniff,referrer-policy,X-Frame-Options,permissions-policy.style-srcbleibt 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
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.