Commit Graph
35 Commits
Author SHA1 Message Date
tillandClaude Opus 5 1d78535662 fix: findings from the code review of the mail and sharing work
Two reviews, both dimensions. The serious ones, verified before fixing:

Sessions never expired server-side and survived a password reset. `Max-Age` is
a request to the browser, not a rule for the server, and `readSession` only
checked the signature -- a leaked cookie stayed valid indefinitely, and the very
reset meant to rescue a compromised account left the intruder logged in. A
demoted admin also kept admin, because the role came from the cookie. Now the
cookie carries a session epoch, `verifySession` checks the account against the
database on every request and takes the role from there, and the epoch is
bumped on password reset and on any role change. This was pre-existing, but the
reset feature promised safety it did not deliver.

"Allow download: off" did nothing for images with transparency -- exactly the
cutouts and stickers this tool exists for. The view path skipped resizing when
`has_alpha`, so the untouched original went out at full resolution, right-click
saveable. Verified: now 1500x2000 instead of 2400x3200, and the download route
returns 403.

The share title went into outgoing mail HTML unescaped while the message was
escaped -- a foreign link could be posted through the account's own
SPF/DKIM-signed mailserver. Both escaped now, plus length limits.

argon2 ran on every request for passphrase-protected links, including every
image: 130 ms and 64 MiB each. A 40-image gallery would have walked into the
memory limit, and a few dozen wrong guesses could have stalled the process.
Checked once, then a short-lived signed cookie scoped to that one link. The
passphrase moved from the query string to POST (it was landing in history and
proxy logs) and wrong guesses are now rate limited.

`days` accepted nonsense and quietly produced an unlimited link -- `Number('abc')`
is NaN and `NaN > 0` is false. `[]` coerced to 0, same result. Now rejected
outright, which matters when the pictures are of children.

Rate limits keyed on `clientAddress`, which is the *first* X-Forwarded-For
value -- set by the caller. Counting from the right instead, with the number of
trusted proxies configurable (TRUSTED_PROXIES, default 1), plus per-username and
per-recipient counters, a real limit on outgoing mail, and a limiter that
expires entries individually instead of clearing everyone's counter.

Also: expired links could still be mailed; missing PUBLIC_BASE_URL sent dead
links while reporting success; the reset endpoint's response time revealed
whether an account existed (send is no longer awaited); invalid UUIDs produced
500s; the email uniqueness race returned an HTML error page; share_views grew
without bound and is now one row per visitor per hour, hashed per link, user
agent reduced to browser and system, deleted after 90 days; double provenance
marking appended a second XMP packet with the older model instead of replacing.

21 new tests (97 total) for shares, mail and provenance -- all three were
completely untested, and that is where both reviews found the most.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BNhLunz586g6DeYMa7Qsd1
2026-09-01 03:25:02 +00:00
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
tillandClaude Opus 5 f695554e5d fix: unique, chronologically sortable result filenames
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
2026-09-01 02:15:29 +00:00
tillandClaude Opus 5 5ebe8026e0 feat: oversize sheets, image duplication, free labelling and a new layout
Prints larger than the sheet no longer just fail. Each cell can now spill over
the whole sheet (20x30 on A4 loses exactly 3 mm, and says so beforehand) or be
tiled across several sheets with a dashed glue fold and a sheet number. The
maths lives in printoversize.ts, dependency-free, so the browser preview and the
server PDF compute the same thing -- including the note strip that keeps the
sheet number off the picture.

The same image can now appear as several cells, so a portrait can be printed at
two sizes on one sheet without copying and re-uploading the file. The footer
line can be switched off, given custom text and placed at one of six positions;
it is only drawn where that edge is free.

Layout reworked for all three widths: work left, sticky preview right on the
desktop, collapsed setting groups paired in a two-column grid that expands full
width; preview first and a fixed action bar on the phone. Verified at 390, 820,
1440 and 1680 px -- no horizontal overflow, no dead white space.

