1.1.0: changelog, README quick start without a clone, help and in-app changelog
The quick start downloads two files and pulls the image; no clone, no build. README figures brought up to date (it still said Astro 5, 124 tests, 16 migrations). Upgrade notes for 1.0 installs: the volume ownership change and the basic-ftp NAT behaviour. Help page and in-app changelog in English and German.
This commit is contained in:
8 files changed
+104
-11
No files matched your search
@@ -1,5 +1,63 @@
|
||||
# Changelog
|
||||
|
||||
## 1.1.0 — 2026-10-10
|
||||
|
||||
**Install without cloning or building**
|
||||
|
||||
- **Prebuilt image** for linux/amd64 and linux/arm64 at
|
||||
`ghcr.io/tillheidrich/klarbild`. The compose files pull `:1`, which follows every
|
||||
1.x release; an update is `docker compose pull && docker compose up -d`. The quick
|
||||
start needs two downloaded files instead of a clone and a build.
|
||||
- The image is two-stage: no compiler, no dev dependencies, no source tree at runtime.
|
||||
It **runs as uid 1000**, not root.
|
||||
|
||||
**First visit**
|
||||
|
||||
- On a print-only instance the print studio no longer offers "Pick from the library" —
|
||||
the library belongs to the AI module, and on an instance without it the button
|
||||
opened an empty picker. In its place: **Try it with a sample photo**, an original
|
||||
illustration that goes through the normal upload.
|
||||
- A favicon and an iPhone home-screen icon.
|
||||
|
||||
**Trash (AI module)**
|
||||
|
||||
- Deleting moves an image into the **trash** instead of removing it on the spot. It is
|
||||
hidden from the library, share links, ZIPs, deliveries, the mirror, print sources and
|
||||
the print history, and comes back unchanged on restore. "Undo" in the toast, a trash
|
||||
view with days left, restore, delete for good, empty.
|
||||
- Maintenance purges after `trash_days` (default 30, Admin → Storage & cleanup). 0
|
||||
switches the trash off.
|
||||
- An image trashed while queued is skipped instead of being generated and billed.
|
||||
- Purging removes only files no other image still uses: alternatives share their source
|
||||
with the original.
|
||||
- API: `DELETE /api/items/:id` now trashes (`?purge=1` for good), `GET|POST|DELETE
|
||||
/api/trash`. See `/llms.txt`.
|
||||
|
||||
**Fixed**
|
||||
|
||||
- Only the file route checked who may see an image. Rename, delete, retry, alternative,
|
||||
reuse and the selection ZIP accepted any id from any signed-in account. One rule now
|
||||
(`src/lib/itemaccess.ts`): read your own, or a shared library but never a private
|
||||
session; change only your own; admins everything. A refused id answers 404.
|
||||
- README said Astro 5; it is Astro 7.
|
||||
|
||||
**Dependencies**
|
||||
|
||||
- nodemailer 10, basic-ftp 6, argon2 0.45, archiver 8, ssh2-sftp-client 12,
|
||||
@astrojs/react 7, Astro 7.3.8, sharp 0.35.5 — `npm audit` 0 (was 7, six high).
|
||||
- Node ≥ 22.12.
|
||||
- Not taken yet: pg-boss 12 (it cannot migrate the v10 queue schema in place) and
|
||||
TypeScript 7.
|
||||
|
||||
**Upgrading from 1.0**
|
||||
|
||||
- If you built 1.0 yourself with the old compose file, your files volume belongs to
|
||||
root. The new image runs as uid 1000, so hand the volume over once:
|
||||
`docker compose run --rm --user root app chown -R 1000:1000 /data`
|
||||
- basic-ftp 6 no longer silently redirects the data connection when an FTP server
|
||||
behind NAT announces its private address. If a delivery target that worked fails
|
||||
now, "Test connection" names the cause.
|
||||
|
||||
## 1.0.0 — first public release
|
||||
|
||||
The first open-source release of Klarbild. Everything before this lived in a
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ You need a PostgreSQL to point `DATABASE_URL` at; `docker compose up db` gives y
|
||||
one. Migrations run themselves on the first request.
|
||||
|
||||
```bash
|
||||
npm test # 124 tests, no database needed
|
||||
npm test # 130 tests, no database needed
|
||||
npx tsc --noEmit # must be clean
|
||||
npm run build
|
||||
```
|
||||
|
||||
@@ -21,12 +21,18 @@ page runs this repository's own layout engine client-side and hands you the PDF.
|
||||
nothing uploaded.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/tillheidrich/klarbild.git && cd klarbild
|
||||
cp .env.example .env # four values at the top; the file says which
|
||||
mkdir klarbild && cd klarbild
|
||||
curl -fsSLO https://raw.githubusercontent.com/tillheidrich/klarbild/main/docker-compose.yaml
|
||||
curl -fsSL https://raw.githubusercontent.com/tillheidrich/klarbild/main/.env.example -o .env
|
||||
nano .env # four values at the top; the file says which
|
||||
docker compose up -d
|
||||
docker compose logs app # your admin password is printed here, once
|
||||
```
|
||||
|
||||
No clone, no build: the compose file pulls a prebuilt image for amd64 and arm64
|
||||
(a Raspberry Pi 4 or 5 runs it). Open `http://localhost:4321`, sign in, and press
|
||||
**Try it with a sample photo** if you have no picture at hand.
|
||||
|
||||

