Commit graph

46 commits

Author SHA1 Message Date
lrnselfreliance
d701846254
Hotspot captive portal: welcome page with the WROLPi's address (#709)
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Controller Tests / controller-tests (push) Waiting to run
Jest Tests / jest-tests (push) Waiting to run
* Hotspot captive portal: welcome page with the WROLPi's address

Devices that join the hotspot probe a fixed plain-HTTP URL to check for
Internet access.  NetworkManager's shared-mode dnsmasq now resolves those
probe hostnames (and `wrolpi`) to the hotspot address via an include the
Controller writes when the hotspot starts, the probes land on the Controller
(port 80), and are redirected to /portal: "This is your WROLPi, its address is
10.42.0.1, open WROLPi / open the Controller".

Only the probe hostnames are hijacked so the hotspot keeps forwarding
everything else when the WROLPi has an uplink.  The include is corrected and
the connection re-activated once if NetworkManager picked a non-default
subnet.  Writing the include is best-effort; the hotspot starts regardless.

`hotspot.captive_portal` (controller.yaml, default true) toggles the probe
redirects and the include; like the other hotspot settings it applies on the
next hotspot start, so saving while the hotspot is up reports
restart_required and both Controller UIs say so.  The status endpoint gains
the hotspot address and portal URL, shown in both UIs.  repair.sh only
creates the include directory so a disabled setting survives upgrades.
Native only: Docker has no hotspot.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* captive portal: address review

* React: re-read the hotspot address whenever the HotspotToggle starts or
  stops the hotspot, matching the fallback UI's refresh-on-status.
* A WPA3 start that reverts to WPA2 leaves the hotspot up, so it now gets
  the captive portal address correction too.
* The corrective `nmcli connection up` logs a warning on a non-zero exit;
  dnsmasq would otherwise keep the old address silently.
* HotspotStatusResponse.captive_portal defaults to true like everywhere else.
* Trim design narrative from the module docstring and React comments.
* The include-before-activate test now observes the include at the moment
  of the `nmcli ... hotspot` call instead of inferring from call order.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* captive portal: never forward any record type for probe hosts

`address=` only synthesizes A/AAAA answers.  iOS also asks for the HTTPS
record type; dnsmasq forwarded that upstream, Apple's answer aliased to
captive.g.aaplimg.com, and the phone fetched the genuine "Success" from
Apple instead of the portal.  Each probe host now also gets `local=`, so
dnsmasq answers every type itself, and the CDN alias is a probe host too.

Observed with dnsmasq query logging on a hotspot with an eth0 uplink.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* captive portal: let acknowledged clients finish so the OS offers "Done"

A phone's sign-in sheet only shows "Done" once the OS re-probes and gets
the genuine success reply.  The Controller now remembers each client (by
hotspot IP) that has loaded the portal page and answers its later probes
with the reply each OS expects (Apple "Success" HTML, Android 204, Windows
"Microsoft Connect Test", ...).  The set is cleared when the hotspot
starts so a new session shows the page again.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* captive portal: acknowledge on Continue, not on view

iOS fetches the portal page in the background before opening the sheet,
then re-probes after the sheet's own navigation.  Acknowledging on view
made that re-probe return "Success" as the sheet's content.  Phones only
re-check the network (and offer "Done") after a full-page navigation, so
the welcome page now has a Continue button leading to /portal/continue,
which is the only place a client is acknowledged.  The Continue page says
to tap Done and links to WROLPi and the Controller.

Also hijack clients3.google.com (older Android probe host).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* help: bump wrolpi-help to the captive portal and Speed Test docs

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* captive portal: comments describe the Continue design; clear acks on stop