Also a public presentation page at /vorstellung (no login, nothing operable),
whose sample sheet is computed by the real packer rather than drawn.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BNhLunz586g6DeYMa7Qsd1
2026-08-20 10:55:11 +00:00
till e87c61435c fix: code-review findings in the print module
Security and robustness:
- EXIF orientation is now applied before any geometry. Phone photos carry the
  rotation only as metadata; sharp was cropping the unrotated raster, so a
  portrait shot came out of the printer sideways and wrongly framed.
- renderCell no longer materialises the padded image at source resolution.
  It is one extract-resize-extend chain now, which is also sharp's internal
  order. A panorama into a narrow contain target used to build a ~960 MB
  intermediate and then fail; it is 90 ms and a few MB now.
- Target size is capped (300 Mpx) and bleedMm is clamped in /api/print/single,
  which had no bound at all.
- The delivery gallery is validated before use - posixpath.join let a crafted
  name escape the target's base folder and create directories there.
- Sheet requests are capped at 500 pieces and the packer has a step budget, so
  a degenerate request cannot block the single-threaded server.
- Print presets: delete only your own (admins all), config size limit, count
  limit, and by_name honours anonymous_generations.
- Telegram callbacks require an active pairing, like every other path.
- Error responses no longer leak storage paths or delivery hostnames.

Correctness:
- allowRotate:undefined now means allowed, consistently with the packer.
- The many-formats shortcut no longer drops a format that only fits rotated.
- unplaced names the format that is actually missing, not the first one.
- Crop marks never sit inside the printed bleed - the offset is raised.
- capacity() computes the grid instead of probing with 200 copies.
- Image keys in the sheet cannot collide with a cell literally named x::rot.
- labelMm keeps real decimals; parseSizeMm reads a:b as width:height, so
  3:4/15 is portrait and 4:3/15 is landscape.
- The footer is skipped when there is no free space at the bottom.
- The UI warns when corner marks do not fit the margin, and when continuous
  guides are used with mixed sizes.

Tests: 21 -> 31, each finding has a regression test.
2026-08-18 07:50:11 +00:00
till 7d42782c41 feat: all AI formats printable, crop-vs-border rule, mobile layout, Passbilder rename
Print now offers every format the AI pipeline knows (9x13 to 60x90, DIN A6-A2,
squares, poster/frame sizes to 70x100, and the screen ratios as physical sizes).
When a picture does not match the target ratio the user picks per image between
cropping to fill and keeping the whole picture on a border colour - never a
stretch. The module is called Passbilder now; /druck redirects.

Fixes two real defects: sharp runs extend after resize, so padded cells came out
oversized (a 35x45 mm cell became 35x171 mm with a border), and the grid packer
rotated a single portrait photo just because more would fit sideways.

Mobile: cards become rows, touch targets ~40px, crop editor as a bottom sheet.
Verified at 390/820/1440px - all three produce the same PDF.
2026-08-18 07:04:08 +00:00
till 4fb34cdab8 feat: sheet templates, margin profiles and delivering the sheet
Seeds the four sheet setups Till actually prints (Kita set, small school set,
full passport sheet, two 10x7,5 on 10x15 paper). A multi-format template
applied to a single image clones it into every format, so one click yields the
whole set. Margin gets one-click borderless / 5 mm / 10 mm profiles, and the
finished PDF can be pushed to Picdrop, the NAS or any extra target.
2026-08-18 06:40:31 +00:00
till e8b3fde652 feat: print API - exact single image, sheet PDF, sheet presets
POST /api/print/single (image at an exact size, dpi headers incl. a real-dpi
warning), GET /api/print/single as a mm-to-px calculator, POST /api/print/sheet
(multi-image sheet as PDF, limits on cells/copies/pages) and print_presets for
reusable sheet setups.
2026-08-18 06:20:46 +00:00
tillandClaude Opus 4.8 ed22a76fda feat: deliver in chosen format (not forced JPEG); wire output_ext through jobs/Telegram/MCP
- delivery.ts: upload the stored result in its chosen format (output_ext,
  global or per-recipe) instead of always converting non-alpha to JPEG.
