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
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
- format.ts parses free formats from the key: sticker<N> -> N×N cm,
<W>x<H> -> W×H cm. Fixes 'Unbekanntes Format: sticker5' (the seeded
'Sticker 5 cm' recipe was unusable) and lets admins define ANY format
without a migration. Guarded to <=300 cm.
- Output file format PNG/JPG: global default (settings.output_ext) plus
per-recipe/per-conversion override. JPG only for non-alpha; cutouts
stay PNG. file.ts sniffs content-type from magic bytes.
- Jobs can now be 'failed' (all items failed) -> red in the queue; add
finished time to queue list + detail. migration 012.
- Studio: custom-measure input + Dateiformat selector; Admin: global
Standard-Dateiformat.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
- Picdrop is a photo-proofing tool that expects handy JPGs; non-alpha
results are now delivered as JPEG q92 (4:4:4) instead of 12-32 MB PNGs.
Cutouts with alpha stay PNG. Fixes images not showing in POSTER LEA.
- buildResultFilename strips doubled dates/UUIDs/hex to avoid messy names.
- picdrop-ls gains action:cleanup_tmp; Admin 'Reste aufräumen' button
removes leftover .tmp- files from the default gallery.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
- studio: quality/model selector (friendly names from active models); library shows model used
- seed 2 models (Schnell/Beste Qualitaet); GET /api/models for studio+library
- filename: YYYY-MM-DD_motif_format.png (was redundant slug, no date)
- picdrop: direct upload to final name (fix .tmp- leftover from failed rename on picdrop); cleanupTmp helper
- configurable default gallery (migration 003, settings + admin field); base_path=SFTP root, gallery=subfolder