Three comments still described acknowledging on view, which c72a7f7b
removed.  Acknowledged clients are now also cleared when the hotspot
stops, so a lease reused in a later session sees the portal page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 20:10:02 -06:00
Roland
76aafc5da9 repair: copy wrolpi/blobs into /opt/wrolpi-blobs on install and upgrade
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>
2026-07-20 19:09:57 -06:00
Roland
4cafb9f00e
Remove stale Postgres references after SQLite cutover.
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.
2026-07-18 13:28:54 -06:00
lrnselfreliance
944d95c717
Bake the app build into images; rebuild on-device only when stale (#536)
* Bake the app build into images; rebuild on-device only when stale

Previously the Raspberry Pi image shipped without app/build/ and compiled
the React bundle on first boot via repair.sh -- a multi-minute, memory-hungry
step that can OOM a 2GB Pi.  The Debian Live ISO already baked the build in,
so the two paths had drifted.

Add scripts/build_app.sh as the single source of truth: it hashes the build
inputs (src/, public/, package.json, package-lock.json, tsconfig.json) and
runs `npm run build` (with GENERATE_SOURCEMAP=false) only when build/ is
missing or those inputs changed, then writes build/.build-stamp.

Wire all three call sites to it:
- pi-gen 03-run-chroot.sh now builds after npm install, baking build/ into
  the Pi image like the Debian hook already does.
- debian-live 9999-wrolpi.hook.chroot uses it so the baked build is stamped.
- repair.sh uses it, so first-boot/manual-repair/install/upgrade all become
  no-ops when the shipped build is current.

Result: fresh images ship a stamped build/, so a weak Pi never compiles on
boot or repair; an on-device rebuild happens only when an upgrade actually
changes app/.  build_app.sh never runs npm install, so repair.sh keeps its
offline guarantee.

* build_app.sh: rebuild when built output is incomplete, not just missing

Addresses review feedback: a matching stamp only proves the inputs are
unchanged, not that the output survived.  repair.sh exists to fix corruption,
so a build/ with index.html + stamp but a missing JS/CSS asset must still
rebuild.  Verify every file referenced by build/asset-manifest.json exists
(and that the manifest itself parses); rebuild otherwise.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 15:19:25 -06:00
Roland
d9303a0ea8
Postgres -> SQLite hard cutover: the database moves into the media directory.
The database now lives at <media>/config/wrolpi.db so a library (files,
configs, AND database) travels with the drive between WROLPi systems.

Core:
- wrolpi/db.py: lazy SQLite engine factory (WAL, busy_timeout=30s,
  foreign_keys=ON); get_db_curs yields a plain sqlite3.Cursor; native
  sqlite3 paramstyle everywhere (no compatibility shims); global sqlite3
  adapters keep raw-SQL datetime formats identical to the ORM's.
- wrolpi/fts.py: FTS5 external-content search replacing tsvector/GIN.
  Injection-proof websearch->FTS5 translator, bm25(10,4,2,1) reproducing
  A/B/C/D weighting, fast(abc)/deep column filtering, snippet headlines.
  Stemming contract pinned by dedicated tests (watch/watches/watched/
  watching are interchangeable).
- wrolpi/schema_ddl.py: SQLite triggers shared verbatim by the alembic
  baseline and test DBs; fixes the channel video_count DELETE/UPDATE
  bugs and a latent total_size bug (it used the video id as channel id).
- wrolpi/db_bootstrap.py: startup auto-create/migrate; refuses databases
  written by a newer WROLPi (db_version_mismatch); SQLite 3.40 floor;
  network-filesystem refusal; never creates/migrates/trusts a database
  beneath an unmounted production media directory (shadow-DB guard).
- Alembic: 85 Postgres-era migrations squashed into one baseline;
  revision ids are dates (2026_07_10_1350) so filenames self-sort and
  alembic_version says when a schema was created; render_as_batch=True.
- All raw Postgres SQL rewritten (ANY->json_each IN, array_agg @> ->
  HAVING COUNT(DISTINCT), unnest->executemany/UPDATE..FROM,
  generate_series histograms->strftime GROUP BY, DISTINCT ON->
  row_number(), writable CTEs->two statements, jsonb_*->json_*, explicit
  NULLS FIRST/LAST, LIMIT-before-OFFSET).  Simple statements converted
  to SQLAlchemy instead (flag upsert, download retry/summary/tag checks,
  channel-by-tag estimate, Directory delete).
- Config imports count skipped entries; background dumps are blocked
  after a lossy import so partial imports can never overwrite YAML
  configs (they are the sole source of truth for rebuilds now).
- Search tolerates explicit JSON nulls for limit/offset (Postgres
  treated OFFSET NULL as 0; SQLite errors).
- file_group.id/doc_section.id render as INTEGER PRIMARY KEY on SQLite
  (BIGINT is not a rowid alias; FTS5 content_rowid requires it).

Ops teardown: docker-compose db service, CI postgres sidecars,
initialize_api_db.sh, controller postgresql service, psycopg2-binary all
removed.  upgrade.sh stops/disables postgresql (data is NOT deleted;
`systemctl enable --now postgresql` restores it); systemd units no
longer Want postgresql.service.

Existing-user upgrade: first boot creates+migrates the DB, configs
import (tags recreate their FileGroups from disk), a refresh rebuilds
the library index.  QA'd on the 10.0.0.8 RPi: rebuild matched the
Postgres baseline exactly; survived an uncontrolled hard reboot with
clean integrity checks.

Accepted behavior changes: FTS5 has no stopwords; bm25 length
normalization shifts some rank orders; fast-path d_headline is
un-highlighted; all-negative queries match nothing.

Deferred: Debian/trixie package lists (incl. sqlite3 CLI), Cypress
re-baseline, VACUUM INTO snapshots, exFAT/NTFS warning, auto-kicked
rebuild refresh with progress UX.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 14:09:37 -06:00
Roland
d6b019c24e Certificates: help.sh validates the CA and leaf certs; fix renew exec bit.
help.sh now detects invalid Caddy certificates: expired or unparseable
CA/leaf certs, a leaf expiring within 30 days, a leaf that does not match
its private key or the current hostname, and Caddy serving a stale cert
after regeneration.  It also checks that the renewal script is executable
and its timer is enabled.

scripts/renew_certificates.sh was committed without the executable bit,
so wrolpi-cert-renew.service failed with "Permission denied".  Set the
bit and make repair.sh restore executable bits on all scripts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 16:00:43 -06:00
Roland
fdf65e218e
repair.sh: heal Postgres cluster ownership non-destructively.
The only self-service fix for a permission-broken cluster was
reset_api_db.sh, which drops the database.  Repair now restores
postgres ownership of /var/lib/postgresql and /etc/postgresql and
restarts Postgres before configuring the database, same as the reset
script but keeping the data.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:30:30 -06:00
Roland
9f2d9ed82b
repair.sh: key the Portable postgres ownership restore off the active bind.
Portable is defined by the live boot medium (wrolpi-bootstrap MODE), and
its visible effect at repair time is the bind of the cluster over
/var/lib/postgresql.  Restore postgres ownership only when that bind is
active, and restore the bind targets themselves; on installed systems a
config/postgresql directory is inert (dead weight from the old layout,
or a user's backup on the drive) and must not be touched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 15:39:03 -06:00
Roland
756c063d15
Upgrade/repair: a down database must never block the pipeline.
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Controller Tests / controller-tests (push) Waiting to run
Jest Tests / jest-tests (push) Waiting to run
scripts/upgrade.sh ran `main.py db upgrade` under set -e before
repair.sh, so a broken Postgres bricked the very upgrade carrying the
fix for it (seen live: the bootstrap bind-mount fix could not be
delivered because the binds had already killed the cluster).  repair.sh
had the same problem with initialize_api_db.sh and the superuser psql.

Warn and continue instead: the migration is retried by
initialize_api_db.sh during repair, and if the database is still down
afterwards the API reports it loudly while the system-level work
(configs, services, bootstrap refresh) still completes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:40:47 -06:00
Roland
9d778d25a5
repair.sh: refresh the Portable bootstrap script on installed boxes.
Boxes installed from the Live USB run /usr/local/sbin/wrolpi-bootstrap.sh
at every boot, but nothing ever updated it after image build — fixes to
the bootstrap (e.g. Postgres bind-mounts on live boots only) could never
reach installed systems.  Copy the script (and its unit, if installed)
from the repo during repair, which both upgrade.sh and install runs
execute.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:34:40 -06:00
Roland
da35d39d53
repair.sh: keep Postgres ownership of config/postgresql on bootstrapped systems.
On bootstrapped (portable) systems the cluster lives under
/media/wrolpi/config/postgresql, bind-mounted over /var/lib/postgresql;
the recursive chown of the config directory swept the cluster to
wrolpi:wrolpi, breaking new backends.  No-op on normal installs where
that directory does not exist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 13:31:38 -06:00
Roland
719905cb77 Moving WROLPi-managed mounts off /etc/fstab onto a drive-resident fstab.yaml.
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>
2026-05-24 12:44:05 -06:00
Roland
e0324ce0fc Migrating Controller from port 8087 to 80 so that it is more discoverable. Reformatting Controller dashboard to make header at the top. Reformatting navigation bar to handle the new links which prevented Admin from being visible in some screens. 2026-04-09 15:01:05 -06:00
Roland
b8a532c8d9
Key changes:
- Add MapLibre GL JS viewer with light/dark themes, terrain layer,
  and embedded static map page for coordinate sharing
- Add map pin support with persistent map_pins.yaml config
- Add map region subscriptions and downloader with progress tracking,
  sourcing pre-extracted PMTiles from a cold CDN
- Add map export to PNG and GPS coordinate copy via context menu
- Add MapViewer React component alongside reworked Map page
- Add map catalog and region-building script (build_map_regions.py)
- Remove OSM import/render scripts, leaflet files, Apache configs,
  renderd service, and osm-map-dumper Docker container
- Update Caddyfile to serve PMTiles and map static assets
- Update build/install/repair scripts for Protomaps dependencies
- Drop map_file table, add/drop map_subscription table (alembic)
- Adding button to skip primary drive mounting during onboarding.
2026-04-04 13:48:19 -06:00
Roland
fbb71e012d Replace nginx with Caddy for TLS termination and reverse proxying.
- Replace nginx with Caddy as the web server for both Docker and native
  (RPi/Debian) deployments. Caddy handles HTTPS termination, reverse
  proxying to all services (API, React app, Controller, Kiwix, Help, Map),
  and serves the media/download file browsers.
- Generate a Root CA and signed leaf certificates for HTTPS. The Root CA
  uses Name Constraints to restrict signing to local addresses only
  (.local, localhost, and RFC 1918 private IP ranges), mitigating MITM
  risk if the CA key is compromised.
- Add an HTTP landing page that guides users through one-time CA
  certificate installation on iOS, Android, macOS, Windows, Linux, and
  Firefox, with auto-redirect when the cert is already trusted.
- Add trailing-slash redirects for /controller, /docs, /media, and
  /download so bare paths don't fall through to the React catch-all.
- Add a Root CA download link on the Settings page under Configs.
- Update help.sh diagnostics for IPv6 listener detection and HTTP/2
  status codes.
- Remove all nginx config files and the https_proxy Docker container.
- Bump wrolpi-help submodule with certificate documentation.
2026-03-07 18:48:44 -07:00
Roland
1deaeb3144
Clearing cached python files in repair.sh. Creating reset_virtual_environments.sh script which deletes the virtual environments and rebuilds them (requires internet)
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Controller Tests / controller-tests (push) Waiting to run
Jest Tests / jest-tests (push) Waiting to run
2026-02-28 16:11:12 -07:00
Roland
f375e4c955
Generating nginx certs on first startup. 2026-02-24 17:14:56 -07:00
Roland
50782f47cc
Checking that map dump is valid before building image. Protecting map
dump file in built image so that it cannot be modified.
2026-02-15 14:49:19 -07:00
Roland
0e5c977471
Creating WROLPi Controller desktop icon. 2026-02-14 17:05:57 -07:00
Roland
b9158b60fe Creating Controller service which will control WROLPi services, admin, SMART, disks, and status. nginx will fallback to Controller UI when React UI is unavailable (such as during upgrades). 2025-12-15 15:48:40 -07:00
Roland
a1e0fb4e92 Creating a new Collection model for use in organizing FileGroups.
Replacing Channels and Domains with Collection.

Moving downloading relationships from Channel to Collection.

Created domains.yaml config to manage tagged domain collections.

Adding turn-key upgrade solution in the UI.
2025-12-06 15:11:37 -07:00
Roland
24a0646f99
Updating MOTD to suggest ip addresses on login.
Some checks failed
CodeQL / Analyze (push) Has been cancelled
2025-08-18 11:33:44 -06:00
Roland
da693f03cf
Build frontend app in repair script now that built app is restored. 2025-03-23 09:59:13 -06:00
Roland
af5d4dfd51
Disabling JIT for map database as recommended by mod_tile because of excess memory usgae. 2025-01-11 11:06:01 -07:00
Roland
11bdbc4dc7
Creating map db cluster in pi-gen build. Removing duplicate pgpass creation. 2025-01-04 10:51:14 -07:00
Roland
3285887a1b
Repairing openstreetmap-carto project.mml in case the port is removed by git, restarting renderd after map services are configured. 2025-01-01 17:58:33 -07:00
Roland
7324e83626
Creating script which migrates map database to new database for custom niceness. 2025-01-01 16:41:21 -07:00
Roland
7d6b7114a7
Adding TRACE level for logging beyond debug. Reworking async_client for tests, not all tests need the App to handle signals, so any test that needs signals can use the new await_switches fixture which primes the app. 2024-12-11 11:52:33 -07:00
Roland
2dd243230f
Adding two MOTD, one for before repair, one that is moved in after repair is successful. 2024-10-16 18:57:08 -06:00
Roland
218fb3e760
Replacing Tag names with escaped tag names to avoid errors in Tags Directory. 2024-10-16 18:56:10 -06:00
Roland
2ad363281f
Delaying map startup because it prevents the API from starting. 2024-10-16 18:56:00 -06:00
Roland
00c4e6ece0
nginx needs to serve downloads from the media directory. 2024-10-02 17:10:05 -06:00
Roland
2f43b34b79
Moving wrolpi user creation above usage. 2024-10-01 11:29:45 -06:00
Roland
0a2a8c0702
Enable apache headers during repair. 2024-06-18 16:58:53 -06:00
Roland
0640cdafda
Removing pi user requirement from pi build process and repair script. 2024-06-17 17:59:49 -06:00
Roland
305764ee38
Removing duplicate 000-default.conf config file copy. 2024-06-13 09:44:34 -06:00
Roland
38de2f9b40
Moving HTTP kiwix/help services to 9085/9086 ports. Nginx will serve HTTPS proxy for kiwix/help services at the old ports. Modifying apache2 to use HTTPS to serve map.
Using new https_proxy docker container to serve HTTPS for zim, map and help.
2024-06-12 21:38:51 -06:00
Roland
56d430df22
Making config directory during repair, but only if the media directory is mounted and empty. 2024-03-16 19:57:01 -06:00
Roland
ef94f2446a
Restart wrolpi help after repair as completed. 2023-11-22 12:36:00 -07:00
Roland
57262442bd
Installing WROLPi help environment for RPi and Debian. 2023-11-22 11:21:49 -07:00
Roland
a491ea7677
RaspberrypiOS now matches location of serve on Debian 12. 2023-11-22 10:47:10 -07:00
Roland
aabe57eac9
Adding wrolpi directory to global git safe directory. 2023-10-09 13:17:12 -06:00
Roland
8230174fcd
Map initialization script will only ask to reset if the map database exists. 2023-10-09 12:20:51 -06:00
Roland
cc6d20dac6
repair.sh will initialize map database if it does not exist. 2023-10-09 12:14:38 -06:00
Roland
7fd8a8527a
Reworking Debian install with real system. 2023-10-03 19:21:12 -06:00
Roland
6e0f9a999c
Combining install scripts into a new repair script. Removing create_db script in favor of repair script. 2023-10-03 12:15:29 -06:00