- jobs POST froze the snapshot without output_ext -> per-conversion/per-recipe
  format choice was silently dropped. Now carried through.
- telegram.ts: recipe snapshots carry output_ext + delivery_target_id
  (custom formats already flow via output_format).
- mcp/klarbild-mcp.mjs: process_images gains tasks, output_ext, orientation,
  crop_mode, contour_mm, picdrop_gallery; job_status shows finished_at + errors.
- Docs: llms.txt, mcp/README, changelog updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
2026-07-24 09:12:07 +00:00
tillandClaude Opus 4.8 ae91f4bfd5 feat: llms.txt (AI-readable docs), job delete, friendly missing-source error
- /llms.txt: public machine-readable capability/API/MCP doc for agents.
- Job delete action + queue button for terminal jobs; cleans objects.
- process.ts: clear 'Quelle nicht mehr vorhanden' message instead of raw
  EISDIR/ENOENT when a source was purged.
- Docs corrected: Picdrop 'missing images' was web-UI sorting, not PNG;
  JPG delivery kept as size/perf improvement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
2026-07-24 08:57:31 +00:00
tillandClaude Opus 4.8 3a061aa612 feat: custom formats (WxH/stickerN), JPG output option, failed-job status
- 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
2026-07-24 08:32:03 +00:00
tillandClaude Opus 4.8 70f3904d0e fix: deliver JPEG (not huge PNG) to Picdrop, clean filenames, tmp cleanup
- 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
2026-07-24 08:07:16 +00:00
tillandClaude Opus 4.8 32f06297a6 feat: per-source .md sidecar toggles, Picdrop folder-list diagnostic, fast JPEG fullscreen preview + thumb-first, thumbnail backfill
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
2026-07-24 07:56:57 +00:00
tillandClaude Opus 4.8 af34cb9cc8 feat: Picdrop-style color tags (Rot/Orange/Gruen/Final) in library + in .md metadata; tap-to-open fullscreen for iPhone Save-to-Photos
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
2026-07-24 07:38:20 +00:00
tillandClaude Opus 4.8 e6b325eb53 feat: per-user visibility (own/shared), anonymous generations, private sessions (generate+forget), NSFW model gating
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
2026-07-23 21:46:47 +00:00
tillandClaude Opus 4.8 295f0ce91e feat: single-image transform, NAS/FTP in delivery, backup-all any target, metadata sidecar, prompt view, preset manager, API token + MCP server
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
2026-07-23 21:29:43 +00:00
tillandClaude Opus 4.8 36f3c8da22 feat: multiple delivery targets (FTP/SFTP), per-preset wiring; The Frame -> TheFrame-Backgrounds gallery
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
2026-07-23 21:04:27 +00:00
tillandClaude Opus 4.8 3a098d708c feat: mirror-all-to-NAS, content reset (danger zone), in-app changelog + CHANGELOG.md, How-To refresh
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
2026-07-23 20:52:48 +00:00
tillandClaude Opus 4.8 79454c0b92 feat: show NAS backup status badge in library
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
2026-07-23 20:43:10 +00:00
tillandClaude Opus 4.8 3bfeadbaa0 feat: bulk ZIP download, bulk move-to-folder, folder rename
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
2026-07-23 20:23:03 +00:00
tillandClaude Opus 4.8 7608bc62da feat: button-driven Telegram menu (reply keyboard + commands), reachability diagnostic, How-To update
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
2026-07-23 20:12:27 +00:00
tillandClaude Opus 4.8 5105c7d529 fix: before/after works for compose (source fallback), hidden for text-only; add reuse result as new Studio input
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
2026-07-23 20:01:17 +00:00
tillandClaude Opus 4.8 78737936fb feat: functional folders (filter/delete/gallery) + always show model + variant switcher
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
2026-07-23 19:39:05 +00:00
tillandClaude Opus 4.8 e59d26fadd feat: regenerate/alternatives grouped by source (variant_of)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
2026-07-23 19:39:04 +00:00
tillandClaude Opus 4.8 f5cbcdf4d1 feat: storage management (retention, source purge) + Synology NAS backup
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
2026-07-23 19:19:40 +00:00
tillandClaude Opus 4.8 b705d77240 feat: low-res webp thumbnails for library previews
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
2026-07-23 19:19:39 +00:00
tillandClaude Opus 4.8 044eb7f1ae feat: compose + text-to-image generation modes (multi-image + free-text)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
2026-07-23 19:19:39 +00:00
till 6d58c6c8fd feat: model selection+visibility, better filenames, picdrop direct upload + configurable gallery
- 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
2026-07-23 18:54:37 +00:00
till d680b3f67a feat: telegram bot (grammy webhook) — full parity channel
- pairing (admin code -> chat link), bundling of forwarded images via drafts+quiet-window sweeper
- recipe pick via inline keyboard, job (origin=telegram) creation, one summary reply with result files + links
- compressed-photo detection/flag; /start /help /rezepte /status
- webhook route (secret token), admin pairing-code + links endpoints, AdminApp telegram section
- worker notifies chat on completion; migration 002 adds jobs.telegram_chat_id; setupTelegram on init
2026-07-23 17:39:49 +00:00
till 26c3e0ef07 feat: picdrop delivery — worker delivers on job completion, /api/deliver for manual, gallery default 'POSTER LEA', library bulk send + status badges 2026-07-23 15:41:04 +00:00
till 25d0a5cd97 feat: Bibliothek + Admin
- Bibliothek: grid, before/after slider, rename, download, folder assign, delete, multi-select
- Admin: stats+costs, OpenRouter key (masked/encrypted), budget, defaults, Picdrop config+test,
  models (list/add from OpenRouter/set default/delete), users (add/set password)
