Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
3706720
Version 2.0.0 “Mergenthaler”
XNinety9 Oct 25, 2026
06b3bd0
Settings from the app, with /etc/printr.env taking precedence
XNinety9 Oct 25, 2026
02330b9
First-run wizard API: language at setup, printer detection, test print
XNinety9 Oct 25, 2026
be305ba
First-run wizard: language choice, printer with test ticket, city, Cl…
XNinety9 Oct 25, 2026
5ee0b27
Settings screen for the administrator
XNinety9 Oct 25, 2026
6f958f6
First network connection: Wi-Fi hotspot with setup tickets
XNinety9 Oct 25, 2026
54890f8
Default to /dev/tm88 when present, so new installs pick the printer i…
XNinety9 Oct 25, 2026
984a168
Wi-Fi setup: wait longer and keep retrying when a known network is mi…
XNinety9 Oct 25, 2026
49627da
Ready-to-flash Raspberry Pi image built with pi-gen
XNinety9 Oct 26, 2026
7146041
Wi-Fi setup: explicit WPA2 hotspot for Raspberry Pi Wi-Fi chips, and …
XNinety9 Oct 26, 2026
214ceba
Wi-Fi setup: hotspot on channel 6
XNinety9 Oct 26, 2026
299e8e3
Wi-Fi setup: keep the hotspot password across boots
XNinety9 Oct 26, 2026
50eccf4
Wi-Fi setup: hotspot on hostapd instead of NetworkManager
XNinety9 Oct 26, 2026
6f56b94
Image: standalone workflow, and no captive portal DHCP option
XNinety9 Oct 26, 2026
bcba423
Accent check: leave out œ, which the printer's code page prints as oe
XNinety9 Oct 26, 2026
3d1d86c
Docs and site: ready-to-flash image, first network connection, wizard…
XNinety9 Oct 26, 2026
32f5309
Illustrations: first start screenshots and tickets from a script
XNinety9 Oct 26, 2026
5cd0bcd
Weather, air quality and date block options (#29, #30, #32)
XNinety9 Oct 26, 2026
a7a6cd7
Family birthdays shared by the whole family (#27)
XNinety9 Oct 26, 2026
ee79b0e
Merge master into version-2.0 (PR #26: configurable Claude endpoint a…
XNinety9 Oct 26, 2026
51e9113
Settings: Claude call timeout
XNinety9 Oct 26, 2026
a7b3bae
Backup and restore from the app
XNinety9 Oct 26, 2026
6c0b052
Signed automatic updates
XNinety9 Oct 26, 2026
819211f
Release keys: a main key and an offline spare
XNinety9 Oct 26, 2026
e454a01
Release public keys: main and spare
XNinety9 Oct 26, 2026
4671ab3
One-command install with signature check, and Printr in Raspberry Pi …
XNinety9 Oct 26, 2026
140c4a9
Weather units: °F and mph as a setting
XNinety9 Oct 26, 2026
709d567
deploy.sh: send the update guard with the install files
XNinety9 Oct 26, 2026
80d573e
Default city for every block with a place
XNinety9 Oct 26, 2026
c0ba526
App: define the default city helpers before the block list
XNinety9 Oct 26, 2026
a073139
CI: check that the app actually starts in a browser
XNinety9 Oct 26, 2026
c63b5c6
CHANGELOG: 2.0 grouped by theme, with upgrade notes
XNinety9 Oct 26, 2026
b68fe93
Illustrations for 2.0
XNinety9 Oct 26, 2026
4a42150
Prepare 2.0.0 “Mergenthaler”: CHANGELOG
XNinety9 Oct 26, 2026
23c0779
CI fixes: self-repository syntax for the image workflow, quoted strin…
XNinety9 Oct 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ jobs:
- run: cargo fmt --check
- run: cargo clippy --all-targets --locked -- -D warnings
- run: cargo test --locked
# node --check ne voit que la syntaxe : l'appli doit aussi démarrer pour de bon (une constante
# utilisée avant sa déclaration l'arrête au chargement, sans erreur de syntaxe).
- name: L'appli démarre dans un navigateur
run: |
cargo build --locked
PRINTR_DATA_DIR=$(mktemp -d) target/debug/printr --dump /dev/null serve --listen 127.0.0.1:8099 &
for _ in $(seq 50); do curl -fs -o /dev/null http://127.0.0.1:8099/ && break; sleep 0.2; done
google-chrome --headless=new --disable-gpu --virtual-time-budget=5000 --dump-dom http://127.0.0.1:8099/ > dom.html
grep -q 'class="login-card"' dom.html || { echo "l'appli ne s'affiche pas :"; head -c 3000 dom.html; exit 1; }

web:
name: Appli web et scripts
Expand Down
92 changes: 92 additions & 0 deletions .github/workflows/image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
name: Image

# Image prête à flasher pour Raspberry Pi (voir image/README.md), construite avec pi-gen sur un
# runner ARM (sans émulation) :
# - appelée par la release, qui joint l'image à la release de l'étiquette ;
# - à la main pour une branche ou une étiquette, l'image restant un artefact du workflow :
# gh workflow run image.yml -f ref=version-2.0
on:
workflow_dispatch:
inputs:
ref:
description: "Branche ou étiquette à construire"
required: true
default: master
workflow_call:
inputs:
tag:
type: string
required: true

permissions:
contents: read

concurrency:
group: image-${{ inputs.tag || inputs.ref }}

env:
REF: ${{ inputs.tag || inputs.ref }}

jobs:
image:
name: Image Raspberry Pi
runs-on: ubuntu-24.04-arm
permissions:
contents: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ env.REF }}
persist-credentials: false
- uses: dtolnay/rust-toolchain@686976e191b89faba57d3206551f0f330d8cb249 # stable
# Runner ARM : le binaire aarch64 se compile directement (glibc plus ancienne que celle de
# Raspberry Pi OS trixie).
- run: cargo build --release --locked
# pi-gen a besoin d'une dizaine de Go.
- name: Place disque
run: sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc /usr/local/.ghcup || true
- name: Image
run: bash image/build.sh --binary target/release/printr --out out
# Raspberry Pi Imager : « Printr » dans sa liste, avec un dépôt personnalisé pointant vers
# https://github.com/XNinety9/Printr/releases/latest/download/printr-imager.json
- name: Description pour Raspberry Pi Imager
if: inputs.tag != ''
run: |
img=$(ls out/*.img.xz)
python3 - "$img" "$REF" "$GITHUB_REPOSITORY" > out/printr-imager.json <<'PY'
import hashlib, json, lzma, os, sys, datetime
img, tag, repo = sys.argv[1:4]
extracted, size = hashlib.sha256(), 0
with lzma.open(img) as f:
while chunk := f.read(1 << 20):
extracted.update(chunk)
size += len(chunk)
download = hashlib.sha256(open(img, "rb").read()).hexdigest()
print(json.dumps({"os_list": [{
"name": "Printr",
"description": "Raspberry Pi OS Lite with Printr: little receipts for the family, set up from your phone.",
"icon": "https://xninety9.github.io/Printr/mark.png",
"website": "https://xninety9.github.io/Printr/",
"url": f"https://github.com/{repo}/releases/download/{tag}/{os.path.basename(img)}",
"extract_size": size,
"extract_sha256": extracted.hexdigest(),
"image_download_size": os.path.getsize(img),
"image_download_sha256": download,
"release_date": datetime.date.today().isoformat(),
"init_format": "cloudinit-rpi",
"devices": ["pi5-64bit", "pi4-64bit", "pi3-64bit"],
}]}, indent=2))
PY
cat out/printr-imager.json
- name: Publication
if: inputs.tag != ''
env:
GH_TOKEN: ${{ github.token }}
run: gh release upload "$REF" out/*.img.xz out/*.img.xz.sha256 out/printr-imager.json --clobber --repo "$GITHUB_REPOSITORY"
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: inputs.tag == ''
with:
name: printr-image
path: out/*.img.xz*
retention-days: 7
compression-level: 0
30 changes: 26 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ name: Release
# - à chaque étiquette vX.Y.Z poussée ;
# - à la main pour une ancienne étiquette : gh workflow run release.yml -f tag=v1.4.0
# Chaque archive contient le binaire et les fichiers d'installation : décompresser, puis
# `sudo bash install.sh`. SHA256SUMS permet de vérifier ce qu'on a téléchargé.
# `sudo bash install.sh`. SHA256SUMS permet de vérifier ce qu'on a téléchargé. S'y ajoute l'image
# prête à flasher pour Raspberry Pi (voir image/README.md).
on:
push:
tags: ["v*.*.*"]
Expand Down Expand Up @@ -70,10 +71,21 @@ jobs:
for f in README.md README.fr.md docs/guide.md docs/guide.fr.md CHANGELOG.md; do [ -f "$f" ] && cp "$f" "dist/$name/"; done
cp _main/LICENSE _main/NOTICE "dist/$name/"
tar -C dist -czf "dist/$name.tar.gz" "$name"
# Binaire seul, pour la mise à jour automatique : printr-<architecture de Rust>. Le
# binaire ARM 32 bits vise ARMv6, et tourne donc aussi en ARMv7.
case "${{ matrix.target }}" in
aarch64-*) cp "target/${{ matrix.target }}/release/printr" dist/printr-aarch64 ;;
arm-*) cp "target/${{ matrix.target }}/release/printr" dist/printr-arm ;;
x86_64-*) cp "target/${{ matrix.target }}/release/printr" dist/printr-x86_64 ;;
esac
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: ${{ matrix.target }}
path: dist/*.tar.gz
path: |
dist/*.tar.gz
dist/printr-aarch64
dist/printr-arm
dist/printr-x86_64

release:
name: Publication
Expand All @@ -92,7 +104,8 @@ jobs:
path: dist
merge-multiple: true
- name: Sommes de contrôle
run: cd dist && sha256sum *.tar.gz > SHA256SUMS && cat SHA256SUMS
# Signée ensuite sur le PC du mainteneur (scripts/signer-release.sh) : SHA256SUMS.minisig.
run: cd dist && sha256sum *.tar.gz printr-aarch64 printr-arm printr-x86_64 > SHA256SUMS && cat SHA256SUMS
- name: Release
env:
GH_TOKEN: ${{ github.token }}
Expand All @@ -110,4 +123,13 @@ jobs:
name=$(grep -m1 "^## \[$version\]" CHANGELOG.md | sed -n 's/.*“\(.*\)”.*/\1/p')
gh release create "$TAG" --verify-tag --title "Printr $version${name:+ “$name”}" --notes-file notes.md
fi
gh release upload "$TAG" dist/*.tar.gz dist/SHA256SUMS --clobber
gh release upload "$TAG" dist/*.tar.gz dist/printr-aarch64 dist/printr-arm dist/printr-x86_64 dist/SHA256SUMS --clobber

image:
name: Image Raspberry Pi
needs: release
permissions:
contents: write
uses: $/.github/workflows/image.yml
with:
tag: ${{ inputs.tag || github.ref_name }}
79 changes: 74 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,79 @@ Koenig, Tolbert Lanston…

## [Unreleased]

## [2.0.0] “Mergenthaler” - 2026-10-26

Printr 2.0 looks after itself: flash a card, plug in the printer, set up the Wi-Fi from your phone, and
everything else happens in the app, up to signed updates at night. No terminal needed any more.

### Upgrading from 1.x
- Run the full installation once (`scripts/deploy.sh HOST --install`, the archive's `install.sh`, or the
new one-command install below): the service now runs the update guard before each start and may write
to `/opt/printr`. A plain binary swap keeps the old service file, and automatic updates would not work.
- Data, accounts and `/etc/printr.env` are kept (and backed up first). What `/etc/printr.env` sets stays in
charge and shows as read-only in the new settings screen; remove a line from it to manage that setting
from the app instead.

### Added
- **Claude blocks on any compatible endpoint**: `ANTHROPIC_BASE_URL` points the API path at
another Messages API endpoint (a local inference server, for instance), and
`PRINTR_CLAUDE_TIMEOUT_SECS` sets the call timeout (120 s by default) for slower local models
(#25, by salazr).
**Install and set up without a terminal**
- **Ready-to-flash image for Raspberry Pi** (Pi 3, 4, 5 and Zero 2 W, 64-bit): Raspberry Pi OS Lite with
Printr installed, the printer's udev rule, `http://printr.local` on port 80 and Barnum. Write it with
Raspberry Pi Imager, plug in the printer, power on. Built with pi-gen (`image/build.sh`), attached to
each release with `printr-imager.json`, which lists Printr in Imager from a custom content repository.
- **First network connection without a screen**: on the image, when no network shows up within a minute
of boot, Printr opens a `Printr-XXXX` Wi-Fi hotspot and prints a ticket with QR codes to join it and open
the setup page (phones open it by themselves, like a hotel Wi-Fi). Pick the home Wi-Fi, type its
password: Printr joins it and prints the app's address. On a wrong password it reopens the hotspot and
prints why; a known Wi-Fi missing after a power cut is retried every five minutes. The hotspot runs on
hostapd (plain WPA2: NetworkManager's own hotspot is refused by phones on Raspberry Pi Wi-Fi chips);
enabled by `PRINTR_WIFI_SETUP=1` (the image sets it).
- **Install in one command** on a machine already running:
`curl -fsSL https://xninety9.github.io/Printr/install.sh | bash` downloads the archive for the machine,
checks the release signature (with `openssl`) and the archive's checksum, and runs `install.sh`.
- **First-run wizard**: after the first account (where the language can now be picked), the admin is
guided through finding the printer (USB, or network printers answering on port 9100), printing a test
ticket and choosing the code page from how the accents came out, then the city, an optional Claude key
and automatic updates.
- **Settings in the app** (My account → Settings, admin only): printer, language, city, weather units,
Claude key, model and timeout, MQTT, API token, limits, update check and automatic updates. They are
saved with the data and applied by a quick automatic restart (under systemd or Docker). Whatever
`/etc/printr.env` sets still wins and is shown read-only; secrets are never sent back to the app.

**Look after itself**
- **Signed automatic updates** (opt-in, offered by the wizard): at night, Printr installs a new release only
if its checksum list is signed with the maintainer's key (minisign, with an offline spare key) and the
program matches, runs and reports the right version; if it does not answer after restarting, the
service's guard puts the previous one back and never retries it. Releases now also carry the bare
binaries (`printr-aarch64`, `printr-arm`, `printr-x86_64`); `scripts/signer-release.sh` signs a release,
`scripts/labo-maj.sh` tests the whole path locally.
- **Backup and restore** from My account (admin): accounts, tickets, schedules, history, shopping list,
family birthdays, settings and photos in one JSON file. Restoring keeps the current state in `backups/`
first; a fresh installation can restore from the first account's screen (`GET /api/backup`,
`POST /api/restore`).

**Blocks**
- **Date** (`date`): another day than today, as a set `date` or an `offset` in days, to print tickets ahead
of a trip, one per day (#32, suggested by Fabwiss).
- **Family birthdays**: a shared list that everyone fills in from the Birthdays block, added to each
block's own list (`family`, on by default; `GET`/`PUT /api/birthdays`) (#27, suggested by Fabwiss).
- **Claude blocks on any compatible endpoint**: `ANTHROPIC_BASE_URL` points the API path at another
Messages API endpoint (a local inference server, for instance), and `PRINTR_CLAUDE_TIMEOUT_SECS` sets the
call timeout (120 s by default) for slower local models (#26, by salazr).

### Changed
- **Weather** (`weather`): `wind` and `sun` hide the wind and UV line, and sunrise and sunset, to save paper
(#29, suggested by Fabwiss); °F and mph with the "weather units" setting (`PRINTR_UNITS=imperial`),
forecasts being still read in °C so the what-to-wear advice keeps working (discussion #34, suggested by
mi3816).
- **Air quality** (`air_quality`): `pollutants` and `pollen` hide their lines (#30, suggested by Fabwiss).
- **The default city is used by every block with a place**: weather, air quality, sun and tonight's sky
take the city from the settings when their own `location` is empty, which is now how the app adds them
(it no longer fills in "Lyon"); the field shows the default city. With neither, the block says where to
set one.
- With no `PRINTR_DEVICE`, Printr uses `/dev/tm88` (the link made by the udev rule) when it exists, then
`/dev/usb/lp0`. `printr.env.example` no longer sets `PRINTR_DEVICE`, so new installs can pick the printer
in the app; existing `/etc/printr.env` files are left as they are.
- The accent check of the test ticket leaves out `œ`, which the printer's code page prints as `oe`.

## [1.11.1] “Lanston” - 2026-10-25

Expand Down Expand Up @@ -319,7 +387,8 @@ First release.
- One-command installation on a Raspberry Pi, no sudo needed afterwards.
- Windows development with a dev container and a printing relay.

[Unreleased]: https://github.com/XNinety9/Printr/compare/v1.11.1...HEAD
[Unreleased]: https://github.com/XNinety9/Printr/compare/v2.0.0...HEAD
[2.0.0]: https://github.com/XNinety9/Printr/compare/v1.11.1...v2.0.0
[1.11.1]: https://github.com/XNinety9/Printr/compare/v1.11.0...v1.11.1
[1.11.0]: https://github.com/XNinety9/Printr/compare/v1.10.0...v1.11.0
[1.10.0]: https://github.com/XNinety9/Printr/compare/v1.9.0...v1.10.0
Expand Down
30 changes: 29 additions & 1 deletion CONTRIBUTING.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,10 @@ d'exemple (voir `barnum.rs`).
langues (exemples des blocs, captures de l'appli, tickets vitrines, image de partage), puis
construit le site ; `--sans-claude` évite tout appel à Claude (les blocs Claude gardent leur image).
- Les captures du site et de la doc se régénèrent avec `docs/demo/photo-session.sh`, toujours
sur des données fictives. N'y mets jamais de vraies données personnelles.
sur des données fictives. N'y mets jamais de vraies données personnelles. Celles de la mise en
route (point d'accès Wi-Fi, assistant, réglages) viennent de `docs/demo/setup-session.sh`, qui fait
tourner Printr dans un espace réseau isolé (`unshare`, sans sudo) avec un faux réseau, un faux
`nmcli` et l'émulateur.
- Les déclinaisons du logo se régénèrent avec
`uv run --with pillow python docs/brand/make-assets.py`.
- Le site se construit avec `docs/build.py` : la page d'accueil (`docs/index.html`) et la
Expand Down Expand Up @@ -319,6 +322,31 @@ compile alors les binaires et publie la release GitHub, avec la section du CHANG
voient dans la journée (« Mon compte » de l'administrateur) : ne pousse une étiquette que pour
une version prête à être installée.

Une fois le workflow terminé, le mainteneur signe la release sur son ordinateur :
`scripts/signer-release.sh v1.1.0`. Le script télécharge `SHA256SUMS` et tous les fichiers listés,
vérifie les empreintes, signe la liste avec la clé des releases (`~/.config/printr-release/printr.key`,
qui ne quitte jamais cet ordinateur) et ajoute `SHA256SUMS.minisig` à la release. Les installations
en mise à jour automatique n'installent qu'une release signée. `scripts/labo-maj.sh` essaie tout
le chemin de mise à jour en local (faux GitHub, faux systemd) : une bonne version, une qui plante
(retour en arrière), une mauvaise clé, un binaire modifié.

Les clés des releases se créent une fois, chacune avec un mot de passe :

```sh
cargo run -q --example signer -- keygen ~/.config/printr-release # clé principale, sur cet ordinateur
cargo run -q --example signer -- keygen /media/usb/printr-secours # clé de secours, directement sur une clé USB
```

Les deux clés publiques vont dans `RELEASE_KEYS` (`src/update.rs`), la principale d'abord : Printr
accepte une signature de l'une ou de l'autre. Sauvegarde la clé privée principale (`printr.key`,
quelques lignes de texte déjà chiffrées par son mot de passe) dans un gestionnaire de mots de
passe, et garde son mot de passe ailleurs. La clé de secours reste hors ligne, jamais sur
l'ordinateur : une clé USB dans un tiroir, ou imprimée sur du papier ordinaire (pas sur un ticket :
le papier thermique s'efface). Si la clé principale est perdue, signe avec celle de secours
(`scripts/signer-release.sh vX.Y.Z --key /media/usb/printr-secours/printr.key`) une version dont
`RELEASE_KEYS` contient une nouvelle clé principale et la même clé de secours : toutes les
installations la prennent d'elles-mêmes.

## Proposer une modification

1. Crée une branche à partir de `master`.
Expand Down
27 changes: 26 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,9 @@ sample response (see `barnum.rs`).
(block examples, app screenshots, showcase tickets, link preview image), then builds the site;
`--sans-claude` avoids any Claude call (Claude blocks keep their current image).
- The screenshots for the site and the docs are regenerated with `docs/demo/photo-session.sh`,
always with fictional data. Never put real personal data in them.
always with fictional data. Never put real personal data in them. Those of the first start (Wi-Fi
hotspot, wizard, settings) come from `docs/demo/setup-session.sh`, which runs Printr in an
isolated network namespace (`unshare`, no sudo) with a fake network, fake `nmcli` and the emulator.
- The logo variants are regenerated with `uv run --with pillow python docs/brand/make-assets.py`.
- The site is built with `docs/build.py`: the home page (`docs/index.html`) and the documentation,
rendered from `docs/guide.md` and `CONTRIBUTING.md`. A change to these files therefore also updates
Expand All @@ -311,6 +313,29 @@ builds the binaries and publishes the GitHub release, with the CHANGELOG section
within the day ("My account" of the administrator): only push a tag for a version that is ready
to be installed.

Once the workflow is done, the maintainer signs the release on their own computer:
`scripts/signer-release.sh v1.1.0`. It downloads `SHA256SUMS` and every file it lists, checks the
checksums, signs the list with the release key (`~/.config/printr-release/printr.key`, which never
leaves that computer) and adds `SHA256SUMS.minisig` to the release. Installations with automatic
updates only install a signed release. `scripts/labo-maj.sh` tests the whole update path locally
(fake GitHub, fake systemd): a good version, a crashing one (rolled back), a wrong key, a tampered
binary.

The release keys are created once, with a password each:

```sh
cargo run -q --example signer -- keygen ~/.config/printr-release # main key, on this computer
cargo run -q --example signer -- keygen /media/usb/printr-spare # spare key, straight to a USB stick
```

Both public keys go into `RELEASE_KEYS` (`src/update.rs`), the main one first: Printr accepts a
signature from either. Back up the main private key (`printr.key` is a few lines of text, already
encrypted by its password) in a password manager, and keep its password somewhere else. The spare
key stays offline, never on the computer: a USB stick in a drawer, or printed on ordinary paper
(not on receipts: thermal paper fades). If the main key is lost, sign with the spare one
(`scripts/signer-release.sh vX.Y.Z --key /media/usb/printr-spare/printr.key`) a version whose
`RELEASE_KEYS` holds a new main key and the same spare: every installation takes it by itself.

## Proposing a change

1. Create a branch from `master`.
Expand Down
Loading
Loading