Commit graph

2158 commits

Author SHA1 Message Date
Roland
715035e5dc
help.sh: port 80 is the Controller's, not Caddy's; fix IPv4 port regex.
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>
2026-07-09 16:00:53 -06:00
Roland
0e67cd6c74 help.sh: only print OK/FAILED/Note lines; robust hostname SAN check.
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>
2026-07-09 16:00:43 -06:00
Roland
e42e4ed06e Controller: add help.sh to the Maintenance Scripts.
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>
2026-07-09 16:00:43 -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
b172a4c9d6 Calculators: never store NaN as the Kelvin temperature.
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
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>
2026-07-09 15:27:34 -06:00
Roland
53f9386c85 Theme: fix white corner cell on inverted definition tables.
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>
2026-07-09 15:27:34 -06:00
Roland
ee0e85a69c Calculators: add 4-Wire Kelvin segment to the Electrical Calculator.
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>
2026-07-09 15:27:34 -06:00
Roland
b98ebf2a7f
Hotspot: guard on all three binaries so iw/rfkill install independently.
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>
2026-07-09 07:51:23 -06:00
Roland
93d9af1480 Hotspot: ship dnsmasq-base; NetworkManager requires it for hotspot DHCP.
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>
2026-07-09 07:51:13 -06:00
Roland
cdd28d0a39
Videos: a Channel's directory beats source_id when assigning a Video.
Some checks failed
CodeQL / Analyze (push) Has been cancelled
Controller Tests / controller-tests (push) Has been cancelled
Jest Tests / jest-tests (push) Has been cancelled
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>
2026-07-07 18:33:15 -06:00
Roland
f65f0aca6b
Restore "Most Views" download order
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>
2026-07-07 16:34:50 -06:00
Roland
6730235b7d
Bump version to 0.27.1-beta
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-07-07 07:55:29 -06:00
Roland
ce05a073dd
Upgrade: mark the pinned GPG key trusted in the throwaway keyring.
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
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>
2026-07-06 20:57:01 -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
3180a29e82
help.sh: real Postgres cluster diagnostics.
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>
2026-07-06 20:30:30 -06:00
Roland
51a12b199e
Controller: keep every wrolpi-* service visible in the UI.
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>
2026-07-06 20:30:30 -06:00
Roland
910503b641
Reconciler: uid/gid and ntfsfix for NTFS secondary mounts.
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>
2026-07-06 20:30:29 -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
4dfb997bfc
Focusing on lone input when opening modals. 2026-07-06 15:35:23 -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
f500600025
Bootstrap: Postgres bind-mounts on live boots only.
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>
2026-07-05 15:33:54 -06:00
Roland
2e660cdbb9
Disks: mount the primary drive directly; clean phantom fstab.yaml entries.
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>
2026-07-05 15:05:20 -06:00
Roland
c25902b92a
Disks: allow persisting the primary mount from the UI; clean NTFS before mounting.
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>
2026-07-05 14:50:56 -06:00
Roland
85bd5619fa
Open in Slicer: launch slicer programmatically from dropdown items.
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
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>
2026-07-05 13:31:40 -06:00
Roland
a580317911
File preview: Open in Slicer button for STL/3MF files.
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>
2026-07-05 13:31: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
7bf7d9aa66
reset_api_db.sh: heal Postgres cluster ownership before resetting.
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>
2026-07-05 13:31:38 -06:00
Roland
eefebe1788
Upgrade: make apt installs non-interactive and heal interrupted dpkg.
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>
2026-07-05 13:09:16 -06:00
Roland
02eb8d70e4
Caddy install: run gpg --dearmor with --batch --yes.
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>
2026-07-05 13:03:11 -06:00
Roland
e6ace5942b
Disambiguate 'git checkout release' in upgrade.sh.
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>
2026-07-05 13:03:11 -06:00
Roland
e8c6d8415b
Certificates: only include SANs permitted by the Root CA name constraints.
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>
2026-07-05 12:38:08 -06:00
Roland
7d6a77a54e
Merge branch 'master' into release 2026-07-04 09:25:52 -06:00
Roland
651e45b414
Bump version to 0.27.0-beta 2026-07-04 09:21:17 -06:00
Roland
43dac5d63d
Controller page: group hotspot settings in a segment below the toggles.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 09:16:44 -06:00
Roland
2887309aae
Hotspot: address review comments.
* 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>
2026-07-04 09:04:17 -06:00
Roland
0d2c13e4e7 Controller: move hotspot settings to controller.yaml with WiFi device dropdown.
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>
2026-07-04 09:03:58 -06:00
Roland
ebbc60992c
Docs: recommend Raspberry Pi Imager instead of Etcher.
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
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>
2026-07-03 18:58:54 -06:00
Roland
75cc0bbe6c
Help: update wrolpi-help submodule with doc screenshots.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 18:58:09 -06:00
Roland
ab6b0fae12
Help: update wrolpi-help submodule with new docs.
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>
2026-07-03 18:35:00 -06:00
Roland
68fab29a4b
Channels: log directory video counts after a refresh
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>
2026-07-03 17:45:10 -06:00
Roland
64b1ca8029
Channels: fix statistics inflated by tagged videos
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>
2026-07-03 17:29:24 -06:00
Roland
8044fd1240
File Browser: add filter input and collapse-all button
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
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>
2026-07-03 11:31:41 -06:00
Roland
2162396212
Module pages: always show the deep-search reminder during fast searches
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>
2026-07-03 10:50:57 -06:00
Roland
73fafcd645
Search: fast ABC mode by default, opt-in deep search
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>
2026-07-03 10:32:16 -06:00
Roland
d364cf10ec
Merge branch 'master' into release
# Conflicts:
#	wrolpi/version.txt
2026-07-02 17:51:52 -06:00
Roland
1522022330
Bump version to 0.26.0-beta
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-07-02 17:30:14 -06:00
Roland
a035945033
Flasher: show connect error next to the detect button too
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>
2026-07-02 17:26:45 -06:00
Roland
c7e77f3a4d Address Greptile review: fix chip-id map (17=C5, 18=P4) and drop stale mock
- 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>
2026-07-02 17:26:37 -06:00
Roland
c0fb9f95e1 Flasher: chip-mismatch guard, WROLPi firmware table, colorblind chip colors
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>
2026-07-02 17:26:37 -06:00