Filtering media was keyed on `data-theme="night"`, which made it a property of
the theme that no user could turn off. Some people want their media tinted to
match the interface; some want to see the actual image.
A theme now declares a filter and whether it starts on, and the user decides:
night night-red on by default. An unfiltered thumbnail undoes dark
adaptation, which is the whole point of the mode.
amber amber-mono off by default. Amber is a look, not a night-vision
aid, so media keeps its real colors until asked for.
light - no filter offered, so no toggle is shown.
dark -
The choice is stored per theme, so wanting amber's tint says nothing about
wanting night's. The toggle lives in the theme picker, which puts it on the
Settings page and in the gallery at once.
Mechanically the rules now key on a `data-media-filter` attribute rather than
`data-theme`: a rule keyed on the theme could not be turned off. index.html
stamps that attribute before first paint alongside data-theme -- without it,
night mode paints a screen of unfiltered thumbnails while the bundle arrives
over a hotspot, costing the user the adaptation the theme exists to protect.
Tests cover both the attribute rule and the fact that the inline script's
duplicated ids and defaults still agree with names.ts.
The amber matrix is the same luminance collapse tinted to #ff9500 (red full,
green 0.584, blue zero) with a gentler gamma than night's: amber carries far
more luminance and clips to a flat wash otherwise.
The gallery gains a Media filtering section showing the WROLPi logo filtered,
beside an unfiltered copy that opts out with `night-unfiltered`. That copy is
behind a click on purpose -- an unfiltered image is a bright patch, and night
mode should not spring one on someone reading in the dark.
ui-design.md gains the media filtering contract it never got in Phase 0,
including the note that a Phase 3 custom theme names an existing filter id and
cannot ship its own matrix, since that would be code rather than data.
Phase 2, part 1. Three files leave Semantic UI (baseline 75 -> 72), and the
components they needed join the library and the gallery:
Header Semantic's shape (as/icon/subheader/dividing). 47 files use
<Header>, and the level now picks the size, so the type scale
stays a scale instead of each call site choosing a font size.
Message gains an icon and a dismiss button.
ActionInput input and button sharing one outline, for <Input action={...}/>.
Loading centered loader with a caption, replacing the
<Loader active inline='centered'> + wrapper div in 19 places.
Modal becomes compound - Modal.Header/.Content/.Actions, plus
Semantic's open, closeIcon, and size names - because 34 files
are written against that shape.
test-utils mounts MantineProvider with ThemeProvider's exact configuration:
migrated components render Mantine internals and every one of their test files
would otherwise need its own provider.
One Semantic bleed found and fixed: semantic.min.css sets
`h1,h2,h3,h4,h5 { font-family: Lato }` and loads after Mantine's element rule,
so headings ignored the theme's body face. Only visible in amber, where the
body is mono and the headings were not. A class-level restatement outranks it;
the rule comes out when Semantic does.
Verified: 925 jest tests, clean production build, and a night-mode audit of
every painted pixel in the gallery (red-only, one known false positive). The
Configs backup and restore flow was exercised against the real API.
Addresses the automated review on #583; all three were real.
The night filter only applied to elements tagged `.media`, and nothing in the
app carried that class - so selecting Night filtered the interface but left
every video, thumbnail, PDF and map full-colour, which is precisely the thing
the mode exists to prevent. The Settings copy promised otherwise, so it was
also wrong.
It now targets the element types instead of an opt-in class, because night mode
has to fail safe: a library of thousands of files reaches the screen through
many components, and one that forgot to opt in would undo half an hour of dark
adaptation. `.media` still works for wrappers that must be filtered as a unit,
and `.night-unfiltered` opts out. The filter is restated on `:fullscreen`, which
a filter on an ancestor stops reaching. Verified on the videos page: all 25
media elements filtered.
Three of the five theme choices rendered a fallback glyph and logged errors,
because the icon map was built by grepping literal name='...' strings and the
navigation picker passes its icon through a variable. Adds sun, moon and
lightbulb, plus a test asserting every theme choice resolves to a real icon.
The pre-paint script wrapped the `matchMedia` fallback in the same try as the
storage read, so a device with storage blocked painted light even when it
prefers dark - recreating the flash the script exists to prevent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The gallery at /theme-sample is now a normal route rather than a development
one, and Settings links to it, so a user can see what a theme actually looks
like before committing to it. Every new component belongs in the gallery: it is
how the design gets reviewed, and how a component that only looks wrong in one
of the four themes gets caught.
Settings gains a Theme section with the new ThemePicker, which explains what
each theme is for - notably that night mode filters videos, documents and maps
to red as well, not just the interface.
Theme names and picker options move to themes/names.ts so the component library
can read them without importing components/Theme.tsx, which still pulls in
Semantic UI. Theme.tsx re-exports them, so existing call sites are unchanged.
The navigation bar's compact picker now reads the same list, so the two cannot
drift apart.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds src/components/ui: buttons, messages, labels, progress, status, tables,
form controls, panels, cards, statistics, modals, tooltips, tabs, and icons.
Everything resolves its colors through theme tokens, so no component branches
on the current theme, and Semantic UI keeps rendering the app meanwhile.
Mantine is bridged rather than restyled: its own CSS variables are pointed at
our tokens. Four details were not obvious, and each caused a visible defect
in night mode before being fixed:
- Structural colors must go in the resolver's light/dark blocks. Mantine
emits its values under [data-mantine-color-scheme], which outranks the
:root block that `variables` becomes, so its grays were winning.
- The dark and gray palettes are mapped shade by shade to tokens. Mantine
reads them directly for table borders, zebra stripes and input chrome;
they can be neither flattened nor left alone.
- Mantine writes --button-* as inline styles, which no stylesheet can
outrank, so the dashed danger treatment sets real properties instead.
- Switch colors are written onto inner elements, so they need scoped CSS
rather than a variable on the root. Adds a --knob token for control
handles, because Mantine's default thumb follows theme.white, which is
near-black in the dark-based themes.
An audit of every painted pixel in the /theme-sample gallery now finds no
non-red channel in night mode.
Icons come from @tabler/icons-react with the 102 Semantic names the app uses
mapped to components; they stroke with currentColor, so they follow the theme
without any rule of their own. Toasts keep the react-semantic-toasts-2
signature so those call sites migrate by changing an import.
TypeScript 5 is required: Mantine's declarations use `const` type parameters,
which 4.9 cannot parse (Mantine 7 is the same, so downgrading is no escape).
That needs two npm overrides to keep clean installs working, both verified
against a Docker build: react-scripts declares peerOptional typescript ^3||^4,
and fork-ts-checker 6.5.2 assigns typescript.performance.mark, which TS 5
made getter-only - crashing the dev server's type checker on every rebuild.
semantic-baseline.test.js ratchets the removal: it fails if a file not already
listed imports Semantic UI. An ESLint rule would have printed 75 warnings per
rebuild, and CI unsets CI= for its build step, so it would guard nothing there.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every color now resolves through CSS custom properties on
html[data-theme=...], so a theme is a table of values rather than code.
Four themes ship: light, dark, night (red hues only, to preserve dark
adaptation off-grid) and amber (monochrome amber terminal).
Fonts are exposed as two roles, --font-body and --font-mono, which a theme
assigns a bundled face to; amber uses the mono face. Faces must be
self-hosted, never fetched from a CDN, so the app keeps working offline.
Night mode also filters media that cannot consume tokens - video, images,
PDFs, embedded pages, canvases - through an SVG color matrix that maps
luminance onto the red channel. Tag such an element with `.media`; a filter
on an app-level ancestor would create a containing block and break fixed
positioning.
ThemeProvider persists the choice and index.html stamps data-theme from
localStorage before the bundle loads, so the first paint already matches the
saved theme on a slow connection. The OS preference is followed only while
"system" is selected; night and amber are explicit choices. The nav toggle
becomes a five-item picker.
Semantic components still render themselves, so only the shell is
token-driven for now; the inverted props stay in place until each component
is migrated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The toggle-hook de-duplication replaced a range of customHooks.js that also
contained these two unrelated hooks, breaking the production build. Restored
byte-identical from master; exports now match master exactly.
Jest missed this because it only compiles what tests import — `npm run build`
is what catches a missing export.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Hotspot, Bluetooth, Desktop, VNC and Throttle toggles had each been written
out longhand at four layers. Collapse the shared parts:
- useSubsystemToggle() replaces five near-identical hooks. This also fixes a
latent bug: only useDesktop/useVnc restored state when a request failed, so a
failed Bluetooth/Throttle/Hotspot toggle left at null and — since the
status poll sees no change — the toggle stayed disabled and read as
unsupported until it remounted.
- SubsystemToggle renders all five, with the Desktop's stop confirmation and
VNC's desktop gating as declared options rather than bespoke copies.
- The fallback UI's four update*/toggle* pairs become one config-driven
implementation (-100 lines).
- _raise_for_action() centralises the 13 endpoint failure checks, including
VNC's 409-vs-500 split.
Deliberately not shared: the six get_*_status_dict() builders (payloads
genuinely differ) and the per-subsystem mechanisms (nmcli/rfkill/sysfs/systemctl).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a VNC toggle to the Controller (both UIs) that starts/stops whichever VNC
backend is installed: wayvnc for Wayland (Raspberry Pi OS), RealVNC for X11.
Runtime-only, like the Desktop toggle: the unit is never enabled, so VNC does
not return after a reboot. Users who want it permanently on can enable it with
raspi-config.
VNC serves the desktop session, so starting it without a desktop gives clients
a blank screen. The gate lives in the backend, not just the UI, so curl callers
get it too (409, distinct from a systemctl failure). Stopping is never gated so
a running VNC server can't be stranded on with no way to turn it off.
Backend selection deliberately avoids raspi-config's live-compositor probe
(pgrep labwc), which reports X11 — and would pick the wrong backend — whenever
the desktop is stopped. wayvnc also requires its config to exist; that is
checked directly rather than via the unit's ConditionResult, which reports 'no'
for any unit that has never been started.
Extracts _unit_properties()/_systemctl_unit() so Desktop and VNC share one
systemd probe and start/stop path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stopping the display manager left the Pi's monitor black with only a blinking
cursor. The console VT is already in the foreground with a getty and a live
shell on it (verified: /dev/vcs1 holds the login banner and prompt) — the
display manager just hands the display back without the console being
repainted, so nothing is visible.
Activate the console VT, then bounce off another VT to force a modeset and
redraw, and print a short message telling the user the Controller stopped the
desktop and how to start it again.
Best-effort throughout: skipped when no getty is on the console VT, a chvt or
console-write failure never fails the stop, and being stranded on the repaint
VT is reported rather than claimed as success. Verified on 10.0.0.8 hardware.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Greptile P1: on failure the status poll never re-syncs (the desktop did
not change), so the toggle stayed disabled until remount.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a Desktop toggle to the Controller (both UIs) that starts/stops
display-manager.service. Fail open: the unit is never enabled/disabled,
so the desktop always returns on reboot; users who want it disabled
across reboots can set the default target themselves.
Rename the hotspot and bluetooth actions to reflect their real behavior:
- enable/disable_hotspot -> start/stop_hotspot (/api/hotspot/start|stop)
- enable/disable_bluetooth -> unblock/block_bluetooth (rfkill semantics,
/api/bluetooth/unblock|block)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add daily_limit_reached to the DownloadManager summary (/api/status):
True when due (new, non-recurring) downloads exist and every one of
them is blocked by the global or per-domain daily download limit.
Show a "Max Daily Downloads Reached" banner on the Downloads page when
set (and the manager is neither disabled nor stopped).
Previously a queue stalled by the daily limit looked broken: the UI
reported 0 pending with no explanation, and the skip reason was only
visible at DEBUG log level.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Reset a saved protocol the newly probed device does not support
instead of offering it (both UIs); a successful probe is
authoritative, the saved value is only kept when probing fails.
- Ignore out-of-order protocol probe responses when switching devices
quickly (both UIs).
- Verify the WPA2 rollback after a failed WPA3 activation; report that
the hotspot may be down instead of claiming the revert succeeded.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Probe the selected WiFi device with iw to determine which hotspot
protocols it can host: WPA2 requires AP mode; WPA3 requires SAE in AP
mode (SAE_OFFLOAD_AP, or a mac80211 driver with the CMAC cipher for
software SAE). The Raspberry Pi's brcmfmac supports neither, so Pis
correctly offer WPA2 only (verified against real Pi 4/Pi 5 iw output).
The protocol is saved as hotspot.protocol in controller.yaml.
enable_hotspot() now always sets the Hotspot connection's key-mgmt
explicitly (sae + required PMF for WPA3) so a reused profile cannot
keep a stale protocol, and reverts to WPA2 if the driver rejects SAE.
New GET /api/hotspot/protocols endpoint; protocol dropdowns added to
both the ControllerPage React UI and the fallback controller UI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The RPi 5 fan connector exposes a tachometer via the pwmfan hwmon
device. Report it as fan_rpm in cpu_stats and show it in both the
Controller fallback UI and the React Status page. Hidden when no fan
is present.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Support swapping in a backup drive: /api/disks/unmount now accepts the
primary mount (previously rejected as reserved). It stops wrolpi-api so
open files are released, then umounts directly since the reconciler
never manages the primary mount. The /etc/fstab entry is left in place
so a reboot restores the mount.
Both UIs now offer Unmount for the primary mount and warn that the API
service will be stopped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Controller listed every wrolpi-* service in one flat table, which was
confusing for new users who couldn't tell always-on infrastructure from
one-off/dev/maintenance units.
Services are now split into two groups computed once in the backend and
exposed as `service.group`:
* Core Services - persistent services enabled at boot (api, app, caddy,
kiwix, ...), the normal running system.
* Optional & Maintenance - one-off/dev/maintenance tasks (bootstrap, repair,
upgrade, *-dev, discovered wrolpi-*), plus persistent
services the user has disabled at boot (e.g. Samba with
no shares configured).
Rule: core iff kind == "persistent" and enabled; otherwise optional. Group
membership only changes when the user toggles Boot, never on start/stop or
polling, so rows stay put. Within each group, failed/stopped services sort
first so problems stay visible.
Feature parity across all three render paths: the React ControllerPage, the
server-side Jinja template, and the client-side poll refresh. Docker
containers have no group field and fall back to a single ungrouped list.
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>
The dashboard nudge is useless on Safari and other browsers that cannot
run the WROLPi extension. Share detectBrowser and gate the banner with
isExtensionSupportedBrowser.
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).
- 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.
Applies non-breaking transitive lockfile bumps for three still-present
advisories on the app dependency tree:
* shell-quote 1.8.0 -> 1.10.0 (critical: unescaped newlines in quote())
* form-data 3.0.4 -> 3.0.5 / 4.0.4 -> 4.0.6 (high: CRLF injection via
unescaped multipart field names/filenames)
* ws 7.5.10 -> 7.5.12 / 8.18.3 -> 8.21.1 (high: memory-exhaustion DoS)
package-lock.json only; no package.json / direct-dependency changes. Runs
`npm audit fix` without --force, so no breaking dev-chain (react-scripts)
upgrades are pulled in. Verified on trixie/Node 20: jest 760/760 and the
production build both green.
Supersedes the stale dependabot PRs #487 (ws), #486 (form-data), and
equal-or-newer patched versions.
Co-authored-by: Roland <roland.henrie@o2x.com>
Co-authored-by: Claude Opus 4.8 (1M context) <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>
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>
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>
* 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>
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>