- Treat virtual disks (vd*/xvd*/hd*) as direct, not ephemeral. A blank
VM disk imaged with the ISO can safely hold a persistence partition;
previously these fell through the wildcard to loop mode and never
accumulated state.
- Move the ephemeral note_action inside the mountpoint guard so a manual
wrolpi-bootstrap.service restart mid-session doesn't re-fire the "NOT
writing to disk" first-boot dialog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
wrolpi-bootstrap.sh now classifies the live medium by device-node
pattern. Real partitions (/dev/sd*, /dev/nvme*, /dev/mmcblk*) continue
to get a persistence partition appended. Loop/device-mapper sources
(/dev/loop*, /dev/dm-*, /dev/mapper/*) — the marker for Ventoy/YUMI/
Easy2Boot/etc — mount a tmpfs at /media/wrolpi instead, so we never
sfdisk-append a partition onto the user's multiboot stick.
First-boot zenity dialog branches on mode and tells ephemeral users
their data won't persist; README documents the behaviour and the
deferred persistence.dat work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address Greptile review on PR #447:
- Fall back to the minimum preset when an unrecognized/stale
water_calculator_preset is read from localStorage, so a bad persisted
value can no longer crash the component on mount.
- Remove the unused Media import.
- Simplify the metric branch of waterWeight (volume * 1 -> volume).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses calculator review feedback:
- Add an "Additional daily demand" (Extra) field for pets, livestock,
guests, or sick household members not covered by the per-person rates.
- Make the preset choice explicit buttons with a note that switching
resets rates to the preset defaults.
- Clamp rate and extra inputs to non-negative via a shared helper that
preserves in-progress decimal entry (so "1.5" is typeable).
- Persist the duration slider to local storage (with an out-of-range
guard), matching the people-count/metric/preset persistence.
- Relabel "Total Weight" as "Water Weight" to avoid implying it includes
container tare weight.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New offline prepper calculator under Calculators:
- Men/women/children/pregnant-nursing counts with editable per-person
daily rates (drinking + hygiene/dish use).
- Minimum/Comfortable presets grounded in CDC/FEMA (1 gal/person/day
floor, store 2 weeks) and National Academies adequate-intake figures;
Pregnant/Nursing adds the CDC/Ready.gov +0.5/+1 gal. Sources cited in
the Daily Use info popup.
- Duration slider on a clean ladder (days → weeks → months → years) so it
never lands on awkward values like "68 days".
- Containers Needed table listing every container (IBC tote → 500 mL
bottle) with capacity and count + spare for the total volume.
- Total water and total weight (8.345 lb/gal, 1 kg/L).
- Imperial/metric, preset, and people counts persist via local storage.
Pure functions (dailyDemand, totalWater, containersNeeded, waterWeight,
formatDuration) covered by 21 unit tests. Registered in the calculator
list and global app search.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address Greptile review: remove the per-render and per-keystroke console.debug
calls (they fired on every React render and every field edit). Keep the single
strategic solveTrain log, which fires only on a settled (debounced) solve.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a SUGGESTED_APPS entry so the Drive Ratio calculator shows under "Apps"
in the global search, matching the other calculator entries.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A new "Drive Ratio" calculator under /more/calculators for belt, gear, and
chain-sprocket drives, supporting a compound train of 2–6 wheels on a single
shared belt/chain (constant size·rpm across the train).
- Pure, unit-tested math: size·rpm solver, ratio, torque, surface/belt speed,
belt length, sprocket pitch diameter, chain length, gear pitch diameter.
- Mode toggle (Pulley/Gear/Sprocket) with imperial/metric units; torque in
lb·ft or N·m.
- Add/remove wheels (2–6), each with a colorblind-friendly color shared between
its inputs and the schematic SVG diagram (Paul Tol bright palette).
- Dependency-free inline-SVG diagram: colored wheels threaded by one belt/chain,
or meshing gear teeth; auto-fitting viewBox.
- Per-mode details: belt/chain length and center distance (2-wheel only, with an
info popup), per-wheel pitch diameters, and constant train speed.
- Guards against impossible geometry (wheels overlapping) and ideal-torque note.
- Integer teeth enforced in gear/sprocket modes; pulley diameters stay continuous.
Also fix Theme.Header dropping the theme's text color when a caller passes a
`style` prop (now merges {...t.style, ...props.style}).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
numfmt with a negative argument:
Under `set -euo pipefail` the upgrade subcommand would die at
`numfmt --to=iec ... $headroom_bytes` whenever the new ISO was larger
than the headroom — numfmt treats the leading `-` as an option flag,
exits non-zero, and the script aborts before printing the descriptive
back-up-and-reflash error message just below. Splitting positive
and negative cases and feeding numfmt the absolute value fixes it;
the eventual error message now actually shows up.
Silent umount during sanity check:
`umount "$child" 2>/dev/null || true` was swallowing both stderr and
the exit code. A busy partition (open file handle from a daemon
that auto-opened the USB) would survive the loop, and `cmd_upgrade`
would proceed straight to `dd if=<iso> of=<dev>` — writing to a
block device whose partition is still mounted, corrupting the
filesystem on that partition. Adds a post-loop assertion: re-read
MOUNTPOINTS and die with an `lsof` hint if anything is still mounted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
wrolpi-bootstrap.service is `WantedBy=multi-user.target` and `Before=`
the wrolpi-api/app/kiwix/controller services, but had no ordering
constraint against lightdm. Systemd was free to start lightdm in
parallel with bootstrap, so XFCE autologin would land before bootstrap
had written /tmp/wrolpi-firstboot-summary.txt — and the
wrolpi-firstboot-notify autostart would fire, find no summary, and
exit silently.
On the live test this manifested as the user seeing the desktop come
up immediately but never seeing the welcome zenity dialog. The notify
script's own log confirmed the timing race:
===== Mon May 25 08:01:46 PM UTC 2026 invoked ... =====
no summary file at /tmp/wrolpi-firstboot-summary.txt — nothing to do
bootstrap was still in the middle of `pg_createcluster` at 20:01:46
and didn't write the summary file until ~20:01:48.
Adding `Before=display-manager.service lightdm.service` makes systemd
wait for bootstrap to complete before bringing up the display manager.
Tested live by logging out + back in on the running ISO (re-firing
autostart with the summary already in place) — the zenity dialog
appeared.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Earlier commit hand-rolled six Type=Link launchers from scratch. The
repo already had the canonical files at etc/raspberrypios/wrolpi.desktop,
wrolpi-help.desktop, wrolpi-help-files.desktop, and controller.desktop,
used on the RPi/Debian bare-metal deployments.
Switching the chroot hook to `cp` those files into ~/Desktop/ keeps the
live ISO in sync with the other deployments — when those .desktop files
get edited (URL changes, new icon, etc.), the live ISO inherits the
change with no chroot-hook edit needed.
Drops the Kiwix Library, WROLPi Files, and Disk Management entries the
previous commit added; the user requested the four canonical entries
only. The trust autostart from the previous commit continues to mark
all *.desktop in ~/Desktop, so the surviving icons launch on
double-click.
All four icons tested on the live ISO at 10.0.0.17:
WROLPi App https://127.0.0.1 ✓
WROLPi Controller http://127.0.0.1 ✓
WROLPi Help http://127.0.0.1:9086 ✓
WROLPi Help Files file:///opt/wrolpi-help/docs ✓
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A Type=Link to file:///opt/wrolpi-help/docs/, which dispatches via
xdg-open to Thunar. Mirrors the wrolpi-help-files.desktop entry from
etc/raspberrypios/ used on the RPi/Debian deployments. Lets the user
browse the raw markdown sources for the Help site directly, in addition
to the rendered HTML at https://localhost:8086/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five Type=Link launchers drop on /home/wrolpi/Desktop/ via the chroot
hook:
WROLPi https://localhost/ /opt/wrolpi/icon.png
WROLPi Controller https://localhost/controller/ controller-logo.png
WROLPi Help https://localhost:8086/ help-browser (icon theme)
Kiwix Library https://localhost:8085/ accessories-dictionary
WROLPi Files file:///media/wrolpi/ folder
All HTTPS URLs land on Caddy on localhost; the file:// URL is
dispatched via xdg-open to Thunar.
XFCE Bookworm rejects any .desktop file in $HOME/Desktop without
metadata::trusted set, even for Type=Link entries (verified live). So
this also restores the trust autostart we previously had for the now-
removed Calamares icon — generalised to walk every *.desktop in
$HOME/Desktop and stamp the trust metadata on each, every session
start. Idempotent; gio set is a no-op when values already match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The original wrolpi-firstboot-notify.desktop was XDG-valid (per
`desktop-file-validate`) but xfce4-session would not pick it up: zero
log entries on first boot, no zenity dialog, no process spawned.
Bringing the file in line with the upstream autostart entries that we
saw working (nm-applet, xfce4-notifyd, etc.) fixes it. The likely
required field is one of `TryExec=` / `Icon=` / `Categories=` —
desktop-file-validate didn't flag the absence of any of them, but
adding them as a set makes the autostart fire.
Also instruments the notify script: it now writes a one-line log entry
to /tmp/wrolpi-firstboot-notify.log on every invocation so a future
"didn't see the dialog" report has a smoking gun.
And the rm of the (root-owned) summary file now tries via sudo first.
/tmp's sticky bit prevented the unprivileged user from removing a
root-owned file — left a stale summary that would re-trigger the
dialog on a subsequent login.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`wrolpi-usb.sh upgrade <iso> <device>` replaces the boot partition of an
existing WROLPi USB with a new ISO while preserving the persistence
partition.
Flow:
1. Locate the persistence partition by label.
2. Read its sector range from sfdisk -d.
3. Confirm the new ISO fits in the headroom (i.e. its byte count <=
persistence_start_sector * 512). If not, bail with instructions
telling the user to back up /media/wrolpi, re-flash from scratch,
and restore. This is the case for USBs flashed before the 8 GiB-
headroom commit.
4. Save the partition table to /tmp/ for emergency recovery.
5. Prompt for "yes".
6. dd the new ISO onto the device. The persistence partition's
sectors are past the ISO's end, so its data is untouched.
7. sfdisk --append to re-add the persistence partition entry at the
saved sector range. The data on disk is still there; we are just
fixing up the partition table the new ISO wrote.
8. partx -u to refresh the kernel view; verify the label is reachable.
`install` subcommand is wired up to a die() for now; will land in a
follow-up commit.
The script refuses to operate on non-removable disks (foot-gun guard).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The persistence partition is created immediately after the ISO partition
ends (1-MiB aligned). When `wrolpi-usb.sh upgrade` overwrites the ISO
partition with a future, possibly-larger ISO, growth past the current
ISO size would overwrite persistence-partition sectors.
Reserve 8 GiB between them on first boot. Future ISO releases up to
roughly 2x the current size (~3.3 GB) can be written into that gap
without touching persistence.
The cost is ~8 GiB of persistence the user can't reclaim. On a typical
128 GB USB this is ~6% of total capacity, which is comparable to
filesystem overhead and rounding loss.
Existing USBs flashed before this commit have no headroom — they need
a one-time backup-and-reflash to pick it up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Splash:
Shifted the logo + text block up by half a logo-height. GRUB's menu
renders centred over the splash; the previous placement put the icon
directly behind the menu entries.
First-boot summary:
wrolpi-bootstrap.sh now accumulates a short list of notable actions
(partition created, Postgres cluster set up, DB initialised) and
writes them to /tmp/wrolpi-firstboot-summary.txt. A new XDG autostart
(wrolpi-firstboot-notify) reads that file once XFCE comes up and shows
a zenity dialog telling the user what just happened — answering the
"wait, what did this thing just do to my USB drive?" question.
Subsequent boots where the bootstrap is a no-op write no summary file,
so the dialog only appears the boot it actually matters. /tmp is tmpfs,
so the file is also naturally per-boot.
Adds zenity to the package list (not pulled by --apt-recommends false).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The rebrand hook from the previous commit failed during lb build because
it referenced files as `binary/boot/grub/grub.cfg` etc., but
live-build's binary_hooks runs each hook with cwd already set to
binary/ (it does `cd binary && ../$HOOK`). The paths now drop the
`binary/` prefix.
Splash regenerated from the new transparent logo
(app/public/icon-transparent.png) so the wifi-pie has a clean white
outline on the dark splash background — better visual separation than
the white-stripped icon.png the previous splash used.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pushes "Off-grid library" down twice its own height from the bottom of
"WROLPi", clearing the visual collision.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The auto-generated live-build splash.png composes a yellow "build worker"
hardhat with "Debian GNU/Linux 12 (bookworm) amd64" baked in as a PNG
overlay. Both GRUB and ISOLINUX use it as the menu background, so it's
the first thing users see at boot.
Two replacement splash images (800x600 for GRUB, 640x480 for ISOLINUX)
land via config/includes.binary/, generated once from the project's
icon.png (the wifi-pie favicon). No build-time composition needed; the
files are checked in as binary blobs.
A *.binary hook then seds the live-build-generated menu entries so the
GRUB and ISOLINUX text reads:
Run WROLPi (live) ← was "Live system (amd64)"
Run WROLPi (live, fail-safe) ← was "Live system (amd64 fail-safe mode)"
Install WROLPi ← was "Start installer"
Install WROLPi (speech) ← was "Start installer with speech synthesis"
We can't override live-build's substitution source without patching the
distro-installed `binary_grub_cfg` script; doing it after the fact in a
.binary hook keeps the customisation in our config tree.
Note: this rebrands what GRUB / ISOLINUX render *around* the d-i menus.
Once the user selects "Install WROLPi" and d-i actually launches, the
yellow hardhat reappears inside d-i's own screens — that logo
(logo_installer.png) lives inside the d-i initrd and would require a
custom initrd rebuild to swap. Punting on that for now.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`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.