Zwei Druckbogen desselben Tages hießen beide `2026-09-01_druckbogen.pdf`. Der
Browser hängte „(1)" an; wer sie in denselben Ordner oder aufs NAS legte, verlor
den ersten. Genau der Fehler, der im August für die Bibliotheksbilder behoben
wurde — im Druckmodul stand er noch.
Druckdateien folgen jetzt derselben Regel wie alles andere:
2026-09-01_143207_druck-9-teile_a4_4eca.pdf
Datum, Uhrzeit auf die Sekunde, was auf dem Bogen liegt, das Papier, ein Kürzel.
Das Kürzel ist die Kennung des Verlaufseintrags — eine PDF-Datei lässt sich also
ihrem Eintrag unter „Zuletzt gedruckt" zuordnen. Dafür entsteht die Kennung vor
dem Dateinamen und wird an `recordRun` durchgereicht.
Einzelbilder erben das Motiv ihrer Quelle:
`2026-09-01_143207_lea-badge_130x180_25b3.jpg` statt `2026-09-01_1318.jpg`.
Dafür gibt `loadSourceNamed` neben den Bilddaten auch Namen und Kennung zurück.
Die Oberfläche erfindet keinen eigenen Namen mehr. Der Server schickt einen, der
Client liest ihn aus `Content-Disposition` — es gab zwei Wahrheiten, und die des
Clients war die schlechtere: Sie enthielt nur das Datum. Aus dem Kopf gelesene
Namen werden entschärft, bevor sie in `a.download` landen.
`sheetFilename` in printsource.ts ist entfallen; die Regeln stehen jetzt
vollständig in `naming.ts`, wo sie schon für die Bibliothek stehen.
12 neue Tests, 123 grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BNhLunz586g6DeYMa7Qsd1
401 lines
27 KiB
Markdown
401 lines
27 KiB
Markdown
# Changelog
|
||
|
||
All notable changes to Klarbild are documented here. Newest first.
|
||
|
||
## 2026-09-01 (later) — Unique names for print files
|
||
|
||
### Fixed
|
||
- **Two print PDFs from the same day had the same name** — `2026-09-01_druckbogen.pdf`,
|
||
every time. The browser appended "(1)"; anyone filing them in the same folder or on the
|
||
NAS lost the first one. Exactly the defect fixed for library images in August, still
|
||
sitting in the print module. Print files now follow the same rule as everything else:
|
||
`2026-09-01_143207_druck-9-teile_a4_4eca.pdf` — date, time to the second, what is on the
|
||
sheet, the paper, and a short tag. **The tag is the id of the history entry**, so a PDF
|
||
can be traced back to its run under "Last printed".
|
||
- Single images carry the motif of their source: `2026-09-01_143207_lea-badge_130x180_25b3.jpg`
|
||
instead of `2026-09-01_1318.jpg`.
|
||
- The browser no longer invents its own name. The server sends one and the client reads it
|
||
from `Content-Disposition` — there were two truths, and the client's was the worse one.
|
||
Names read from that header are stripped of anything path-like before use.
|
||
|
||
## 2026-09-01 (later) — Printing from the library, and a print history
|
||
|
||
### Added
|
||
- **Pick images from the library** in the print area. Until now a file had to be
|
||
uploaded even when Klarbild already had it — a studio result went to disk and back
|
||
through the uploader. The picker has search (filename and description), a folder
|
||
filter and multi-select, reachable from the empty tray, from the card header and from
|
||
each cell group. Only finished, visible images are offered, under the same rules as
|
||
the library itself.
|
||
- **Last printed** (card 4 on `/drucken`): what was printed recently, with a button to
|
||
produce the exact same sheet again. **Only the instruction is stored, never the PDF** —
|
||
an A3 sheet runs to 20–80 MB, an entry to 1–3 KB. That works because the print module
|
||
computes without a model: the same job yields the same sheet. Verified — two runs of
|
||
one configuration produced PDFs differing *only* in `CreationDate` and `ModDate`; the
|
||
image streams are byte-identical. Repeat is offered only when every cell came from the
|
||
library and every image still exists; otherwise the entry says which of the two reasons
|
||
applies. The last 100 runs per account are kept, older ones drop off by themselves.
|
||
You see your own runs; admins see all.
|
||
- **Mail access from the server environment.** Sending now falls back to `SMTP_*`
|
||
environment variables when nothing is entered under Admin → E-mail, so a fresh install
|
||
can send straight away — and with it, password reset works. Credentials live in Coolify,
|
||
never in the repo. The admin card says which source is actually in effect.
|
||
|
||
### Fixed
|
||
- The run summary listed every cell separately: "10 × 15 cm, 10 × 15 cm". Identical sizes
|
||
are now merged and their counts added. The first attempt turned "13 × 18 cm" into
|
||
"13 cm ×18" — the × inside a measurement looked like a count. Anchored to the end of the
|
||
line now, with seven tests holding it there.
|
||
- "1 image is missing" for a sheet whose source was simply an upload: one counter was
|
||
serving two different causes. Two counters, two explanations.
|
||
- "the PDF was 0.0 MB" for a 20 KB sheet — below a megabyte it now counts in KB.
|
||
- A file missing from disk showed the browser's broken-image icon in the picker and the
|
||
history. The tile stays quiet now, and still clickable.
|
||
- On narrow screens the right-hand column was pulled to the top (`order: -1`, so the
|
||
preview is visible straight away). With a fourth card you would have scrolled past a
|
||
whole block before reaching the upload area. The cards now run in their own numbering —
|
||
and the numbers in the card headers are visible again, because they are correct again.
|
||
|
||
## 2026-09-01 — Sharing, e-mail, password reset, and a security pass
|
||
|
||
### Added
|
||
- **Share links** (`/s/<slug>`) for a single image or a collection: someone without an
|
||
account can view them and download at full resolution. These are photos of children,
|
||
so the defaults lean safe — every link expires by itself (30 days, configurable), can
|
||
be revoked and un-revoked, optionally carries a passphrase, download can be switched
|
||
off entirely, and pages 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.
|
||
- **Download everything at once**: a share with several pictures gets one button above the
|
||
grid for a ZIP at full resolution. Written to stream, not buffer -- one file at a time,
|
||
waiting for archiver's `entry` before reading the next. Measured: 440 MB of images, RSS
|
||
peaked at 196 MB against a 120 MB baseline. Capped at 200 files and 500 MB, past which
|
||
the archive closes with a note saying what happened. Respects the download lock and the
|
||
passphrase; rate limited per link and address.
|
||
- **Passing it on** by e-mail (several recipients, 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 needs a Meta account, approved templates and
|
||
per-conversation fees; this works today for free and the API can slot in behind it.
|
||
- **E-mail through the account's own mailbox** (SMTP, credentials AES-256-GCM encrypted
|
||
like every other secret). Powers password reset, optional job-done notifications, and
|
||
mailing share links. nodemailer has zero dependencies and no native postinstall —
|
||
checked before adding it, given the history.
|
||
- **Password reset**: single-use token, only its hash stored, one hour, identical
|
||
response whether or not the address exists, rate limited per address and per sender.
|
||
- **`/konto`** for the e-mail address and notifications, **`/teilen`** for managing links
|
||
with view and download counts.
|
||
- **IPTC/XMP provenance** on AI results (`trainedAlgorithmicMedia`, or
|
||
`compositeWithTrainedAlgorithmicMedia` for edited photos) plus the model, written by
|
||
hand into PNG iTXt and JPEG APP1 rather than pulling in a sharp upgrade. exiftool reads
|
||
it back from both formats; pixels, dimensions and dpi provably untouched.
|
||
- **CSP with real hashes** for every inline script, plus nosniff, referrer-policy,
|
||
X-Frame-Options and a permissions-policy. `style-src` stays open because React sets
|
||
styles as attributes and attributes cannot be hashed — measured, the hash-only policy
|
||
breaks the print preview.
|
||
- `mem_limit: 3g` in compose. sharp can spike; without a limit an outlier takes the whole
|
||
container down instead of just the process.
|
||
|
||
### Fixed
|
||
- **Sessions never expired server-side and survived a password reset.** `Max-Age` is a
|
||
request to the browser, not a rule for the server. 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. Cookies now carry a session epoch, checked against the database on every
|
||
request, and the epoch is bumped on password reset and on any role change.
|
||
- **"Allow download: off" did nothing for images with transparency** — exactly the
|
||
cutouts and stickers this tool exists for. Verified: now 1500x2000 instead of the
|
||
2400x3200 original, and the download route returns 403.
|
||
- **The e-mail settings card saved nothing** while reporting success. Found by the design
|
||
critique an hour after shipping.
|
||
- **The public page explained how to bypass "view only"**, printing the long-press hint
|
||
regardless of the setting.
|
||
- The share title went into outgoing mail HTML unescaped while the message was escaped.
|
||
- argon2 ran on every request for protected links, including every image (130 ms, 64 MiB
|
||
each). Checked once now, then a short-lived signed cookie scoped to that link. The
|
||
passphrase moved from the query string to POST.
|
||
- `days` accepted nonsense and quietly produced an unlimited link (`Number('abc')` is
|
||
NaN, `NaN > 0` is false; `[]` coerces to 0).
|
||
- Rate limits keyed on the *first* X-Forwarded-For value, which the caller sets. Counting
|
||
from the right now, trusted-proxy count configurable.
|
||
- **The path-escape hole in delivery** the wiki had flagged twice as open.
|
||
- The crop maths lived twice with nothing holding the two copies together. One module
|
||
now, 60000 randomised cases asserting both entry points agree — which immediately
|
||
found a real bug: the zoom limits also clamped the *initial* crop, so an extreme format
|
||
silently broke the aspect ratio.
|
||
- Smaller: expired links could still be mailed; missing PUBLIC_BASE_URL sent dead links
|
||
while reporting success; the reset response time revealed whether an account existed;
|
||
`share_views` grew without bound; double provenance marking appended a second packet
|
||
with the older model; the colour flag covered the selection circle in the library; the
|
||
seventh nav item pushed logout off screen at 820px; `/konto` was unreachable on a
|
||
phone; network failures rendered as empty states.
|
||
|
||
### Changed
|
||
- 42 new tests (97 total). `shares`, `mail` and `provenance` had none, and that is where
|
||
both reviews found the most.
|
||
- `PRODUCT.md` and `DESIGN.md` added, written from the code and the wiki.
|
||
|
||
## 2026-08-21 — Unique, chronologically sortable filenames
|
||
|
||
### Fixed
|
||
- **Pictures overwrote each other at Picdrop.** Two badges of the same person in the
|
||
same format on the same day both came out as `2026-07-23_lea_the-frame.png`, so the
|
||
second upload silently replaced the first. Reported from real use — Till lost images
|
||
this way. The name now carries the time down to the second plus a short tag derived
|
||
from the item id:
|
||
|
||
2026-08-20_143207_lea-badge_the-frame_a3f9.jpg
|
||
|
||
Two pictures can no longer collide: the tag comes from the item id, which is unique.
|
||
A belt-and-braces loop appends `-2`, `-3` … if a name is somehow still taken —
|
||
deliberately a code loop rather than a UNIQUE constraint, because a rejected write
|
||
would fail an already-paid AI job at the very last step.
|
||
|
||
### Added
|
||
- **Backfill for the existing library** (Admin → Dateinamen). Preview first — it reports
|
||
how many names are currently duplicated — then apply. It re-stamps from `created_at`
|
||
and the item id while keeping the motif and format already chosen, so it is idempotent:
|
||
a second run changes nothing. Stored files are untouched; they live under their id,
|
||
not their name.
|
||
- **Re-deliver everything** under the new name, for copies already sitting at Picdrop.
|
||
The old files stay where they are — deleting a customer's files automatically is the
|
||
wrong reflex; that stays a manual decision.
|
||
|
||
### Changed
|
||
- Timestamps use **Europe/Berlin** (override with `KLARBILD_TZ`) rather than UTC. A
|
||
picture taken at 00:30 should not be dated to the previous day.
|
||
- Naming moved into `src/lib/naming.ts` — dependency-free, like `printlayout.ts`, so the
|
||
rules are testable and identical in every path (jobs, Telegram, MCP, backfill).
|
||
The generator strips its own stamp when a result is fed back in as a source, so
|
||
re-processing cannot accumulate timestamps.
|
||
- 17 new tests (56 total), including the reported bug as an explicit regression test and
|
||
a check that lexicographic sorting equals chronological sorting across a DST boundary.
|
||
|
||
## 2026-08-20 — Oversize sheets, duplicate images, free labelling, new layout
|
||
|
||
### Added
|
||
- **Prints larger than the sheet.** The everyday case is 20 × 30 cm on A4 photo paper, where
|
||
exactly 3 mm are missing. Instead of refusing, each cell now offers two ways out
|
||
(`src/lib/printoversize.ts`, dependency-free like `printlayout.ts` so browser and server compute
|
||
the same thing):
|
||
- `overflow` centres the piece on the **whole sheet**, not the printable area — otherwise you
|
||
throw away the margin a borderless printer can actually use. How much is lost per side is
|
||
computed up front and shown before printing.
|
||
- `tile` splits the piece across several sheets. Tiles overlap by a glue fold (10 mm by default),
|
||
printed on both neighbours and marked with a dashed line. Each sheet carries its number, row
|
||
and column. 30 × 40 cm on A4 → 2 × 2 sheets.
|
||
Verified by rasterising the PDFs at 254 dpi: the overflow case measures 200,1 mm wide at x = 5 mm
|
||
and bleeds off both long edges; the tiles measure 200 × 286 and 110 × 124 mm, overlapping by 10.
|
||
- **Duplicate an image.** The same picture can now appear as several cells and be printed in two
|
||
different sizes on one sheet. Previously you had to copy the file, rename it and upload it again.
|
||
- **Configurable label.** The footer line can be switched off, given custom text, and placed at one
|
||
of six positions. It is only drawn if that edge is actually free — it never lands on a picture.
|
||
- **Public presentation page** at `/vorstellung` (no login, nothing operable). The sample sheet on it
|
||
is not a drawing: it runs the real packer at render time, so it follows the algorithm.
|
||
|
||
### Fixed
|
||
- **The tile number overlapped the picture.** The sheet number is printed at 8 pt from 3 mm off the
|
||
top edge, which needs about 6 mm — with a 5 mm paper margin it ran 0,8 mm into the motif.
|
||
`noteInset()`/`tileSheet()` now shift the tile down by the shortfall and shorten the sheet by the
|
||
same amount, so nothing sticks out at the bottom. Both the browser preview and the PDF call the
|
||
same helper, so the previewed sheet count cannot drift from the printed one.
|
||
|
||
### Changed
|
||
- **Desktop layout.** Work on the left, preview on the right, and the preview column is sticky so it
|
||
follows along — no more endless column next to an empty one. Setting groups sit collapsed side by
|
||
side in a two-column grid and expand across the full width, so three-way switches keep their
|
||
labels instead of being squeezed to 50 px.
|
||
- **Mobile layout.** Preview first, "Druck-PDF erzeugen" as a fixed bar above the tab navigation,
|
||
each image row showing thumbnail and controls side by side, touch targets at 38–40 px, and the
|
||
remove button moved onto the thumbnail so it stops covering the format dropdown. The step numbers
|
||
("1 ·", "2 ·") are hidden below 1080 px, where the preview comes first and the numbers would lie.
|
||
- Advanced setting groups (margins, quality, labelling, delivery) start collapsed — their summary
|
||
line already says what is set.
|
||
|
||
## 2026-08-18 (4) — Renamed to "Drucken", 86 sizes, crop editor fixed
|
||
|
||
### Fixed
|
||
- **The crop editor distorted the picture.** `.crop-box` had `width:100%` *and*
|
||
`max-height`, so the height was clamped while the width stayed put — the frame no
|
||
longer had the target aspect ratio, and `object-fit:fill` stretched the image into it.
|
||
Measured on a 390 px viewport: frame 0.843 instead of 0.778, picture **8.3 % too wide**.
|
||
The frame width is now derived from the allowed height and the ratio, and the image
|
||
carries its own `aspect-ratio` instead of a second percentage — it can no longer stretch,
|
||
even if the frame is ever off.
|
||
- **Dragging felt broken.** At "fill" zoom the crop sits exactly at the stop in one axis,
|
||
so pulling that way does nothing — correct, but invisible. The editor now says which way
|
||
the picture can move, double-click/double-tap zooms a step, arrow keys nudge (shift for
|
||
bigger steps), and the zoom slider shows its factor.
|
||
|
||
### Changed
|
||
- **Renamed to "Drucken"** — the tab, page, title and Telegram button. `/druck` and
|
||
`/passbilder` both redirect permanently to `/drucken`.
|
||
- **86 image sizes in ten groups** (was 41): passport formats per country, cm small sizes,
|
||
classic photo sizes, **US inch sizes** (wallet to 24 × 36), **instant-film picture areas**
|
||
(Instax mini/Square/Wide, Polaroid), poster and frame sizes up to 100 × 140, squares,
|
||
**DIN A0–A7 incl. A3+**, cards (business card, postcard, DIN lang, cheque card) and
|
||
aspect ratios as physical sizes (16:9, 9:16, 21:9, 2:1, 3:2, 2:3, 4:3, 3:4, 5:4, 4:5, 1:1).
|
||
- Paper list grew to 19: DIN A1, square photo paper, and the US inch cuts.
|
||
|
||
## 2026-08-18 (3) — Code review: fixes
|
||
|
||
### Fixed
|
||
- **EXIF orientation was ignored.** Phone photos carry their rotation as metadata only.
|
||
sharp cropped the unrotated raster, so a portrait shot came out of the printer sideways
|
||
and with the wrong framing. Orientation is now applied before any geometry, and
|
||
`/api/uploads` reports oriented dimensions so the crop editor agrees with the render.
|
||
- **Memory blow-up on "Rand lassen".** The padded image was materialised at source
|
||
resolution before the resize. A panorama into a narrow contain target built a ~960 MB
|
||
intermediate and then failed outright; it is one extract→resize→extend chain now
|
||
(sharp's own order) — 90 ms and a few MB.
|
||
- **`bleedMm` was unbounded** in `/api/print/single` (the sheet endpoint clamped it).
|
||
- **Delivery gallery could escape the target's base folder** — `posixpath.join` happily
|
||
resolves `../..`, and the folder is created before upload. Names are validated now.
|
||
- **Denial of service:** sheet requests are capped at 500 pieces and the packer has a
|
||
step budget, so a degenerate request can no longer block the single-threaded server.
|
||
- **Print presets:** delete only your own (admins all), config size and count limits,
|
||
and `by_name` honours `anonymous_generations`.
|
||
- **Telegram callbacks** now require an active pairing, like every other path.
|
||
- **Error responses** no longer leak storage paths or delivery hostnames.
|
||
- `allowRotate: undefined` meant "no rotation" in one place and "rotation allowed" in
|
||
two others — a picture that only fits rotated was reported as unplaceable.
|
||
- The many-formats shortcut dropped a format that only fits rotated.
|
||
- `unplaced` blamed the first format instead of the one actually missing.
|
||
- Corner marks could land inside the printed bleed; the offset is raised to clear it.
|
||
- `capacity()` silently capped at 200.
|
||
- Image keys could collide with a cell literally named `x::rot`.
|
||
- `labelMm` rounded away real decimals (11,25 cm became 11,3); `parseSizeMm` ignored the
|
||
order in `a:b`, so `3:4/15` and `4:3/15` produced the same portrait size.
|
||
- The footer was drawn over the artwork when the margin was small.
|
||
- The UI now warns when corner marks do not fit the margin, and when continuous guides
|
||
are combined with mixed sizes (they cannot run through).
|
||
|
||
### Changed
|
||
- Tests grew from 21 to 31 — every finding above has a regression test.
|
||
|
||
## 2026-08-18 (2) — Passbildfunktion: all sizes, fit rules, mobile
|
||
|
||
### Added
|
||
- **Renamed to "Passbilder"** — the tab, page and Telegram button now say what it is.
|
||
`/druck` permanently redirects to `/passbilder`.
|
||
- **Every size the AI pipeline knows** is now printable too: 9×13 … 60×90, DIN A6–A2,
|
||
squares, plus poster/frame sizes up to 70×100 and the screen ratios (16:9 "The Frame",
|
||
9:16, 3:2, 4:3, 5:4) as physical measurements. 41 presets in seven groups.
|
||
- **Fit rule per image** — when the picture does not match the target ratio, choose
|
||
**Zuschneiden** (fill the format, crop the overflow — default) or **Rand lassen**
|
||
(keep the whole picture, pad with a border colour: white, black, paper or custom).
|
||
Nothing is ever distorted. Works in the crop editor, the sheet preview, the PDF,
|
||
`/api/print/*` (`fit`, `bg`) and the saved templates.
|
||
- **Mobile layout** — image cards become a row on phones, touch targets grow to ~40 px,
|
||
the crop editor turns into a full-width bottom sheet, no horizontal overflow.
|
||
Verified at 390 / 820 / 1440 px; all three produce the identical PDF.
|
||
|
||
### Fixed
|
||
- **Sheet cells could come out oversized.** sharp applies `extend` *after* `resize`, so
|
||
padding was added on top of the finished size — a 35×45 mm cell became 35×171 mm in
|
||
"Rand lassen" mode, and bleed near an image edge was off too. Crop and padding now run
|
||
in their own pass. Covered by `tests/printrender.test.ts`.
|
||
- **Orientation is no longer overridden.** A single portrait passport photo was laid down
|
||
sideways just because more would fit that way. Rotation now only happens when it
|
||
actually saves a sheet.
|
||
|
||
## 2026-08-18 — Print module: exact sizes, sheets & crop marks (no AI)
|
||
|
||
### Added
|
||
- **Print module (`/druck`)** — a lab-style, completely AI-free path: crop, scale and place
|
||
images at exact physical sizes. No model call, no cost, no queue.
|
||
- **Interactive crop editor** — pan and zoom on a fixed target aspect ratio (rule-of-thirds
|
||
overlay); the crop is stored relative (0..1) so it survives any source resolution.
|
||
- **Size presets & free input** — biometric passport 35 × 45 mm, 2 × 3 / 3 × 4 / 4 × 5 / 4,5 × 6 cm,
|
||
9 × 13 … 50 × 70 cm, squares, DIN. Free input understands `12x15` (cm), `35x45mm`, `5`
|
||
(= 5 × 5 cm) and `4:3/15` (ratio + long edge).
|
||
- **Sheet layout** — paper presets DIN A6–A2 incl. **A3+ (329 × 483 mm)**, photo-paper cuts
|
||
(9 × 13 … 20 × 30), US Letter/Legal, plus any custom paper size. Portrait/landscape,
|
||
printer margin, centering, per-image copy count.
|
||
- **Automatic nesting** — identical sizes produce an exact grid; mixed sizes go through a
|
||
MaxRects packer that fixes one orientation per format and tries every combination,
|
||
so eight passport photos land in the free space next to the 13 × 18.
|
||
- **Crop marks** — `corner` (fine 0.25 pt marks outside the trim box, Photoshop/InDesign
|
||
convention, 4 mm long / 3 mm offset by default) or `grid` (continuous guides across the
|
||
sheet that never cross another image). Gap between images adapts to the chosen marks.
|
||
- **Bleed** (0–10 mm) — the image extends past the trim box, edge pixels are copied rather
|
||
than stretched; the gap is always ≥ 2 × bleed so neighbouring bleeds cannot overlap.
|
||
- **PDF output at 1:1** — the PDF page is exactly the sheet size (pdf-lib), with a footer
|
||
stating the sizes, dpi and the "print at 100 %, not fit-to-page" reminder.
|
||
- **Single export** — one image at an exact size as PNG/JPG with dpi metadata, plus a
|
||
resolution warning when the source cannot hold the requested dpi.
|
||
- **Sheet templates** — save the whole sheet setup (paper, marks, formats, counts) as a
|
||
reusable preset (`print_presets`, migration `013`).
|
||
- **All three entrances** — `/druck` in the web app, `📐 Druckbogen` in Telegram (fully
|
||
button-driven, returns an A4 PDF), and the MCP tools `exact_size` / `print_sheet`.
|
||
- **API** — `POST /api/print/single`, `GET /api/print/single?size=…` (mm → px calculator),
|
||
`POST /api/print/sheet`, `GET|POST|DELETE /api/print/presets`. Documented in `/llms.txt`.
|
||
- **Seeded sheet templates** — "Kita-Satz (A4)", "Schulsatz klein (A4)", "Passbildbogen 35×45 (A4)"
|
||
and "2 × 10×7,5 auf Fotopapier 10×15". Loading a multi-format template with a single image
|
||
clones that image into every format, so one click produces the whole set.
|
||
- **Margin profiles** — borderless (0 mm), standard (5 mm) and safe (10 mm) as one-click buttons.
|
||
- **Deliver the sheet** — the finished PDF can additionally be pushed to Picdrop, the NAS or any
|
||
extra delivery target (`deliver` in the sheet request).
|
||
- **Tests** — `tests/printlayout.test.ts` (`npm test`) covers size parsing, exact mm → px,
|
||
non-overlap, margins, bleed spacing, mark geometry and multi-sheet paging.
|
||
|
||
## 2026-07-23 — Compose, generate & second backup
|
||
|
||
### Added
|
||
- **Compose mode** — combine multiple images + a text description into one new image.
|
||
- **Generate mode** — create images from text only, no source needed.
|
||
- **The Frame "extend"** — outpaint more scene around the motif instead of only cropping.
|
||
- **Reuse** — take a finished result back into the studio as a new source to edit further.
|
||
- **Alternatives** — generate additional versions of the same image, grouped with a switcher.
|
||
- **Folders** — filter, rename and delete in the library; per-folder delivery gallery mapping.
|
||
- **Bulk actions** — download selected results as a ZIP, move several into a folder at once.
|
||
- **Thumbnails** — low-res previews for the library grid.
|
||
- **Models** — quality tiers incl. open-source (FLUX.2); the used model is shown per image.
|
||
- **Color tags** — Picdrop-style flags (red / orange / green / final) per image, filterable, and
|
||
written into the companion `.md` metadata.
|
||
- **Tap-to-save** — "Laden" opens the image full-bleed so on iPhone/iPad a long-press offers
|
||
"Save to Photos" (plus a classic file download).
|
||
- **Access control & privacy** — library visibility (own-only vs shared), optional anonymous
|
||
generations (creator hidden from non-admins), admins always see all.
|
||
- **Private sessions** — generate-and-forget: no library entry, no delivery/backup, no stored
|
||
prompt, sources purged, result auto-deleted shortly after. Enabled globally by admin or forced
|
||
per user.
|
||
- **NSFW gate** — models flagged NSFW are only selectable/visible when the admin enables it.
|
||
Operator responsibility; content involving minors is never permitted.
|
||
- **Single-image transform** — compose now accepts one image, so a single photo can be
|
||
transformed by prompt (e.g. "make it an oil painting") and put onto a format.
|
||
- **Multiple delivery targets** — besides the default Picdrop, add any number of FTP/SFTP
|
||
destinations; selectable in the studio ("Wohin?", incl. NAS); a preset or folder can be
|
||
hard-wired to a specific target + gallery.
|
||
- **Backup to any target** — mark any target as a backup destination (like the NAS); one-click
|
||
"back up all images" mirrors to all backup destinations.
|
||
- **Metadata sidecar** — optionally ship a companion `.md` (prompt, model, format …) next to
|
||
each image on Picdrop/NAS/FTP.
|
||
- **Prompt view** — every generated image shows its prompt in the library (with copy).
|
||
- **Preset manager** — save current settings as a preset, load and delete presets.
|
||
- **API token + MCP server** — programmatic access so an assistant can push images from a
|
||
local folder or iMessage into Klarbild (`mcp/klarbild-mcp.mjs`).
|
||
- **The Frame** preset now delivers to its own gallery ("TheFrame-Backgrounds").
|
||
- **Second backup** — mirror every result over SFTP/FTPS to a NAS (works over Tailscale too),
|
||
with a "NAS ✓" badge and a "mirror all existing images" action.
|
||
- **Storage management** — thumbnails, delete sources after processing, auto-retention.
|
||
- **Telegram** — fully button-driven (persistent keyboard + command menu): edit, compose,
|
||
"new image", reuse — no slash commands required.
|
||
- **Reset** — wipe library, jobs and folders cleanly (keeps users, settings, models, recipes),
|
||
guarded by a typed confirmation.
|
||
- **Help page**, in-app **changelog**, and a NAS **reachability diagnostic**.
|
||
|
||
### Fixed / hardened
|
||
- Real per-image cost cap (monthly budget) enforced by the worker.
|
||
- Idempotent job completion (no duplicate delivery/notification).
|
||
- Before/after comparison works for composed images; hidden for text-only results.
|
||
|
||
## 2026-07-22 — Foundation
|
||
|
||
### Added
|
||
- Astro 5 + React islands UI (mobile-first), PostgreSQL, fs/S3 storage abstraction.
|
||
- pg-boss job queue with in-process worker; sharp pipeline (exact cm/dpi print sizes,
|
||
sticker contour).
|
||
- OpenRouter image gateway; admin-managed models.
|
||
- Picdrop delivery over SFTP/FTPS; recipes; library with before/after.
|
||
- Telegram bot (grammY webhook); argon2 auth; AES-256-GCM secret encryption.
|