Commit graph

96 commits

Author SHA1 Message Date
Roland
318b835faf
ai: package llama-server for all three deployments, with idle unload
install_llama_cpp.sh builds llama-server from the SHA-pinned v0.3.0
source tarball (single source of truth: on-device upgrade, Pi image,
Live ISO, and the new docker/llm container all call it; version-marker
makes re-runs no-ops).  start_llama_server.sh refuses to start unless
ai.yaml enables AI and the active model file exists — which also covers
unmounted media.  wrolpi-llm.service is installed but deliberately
never enabled at boot: the Controller starts it on demand (registered
in managed_services; the llm compose service follows the samba
pattern), and the API's perpetual idle-unload worker asks the
Controller to stop it after the configured idle minutes.  llama-server
is localhost-only natively, internal-network-only in docker, and never
proxied by Caddy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-13 20:10:16 -06:00
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
dependabot[bot]
f39eba81e6
build(deps): update pydantic requirement from >=2.13.4 to >=2.13.5 in /controller (#691)
Updates the requirements on [pydantic](https://github.com/pydantic/pydantic) to permit the latest version.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/v2.13.5/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.13.4...v2.13.5)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-version: 2.13.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-03 16:43:12 -06:00
dependabot[bot]
65217f1a0c
build(deps): update uvicorn requirement from >=0.52.1 to >=0.52.4 in /controller (#689)
Updates the requirements on [uvicorn](https://github.com/Kludex/uvicorn) to permit the latest version.
- [Release notes](https://github.com/Kludex/uvicorn/releases)
- [Changelog](https://github.com/Kludex/uvicorn/blob/main/docs/release-notes.md)
- [Commits](https://github.com/Kludex/uvicorn/compare/0.52.1...0.52.4)

---
updated-dependencies:
- dependency-name: uvicorn
  dependency-version: 0.52.4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-03 16:25:58 -06:00
dependabot[bot]
d5b933920a
build(deps): update fastapi requirement from >=0.140.13 to >=0.141.1 in /controller (#690)
Updates the requirements on [fastapi](https://github.com/fastapi/fastapi) to permit the latest version.
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.140.13...0.141.1)

---
updated-dependencies:
- dependency-name: fastapi
  dependency-version: 0.141.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-03 16:15:29 -06:00
dependabot[bot]
753cc31a81
build(deps): bump aiohttp from 3.14.1 to 3.14.3 (#633)
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.14.1 to 3.14.3.
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.14.1...v3.14.3)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-version: 3.14.3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-08 16:33:19 -06:00
dependabot[bot]
eabeee59c4
build(deps): update uvicorn requirement in /controller (#605)
Updates the requirements on [uvicorn](https://github.com/Kludex/uvicorn) to permit the latest version.
- [Release notes](https://github.com/Kludex/uvicorn/releases)
- [Changelog](https://github.com/Kludex/uvicorn/blob/main/docs/release-notes.md)
- [Commits](https://github.com/Kludex/uvicorn/compare/0.49.0...0.52.1)

---
updated-dependencies:
- dependency-name: uvicorn
  dependency-version: 0.52.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-08 16:33:16 -06:00
dependabot[bot]
2be657a788
build(deps): update fastapi requirement in /controller (#604)
Updates the requirements on [fastapi](https://github.com/fastapi/fastapi) to permit the latest version.
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.138.2...0.140.13)

---
updated-dependencies:
- dependency-name: fastapi
  dependency-version: 0.140.13
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-08 16:30:02 -06:00
dependabot[bot]
03a0bd6dd4
build(deps): update docker requirement in /controller (#603)
Updates the requirements on [docker](https://github.com/docker/docker-py) to permit the latest version.
- [Release notes](https://github.com/docker/docker-py/releases)
- [Commits](https://github.com/docker/docker-py/compare/7.1.0...7.2.0)

---
updated-dependencies:
- dependency-name: docker
  dependency-version: 7.2.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-08 16:29:59 -06:00
Roland
07af0c9f28 controller: de-duplicate the hardware toggles
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>
2026-07-29 19:48:08 -06:00
Roland
5313ab57ef controller: report and stop every installed VNC backend
Greptile P1 (security): with both stacks installed, selection keyed only off
wayvnc's unit and config. If RealVNC was the one running, status reported VNC
as off and stop targeted wayvnc — leaving the screen remotely accessible while
the UI claimed VNC was off. Reproduced on 10.0.0.8: RealVNC serving on 5900
reported as running=false.

Now every installed backend is reported with its own active state: status is on
when any backend runs and names the one actually serving, and stop stops all
running backends. wayvnc is still preferred for starting.

Greptile P2 (latency): both units are probed in a single multi-unit
`systemctl show`, and the status dict no longer re-probes, so VNC adds one
subprocess to /api/stats rather than three.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:10:05 -06:00
Roland
2328524ab2 controller: VNC toggle, gated on the Desktop running
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>
2026-07-29 18:10:05 -06:00
Roland
a0fa03e6c5
controller: leave a usable terminal when stopping the desktop
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
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>
2026-07-29 16:07:36 -06:00
Roland
ab5f416b3f
upgrade: report git branch via --show-current so a stale release tag can't break upgrades
git rev-parse --abbrev-ref HEAD reports "heads/release" when a stale
tag named release exists alongside the release branch (tags outrank
heads in git ref resolution).  check_for_update then looked up the
nonexistent origin/heads/release and permanently reported no updates
available, and the Controller UI pre-filled "heads/release" as the
upgrade branch.

Use git branch --show-current everywhere instead; it never
disambiguates.  A detached HEAD (empty output) now defaults to the
release branch for upgrades; a genuine git failure still returns None.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 16:03:33 -06:00
Roland
3e559e1bf8 controller: Desktop start/stop toggle; rename hotspot/bluetooth actions to what they do
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>
2026-07-29 15:19:36 -06:00
Roland
2baa9cbd2e
controller: address hotspot protocol review findings
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
- 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>
2026-07-28 21:12:34 -06:00
Roland
8687816b84 controller: selectable hotspot protocol (WPA2/WPA3) with device capability probing
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>
2026-07-28 21:12:24 -06:00
Roland
1a88fb4a5b
controller: report idle (0 RPM) fans; prefer the CPU fan over case fans
The RPi 5 firmware stops the fan entirely when cool, so a 0 RPM
reading is normal and must not be hidden. On multi-fan machines,
prefer a CPU-labeled hwmon group over faster case fans.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 19:52:58 -06:00
Roland
42a017294e controller: report cooling fan RPM in CPU status
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>
2026-07-23 19:52:47 -06:00
Roland
f635671266
controller: don't strand the API stopped on a failed primary unmount
Review feedback: skip the service stop when the primary mount is already
unmounted (a no-op request must not cause an API outage), and restart
wrolpi-api (best effort) when umount fails since the drive is still
mounted and usable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 19:23:05 -06:00
Roland
43f6ed2b8e controller: allow unmounting the primary drive, stopping the API first
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>
2026-07-23 19:22:53 -06:00
Roland
5a11e23a56
controller: expose kind/group in the services API response
The React ControllerPage partitions services on `service.group`, but
ServiceStatusResponse did not declare the new kind/group fields, so
Pydantic silently stripped them from /api/services and the React UI
rendered one flat, ungrouped table. The fallback UI was unaffected
because it is server-rendered without the response model.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 18:44:59 -06:00
Roland
e51e5fb2fb controller: group services into Core vs Optional & Maintenance
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>
2026-07-22 19:46:13 -06:00
Roland
46ba804a1b
Controller: hide Root CA download until drive is ready
During onboarding the media drive is not mounted and ca.crt does not exist
yet (or still holds only a throwaway pre-drive state). The cert banner still
offered Download WROLPi Root CA, which 404'd. Gate the button and HTTPS
cert-setup banner on drive mounted, ca.crt present, and repair not running
so an existing CA on the drive can be served after mount/repair.
2026-07-18 13:44:35 -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
Roland
860172e183
Bump safe dependencies (combined Dependabot updates)
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
Combines all Dependabot bumps verified safe to merge (green CI, or only
the known-flaky test_download_channel / cypress E2E flake failing).

controller/requirements.txt:
  - fastapi >=0.136.3 -> >=0.138.2   (#507, #516)
  - uvicorn[standard] >=0.48.0 -> >=0.49.0   (#517, #510)
  - aiohttp 3.14.0 -> 3.14.1   (#482, #514)
  - pytest >=9.0.3 -> >=9.1.1   (#515)

requirements.txt:
  - aiohttp 3.14.0 -> 3.14.1   (#481)
  - cryptography 46.0.7 -> 48.0.1   (#485)
  - py7zr >=1.1.0 -> >=1.1.3   (#508)

app/ (package.json + package-lock.json):
  - @types/node ^25.9.1 -> ^26.0.1 (undici-types ~8.3.0)   (#509)
  - websocket-driver 0.7.4 -> 0.7.5   (#539)

Excluded: #505 (cypress 13->15 major, needs review), #476 (lockfile
conflict, needs rebase), #512 & #506 (blocked on SQLAlchemy 2.0 migration).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 20:07:21 -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
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
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
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
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
lrnselfreliance
dac849f9f6
Surface coarse SMART health for USB-bridge drives (#498)
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
* Surface coarse SMART health for USB-bridge drives

USB enclosures that reject ATA pass-through (e.g. Seagate Expansion) are
invisible to pySMART/DeviceList, so the drive shows no SMART at all on the
Controller page. Fall back to smartctl over the SCSI transport, which many
such bridges still answer, to report a coarse PASS/FAIL health plus drive
identity (model/serial/capacity).

The health (-d scsi -H) and identity (-d scsi -i) reads are issued as
separate calls: combining them makes smartctl suppress the health line when
the identity read reports "SMART support is: Unavailable" (which these
bridges do). The entry is marked smart_limited (labelled "(limited)" in both
UIs) so a green PASS is not mistaken for full monitoring — these drives
expose no attributes or temperature, and some bridges report "OK" statically.

* Harden SCSI health fallback (Greptile review)

- _scsi_health: treat any unrecognised status (e.g. "UNKNOWN", "N/A") as
  unreadable (None) instead of a false FAIL. Only "OK" -> PASS and "FAIL*"
  -> FAIL; anything else omits the drive rather than showing a red FAIL that
  contradicts the smart_limited disclaimer.
- get_all_smart_status: isolate the SCSI supplement loop in its own
  try/except so an error there cannot discard the pySMART results already
  collected.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 09:05:06 -06:00
Roland
c3110f3940
Warning when SMART reports unreadable sectors. Parse Seagate drive hours correctly. 2026-06-24 15:59:34 -06:00
lrnselfreliance
1bd102c768
Warn in the navbar when a hard drive runs hot (#470)
* Warn in the navbar when a hard drive runs hot.

The navbar already warns about high CPU temperature; add the equivalent
for drive temperature, since an overheating disk risks data loss and a
user running an off-grid library won't be watching for it.

- controller/lib/smart.py: build_smart_stats() returns the per-drive list
  plus warning thresholds (55C warn / 65C critical) so the frontend stays
  dumb. Reading SMART wakes a spun-down drive, so drives in standby are
  detected with `smartctl -n standby` (which works through the USB-SATA
  bridges where `hdparm -C` only reports "unknown") and NOT read; their
  last-known reading is reused instead.
- controller/lib/status_worker.py: collect SMART on a slow 60s cadence
  (cached on app.state) rather than the 5s status loop, so the poll does
  not keep USB drives awake. The result flows into the same cached payload
  via a new collect_all_status(smart_stats=...) kwarg.
- Frontend: useDriveTemperature() reports the hottest drive; Nav.js shows
  a yellow/red `hdd` icon with a popup, mirroring the CPU warning.

Verified on hardware: smart_stats reports real temps and SMART runs once
a minute, not every 5s.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Address Greptile review on HDD temperature warning.

- _drive_is_asleep also treats SLEEP mode as asleep, not just STANDBY.
  smartctl -n standby exits "Device is in SLEEP mode, exit(2)" for the
  deeper power mode; matching only "STANDBY" meant a SLEEP drive was read
  and woken — the exact failure the check prevents.  (TDD: added a failing
  SLEEP test, then fixed.)
- useDriveTemperature uses ?? instead of || for the temperature fallback
  to express "fall back only on null/undefined".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 19:46:33 -06:00
Roland
ef4512614a
Bump controller pydantic requirement to >=2.13.4 (dependabot #463).
Applied directly since the dependabot PR could not auto-rebase after the
grouped merges.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 19:46:33 -06:00
dependabot[bot]
70bc0a17c9
Update fastapi requirement from >=0.136.1 to >=0.136.3 (#454)
Updates the requirements on [fastapi](https://github.com/fastapi/fastapi) to permit the latest version.
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.136.1...0.136.3)

---
updated-dependencies:
- dependency-name: fastapi
  dependency-version: 0.136.3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-11 19:46:33 -06:00
dependabot[bot]
2bc475310b
Update pysmart requirement from >=1.4.2 to >=1.4.3 in /controller (#462)
Updates the requirements on [pysmart](https://github.com/truenas/py-SMART) to permit the latest version.
- [Release notes](https://github.com/truenas/py-SMART/releases)
- [Changelog](https://github.com/truenas/py-SMART/blob/master/CHANGELOG.md)
- [Commits](https://github.com/truenas/py-SMART/compare/v1.4.2...v1.4.3)

---
updated-dependencies:
- dependency-name: pysmart
  dependency-version: 1.4.3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-11 19:46:32 -06:00
dependabot[bot]
586ba4f7c4
Update uvicorn requirement from >=0.46.0 to >=0.48.0 (#459)
Updates the requirements on [uvicorn](https://github.com/Kludex/uvicorn) to permit the latest version.
- [Release notes](https://github.com/Kludex/uvicorn/releases)
- [Changelog](https://github.com/Kludex/uvicorn/blob/main/docs/release-notes.md)
- [Commits](https://github.com/Kludex/uvicorn/compare/0.46.0...0.48.0)

---
updated-dependencies:
- dependency-name: uvicorn
  dependency-version: 0.48.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-11 19:46:32 -06:00
dependabot[bot]
ecd5aec2f8
Bump the python-dependencies group with 20 updates (#452)
---
updated-dependencies:
- dependency-name: pint
  dependency-version: 0.25.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-dependencies
- dependency-name: pyyaml
  dependency-version: 6.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-dependencies
- dependency-name: aiohttp
  dependency-version: 3.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-dependencies
- dependency-name: alembic
  dependency-version: 1.18.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-dependencies
- dependency-name: beautifulsoup4
  dependency-version: 4.14.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-dependencies
- dependency-name: ebooklib
  dependency-version: '0.20'
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-dependencies
- dependency-name: feedparser
  dependency-version: 6.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-dependencies
- dependency-name: jc
  dependency-version: 1.25.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-dependencies
- dependency-name: libzim
  dependency-version: 3.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-dependencies
- dependency-name: mock
  dependency-version: 5.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-dependencies
- dependency-name: psycopg2-binary
  dependency-version: 2.9.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-dependencies
- dependency-name: pytest-asyncio
  dependency-version: 1.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-dependencies
- dependency-name: pytest-xdist
  dependency-version: 3.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-dependencies
- dependency-name: python-docx
  dependency-version: 1.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-dependencies
- dependency-name: pytz
  dependency-version: '2026.2'
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-dependencies
- dependency-name: sanic
  dependency-version: 25.12.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-dependencies
- dependency-name: secretstorage
  dependency-version: 3.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-dependencies
- dependency-name: selenium
  dependency-version: 4.44.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-dependencies
- dependency-name: tqdm
  dependency-version: 4.67.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-dependencies
- dependency-name: vininfo
  dependency-version: 1.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-11 19:46:32 -06:00
Roland
ead47e916b
Fix SMART temperature always reporting null.
Real drives report attribute 194 raw values like "52 (Min/Max 16/57)"
which int() cannot parse, so every drive's temperature was null.
Prefer pySMART's `temperature` property, which parses these formats
and also works for NVMe devices that have no ATA attribute table.

Verified on real hardware (WD100EMAZ over USB-SAT): 51-53C now
reported where null was returned before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 19:46:31 -06:00
Roland
9d6eb0873e
Controller can unmount foreign mounts; protect the primary drive.
The /api/disks/unmount endpoint only removed entries from fstab.yaml and
asked the reconciler to unmount, but the reconciler deliberately never
unmounts paths it has not claimed.  Foreign mounts (udisks2 desktop
automounts, manual mounts) therefore could not be unmounted from the UI,
failing with a confusing "check journalctl" error.

- Fall back to a direct umount(8) when a reconcile leaves the requested
  mount point live.
- Reject unmounting the primary WROLPi drive (/media/wrolpi) with a
  clear 400, normalized to catch path traversal.
- Require the normalized mount point to be strictly under /media/
  before any direct umount, since the fallback runs as root.
- Show "System" instead of an Unmount button for /media/wrolpi in both
  the React UI and the Controller's simplified UI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 19:46:30 -06:00
Roland
cccbac9ae9
Fetching processes from PROCFS_PATH in docker container.
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-05-30 15:59:14 -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
2806673e25
Consolidating controller tests. Fixing controller test cleanup which slowed down tests significantly.
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
2026-05-15 21:10:30 -06:00
lrnselfreliance
8f0e4cd93f
Adding support for mounting NTFS drives in the Controller. (#440) 2026-05-15 19:25:43 -06:00
Roland
cd8792f697
Warning users when files exist in mount location. Users may have accidentally downloaded files into the media directory, we want to warn them.
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-05-14 17:02:52 -06:00
dependabot[bot]
76833bdc09 Bump the controller-dependencies group in /controller with 12 updates
Updates the requirements on [fastapi](https://github.com/fastapi/fastapi), [uvicorn](https://github.com/Kludex/uvicorn), [pydantic](https://github.com/pydantic/pydantic), [jinja2](https://github.com/pallets/jinja), [psutil](https://github.com/giampaolo/psutil), [pysmart](https://github.com/truenas/py-SMART), [pyyaml](https://github.com/yaml/pyyaml), [docker](https://github.com/docker/docker-py), [pytest](https://github.com/pytest-dev/pytest), [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio), [pytest-xdist](https://github.com/pytest-dev/pytest-xdist) and [httpx](https://github.com/encode/httpx) to permit the latest version.

Updates `fastapi` to 0.136.1
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.100.0...0.136.1)

Updates `uvicorn` to 0.46.0
- [Release notes](https://github.com/Kludex/uvicorn/releases)
- [Changelog](https://github.com/Kludex/uvicorn/blob/main/docs/release-notes.md)
- [Commits](https://github.com/Kludex/uvicorn/compare/0.23.0...0.46.0)

Updates `pydantic` to 2.13.3
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.0...v2.13.3)

Updates `jinja2` to 3.1.6
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.0...3.1.6)

Updates `psutil` to 7.2.2
- [Changelog](https://github.com/giampaolo/psutil/blob/master/docs/changelog.rst)
- [Commits](https://github.com/giampaolo/psutil/compare/v5.9.0...v7.2.2)

Updates `pysmart` to 1.4.2
- [Release notes](https://github.com/truenas/py-SMART/releases)
- [Changelog](https://github.com/truenas/py-SMART/blob/master/CHANGELOG.md)
- [Commits](https://github.com/truenas/py-SMART/compare/v1.3.0...v1.4.2)

Updates `pyyaml` to 6.0.3
- [Release notes](https://github.com/yaml/pyyaml/releases)
- [Changelog](https://github.com/yaml/pyyaml/blob/6.0.3/CHANGES)
- [Commits](https://github.com/yaml/pyyaml/compare/6.0...6.0.3)

Updates `docker` to 7.1.0
- [Release notes](https://github.com/docker/docker-py/releases)
- [Commits](https://github.com/docker/docker-py/compare/6.0.0...7.1.0)

Updates `pytest` to 9.0.3
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/7.0.0...9.0.3)

Updates `pytest-asyncio` to 1.3.0
- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases)
- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.21.0...v1.3.0)

Updates `pytest-xdist` to 3.8.0
- [Release notes](https://github.com/pytest-dev/pytest-xdist/releases)
- [Changelog](https://github.com/pytest-dev/pytest-xdist/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-xdist/compare/v3.0.0...v3.8.0)

Updates `httpx` to 0.28.1
- [Release notes](https://github.com/encode/httpx/releases)
- [Changelog](https://github.com/encode/httpx/blob/master/CHANGELOG.md)
- [Commits](https://github.com/encode/httpx/compare/0.24.0...0.28.1)

---
updated-dependencies:
- dependency-name: fastapi
  dependency-version: 0.136.1
  dependency-type: direct:production
  dependency-group: controller-dependencies
- dependency-name: uvicorn
  dependency-version: 0.46.0
  dependency-type: direct:production
  dependency-group: controller-dependencies
- dependency-name: pydantic
  dependency-version: 2.13.3
  dependency-type: direct:production
  dependency-group: controller-dependencies
- dependency-name: jinja2
  dependency-version: 3.1.6
  dependency-type: direct:production
  dependency-group: controller-dependencies
- dependency-name: psutil
  dependency-version: 7.2.2
  dependency-type: direct:production
  dependency-group: controller-dependencies
- dependency-name: pysmart
  dependency-version: 1.4.2
  dependency-type: direct:production
  dependency-group: controller-dependencies
- dependency-name: pyyaml
  dependency-version: 6.0.3
  dependency-type: direct:production
  dependency-group: controller-dependencies
- dependency-name: docker
  dependency-version: 7.1.0
  dependency-type: direct:production
  dependency-group: controller-dependencies
- dependency-name: pytest
  dependency-version: 9.0.3
  dependency-type: direct:production
  dependency-group: controller-dependencies
- dependency-name: pytest-asyncio
  dependency-version: 1.3.0
  dependency-type: direct:production
  dependency-group: controller-dependencies
- dependency-name: pytest-xdist
  dependency-version: 3.8.0
  dependency-type: direct:production
  dependency-group: controller-dependencies
- dependency-name: httpx
  dependency-version: 0.28.1
  dependency-type: direct:production
  dependency-group: controller-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-03 11:56:30 -06:00