- api: items/:id PATCH+DELETE, folders CRUD, admin/{settings,stats,models,test-picdrop,users}
- lib/picdrop.ts (SFTP/FTPS test+upload, temp->rename)
2026-07-23 15:22:12 +00:00
till 9d075b5109 feat: storage abstraction (fs volume default | s3), health via storageOk 2026-07-23 14:27:12 +00:00
till 1fb42862be feat: Studio UI (upload/recipe/submit island) + queue view + library/file API
- StudioApp.tsx: upload (file/DnD/paste, HEIC, to /api/uploads), combinable task
  toggles with validation (contour needs cutout, freistellen+theframe blocked),
  format+orientation+live px+crop, contour mm, custom instruction, delivery, recipe prefill
- QueueApp.tsx + warteschlange.astro: live polling, progress+ETA, per-item status,
  retry/pause/resume/cancel, result thumbnails+download
- api/items (library filters), api/items/:id/file (stream result/source from S3)
- index.astro renders Studio island
2026-07-23 14:08:13 +00:00
till c12d55368c feat: upload/recipe/job API routes + Klarbild visual identity
- api: /uploads (multipart, HEIC, 25MB guard, S3), /recipes (GET/POST),
  /jobs (create+snapshot+enqueue, list), /jobs/:id (status+items),
  /jobs/:id/:action (pause/resume/cancel/retry-failed), /items/:id/retry
- design: adopt Till mockup identity — Space Grotesk + Space Mono (self-hosted,
  no google/US), blue accent, paper/card, registration-mark motif; OKLCH tokens
- verified: astro build passes
2026-07-23 11:47:27 +00:00
till b46dbbe889 feat: scaffold klarbild foundation (astro+postgres+s3, auth, migrations, health)
- Astro 5 SSR (node standalone) + React, OKLCH tokens (no tailwind)
- migrations/001_init.sql: full schema per 03-datenmodell-api
- lib: db+migrations, crypto (AES-256-GCM), auth (argon2+signed session, ratelimit),
  storage (S3/MinIO, presigned URLs), openrouter (POST /v1/images, cost)
- middleware: init-once + session guard + admin gate; /api/health (db+storage)
- login + studio placeholder; seeds (till/lea, default recipes); Dockerfile
- verified: astro build passes
2026-07-23 11:11:08 +00:00