tillandClaude Opus 5 63d4b7d894 feat: e-mail, password reset and shareable links for images and collections
Mail goes out through the account's own mailbox (SMTP) rather than a sending
service: the domain lives there anyway, so SPF and DKIM line up by themselves
and no third party gets to see the messages. Credentials are AES-256-GCM
encrypted in settings like every other secret, never in the repo. nodemailer
has zero dependencies and no native postinstall -- checked before adding it,
given the history.

Password reset: single-use token, only its hash is stored, one hour, and the
request endpoint answers identically whether or not the address exists so it
cannot be used to enumerate accounts. Rate limited per address.

Share links (/s/<slug>) let someone without an account see the pictures and
download them at full resolution. These are photos of children, so the
defaults lean safe: every link expires by itself (30 days), can be revoked,
optionally carries a passphrase, and the page and files are noindex. Access is
bound to the link, not the image -- swapping in another item id on a valid link
returns 404. Private-session results can never be shared at all.

Tracking records when and with what, never who: IP addresses are only stored
salted-and-hashed, so repeat visitors can be counted but nobody identified.

Sharing goes out by mail (several recipients at once, no attachment -- a link
can be withdrawn later, an attachment cannot) or via wa.me, which opens
WhatsApp with the text prepared. The WhatsApp Business API would need a Meta
account, approved templates and per-conversation fees; this works today for
free and the API can slot in behind it later.

Verified end to end against a local SMTP server: reset issues and consumes its
token exactly once, the old password stops working, an unknown address gives
the same answer, share mail reaches several recipients. Link behaviour checked
for expiry (410), revocation (410), wrong passphrase (401) and download lock
(403). Outbound SMTP is blocked in the sandbox, so the real mailbox still needs
one test from the live server.

Also fixes a regression this introduced: the seventh nav item pushed the logout
button off screen at 820px. The bar now shrinks and scrolls, logout never does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BNhLunz586g6DeYMa7Qsd1
2026-09-01 03:02:33 +00:00

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) 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.
  • Admin: Key/Picdrop/Modelle/Nutzer/Kostendeckel/Presets/Speicherverwaltung, 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).

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.

S
Description
Klarbild — Screenshots & Fundstuecke zu druckfertigen Bildern (Astro+Postgres+S3)
Readme
2.4 MiB
0 Stars 1 Watchers 0 Forks
Languages
TypeScript 86.6%
Astro 11.3%
JavaScript 1.7%
CSS 0.3%
Dockerfile 0.1%