The Caddyfile only binds 443/8084/8085/8086 and reverse-proxies to the
Controller on port 80, which the Controller health check already covers.
The IPv4 alternative in the port grep was missing its colon, so only the
IPv6 ":::" listeners ever matched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Connect to Postgres directly instead of via `sudo -i -u wrolpi` so
sudo/PAM stop spamming the journal when help.sh runs as a systemd
service. Drop the raw pg_lsclusters table and the per-file PMTiles
listing. Use `openssl x509 -checkhost` for the hostname SAN check so a
short hostname (e.g. "pi") cannot substring-match another SAN and
wrapped SAN lines cannot cause a false failure.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A new wrolpi-help-script.service oneshot runs /opt/wrolpi/help.sh so the
Controller UI can run system diagnostics and show the output from the
journal. The confirmation modal hides the warnings block for scripts
with no warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Clearing the temperature input stored parseFloat('') = NaN, rendering
every Kelvin cell as NaN and persisting the invalid value in localStorage
across reloads. Clearing now falls back to room temperature, non-numeric
input is ignored, and a bad stored value decodes to the default.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Semantic UI paints a definition table's empty corner cell white to fake a
"cut-out" corner against a white page. Our dark theme has a dark page, so
the corner rendered as a white square. Override the background and border
box-shadows to transparent for inverted tables only.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Shows the expected millivolt drop across a single wire at 1A, 5A, and 10A
so a wire can be verified with a known current and sense leads. Shares the
length, units, and wire type with the Power Loss calculator (state lifted
to the parent, same localStorage keys). Length is not doubled because the
sense leads measure across one conductor.
Includes a wire temperature input (default room temperature, °F/°C follows
the SAE toggle, stored canonically in Celsius) which corrects resistance
using the material's temperature coefficient.
Adds Solid/Stranded Aluminum wire types with published resistance tables
for both unit systems, and aluminum's 0.403%/°C coefficient.
Fixes erroneous copper resistance values: solid 6 AWG was 0.3134 instead
of the published 0.3951 Ohm/kft, and stranded 0/2/4 AWG were decimal typos
(0.022/0.019/0.038 instead of 0.122/0.194/0.308 - the table is solid x
1.24 throughout). New tests enforce monotonicity and ratio consistency.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On systems that already have dnsmasq (e.g. Ubuntu-style installs), the
previous guard skipped the whole block, silently leaving iw and rfkill
uninstalled. apt-get is a no-op for packages already present, so one
combined install stays idempotent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The amd64 ISO is built with --apt-recommends false, so network-manager's
Recommends (including dnsmasq-base) were never installed. NetworkManager's
shared/hotspot mode spawns dnsmasq for DHCP, so the hotspot failed to
activate on every ISO install. Add it to the package list (which
install_debian_12.sh also installs from) and as an idempotent upgrade.sh
step so existing installs are fixed on their next upgrade.
Also add iw and rfkill, the standard WiFi diagnostic tools.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A user reported videos placed in a Channel's directory did not appear on
that Channel's page. Their info json named another existing Channel, and
get_channel preferred the source_id match over the directory, so the
videos were assigned to the other Channel.
Reorder get_channel so the directory match (with its parent-directory
walk) runs before the source_id/url matches. A Video's files must live
in their Channel's directory, so the directory reflects the user's
intent and beats yt-dlp metadata. This also aligns validation with
VideoDownloader._get_channel, which already prefers the destination
directory, and stops validation from clobbering that decision.
channel_id still wins over all other params; source_id/url still apply
when the file is not in any Channel's directory.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
yt-dlp 2026.7.4 (via upstream PR #16965) again extracts view_count from
YouTube's lockupViewModel channel/playlist listings, so the 'views'
download order deprecated in fcb64fed can come back — verified live
against a real channel listing.
Unlike the old implementation, the sort degrades gracefully:
- Entries missing view_count sort last instead of raising KeyError.
- If NO entry has a view_count (yt-dlp older than ~2026.07), the
download fails cleanly with UnrecoverableDownloadError telling the
user to update yt-dlp or switch to Newest/Oldest — no retry loop.
Listing counts are approximate ("3K" -> 3000), which is fine for
ordering. No requirements.txt change needed; the >=2026.03.17 range
picks up 2026.7.4 automatically.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
verify-commit judges signature validity regardless of ownertrust, so the
upgrade's security model (possession of the pinned key is the trust
anchor) is unchanged — but gpg printed "WARNING: This key is not
certified with a trusted signature!" on every healthy upgrade, making
valid-signature logs look like something continued past a failure.
Import ownertrust for the sole key in the keyring to silence it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
The umbrella postgresql.service reports active with every cluster down,
which made two cluster failures look green. Show pg_lsclusters, check
the first cluster is online, verify the data directory is under
/var/lib/postgresql, and reveal a mount over /var/lib/postgresql (the
Portable bind layout — wrong on an installed system).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
show_only_when_running hid wrolpi-upgrade the moment it exited — exactly
when its logs matter after a failed upgrade — and hid wrolpi-api-dev
whenever it was stopped. Reinterpret the flag: hide a service only when
its unit is not installed on the box (LoadState=not-found). Discovery
now unions loaded units (--all, catching transient and exited units)
with installed unit files, and discovered services are listed regardless
of running state.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
QA on 10.0.0.8 (mount-issues.md findings 2 and 3) showed the reconciler
path missed both NTFS accommodations that mount_drive() and
add_fstab_entry() already have: secondary NTFS drives mounted owned by
root (writable only via ntfs-3g's permissive default), and a dirty NTFS
volume would fail to mount at boot with no cleanup attempted.
Inject uid/gid for ntfs/ntfs3 in the reconciler, and run ntfsfix -d in
SubprocessMountExecutor before NTFS mounts (translating UUID=/LABEL=
specs to /dev/disk/by-* paths, which ntfsfix needs). Also return an
error MountResult instead of raising when a binary is missing, per the
executor protocol.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
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>
wrolpi-bootstrap.service applied the /media/wrolpi/config/postgresql
bind-mounts over /var/lib/postgresql and /etc/postgresql at every boot,
including on installed systems. The binds exist for live boots, where
the root filesystem is an ephemeral overlay; on an installed system they
put the cluster in the path of repair.sh's config chown
(pg_filenode.map permission errors) and, once the user mounts an
NTFS/exFAT primary drive over /media/wrolpi, point Postgres at a
filesystem that cannot host a data directory at all — no clusters,
connection refused, while the umbrella postgresql.service stays green.
Installed systems now keep Postgres at the normal Debian paths on the
root filesystem. A box that still has a stale bind active from an old
boot gets a log note to reboot; on the first clean boot the existing
pg_createcluster/initialize_api_db logic builds the root-disk cluster
automatically and the database rebuilds from configs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
POST /disks/mount wrote every mount into fstab.yaml and waited for the
reconciler — but the reconciler skips /media/wrolpi as a reserved mount
point, so mounting the primary drive from the Disks page could never
succeed and left a phantom fstab.yaml entry behind (visible as
"1 desired, 1 skipped: reserved mount point" in the journal). Mount the
primary drive directly via mount_drive() and persist it to /etc/fstab,
as onboarding does.
Phantom entries are now also invisible to the fstab list endpoint (they
were reported as active persistence) and are removed whenever the
primary mount is mounted, persisted, or un-persisted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Disks page treated /media/wrolpi as fully protected, so a
manually-mounted primary drive could not be persisted from the UI — and
the Persist toggle would have been wrong anyway: it wrote fstab.yaml,
which the wrolpi-mounts reconciler refuses to apply for the primary
mount (RESERVED_MOUNT_POINTS). Route Persist for the primary mount to
the host's /etc/fstab (as onboarding does), report its entry in the
fstab list endpoint, and enable the toggle in both UIs while keeping
Unmount blocked. Mounts outside /media (e.g. /boot/efi, which showed
an Unmount button) are now entirely untouchable System mounts.
Also:
- Run `ntfsfix -d` before mounting ntfs/ntfs3 volumes so the Windows
dirty flag (Fast Startup, unclean unplug) cannot block mounting;
non-fatal and a no-op on clean volumes.
- Add uid/gid fstab options for ntfs/ntfs3, matching mount_drive().
- Log reconcile diagnostics (desired/already-mounted/skipped with
reasons) so an idle wrolpi-mounts run explains itself in the journal.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Semantic UI may activate a Dropdown.Item from the keyboard without
following its href, which would save the slicer preference without
launching the slicer. Prevent the default anchor navigation and call
window.location.assign() so every activation path behaves the same.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a split button to the STL and 3MF preview toolbars that opens the
model directly in a desktop slicer via its URL scheme
(scheme://open?file=<absolute media URL>). The slicer downloads the
file itself over the network.
Offers Orca Slicer, Bambu Studio, and Cura; the last-used slicer is
remembered in localStorage and becomes the split button's default,
since browsers cannot detect which protocol handlers are installed.
PrusaSlicer is deliberately omitted: it only downloads from domains
whitelisted by Prusa (printables.com, thingiverse.com, cults3d.com),
so it cannot fetch from a WROLPi.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
If cluster files lose postgres ownership (stray recursive chown,
interrupted install), the postmaster stays up but every new backend
dies with FATAL: could not open file "global/pg_filenode.map":
Permission denied — and dropdb/psql cannot work either. Restore
ownership of /var/lib/postgresql and /etc/postgresql and restart
postgres before recreating the database, and start the API when done.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The caddy package install hit a dpkg conffile prompt because repair.sh
already wrote /etc/caddy/Caddyfile; under the tty-less
wrolpi-upgrade.service the prompt got EOF on stdin and apt exited 100.
Run the caddy/samba/gnome-disk-utility installs with
DEBIAN_FRONTEND=noninteractive and --force-confdef/--force-confold
(keep the current conffile; repair.sh manages the Caddyfile anyway),
and run `dpkg --configure -a` first so a box left half-configured by a
previously interrupted run is healed automatically.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
If the caddy keyring file already exists (left by a prior partial
install), gpg prompts to overwrite via /dev/tty, which fails under the
tty-less wrolpi-upgrade.service ("cannot open '/dev/tty'") and aborts
the upgrade. --batch --yes overwrites silently, making the caddy
install idempotent in all four scripts that share this pattern.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same fix install.sh received in bf88ac9f: on a box with no local
release branch (installed before that patch), the tracked release/
directory makes a bare `git checkout -f release` fail with
"could be both a local file and a tracking branch", aborting upgrades.
`checkout -f -B <branch> origin/<branch>` is unambiguous and already
resets the branch to origin, replacing the separate reset --hard.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Global IPs from `hostname -I` (e.g. routable IPv6 assigned via SLAAC)
violated the Root CA's critical nameConstraints, failing leaf verification
with "permitted subtree violation" (error 47) and aborting
install/repair/upgrade. Filter SAN candidates, including EXTRA_SANS, to the
permitted private ranges and log skipped entries; normalize the IP:/DNS:
prefix case in EXTRA_SANS since openssl rejects lowercase prefixes.
Also guard the service stops in upgrade.sh so upgrading a box whose
install died before the units were installed can still recover.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* POST /api/hotspot/settings checks is_docker_mode() directly instead of
string-matching the error message (still 500, matching the sibling
hotspot/bluetooth/throttle endpoints).
* Escape \ ; , " in the WiFi QR code per the WPA QR format.
* Skip the controller.yaml write when a settings update changes nothing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hotspot was reported as "not supported" on x86 machines (Ubuntu/Debian PCs)
because the Controller only watched the hardcoded default wlan0 device, while
predictable interface naming produces names like wlp2s0. The Settings page
saved hotspot_device to wrolpi.yaml, which the Controller never read.
The Controller now owns hotspot settings in controller.yaml:
* New endpoints: GET/POST /api/hotspot/settings, GET /api/hotspot/devices
(WiFi interfaces discovered via nmcli).
* Legacy hotspot_* values in wrolpi.yaml are migrated into controller.yaml
once on startup, unless controller.yaml already sets them.
* The Hotspot SSID/password/device fields moved from the Settings page to the
Control page next to the Hotspot toggle; the device is now a dropdown of
the WiFi devices found on the machine.
* The Controller fallback UI gets the same hotspot settings form for parity.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Etcher is privacy-invasive; replace it in INSTALL.md, the USB upgrade
tool messages, and the live-boot ephemeral warning.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds Tags, Configs, FileGroups, Downloads, and Upgrade docs, the
WROLPi Portable section in Getting Started, and a synced TOC.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
After a Channel refresh, log how many video/audio FileGroups are in the
Channel's directory, and how many are assigned to this Channel, other
Channels, or no Channel. Helps debug Videos missing from their Channel
after files are moved.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tag_file join in Channel.get_statistics() produced one row per tag,
so a Video with N Tags was counted N times in video_count, size, and
length. Count tagged Videos in a subquery instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a client-side filter above the File Browser table. Typing reduces
the visible rows to files/directories whose name matches, plus their
parent folders. A folder whose own name matches keeps its entire
subtree, and opening a matching folder shows its contents unfiltered.
* 'f' focuses the filter input, like the search inputs on other pages.
* An attached X button clears the filter; disabled while empty.
* A collapse-all (angle double up) button closes all open folders,
clearing the ?folders= query param and deselecting hidden children.
* The filter row is right-aligned and double-width on desktop, and
fills a single row on mobile.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The DeepSearchHint only appeared on zero results, so a fast search with
matches gave no indication that captions/contents were not searched.
Module searches have no deep estimate, so show a generic reminder
whenever a fast search is active: "More results may be available in
deep search." with results, "Not finding what you need?" without.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The file_group.textsearch tsvector indexes all four text columns;
d_text (captions, document text, zip contents) is ~90% of the data,
making ts_rank searches take seconds on large libraries. Add a
textsearch_abc generated column (title/author/description only, ~8x
smaller) and default all searches to it, with deep=true opting into the
full column.
Database:
- Migration adds the generated column and a GIN index (rewrites the
table; a few minutes on large libraries).
Backend:
- FilesSearchRequest, VideoSearchRequest, ArchiveSearchRequest, and
DocSearchRequest accept `deep`; each search function picks the column.
- /api/search_file_estimates returns both counts in one index-bound
query (COUNT FILTER on the abc subset): `file_groups` (fast) and
`file_groups_deep`.
UI (one coherent story across the count surfaces):
- The typing dropdown always suggests the deep count (what is findable).
- The tab badge shows the active mode's count so it matches the result
list.
- The gap is reconciled by a hint offering deep search: count-aware on
the global search ("No results in fast search. N results available in
deep search." / "N more results available..."), generic on the module
pages (no deep estimate there). The hint resets pagination.
- "Search Depth" toggle in the search filter modals, persisted as
?deep=true.
Tests that relied on d_text matches now pass deep=True, with fast-mode
assertions alongside. Also skip flaky test_download_channel on
CircleCI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A failed "Filter files by detecting device" only surfaced the download-mode
reminder down in the Connect segment, off-screen. Extract the message into a
shared renderer and show it both places, so the error appears right where the
user clicked on the "Choose from your WROLPi" tab.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ESP_CHIP_ID_NAMES disagreed with the backend ESP_CHIP_IDS: it mapped 18 to
ESP32-C5 (18 is ESP32-P4) and omitted 17 entirely, so C5 firmware showed as
"chip id 17" and P4 firmware was mislabeled "ESP32-C5". Now matches the
backend (17=ESP32-C5, 18=ESP32-P4), with a test guarding it.
- Removed the stale filesSearch import/mock/beforeEach from the test; the
flasher no longer calls it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Prevents the wrong-chip flash that bricks a board (e.g. an ESP32-S2 bootloader
in an ESP32 set): after connecting, each selected file's ESP image header is
checked against the device's chip_id and the offending row is flagged (a
warning, not a block). Changing the firmware selection now disconnects so the
check is always re-run against a freshly detected chip.
- "Choose from your WROLPi" is now a table (File / Path / Size / Chip / Kind
/ add), served by a single /flasher/search call
- search_esp_firmware returns every .bin annotated when unfiltered (non-ESP
parts like partition tables, boot_app0, and littlefs stay pickable); a chip
filter still narrows to that chip's ESP images
- Detect-device filter now searches by the canonical chip name (CHIP_NAME,
e.g. "ESP32") instead of the variant description ("ESP32-D0WD-V3"), which
matched nothing
- Chip column uses deterministic, colorblind-safe colors; same-width chip
names always get distinct colors, and the palette has headroom to grow
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>