|
||||
|
||||
---
|
||||
@@ -75,7 +81,7 @@ of the PDF above measures 210.00 × 297.00 mm, because that is what A4 is.
|
||||
| Module | What it adds | What it needs |
|
||||
|---|---|---|
|
||||
| `print` | everything above | nothing — always on |
|
||||
| `ai` | clean up screenshots, convert, combine, generate | a key for an images API (paid per image) |
|
||||
| `ai` | clean up screenshots, convert, combine, generate — plus the library, with folders, flags and a 30-day trash | a key for an images API (paid per image) |
|
||||
| `delivery` | push finished files to an SFTP/FTPS server you already have | that server |
|
||||
| `mirror` | a second copy of every result, sorted by month | a second target |
|
||||
| `share` | public links: expiring, revocable, optional passphrase, ZIP download | a public URL |
|
||||
@@ -119,12 +125,12 @@ visitor per hour, deleted after 90 days.
|
||||
|
||||
## Stack and shape
|
||||
|
||||
Astro 5 (SSR, Node standalone) with React islands · PostgreSQL · `sharp` ·
|
||||
Astro 7 (SSR, Node standalone) with React islands · PostgreSQL · `sharp` ·
|
||||
`pdf-lib` · `pg-boss` · OKLCH design tokens, no Tailwind. Files go to a volume by
|
||||
default; S3/MinIO is available but not required.
|
||||
|
||||
About 13,700 lines across 117 files, 124 tests, 16 forward-only idempotent
|
||||
migrations that run themselves on boot. English and German interface — 1,067
|
||||
About 13,800 lines across 115 files, 130 tests, 17 forward-only idempotent
|
||||
migrations that run themselves on boot. English and German interface — 1,095
|
||||
entries, covering every translatable string in the source. The English sentence is
|
||||
the translation key, so there is no key file to keep in sync and a missing entry
|
||||
degrades to readable English rather than to `print.sheet.title`.
|
||||
@@ -136,6 +142,17 @@ and Postgres, files on a volume. `docker-compose.full.yaml` adds MinIO and turns
|
||||
every module on. Both read their secrets from `.env` and refuse to start rather than
|
||||
boot with a default password.
|
||||
|
||||
Both pull `ghcr.io/tillheidrich/klarbild:1`, which follows every 1.x release.
|
||||
Updating is
|
||||
|
||||
```bash
|
||||
docker compose pull && docker compose up -d
|
||||
```
|
||||
|
||||
and the migrations run themselves on the next boot. The container runs as uid
|
||||
1000, not root. Prefer to build the image yourself? Swap the `image:` line in the
|
||||
compose file for `build: .` and run it from a clone.
|
||||
|
||||
**From source.**
|
||||
|
||||
```bash
|
||||
|
||||
Generated
+3
-3
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "klarbild",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "klarbild",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^11.1.7",
|
||||
@@ -39,7 +39,7 @@
|
||||
"typescript": "^5.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.6"
|
||||
"node": ">=22.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@astrojs/compiler-binding": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "klarbild",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"description": "Self-hosted print studio: put pictures on paper at exactly the size you asked for. Sheet layout with crop marks, tiling, 1:1 PDF. Optional AI, delivery, sharing and Telegram modules.",
|
||||
"license": "AGPL-3.0-only",
|
||||
"author": "Till Heidrich",
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
"1 year": "1 Jahr",
|
||||
"1 year from now": "1 Jahr ab jetzt",
|
||||
"1 · Edit an image": "1 · Bild bearbeiten",
|
||||
"1.1 — a trash, a sample photo, and an image you do not have to build": "1.1 — ein Papierkorb, ein Beispielfoto und ein Image, das du nicht selbst bauen musst",
|
||||
"10 Oct 2026": "10.10.2026",
|
||||
"13 × 18 cm → measured 130.1 × 180.1 mm": "13 × 18 cm → gemessen 130,1 × 180,1 mm",
|
||||
"150 — draft": "150 — Entwurf",
|
||||
"16:9 TV (30 cm wide)": "16:9 TV 16:9 (30 cm breit)",
|
||||
@@ -86,6 +88,8 @@
|
||||
"<b>Cutting guides</b>: <b>corner marks</b> (fine marks outside the final format, as in Photoshop and InDesign) or <b>continuous lines</b> across the whole sheet for the cutting ruler. The continuous lines never run through another subject.": "<b>Schnitthilfen</b>: <b>Eckmarken</b> (feine Marken außerhalb des Endformats, wie in Photoshop und InDesign) oder <b>durchgehende Linien</b> über den ganzen Bogen fürs Schneidelineal. Die durchgehenden Linien laufen nie durch ein anderes Motiv.",
|
||||
"<b>Decide the caption yourself.</b> The small line under the sheet can be switched off, given your own text and placed in one of six spots (top or bottom, left, centre or right). If there is no room free at the chosen edge it is dropped automatically — nothing is ever printed over an image.": "<b>Beschriftung selbst bestimmen.</b> Die kleine Zeile unter dem Bogen lässt sich ausschalten, mit eigenem Text versehen und an eine von sechs Stellen legen (oben oder unten, links, mittig oder rechts). Ist an der gewählten Kante kein Platz frei, entfällt sie automatisch — über ein Bild wird nie gedruckt.",
|
||||
"<b>Delete a job</b> straight from the queue (for old or failed jobs). Missing sources now report a clear notice instead of a cryptic technical error.": "<b>Auftrag löschen</b> direkt aus der Warteschlange (für alte/fehlgeschlagene Aufträge). Fehlende Quellen melden jetzt einen klaren Hinweis statt kryptischer Technikfehler.",
|
||||
"<b>Deleting is no longer final on the spot.</b> ✕ moves an image into the trash, and for a few seconds the bottom of the screen offers <b>Undo</b>. Noticed later? The library shows a <b>Trash</b> button; everything in there can be restored for 30 days, then Klarbild clears it away by itself.": "<b>Löschen ist nicht mehr sofort endgültig.</b> ✕ legt ein Bild in den Papierkorb, und unten steht für ein paar Sekunden <b>Rückgängig</b>. Später gemerkt? In der Bibliothek steht dann <b>Papierkorb</b>; alles darin lässt sich 30 Tage lang wiederherstellen, danach räumt Klarbild es von selbst weg.",
|
||||
"<b>Deleting</b> (✕) moves an image into the <b>trash</b>, with <b>Undo</b> right after. The trash (a button top right, once something is in it) keeps it for 30 days — and while it is there it appears in no share link and no delivery.": "<b>Löschen</b> (✕) legt ein Bild in den <b>Papierkorb</b>, direkt danach mit <b>Rückgängig</b>. Der Papierkorb (Knopf oben rechts, sobald etwas drinliegt) hebt es 30 Tage auf — und solange es dort liegt, taucht es in keinem geteilten Link und bei keiner Auslieferung auf.",
|
||||
"<b>Delivery-target tools in Admin:</b> “Show folders” (what sits where over SFTP) and “Clean up leftovers” (remove .tmp files left behind).": "<b>Werkzeuge fürs Auslieferungsziel im Admin:</b> „Ordner anzeigen\" (was liegt per SFTP wo) und „Reste aufräumen\" (liegengebliebene.tmp-Dateien entfernen).",
|
||||
"<b>Download singly</b>: one image at exactly its size as PNG/JPG with dpi metadata — without a sheet.": "<b>Einzeln herunterladen</b>: ein Bild exakt auf Maß als PNG/JPG mit dpi-Metadaten — ohne Bogen.",
|
||||
"<b>Download</b> saves the file at full resolution. On a phone it also works like this: press and hold an image, then save it.": "<b>Herunterladen</b> speichert die Datei in voller Auflösung. Am Handy geht es auch so: lange auf ein Bild drücken und speichern.",
|
||||
@@ -114,6 +118,7 @@
|
||||
"<b>If the image does not fit the format</b>, you decide for each image: <b>Crop</b> (the format is filled, something is missing at the edges — the default) or <b>Letterbox</b> (the whole image stays visible, with the <b>border colour</b> around it — white, black, paper-coloured or freely chosen). <b>Nothing is ever distorted.</b>": "<b>Wenn das Bild nicht zum Format passt</b>, entscheidest du je Bild: <b>Zuschneiden</b> (das Format wird ausgefüllt, außen fehlt etwas — Standard) oder <b>Rand lassen</b> (das ganze Bild bleibt sichtbar, außen steht die <b>Randfarbe</b> — weiß, schwarz, papierfarben oder frei gewählt). <b>Verzerrt wird nie.</b>",
|
||||
"<b>Images from the library into the print area.</b> Until now you had to upload a file even when Klarbild had it already. The print area now offers <b>“Choose from the library”</b> — with search, by folder, and several at once.": "<b>Bilder aus der Bibliothek in den Druckbereich.</b> Bisher musstest du eine Datei hochladen, auch wenn Klarbild sie längst hatte. Jetzt steht im Druckbereich <b>„Aus der Bibliothek wählen\"</b> — mit Suche, nach Ordner, und mehrere auf einmal.",
|
||||
"<b>Important when printing:</b> in the print dialog choose “Actual size” or “100%” — <b>not</b> “fit to page”. Otherwise the size no longer holds. If you can print borderless, set the margin to 0; otherwise 3–5 mm, or the printer cuts the marks away.": "<b>Wichtig beim Drucken:</b> im Druckdialog „Tatsächliche Größe\" bzw. „100 %\" wählen — <b>nicht</b> „an Seitengröße anpassen\". Sonst stimmt das Maß nicht mehr. Wer randlos drucken kann, setzt den Rand auf 0; sonst 3–5 mm, sonst schneidet der Drucker die Marken weg.",
|
||||
"<b>Its own icon</b> in the browser tab and on the iPhone home screen.": "<b>Ein eigenes Symbol</b> im Browser-Tab und auf dem iPhone-Homebildschirm.",
|
||||
"<b>Keep editing:</b> simply send a finished image from the bot back to it.": "<b>Weiterbearbeiten:</b> ein fertiges Bild vom Bot einfach wieder zurückschicken.",
|
||||
"<b>Keep editing:</b> take a finished image as a new source and edit it again.": "<b>Weiterbearbeiten:</b> ein fertiges Bild als neue Vorlage übernehmen und erneut bearbeiten.",
|
||||
"<b>Keep editing</b>: takes a finished image into the studio as a new source — edit it again there, bring it to the next format, or mix it with further images in Combine mode.": "<b>Weiterbearbeiten</b>: übernimmt ein fertiges Bild als neue Vorlage ins Studio — dort erneut bearbeiten, ins nächste Format bringen oder im Modus Kombinieren mit weiteren Bildern mischen.",
|
||||
@@ -162,6 +167,7 @@
|
||||
"<b>The same image at two sizes?</b> Tap <b>Duplicate</b> — the image appears a second time in the list and gets its own size there. No more renaming and uploading again.": "<b>Dasselbe Bild in zwei Maßen?</b> <b>Duplizieren</b> antippen — das Bild erscheint ein zweites Mal in der Liste und bekommt dort sein eigenes Maß. Kein Umbenennen und erneutes Hochladen mehr.",
|
||||
"<b>The time is local time</b>, not UTC. An image made at 00:30 now carries the right date instead of the previous day's.": "<b>Uhrzeit in deutscher Zeit</b>, nicht in UTC. Ein Bild von 0:30 Uhr trägt jetzt das richtige Datum und nicht mehr das des Vortags.",
|
||||
"<b>Thumbnails</b> for the library (fast & frugal).": "<b>Vorschaubilder</b> für die Bibliothek (schnell & sparsam).",
|
||||
"<b>Try it with a sample photo.</b> An instance with only the print studio used to offer “Pick from the library” — without having a library. Now that place holds a sample photo, so a first visit ends in a printed sheet.": "<b>Mit einem Beispielfoto ausprobieren.</b> Eine Installation nur mit Druckstudio bot bisher „Aus der Bibliothek wählen“ an — ohne eine Bibliothek zu haben. An der Stelle steht jetzt ein Beispielfoto, damit der erste Besuch mit einem fertigen Bogen endet.",
|
||||
"<b>Where to?</b>: the finished sheet can also be put on the delivery target, on the backup mirror or on an extra target — like the images from the studio.": "<b>Wohin?</b>: der fertige Bogen kann zusätzlich an das Auslieferungsziel, auf den Spiegel oder an ein Zusatzziel gelegt werden — wie die Bilder aus dem Studio.",
|
||||
"<b>Without filling up the disk:</b> what is kept is not the PDFs — an A3 sheet is quickly 20 to 80 MB — but only the settings, a few kilobytes. Because the print area computes without AI, the same job yields exactly the same sheet again. That is why “Print again” only appears when all the images came from the library and are still there; otherwise the entry says what is in the way. The last 100 prints are kept.": "<b>Ohne die Platte vollzumachen:</b> Aufgehoben werden nicht die PDFs — ein A3-Bogen wären schnell 20 bis 80 MB — sondern nur die Einstellung, ein paar Kilobyte. Weil der Druckbereich ohne KI rechnet, ergibt derselbe Auftrag wieder exakt dasselbe Blatt. „Nochmal drucken\" gibt es deshalb nur, wenn alle Bilder aus der Bibliothek kamen und noch da sind; sonst steht am Eintrag, woran es liegt. Aufgehoben werden die letzten 100 Drucke.",
|
||||
"<b>“Deliver again”</b> uploads every already delivered image once more under its new name. The old files stay behind on the target — you clear those away there yourself, so that nothing disappears from the client's view by accident.": "<b>„Erneut ausliefern\"</b> lädt alle schon ausgelieferten Bilder unter dem neuen Namen erneut hoch. Die alten Dateien bleiben am Ziel liegen — die räumst du am Ziel selbst weg, damit nichts versehentlich beim Kunden verschwindet.",
|
||||
@@ -919,6 +925,7 @@
|
||||
"When fitting in, the border colour stays around the outside.": "Beim Einpassen bleibt außen die Randfarbe stehen.",
|
||||
"When it is reached the queue stops.": "Bei Erreichen hält die Warteschlange an.",
|
||||
"Where to?": "Wohin?",
|
||||
"While an image is in the trash it is gone from <b>share links</b> too — and comes back there when you restore it. An image deleted while it is still waiting in the queue is not generated at all, so it costs nothing.": "Solange ein Bild im Papierkorb liegt, ist es auch aus <b>geteilten Links</b> verschwunden — und kommt beim Wiederherstellen dorthin zurück. Ein Bild, das noch in der Warteschlange wartet, wird gar nicht erst erzeugt und kostet nichts.",
|
||||
"Who generated an image is hidden from non-admins.": "Wer ein Bild erzeugt hat, wird für Nicht-Admins ausgeblendet.",
|
||||
"Who sees which generations?": "Wer sieht welche Generierungen?",
|
||||
"Whoever sent you this link can see <b>when</b> it was opened and with what kind of device. Not <b>who</b> opened it: only an unrecognisable number is stored for your internet address, and that is deleted after 90 days.": "Wer dir diesen Link geschickt hat, sieht, <b>wann</b> und mit welcher Art Gerät er geöffnet wurde. Nicht, <b>wer</b> das war: Von deiner Internet-Adresse wird nur ein unkenntlicher Zahlenwert gespeichert, und der wird nach 90 Tagen gelöscht.",
|
||||
|
||||
@@ -13,6 +13,16 @@ const dict = dictFor(locale);
|
||||
</div>
|
||||
<p class="lead">{t('What is new in Klarbild. Newest changes at the top.')}</p>
|
||||
|
||||
<section>
|
||||
<div class="ver"><span class="tag">{t('10 Oct 2026')}</span><h2>{t('1.1 — a trash, a sample photo, and an image you do not have to build')}</h2></div>
|
||||
<ul>
|
||||
<li set:html={t('<b>Deleting is no longer final on the spot.</b> ✕ moves an image into the trash, and for a few seconds the bottom of the screen offers <b>Undo</b>. Noticed later? The library shows a <b>Trash</b> button; everything in there can be restored for 30 days, then Klarbild clears it away by itself.')} />
|
||||
<li set:html={t('While an image is in the trash it is gone from <b>share links</b> too — and comes back there when you restore it. An image deleted while it is still waiting in the queue is not generated at all, so it costs nothing.')} />
|
||||
<li set:html={t('<b>Try it with a sample photo.</b> An instance with only the print studio used to offer “Pick from the library” — without having a library. Now that place holds a sample photo, so a first visit ends in a printed sheet.')} />
|
||||
<li set:html={t('<b>Its own icon</b> in the browser tab and on the iPhone home screen.')} />
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="ver"><span class="tag">{t('1 Sep 2026')}</span><h2>{t('Print: images from the library, and what you printed last')}</h2></div>
|
||||
<ul>
|
||||
|
||||
@@ -46,6 +46,7 @@ const dict = dictFor(locale);
|
||||
<li set:html={t('<b>Before/after</b>: shows the comparison source ↔ result (not available for images made from text alone).')} />
|
||||
<li set:html={t('<b>Manage folders</b>: tap the folder chip, then ✎ to rename it (including its folder on the target) or ✕ to delete it (the images are kept).')} />
|
||||
<li set:html={t('<b>Several at once</b>: select images, then <b>download them as a ZIP</b> or move them <b>into a folder</b> together.')} />
|
||||
<li set:html={t('<b>Deleting</b> (✕) moves an image into the <b>trash</b>, with <b>Undo</b> right after. The trash (a button top right, once something is in it) keeps it for 30 days — and while it is there it appears in no share link and no delivery.')} />
|
||||
</ul>
|
||||
<p class="notice" set:html={t("<b>File names are unique.</b> Every result is called <code>YYYY-MM-DD_HHMMSS_subject_format_shortid</code>, for example <code>2026-08-20_143207_ada-badge_the-frame_a3f9.jpg</code>. Because date and time come first, every file listing — the delivery target, the backup mirror, the file manager — sorts chronologically by itself. The short id at the end comes from the image's id: <b>two images can no longer overwrite each other on delivery</b>, even if you generate the same subject in the same format twice on one day. (Before 21 Aug 2026 that was different — the first image was lost in such a case. Under <b>Admin → File names</b> the existing library can be renamed after the fact.)")} />
|
||||
</section>
|
||||
|
||||
Reference in new issue
Block a user