`cmd = ('sudo', '/usr/bin/systemctl', 'restart' 'wrolpi-kiwix.service')`
^ missing comma here
Python implicit string-literal concatenation joined the two strings into
'restartwrolpi-kiwix.service', so systemctl saw one argument it didn't
recognise and exited 2 (INVALIDARGUMENT). Net effect: kiwix never
restarted after a Zim download, the user still had to manually
`sudo systemctl restart wrolpi-kiwix` to see the new Zim.
Adds a unit test that pins the argv shape so an adjacent-literal typo
here surfaces in CI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Calamares desktop-icon path doesn't launch reliably in our wrolpi
auto-login session. We chased two stacks of fixes — gvfs + tumbler
for xfdesktop's launch path, lxpolkit for pkexec's GUI auth agent,
metadata::trusted + xfce-exe-checksum for XFCE's untrusted-launcher
check — and the icon still does nothing when double-clicked on a fresh
boot. The upstream Debian Installer reached via GRUB has none of these
problems and is the better-tested path for installing onto disk.
build.sh adds:
--debian-installer live live-installer copies the squashfs
onto the target instead of doing a
network re-install
--debian-installer-gui true graphical d-i frontend available
GRUB will now show Live / Install / Graphical Install / Advanced
entries; the user picks at boot.
Drops:
- calamares + calamares-settings-debian from the package list
- /etc/calamares/branding/wrolpi/ + settings.conf + modules
- The install-wrolpi.desktop launcher + the trust autostart we shipped
to try to make it click-able
Net ISO size will drop ~50-100 MB (Calamares + Qt5).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three classes of UX gaps surfaced when test-booting the previous build:
Audio silent.
--apt-recommends false stripped task-xfce-desktop's recommend chain,
which is what normally pulls the PulseAudio server. We only had the
panel plugin + client libs. Adding pulseaudio + pulseaudio-utils +
pavucontrol + alsa-utils explicitly.
No WiFi UI.
NetworkManager daemon was running and could see the radio, but
network-manager-gnome (nm-applet) wasn't pulled by recommends either.
Adding it explicitly.
Desktop "Install WROLPi" icon does nothing.
Two missing pieces stacked: (a) xfdesktop's launch path needs gvfs
(for metadata::trusted) and tumbler (thumbnail dispatch raises a
GLib critical without it). (b) Even when xfdesktop dispatches, pkexec
can't authenticate without a registered GUI polkit agent — task-xfce-
desktop's recommend chain would have provided one, but it didn't, so
pkexec falls back to a TTY agent that fails in a Thunar-spawned
process (no /dev/tty). Adding gvfs, lxpolkit, tumbler. Rewrites
the desktop launcher as Type=Link (skips the executable-trust check
entirely) and ships a session autostart that gio-sets
metadata::trusted + metadata::xfce-exe-checksum so xfdesktop accepts
user-owned .desktop files.
Also fixes a Calamares FATAL on boot: branding.desc requires a slideshow
key even when there's no real slideshow content; ships a minimal show.qml
that just renders the WROLPi logo + a welcome line.
Also adds xfce4-notifyd and xfce4-power-manager to desktop.list.chroot —
not directly tied to the three issues above, but obvious omissions from
the same recommend-stripped XFCE install.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The installer now identifies as WROLPi rather than Debian: product name,
sidebar logo (the wifi-pie favicon from /icon.png), bootloader entry name,
and URLs all point at WROLPi. Sidebar colors are WROLPi purple.
Three new files under etc/calamares/:
- branding/wrolpi/branding.desc — product strings + image + color scheme
- branding/wrolpi/wrolpi-logo.png — copy of the project favicon
- settings.conf — override picks branding=wrolpi and adds
shellprocess to the exec sequence
- modules/shellprocess.conf — rm /home/wrolpi/Desktop/install-wrolpi.desktop
on the target so the installed system
doesn't ship with a broken icon
Slideshow QML is intentionally omitted: producing our own QML/asset pack
is deferred polish. Calamares falls back to a static welcome screen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pulls in `calamares` + `calamares-settings-debian` (the upstream Debian
theme/config package). Calamares runs from the desktop via the
"Install WROLPi to disk" icon: pkexec elevates, the user types the
wrolpi account password, the standard install wizard opens.
The launcher is the upstream install-debian.desktop with Name and
Comment rebranded to "Install WROLPi to disk" / "Install WROLPi onto
this computer's internal drive". We keep the upstream install-debian
wrapper script (it handles xhost + pkexec calamares correctly) and just
override the visible labels.
After the chroot creates the wrolpi user the hook copies the launcher
to /home/wrolpi/Desktop/install-wrolpi.desktop and marks it executable
so XFCE launches it without an "untrusted application" warning.
Behaviour:
Live USB boot XFCE autologs to wrolpi. Install icon visible.
Click → polkit prompts → Calamares wizard.
After install + reboot wrolpi-bootstrap.service sets up Postgres on
/media/wrolpi/config/postgresql/, runs DB init,
starts wrolpi-api / app / kiwix / controller.
Two known phase-3 polish items deferred:
- WROLPi branding for Calamares (logo, colors, slideshow); using stock
Debian branding for now.
- An orphan install-wrolpi.desktop icon survives on the installed
system (Calamares removes calamares-settings-debian, taking
install-debian with it, but not the copy in the user's home). A
Calamares shellprocess will rm it in phase 3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two parallel live-build configs (debian-live-config/ for the installer
ISO and wrolpi-portable/ for the Live USB) carry the same chroot hook
with subtly different fixes — the portable side had accumulated the
better implementation (npm build, SHA-pinned blobs, dynamic Postgres
version, partition-by-start-sector, controller.yaml sentinel, autologin,
branding). This commit folds those improvements into debian-live-config/
and retires wrolpi-portable/.
The Postgres data + config directories now bind-mount to
/media/wrolpi/config/postgresql/{data,config} regardless of deployment
shape (Live USB, Calamares-installed system with external drive,
Calamares-installed system with no external drive). Result: pulling
a WROLPi external drive from one host and plugging into another
brings the entire indexed library along — no re-indexing. Previously
the DB lived on /mnt/persistence/postgresql-{data,config} on Portable
and on /var/lib/postgresql (root) everywhere else; moving a drive
required rebuilding the DB on the new host.
Single wrolpi-bootstrap.{sh,service} replaces wrolpi-portable-bootstrap.
The script detects deployment by probing for a live-boot medium mount:
present → Live (create persistence partition if missing); absent →
Installed (skip partition step; Postgres bind-mount and cluster init
still run). --debian-installer is dropped from lb config; Calamares
will land in a follow-up commit as the graphical installer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The fallback after `partx -a` (which is wrapped in `|| true` and can
silently no-op) was `lsblk -lnpo NAME "$BOOT_DRIVE" | tail -1`. If the
new partition node has not appeared in the kernel's partition table yet,
that fallback returns the previous last partition — typically sdb2 (the
ESP) or sdb1 (the live ISO data partition). Both are valid block
devices, so the `[ -b ... ]` guard passes; the next line is
`mkfs.ext4 -L persistence -F`, which wipes whichever wrong partition the
fallback returned. Narrow conditions, but catastrophic when triggered.
Switching to matching on the start sector we just assigned via sfdisk:
no existing partition can share it, so the result is unambiguous. Retry
loop allows up to 5s for udev to register the new node.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A WROLPi drive can move between hosts and across deployment styles
(Portable, RPi, Debian). /etc/fstab is host-specific and on the Portable
image it's part of a read-only squashfs, so persisting secondary mounts
there was both wrong-host and lost-on-reboot. This change makes
/media/wrolpi/config/fstab.yaml the single source of truth for WROLPi-
managed mounts and introduces a new wrolpi-mounts.service that reconciles
live state to match.
Architecture:
- The Controller writes intent (fstab.yaml) but never calls mount(8) or
umount(8) itself. Every mutation triggers `systemctl restart
wrolpi-mounts.service`; the Controller then verifies the outcome via
the same MountExecutor abstraction the service uses.
- wrolpi-mounts.service runs a thin entry point that constructs a real
Reconciler with a SubprocessMountExecutor and calls apply(). The
reconciler mounts what's in fstab.yaml but missing from the live state,
and unmounts paths it previously claimed that are no longer in
fstab.yaml. A managed-set file in /run/wrolpi-mounts/managed prevents
the reconciler from ever touching user-mounted paths it didn't own.
- One-time migration (marker-gated) lifts both /etc/fstab WROLPi entries
and any controller.yaml drives.mounts entries into fstab.yaml.
/etc/fstab is backed up to /etc/fstab.wrolpi-migration.backup;
migrated lines are commented in place with a "migrated to fstab.yaml:"
prefix so a host admin can hand-recover.
Test approach: dependency injection in place of patching. The
MountExecutor protocol has SubprocessMountExecutor (production) and
FakeMountExecutor (in-memory, records calls). The Reconciler accepts an
executor in its constructor, so the 20 reconciler tests + 13 migration
tests + 18 fstab_yaml tests use real files in tmp_path with zero
mock.patch calls. test_disks_api.py drops to mocks only for the
host-touching read helpers; the mount/unmount paths now run the real
Reconciler against the fake executor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
persistence.conf was writing `union,source=...` entries. On the first boot
that did not matter because the bootstrap performed a real `mount --bind`
itself; from the second boot onward, however, live-boot's initramfs reads
persistence.conf and applies the entries as OverlayFS union mounts.
PostgreSQL does not support OverlayFS — switching to `bind,source=...`
makes the in-band mount and the initramfs mount match and avoids potential
data corruption.
The bootstrap script hardcoded Postgres version 15 (cluster check, create,
service-start), inconsistent with the chroot hook which already derives it
dynamically. Switched to discovery via `/usr/lib/postgresql/<ver>` so a
postgresql-16 bump no longer requires touching this script.
The tippecanoe source tarball was the only externally-fetched artifact in
the chroot hook without a SHA256 pin. Added one (computed from upstream
v2.79.0).
`RequiresMountsFor` covered only `/usr/lib/live/mount/medium`; live-boot on
newer releases mounts at `/run/live/medium`. Listed all three paths the
bootstrap script itself searches so systemd orders the unit after whichever
mount actually exists.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The chroot hook only ran `npm install`, leaving /opt/wrolpi/app/build/
missing. wrolpi-app.service runs `serve build -l 3000 ...`, so it started
fine but returned 404 for every path. RPi-OS / Debian installs handle this
via repair.sh at first boot; we bake the build into the squashfs so the
live image works offline immediately.
The Controller's `is_primary_drive_mounted()` checks for
/media/wrolpi/config/controller.yaml — without it the Controller renders
its onboarding wizard and offers the persistence partition (sdb3, mounted
at /mnt/persistence) as an unconfigured candidate, which would corrupt the
running system if selected. Touch an empty controller.yaml after creating
/media/wrolpi/config so the Controller treats the drive as configured.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LAST_END parser was assigning to a scalar (not MAX), so on iso-hybrid layouts
where the small ESP is listed after the much larger boot partition, sfdisk
got a start sector inside an existing partition. Also sfdisk -d pads values
with whitespace (`start= 64,`), which broke a naive awk tokenizer.
Switched to a sed parser that strips the padding, and computes the true
MAX(start+size).
`systemctl enable --now wrolpi-api ...` deadlocked: the runtime units have
implicit `After=wrolpi-portable-bootstrap.service` via this unit's `Before=`,
so --now waited forever for them to start while they waited for bootstrap to
finish. Split enable from start; use --no-block so systemd queues the
starts after we exit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bootstrap script can take 30-60s on first boot (partition creation,
Postgres cluster init, Alembic migrations). Plymouth is up that whole time
since lightdm waits for bootstrap to finish. Push a status line via
'plymouth display-message --text=...' at each major milestone so the user
sees what's happening instead of staring at a frozen splash.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
wrolpi-portable-bootstrap.service runs at every boot (idempotent throughout)
and owns the persistence partition lifecycle:
- First boot: creates an ext4 partition labeled 'persistence' in free space
at the end of the boot drive (sfdisk --append --no-reread --force because
the live medium is mounted from the same disk), bind-mounts source dirs
over /var/lib/postgresql, /etc/postgresql, and /media/wrolpi, creates a
fresh Postgres cluster, and runs initialize_api_db.sh.
- Subsequent boots: live-boot's persistence machinery applies the bind
mounts in initramfs (kernel cmdline now includes 'persistence
persistence-label=persistence persistence-media=removable'); the script
mostly no-ops apart from regenerating the leaf TLS cert.
After bootstrap completes the script enables and starts wrolpi-api,
wrolpi-app, and wrolpi-kiwix — these stay disabled in the chroot hook so
they cannot start until the DB and configs are ready (avoiding the
"half-finished state with empty configs" failure mode).
Also adds parted, gdisk, dosfstools, and exfatprogs to the package list so
the live system can fully diagnose / repair / partition without internet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The chroot hook applies chattr +i to /opt/wrolpi-blobs/*, which then
prevents rm -rf from wiping BUILD_DIR on subsequent runs. lb's
chroot_includes_after_packages rsync then fails with "Operation not
permitted" trying to overwrite the leftovers. Strip +i before rm.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops straight into the XFCE session at boot. The wrolpi password is still
required for sudo and screen unlock.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- desktop.list.chroot: add user-setup (was implicitly provided by the
installer ISO's --debian-installer dep tree; the live-only build lost it
and live-config's user-creation step silently exits without it, leaving
the squashfs with no login-capable account).
- desktop.list.chroot: add plymouth-themes for non-Debian boot splash.
- 9999-wrolpi.hook.chroot: pre-create the wrolpi user (UID 1000) with
password 'wrolpi' and desktop groups; live-config sees the existing user
and skips its own user creation, preserving our chosen password.
- 9999-wrolpi.hook.chroot: set hostname to 'wrolpi', /etc/issue banner,
Plymouth theme to 'text' (no Debian logo), and a LightDM greeter banner
saying 'WROLPi Portable'.
- build.sh: add live-config.username=wrolpi, live-config.hostname=wrolpi,
live-config.user-fullname=WROLPi to bootappend-live so live-config's
other scripts (sudo, lightdm autologin) target the wrolpi user.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- build.sh: strip whitespace from VERSION before interpolating into lb config
metadata; wrap lb build in set -o pipefail so a build failure is not masked
by tee's success exit code.
- Hook: pin SHA256 for Deno and pmtiles downloads so tampered release
artifacts cannot be baked into the squashfs unnoticed.
- Hook: derive Postgres cluster version dynamically from pg_lsclusters
instead of hardcoding 15, so this stays correct when the package list is
bumped past postgresql-15.
- Hook: fix pmtiles URL (Linux_amd64 -> Linux_x86_64); the v1.30.1 release
ships the asset as x86_64 and the old URL returned 404.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New debian-live-usb/ directory produces a live ISO that boots into a full
WROLPi desktop on any x86 PC without installing to the host disk. Distinct
from debian-live-config/ (installer ISO). Phase 1: live boot only; Phase 2
will add persistence partition for the Postgres DB and media directory.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Greptile flagged that if the session is in an aborted tx (e.g. the
external commit was followed by a failed statement before our error
fired), Postgres rejects DDL on the session and the committed work
table orphans. Fall back to a fresh connection in that case. Adds
a regression test that deterministically reproduces the orphan.
compare_file_groups created a TEMP TABLE ... ON COMMIT DROP on the
session. In production each get_db_session() returns a fresh connection,
but tests share one session across every caller, so any concurrent
get_db_session(commit=True) commits this session too and drops the work
table before the batch insert runs. Use a uniquely-named regular table
with explicit DROP IF EXISTS in finally instead.