Fresh installs (and SSH sessions that forward LC_* variables from the client)
can reference an ungenerated locale, causing apt/perl/python to emit
"Cannot set LC_CTYPE to default locale" warnings throughout the install log.
Export LC_ALL=C.UTF-8 for the install run. C.UTF-8 is built into glibc, always
present, and locale-neutral, so it silences the warnings without generating a
locale, without adding a dependency, and without changing the user's configured
locale (e.g. an en_GB.UTF-8 system stays en_GB.UTF-8).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Controller venv (/opt/wrolpi/controller/venv) was only ever created by
upgrade.sh and reset_virtual_environments.sh, neither of which runs during a
fresh install. install_raspberrypios.sh and install_debian_12.sh created only
the main API venv, so on a clean machine wrolpi-controller crash-looped with
203/EXEC ("Unable to locate executable /opt/wrolpi/controller/venv/bin/python")
because repair.sh unconditionally starts the service.
Create the Controller venv inline right after the main venv, mirroring how the
main venv is already built in these scripts. Verified on a fresh Raspberry Pi OS
Trixie install (10.0.0.8): wrolpi-controller comes up active with 0 restarts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two fixes from PR review:
- `PRAGMA journal_mode=WAL` returns the mode SQLite actually selected and can leave the
prior rollback journal without raising. Confirm the returned mode instead of assuming
the request succeeded, so a silent WAL refusal correctly falls back to TRUNCATE +
synchronous=FULL rather than caching WAL with the weaker synchronous=NORMAL.
- `fuseblk` (block-backed FUSE such as ntfs-3g) has unreliable byte-range locking that
can corrupt the database under the workers' concurrent access. Move it out of the
WAL-incompatible allow-list into an UNSAFE_LOCKING_FS_TYPES refuse-list (renamed from
NETWORK_FS_TYPES). NTFS on a modern kernel mounts via the native `ntfs3` driver, which
stays allowed; exFAT is unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A media drive formatted for Windows compatibility (exFAT/FAT/NTFS) cannot support
SQLite WAL: it needs mmap'd shared-memory those filesystems don't provide, so
`PRAGMA journal_mode=WAL` raised `(sqlite3.OperationalError) disk I/O error` on
every connection, taking down the whole API (observed on 10.0.0.8; the tell was
`Media directory has the wrong permissions: 0o40777`, since these filesystems can't
store Unix permission bits).
The engine now tries WAL first and, on failure, falls back to a TRUNCATE rollback
journal with synchronous=FULL (crash-safe, since WROLPi is off-grid). The decision
is memoized per-engine so WAL isn't re-attempted or re-logged on every fresh
NullPool connection. The bootstrap environment check no longer refuses these
drives; it warns once that write-concurrency is reduced and recommends ext4.
Network filesystems remain refused: their locking is unsafe regardless of journal mode.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The download dispatcher (_dispatch_new_downloads) reads the `new` downloads
then claims each by writing last_download_attempt, all in one ORM
transaction. Since ORM transactions begin *deferred*, that write is a lock
upgrade (SHARED->RESERVED). SQLite skips busy_timeout for lock upgrades to
avoid deadlock, so under concurrency the write failed instantly with
"database is locked" instead of waiting out the 30s busy_timeout. This
surfaced repeatedly on the SQLite build after the Postgres->SQLite cutover.
Add get_immediate_db_session(), a committing session that takes the write
lock up front (BEGIN IMMEDIATE) so busy_timeout absorbs the contention --
mirroring what the raw-cursor get_db_curs path already does. Plain
get_db_session stays deferred so ordinary writes and reads keep WAL
read-concurrency; only read-then-write call sites opt in. Convert the
dispatcher to use it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Triage of Greptile findings on the default-Zim / configurable-directory PR:
- Stale config on restart (P1): settings update() only queues an async
background_save, and the restart switch has no ordering guarantee (the switch
worker pops LIFO), so start_kiwix_serve.sh could read the old zims_destination.
Save the config synchronously before activating the restart switch.
- YAML null (P1): read_config_value.sh now treats an unquoted null/~ as unset
(returns the default), matching PyYAML/the Python config layer. A quoted
"null" remains the literal string.
- Path traversal (P1/security): the settings API now rejects a zims_destination
containing "..", and start_kiwix_serve.sh defends in depth by falling back to
`zims` for any absolute/traversal/empty value, so Kiwix can never scan or
rewrite library.xml outside the media root.
- Reproducible build (P2): pin the zim-tools image to 3.7.0 instead of :latest.
Tests added for null handling, traversal rejection (API + script).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The image builds (pi-gen, debian-live, Docker) already ship wrolpi/blobs, but a
fresh install on existing Debian and an in-place upgrade did not pick up the
default Zim. repair.sh runs on both paths (both install scripts and upgrade.sh
call it) and already manages /opt/wrolpi-blobs immutability, so copy the
committed blobs there -- after the immutable flag is cleared and before it is
re-applied. Local copy only, preserving repair.sh's no-internet guarantee.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two related gaps in how WROLPi serves Kiwix content:
1. A fresh WROLPi (or one whose zims are all corrupt/absent) showed a blank
Kiwix. Every image now ships a small built-in default Zim that Kiwix falls
back to, telling the user to download real Zim files.
2. The startup scripts hardcoded the `/zims` directory, ignoring wrolpi.yaml's
`zims_destination` (which the Python side already honors).
Changes:
- New wrolpi/scripts/read_config_value.sh: a zero-dependency (awk) reader so
non-Python scripts can reliably read top-level wrolpi.yaml scalars. Also a
config_value() wrapper in wrolpi/scripts/lib.sh.
- New wrolpi/scripts/default_zim/: HTML source + a reproducible builder
(ghcr.io/openzim/zim-tools) that outputs the committed wrolpi/blobs/default.zim.
- New wrolpi/blobs/: any committed blob here is copied to /opt/wrolpi-blobs by
the pi-gen and debian-live builds; the Docker zim image bakes it in.
- start_kiwix_serve.sh honors zims_destination and falls back to the default
Zim when no functioning zim is found.
- docker/zim/entrypoint.sh gains the same default-Zim fallback. It needs no
config read: it already scans the whole mounted media dir, and
zims_destination is always a subdirectory of that mount.
- Changing zims_destination via the settings API now restarts Kiwix so it
serves the new directory without a reboot.
Tests: new test_read_config_value.py, extended test_start_kiwix_serve.py
(configured dir + default-Zim fallback), and a root_api test for the restart.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Kiwix section only checked library.xml and the Caddy HTTPS proxy on 8085,
which made a common failure hard to diagnose: kiwix-serve down while the
wrolpi-kiwix unit still reads "running" (its ExecStart is a bash wrapper), and
corrupt/incomplete zims silently missing from the library.
- Check the wrolpi-kiwix systemd unit exists and the service is up (parity
with the api/app/controller sections).
- Check kiwix-serve is actually listening on 9085 and responds over plain
HTTP there, isolating a kiwix failure from a Caddy proxy failure on 8085.
- Compare *.zim files on disk to <book> entries loaded in library.xml; a
shortfall flags corrupt or incompletely-downloaded zims.
Verified against a healthy Debian 13 RPi (10.0.0.9).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When downloading is enabled at startup, the download worker races a burst
of concurrent SQLite writes on the single DB file: retry_downloads() has
just marked every download 'new', RSS catalog refreshes call
create_downloads() (which fires its own background dispatch_downloads()),
multiple download workers claim downloads, and save_downloads_config
re-saves the config. dispatch_downloads reads-then-commits in a deferred
transaction, so when another writer commits mid-cycle it fails with
SQLITE_BUSY_SNAPSHOT ("database is locked", not absorbed by busy_timeout).
This is self-healing -- the perpetual worker retries on its next cycle and
downloads succeed -- but the raised OperationalError flooded the logs with
full tracebacks and tripped the Sanic traceback renderer (tracerite) into
its "Variable inspector failed" secondary errors.
Split the claim-and-dispatch DB work into _dispatch_new_downloads() and
have dispatch_downloads() swallow a transient lock (debug log, return) so
the next cycle retries cleanly. Non-lock database errors still propagate.
The split keeps the retry decision outside the transaction that also
awaits the download signal dispatches, so a lock is never handled by
re-dispatching (no double-dispatch).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A refresh on a large library aborted the whole doc_modeler run on the
first lock error, so the ~20k stranded docs never got modeled.
doc_modeler held one write transaction across a whole batch of 10 docs
while doing slow per-doc extraction (a PDF/epub can be tens of MB). When
another connection committed mid-batch, the deferred transaction failed
to upgrade to a write with SQLITE_BUSY_SNAPSHOT ("database is locked",
which busy_timeout does not absorb -- the same trap that motivated the
short-transaction rewrite of apply_indexers). The per-item error handler
then did `logger.error(f'... {file_group}')`, which lazy-loaded an
attribute on the now-rolled-back session and raised a *second* exception,
escaping the handler and aborting the entire modeler.
Restructure:
- Pick each batch of candidate ids in a short read-only transaction, then
model each doc in its own short write transaction. A lock/error on one
doc rolls back only that doc and cannot poison the rest of the run.
- Retry a doc a few times on "database is locked" -- a fresh transaction
gets a fresh snapshot and succeeds.
- Log the plain file_group id on failure; never touch the ORM object on a
possibly rolled-back session.
- Track only *failed* ids for the termination guard (successes are
excluded by the Doc.id IS NULL gate), keeping the notin_ set small and
well under SQLite's bound-variable limit.
Adds test_doc_modeler_survives_database_locked.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The zim Docker image is FROM debian:13, whose kiwix-tools 3.7.0 rejects the
literal "--address 0.0.0.0" ("IP address is not available on this system"),
so the container failed to start after loading the library. Omit --address;
kiwix-serve then defaults to binding all interfaces.
This mirrors 9e92732a, which applied the same fix to the systemd
start_kiwix_serve.sh path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses review feedback: the new `Doc.id IS NULL` gate keeps matching a
doc whose _model_doc fails without persisting a Doc row. With
>=DOC_PROCESSING_LIMIT such docs, every batch re-selected the same failing
files, `processed` never dropped below the limit, and the loop hung --
starving later docs. Reproduced with a patched _model_doc that always
raises.
Track attempted FileGroup ids per run and exclude them from the query, so
the candidate pool strictly shrinks and the loop always terminates. Failed
docs are retried on the next refresh (unchanged behavior).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The doc_modeler gated its query on `FileGroup.indexed == False`, but
`indexed` is a full-text-search flag that `apply_indexers` sets on every
FileGroup during a refresh. Because the refresh pipeline runs
apply_modelers -> apply_indexers, any doc file that reached apply_indexers
without first receiving a Doc row (modeler errored, was interrupted, or
files imported already-indexed) got `indexed=1` and was then excluded by
the `indexed == False` gate forever -- leaving it with model=NULL, no Doc
row, and invisible in /api/docs.
On one library this stranded 20,795 PDFs/epubs/mobis (only the 10
individually-tagged docs, modeled via the per-file do_model path, showed
up). No doc-mimetype files were left with indexed=0, so nothing would
ever recover them.
Gate the modeler on "no Doc row yet OR explicitly flagged for
re-indexing" instead, so already-indexed docs are picked up on the next
refresh while preserving re-index behavior and skipping already-modeled
docs. Self-healing: a global refresh now models the backlog.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A corrupt or partial *.zim (e.g. an interrupted download) is rejected by
kiwix-manage ("Cannot add zim ..."). start_kiwix_serve.sh could then leave
Kiwix down and reported a misleading "Could not find any Zim files" — and
if the bad zim was the only one, exit 2 killed the service with no restart.
- Import each zim independently; log and skip a failed add so the remaining
valid zims are still served.
- Always ensure a library.xml exists (empty if needed) and exec kiwix-serve
unconditionally, so a corrupt/absent zim never stops the server starting.
- Fix the mount-wait loop, which never incremented COUNT (infinite wait
instead of a 30s timeout).
- Add MEDIA_DIRECTORY override so the script is testable.
- Add Restart=on-failure to wrolpi-kiwix.service (matches wrolpi-api.service)
so transient failures self-heal.
- New test harness runs the script with fake kiwix-manage/kiwix-serve shims.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The themed Button forwards its ref straight to SButton, a class
component, so the ref resolves to the class instance instead of the DOM
node. Semantic UI's Modal/Popup Portal calls node.contains(...) on the
trigger's ref during handleDocumentClick, which throws on a non-DOM
value. This crashed the Control page whenever a Service Logs (or QR /
download-error) modal was open and the document was clicked.
Wrap SButton in Ref (findDOMNode) so a forwarded ref always resolves to
the underlying <button> element, fixing every Modal/Popup that uses a
themed Button as its trigger.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Refreshing all files failed on a large media directory with
RuntimeError: values_clause: too many parameters (44454 rows x 3).
After the Postgres->SQLite cutover, values_clause guards against
exceeding SQLite's ~32k bound-variable limit, but upsert_directories
still built a single multi-row VALUES clause over every discovered
directory.
Insert in BULK_SQL_CHUNK_SIZE batches, matching the existing bulk
tag-file callers. Adds a regression test that inserts more directories
than a single statement can bind.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A Debian major upgrade (e.g. 12 -> 13) replaces /usr/bin/python3
(3.11 -> 3.13). Existing venvs still resolve `python3` to the system
interpreter, but their packages live in lib/python3.11/site-packages,
which the new interpreter never searches -- every import fails and
`pip install --upgrade` cannot repair them, taking down the API,
Controller, yt-dlp, and Help.
upgrade.sh now compares each venv's recorded Python version against the
current one and does a full rebuild via reset_virtual_environments.sh
when they differ. Detecting a mismatch (rather than literally "3.11")
also self-heals future bumps and is a no-op once versions match or on a
fresh install with no pyvenv.cfg yet.
reset_virtual_environments.sh gains a -y/--yes flag to skip its
confirmation prompt; the interactive yes_or_no read loops forever on EOF
under the tty-less wrolpi-upgrade.service.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When importing hundreds of tag_files for files that exist on disk but
have no FileGroup yet (a fresh import before a file refresh), the import
crawled at ~a dozen rows/second. Every tagged file hit a fallback that
globbed its parent directory (O(files²) for large Channel/Domain dirs)
and flushed the DB once per row.
Batch the work instead:
- Cache each directory's listing so it is scanned once, not per file.
- Create new FileGroups under no_autoflush and flush them in a single
batch, then create TagFiles in a second pass (one batched INSERT).
- Cache every member path of a newly-created FileGroup so a file tagged
by multiple tags, or another config entry for a different member of
the same shared-stem group, reuses one FileGroup instead of inserting
a duplicate primary_path that would abort the import.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
get_special_directories runs during every refresh ignore filter. Calling
get_videos_directory/get_no_channel_directory created videos/ and
NO CHANNEL under an empty media tree, which broke empty-refresh and
directory listing tests. Resolve those paths without creating them.
Zim Manage and search depend on FileGroup modeling of files under
zims/. Ignoring that directory left Kiwix serving files on disk while
/api/zim stayed empty. Refuse to ignore zims (alongside other special
dirs), strip it from existing configs on settings load, and share
normalized ignore handling between refresh and upsert.
`git rev-parse --abbrev-ref HEAD` prints the literal "HEAD" on a detached
HEAD, which get_current_branch() returned as if it were a branch. check_for_update
then compared origin/HEAD (the remote's default branch, usually master) and
reported a bogus "N commits behind on HEAD" — a false "update available".
Return None for a detached HEAD so no phantom upgrade is offered.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Debian 12 guard fetched the cutoff tag with a non-forcing refspec, so a
box that had already fetched an earlier `debian12-final` kept its stale local
tag ("would clobber existing tag" is rejected). When the tag later moved
(e.g. a version-bumped final release), the guard pinned to the OLD target
instead of the new one. Force the fetch (+ / --force) so a moved tag wins.
Safe: the commit's signature is still verified before checkout.
Caught on real Debian 12 hardware (10.0.0.8) after moving the tag for a patch
bump; added a sandbox scenario covering the moved-tag/stale-local-tag case.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Debian 12 (bookworm) cannot run the Postgres->SQLite / Debian 13 rebase,
and there is no in-place 12->13 upgrade. Guard the on-device upgrade so a
Debian 12 machine pins to the signed `debian12-final` tag and never advances
past it, while Debian 13+ upgrades normally.
./upgrade.sh already fetched+checked out the latest release by the time
scripts/upgrade.sh runs, so the guard runs first: on Debian 12 it verifies
the guard commit's signature, rolls back to debian12-final, re-execs that
version's script (which warns to reinstall on Debian 13 and then runs its
normal upgrade so services restart). Aborts rather than run newer code if the
tag is missing or unsigned. Inert on Debian 13+.
Verified with a sandbox harness covering: D13 pass-through, D12-at-tag
warn+proceed, D12-past-tag verify+rollback+re-exec (no loop), and
missing-tag refusal.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collapse the four test_list_tar_*_contents and four
test_download_single_member_tar* functions into two parametrized tests
covering gz/xz/plain/bz2. All eight format cases still run; no coverage
loss. Identified via pytest-deduplicate (identical line coverage).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The dashboard nudge is useless on Safari and other browsers that cannot
run the WROLPi extension. Share detectBrowser and gate the banner with
isExtensionSupportedBrowser.
During onboarding the media drive is not mounted and ca.crt does not exist
yet (or still holds only a throwaway pre-drive state). The cert banner still
offered Download WROLPi Root CA, which 404'd. Gate the button and HTTPS
cert-setup banner on drive mounted, ca.crt present, and repair not running
so an existing CA on the drive can be served after mount/repair.
Add `git config --global --add safe.directory /opt/wrolpi` before the
`git reset --hard`, guarding against git's "dubious ownership" refusal.
Set HOME=/root first so the --global write cannot fail (and abort the ISO
build under `set -e`) if the live-build chroot leaves HOME unset -- the
same pattern the pi-gen hook already uses.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pi-gen and Debian Live chroot hooks run `npm install`, which rewrites
the tracked app/package-lock.json, then build_app.sh stamps
build/.build-stamp from that dirty working tree. On first boot repair.sh
runs `git reset --hard` (reverting the lock) *before* it recomputes the
stamp, so the baked stamp can never match -- and the device rebuilds the
React app every time, the exact OOM-prone step baking the build in (#536)
was meant to avoid.
Verified against WROLPi-v0.27.2: the shipped image had `M app/package-lock.json`,
baked stamp a9c6ef61..., post-reset stamp 2b908c6c... (guaranteed mismatch).
Mirror repair.sh in both image hooks: `git reset --hard HEAD` right after
npm install and before build_app.sh, so the baked stamp represents committed
source and matches what the device recomputes. Only the lock is reverted;
build/, node_modules, and the venvs are untracked and survive.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop .pgpass handling, Postgres ownership repairs, and the docker db
container viewability rules. Update comments and pi-gen map docs for
SQLite/PMTiles. Help submodule documents the new database layout.
Catch useForm fetcher errors and surface them so DomainEditPage can show a not-found message instead of hanging on the loader (and so the webpack overlay no longer flakes Cypress).
- strip_youtube_caption_positioning now writes to a temp file in the same
directory and os.replace()s the original, so a failed/partial write
(e.g. disk full) can never truncate the existing caption file.
- Replace the substring `'youtube.com' in url` gate with is_youtube_url(),
which parses the hostname and matches youtube.com / youtu.be (and
subdomains), rejecting lookalikes like youtube.com.example.org and
notyoutube.com. Add a parametrized test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
YouTube auto-generated captions (via yt-dlp) stamp every cue timing line
with `align:start position:0%`, pinning the on-screen text to the
bottom-left of the player. This strips that exact cue setting from the
downloaded .vtt files so browsers render the captions centered.
- add strip_youtube_caption_positioning() to wrolpi/captions.py — a
targeted text replacement (not a webvtt round-trip) so the `<c>`
word-timing tags in the cue text are preserved; matches only the exact
auto-caption signature, so deliberately positioned cues (e.g. with a
`line` setting) are left untouched; idempotent.
- call it from VideoDownloader after normalize_video_file_names, gated on
a youtube.com / youtu.be source URL. It runs before the FileGroup is
created, so the first index reads the corrected file — no re-index
plumbing needed.
Complements the client-side fix in #543 (which centers captions in the
player for existing/any-source files); this makes new YouTube downloads
correct at the source.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Require `line === 'auto'` in the auto-caption signature so a cue that
is deliberately placed (e.g. `line:0` for top-edge) is left alone even
if it is also `align:start position:0%`. YouTube auto-captions never
set `line`.
- For lazily-loaded language tracks, only detach the cuechange listener
after the cue list is actually populated, in case a cuechange fires
before cues load.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
YouTube auto-generated captions (downloaded via yt-dlp) stamp every cue
with `align:start position:0%`, which pins the caption text to the
bottom-left of the player and makes it hard to read. Properly-authored
VTT files have no cue settings and already render centered.
Detect that auto-caption signature and reset only those cues to the
browser's centered default (align:center / position:auto / line:auto).
This mutates the in-memory VTTCue objects the browser parsed; the .vtt
file on disk is never modified, and deliberately-positioned cues are
left untouched.
Wired via each <track>'s onLoad. Non-default language tracks are
`disabled` at load (no cues yet), so they are normalized lazily on the
first cuechange when the user selects them.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The React /map page rendered blank (only pins) on fresh builds while the
standalone :8084 viewer worked. maplibre-gl's Web Worker (inlined into the
production main.js) threw `ReferenceError: a is not defined` in its setLayers
handler, so the worker's style-layer index never built and every vector
source's tiles failed to parse ("Cannot read properties of undefined
(reading '<source>')"). The unminified dev build was unaffected — it is a
production minification bug: Terser mangles the inlined worker into broken code.
Root cause: the "npm audit fix" in 62660529 (intended for shell-quote/
form-data/ws) regenerated the lockfile and dragged @babel/core 7.20.12 ->
7.29.7 (whole @babel core pipeline). Babel 7.29 transpiles the maplibre worker
differently; CRA's (unchanged) Terser mangle pass then breaks it. Proven on
10.0.0.8: rebuilding the minified prod bundle with @babel 7.20 renders the map
(0 worker errors); rebuilding back to 7.29 breaks it again with the identical
bundle hash. The transform plugins were never bumped, so pinning @babel/core
alone is insufficient — the core AST pipeline (traverse/generator/types/
helpers) must be pinned too.
Fix: pin the 16 @babel packages that differ from the last-good tree
(6b7bd079) via package.json overrides, keeping the shell-quote/form-data/ws
security fixes. Frontend jest suite passes (810/810).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Verify ARRL wire-antenna formulas (936/468/234/585), fix feet/inches
rollover ("N ft 12 in"), make wavelength edits update lengths, and cover
the math with 50 unit tests.
go-pmtiles release assets are named ..._Linux_x86_64.tar.gz, not _amd64;
the #538 consolidation used $arch=amd64 in the URL, so every amd64 build
(Debian Live ISO, docker) 404'd installing pmtiles and the wrolpi chroot
hook failed. Use x86_64 for the URL arch on x86_64 hosts; the SHA var and
the aarch64 (arm64) path are unchanged and already correct.
Verified: both v1.30.1 tarballs download and match the pinned SHAs
(x86_64 23a2a222..., arm64 a1f9f42d...).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pmtiles CLI + tippecanoe install was duplicated across three places
(scripts/install_protomaps.sh, the Debian Live hook, and pi-gen), so a version
bump or logic change had to be made in all of them in lockstep. Move it into a
single shared, version-guarded, SHA-pinned installer and have all three callers
invoke it.
- New wrolpi/scripts/install_map_tools.sh: installs pmtiles (per-arch binary,
SHA-pinned for amd64 and arm64) and builds tippecanoe (source tarball,
arch-independent SHA). Version-guarded, so an image that already baked in the
pinned versions makes the on-device run a no-op until a version bumps.
- pi-gen 03-run-chroot.sh, the Debian hook, and install_protomaps.sh now just
call the shared script. This also adds SHA pinning to the on-device pmtiles
path (previously unpinned).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The Debian Live image builds tippecanoe at image time, but pi-gen did not,
so a fresh Pi compiled it on first boot/upgrade via install_protomaps.sh --
a multi-minute, CPU-heavy C++ build on the weakest hardware WROLPi targets.
This is the same "Debian bakes it, pi-gen doesn't" gap recently fixed for
the React app build.
Build tippecanoe 2.79.0 in the stage2 chroot, mirroring the Debian hook
(SHA-pinned source tarball, which is arch-independent). Add the two missing
build deps (libsqlite3-dev, zlib1g-dev) to the package list; build-essential
was already present.
With tippecanoe present at the pinned version, install_protomaps.sh's version
guard skips the build on first boot and on upgrades, so the on-device compile
only recurs when the pinned version actually bumps.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>