Commit graph

2061 commits

Author SHA1 Message Date
Roland
df7d2b3fd8
Fixing bug where zim articles were not aligned in modals. 2026-06-09 19:44:35 -06:00
lrnselfreliance
20f65e1063
Add ordered collections (playlists): polymorphic items + item API (#475)
* Add Playlists: ordered, mixed-type collections

A playlist (Collection kind='playlist') is a manually-curated, ordered list that
mixes item types. Squash of the playlists feature branch (24 commits).

Data model & API:
- CollectionItem is polymorphic via item_kind: 'file' (FileGroup), 'zim'
  (zim_id + zim_entry), or 'url' (link to anything the WROLPi browser opens,
  e.g. /map?lat=&lon=&z=). CHECK constraint enforces exactly one kind; per-kind
  unique constraints; migration a7b1c2d3e4f5.
- Endpoints: create playlist; add/remove/reorder items (idempotent adds: 201 new,
  200 existing); GET returns the ordered items. All mutations wrol_mode_check'd.

Config (source of truth):
- playlists.yaml (PlaylistsConfig) stores each playlist's metadata AND ordered
  items (file by relative path, zim by path+entry, url inline); round-trips a
  DB wipe. Supports backup import (DB_CONFIG_FILE_NAMES).
- The redundant generic CollectionsConfig (collections.yaml) is deleted:
  channels/domains own their configs, authors/subjects are re-derived.

On-disk sync (sync_playlists_directory switch):
- Each playlist materializes into the configurable Playlists Directory
  (playlists_destination, Settings UI input, excluded from indexing) with
  position-prefixed entries: NNNN_<file> hard links (copy fallback across
  filesystems) and NNNN_<title>.html stubs for zim/url items.
- Tagged playlists are namespaced like channels: <playlists>/<tag>/<name>;
  a tag change moves the directory and prunes the empty old tag dir.
- Deletion safety: managed files are only removed when they are our marked
  stubs or still have another hard link (st_nlink > 1) -- never a last copy.
  Stray loose files at the playlists root are removed; stub URLs are
  scheme-allowlisted (no javascript:/data:).

Frontend:
- /playlists list page (CollectionTable, name filter, New Playlist modal, Tag
  column) between Files and Zim in the nav.
- View-only detail page (Name link) and edit page (Edit button): themed table
  with model icons/colors + poster thumbnails, items link to their model page
  (video/archive/doc) or preview, up/down reorder, remove, add-URL form,
  single-tag selector; all editing disabled in WROL Mode.
- Reusable green "Add to Playlist" button on Video/Archive/Doc detail pages,
  the FilePreview toolbar, Zim search results, and Map pins (added as a /map
  URL item with a map-marker icon).

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

* Playlist edit page parity: rename, custom directory, tag modal move suggestion

Match the ChannelEditPage/DomainEditPage UX on the playlist edit page:
Back/View buttons, an edit form (name, description, directory), and the
shared CollectionTagModal with the violet "Tag" button.  The tag modal now
suggests moving the playlist to its managed tag location
(playlists/<tag>/<name>); accepting the suggestion stores directory=None so
the sync keeps auto-managing the location.  Tag buttons on the Channel and
Domain edit pages are now violet to match.

Backend: update_collection learns rename (with per-kind validation and
duplicate checks) and playlist custom directories (cleanup of the abandoned
directory, config round-trip, commit-before-switch to avoid the background
sync racing pre-commit state).  tag_collection's duplicated untag/tag
branches are merged, and the per-kind config-save dispatch is consolidated
into _activate_config_save().

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

* Deduplicate Collection error handling and Tag creation

Delete the duplicate UnknownCollection in wrolpi/collections/errors.py and
re-export the canonical one from wrolpi/errors.py, which carries
status_code=NOT_FOUND (the local copy inherited 500).  Replace the repeated
query/raise pattern in reorganize.py with Collection.find_by_id.

Add Tag.get_or_create_tag and use it for collection tagging; flush the new
Tag before returning so callers can read .id immediately -- previously
tagging a collection with a brand-new tag name set tag_id=None.  Regression
test included.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 19:37:34 -06:00
Roland
f365bb5411
Skipping flaky tests on CircleCI.
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-06-04 17:39:43 -06:00
Roland
e58f5cfe45
Removing extremely flaky test. 2026-06-04 17:31:15 -06:00
lrnselfreliance
4fe58d8675
Fix Downloads stuck "pending": persist finalize_download mutations (#473)
* Persist finalize_download mutations; isolate child-download enqueue

A channel/RSS Download could get stuck permanently "pending".  Root cause:
the phase-split dispatch in signal_download_download passes the Download
loaded in an already-closed session to finalize_download, so it is
DETACHED.  ChannelDownloader.finalize_download sets
`download.sub_downloader = 'video'` (and info_json/collection_id) on that
detached object, so the write never reaches the DB.  The status block then
re-queries the Download fresh, reads sub_downloader=None, and calls
create_downloads(downloader_name=None) ->
`InvalidDownload: Cannot find downloader with name None`.

That exception was raised inside the status-commit transaction (outside the
inner try), so it (a) rolled back the parent's complete(), reverting it to
the `pending` set by started(), and (b) was swallowed by the outer bare
`except Exception`, which only logs.  Result: the parent never failed, never
completed — it sat `pending` forever, re-crashing the same way on every
restart.

Fixes:
- finalize_download is now given a Download re-loaded in (attached to) its
  own session, so its mutations persist.
- Child-download creation moves to its own transaction after the parent's
  status is committed.  A failure there is logged and cannot roll back or
  strand the parent.

The existing test harness reuses one shared DB session for every
get_db_session call, so detached-object and rollback bugs never reproduced
under it (which is why this shipped).  Add `production_like_sessions` to
conftest.py — a context manager handing out a fresh Session per call, like
production — and two regression tests that fail without the fix.

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

* Re-attach Download in prepare_download too (consistency)

prepare_download receives the same detached Download as finalize_download
did, so VideoDownloader.prepare_download's writes to the row
(destination/tag_names) are not reliably persisted.  Re-load the Download
attached to the prepare session, mirroring the finalize fix, so the row's
metadata is accurate.

Unlike sub_downloader (which the status block re-reads and feeds to
create_downloads), nothing downstream re-reads these values, so this is a
consistency/cosmetic improvement, not a functional bug — and it cannot be
made to fail in a test (the loaded Download's session outlives the worker),
so no dedicated test is added.

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

* Log (not swallow) session-close failures in production_like_sessions

Addresses Greptile review: surface a failed session.close() at debug level
instead of silently passing.  engine.dispose() still does the authoritative
cleanup, so the close failure remains non-fatal.

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-04 17:22:22 -06:00
lrnselfreliance
57d8030f09
Warn in the navbar when a drive's SMART health is failing. (#471)
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
A failing SMART self-assessment predicts imminent drive failure, so it
deserves a prominent warning.  useDriveHealth() reads the assessment
already present in smart_stats (pySMART reports PASS / FAIL / null) and
flags any drive whose assessment is FAIL.  Nav.js renders a red warning
icon in its own always-on slot — like the power warning, and unlike the
single-slot priority chain where a transient high temp could hide it.

Frontend-only: the assessment field is already in the cached payload.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 20:04:07 -06:00
lrnselfreliance
51f89593a4
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-03 18:46:44 -06:00
Roland
b112f07b4a
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-03 18:25:28 -06:00
Roland
18f0fc2d44
Pin alembic back to 1.10.4 (dependabot #452 broke install).
alembic 1.18.4 requires SQLAlchemy>=1.4.23, but the project pins
SQLAlchemy==1.3.22, making the dependency set unresolvable. This slipped
through because the backend pytest suite is not a required CI check
(only controller-tests and jest-tests are).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 18:21:48 -06:00
dependabot[bot]
509918c4ba
Bump tmp from 0.2.4 to 0.2.7 in /app (#445)
Bumps [tmp](https://github.com/raszi/node-tmp) from 0.2.4 to 0.2.7.
- [Changelog](https://github.com/raszi/node-tmp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/raszi/node-tmp/compare/v0.2.4...v0.2.7)

---
updated-dependencies:
- dependency-name: tmp
  dependency-version: 0.2.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-03 18:12:31 -06:00
dependabot[bot]
7a2c87fd21
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-03 18:12:27 -06:00
dependabot[bot]
7fa8f8afaa
Update pymupdf requirement from >=1.27.0 to >=1.27.2.3 (#456)
Updates the requirements on [pymupdf](https://github.com/pymupdf/pymupdf) to permit the latest version.
- [Release notes](https://github.com/pymupdf/pymupdf/releases)
- [Changelog](https://github.com/pymupdf/PyMuPDF/blob/main/changes.txt)
- [Commits](https://github.com/pymupdf/pymupdf/compare/1.27.1...1.27.2.3)

---
updated-dependencies:
- dependency-name: pymupdf
  dependency-version: 1.27.2.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-03 18:12:24 -06:00
dependabot[bot]
bd25c27fcd
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-03 18:05:41 -06:00
dependabot[bot]
eda2b542de
Update uvicorn requirement from >=0.46.0 to >=0.48.0 in /controller (#464)
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-03 18:05:36 -06:00
dependabot[bot]
a037f97929
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-03 18:05:32 -06:00
dependabot[bot]
5335f42de3
Bump the npm-dependencies group in /app with 12 updates (#450)
Bumps the npm-dependencies group in /app with 12 updates:

| Package | From | To |
| --- | --- | --- |
| [maplibre-gl](https://github.com/maplibre/maplibre-gl-js) | `5.21.1` | `5.24.0` |
| [pmtiles](https://github.com/protomaps/pmtiles) | `4.4.0` | `4.4.1` |
| [query-string](https://github.com/sindresorhus/query-string) | `9.3.1` | `9.4.0` |
| [react-colorful](https://github.com/omgovich/react-colorful) | `5.6.1` | `5.7.0` |
| [react-hotkeys-hook](https://github.com/JohannesKlauss/react-keymap-hook) | `5.2.1` | `5.3.2` |
| [react-qr-code](https://github.com/rosskhanas/react-qr-code) | `2.0.11` | `2.0.21` |
| [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) | `7.12.0` | `7.16.0` |
| [semantic-ui-react](https://github.com/Semantic-Org/Semantic-UI-React) | `2.1.4` | `2.1.5` |
| [three](https://github.com/mrdoob/three.js) | `0.154.0` | `0.184.0` |
| [web-vitals](https://github.com/GoogleChrome/web-vitals) | `5.2.0` | `5.3.0` |
| [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash) | `4.17.21` | `4.17.24` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.8.0` | `25.9.1` |


Updates `maplibre-gl` from 5.21.1 to 5.24.0
- [Release notes](https://github.com/maplibre/maplibre-gl-js/releases)
- [Changelog](https://github.com/maplibre/maplibre-gl-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/maplibre/maplibre-gl-js/compare/v5.21.1...v5.24.0)

Updates `pmtiles` from 4.4.0 to 4.4.1
- [Commits](https://github.com/protomaps/pmtiles/commits)

Updates `query-string` from 9.3.1 to 9.4.0
- [Release notes](https://github.com/sindresorhus/query-string/releases)
- [Commits](https://github.com/sindresorhus/query-string/compare/v9.3.1...v9.4.0)

Updates `react-colorful` from 5.6.1 to 5.7.0
- [Release notes](https://github.com/omgovich/react-colorful/releases)
- [Changelog](https://github.com/omgovich/react-colorful/blob/master/CHANGELOG.md)
- [Commits](https://github.com/omgovich/react-colorful/commits/5.7.0)

Updates `react-hotkeys-hook` from 5.2.1 to 5.3.2
- [Release notes](https://github.com/JohannesKlauss/react-keymap-hook/releases)
- [Changelog](https://github.com/JohannesKlauss/react-hotkeys-hook/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JohannesKlauss/react-keymap-hook/compare/v5.2.1...v.5.3.2)

Updates `react-qr-code` from 2.0.11 to 2.0.21
- [Release notes](https://github.com/rosskhanas/react-qr-code/releases)
- [Changelog](https://github.com/rosskhanas/react-qr-code/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rosskhanas/react-qr-code/commits/2.0.21)

Updates `react-router` from 7.12.0 to 7.16.0
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@7.16.0/packages/react-router)

Updates `semantic-ui-react` from 2.1.4 to 2.1.5
- [Release notes](https://github.com/Semantic-Org/Semantic-UI-React/releases)
- [Changelog](https://github.com/Semantic-Org/Semantic-UI-React/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Semantic-Org/Semantic-UI-React/compare/v2.1.4...v2.1.5)

Updates `three` from 0.154.0 to 0.184.0
- [Release notes](https://github.com/mrdoob/three.js/releases)
- [Commits](https://github.com/mrdoob/three.js/commits)

Updates `web-vitals` from 5.2.0 to 5.3.0
- [Changelog](https://github.com/GoogleChrome/web-vitals/blob/main/CHANGELOG.md)
- [Commits](https://github.com/GoogleChrome/web-vitals/compare/v5.2.0...v5.3.0)

Updates `@types/lodash` from 4.17.21 to 4.17.24
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash)

Updates `@types/node` from 25.8.0 to 25.9.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: maplibre-gl
  dependency-version: 5.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: pmtiles
  dependency-version: 4.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: query-string
  dependency-version: 9.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: react-colorful
  dependency-version: 5.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: react-hotkeys-hook
  dependency-version: 5.3.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: react-qr-code
  dependency-version: 2.0.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: react-router
  dependency-version: 7.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: semantic-ui-react
  dependency-version: 2.1.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: three
  dependency-version: 0.184.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: web-vitals
  dependency-version: 5.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@types/lodash"
  dependency-version: 4.17.24
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.9.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-03 18:05:08 -06:00
dependabot[bot]
c3adc0a53e
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-03 18:05:03 -06:00
Roland
87ed286f7d
Fix SMART temperature always reporting null.
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
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-02 13:58:01 -06:00
Roland
6088cbfe2a
Retrying build image uploads multiple times before failing release attempt. 2026-06-02 13:44:16 -06:00
Roland
5313b0dccc 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-02 13:43:28 -06:00
Roland
ed4546d1ad Bump version to 0.23.2-beta 2026-06-02 13:43:28 -06:00
Roland
68d8cd39c1
Including hdparm for drive handling.
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-06-01 19:31:17 -06:00
Roland
f89b131ddd
Bump extension to v0.2.4
Embeds cross-origin images in SingleFile archives (extension 382d7f8).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 19:23:39 -06:00
Roland
3929642167
pi-gen: clear leftover BUILD_DIR at startup (robust to killed builds)
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
A SIGTERM/kill (e.g. `systemctl stop`) skips the EXIT-trap cleanup, leaving
/var/tmp/wrolpi-build-pi-gen behind. The next build's `mv pi-gen-bookworm-arm64
wrolpi-build-pi-gen` would then nest into the stale dir and fail. Remove the
leftover (and intermediate zip/unzip dir) at startup, mirroring the debian
build, stripping immutable flags off blob files first.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 18:06:31 -06:00
Roland
33498504b0
release-watch: dedup against the Spaces origin, not the CDN
The watcher read latest.json through the CDN, which caches it ~5 min. A
catch-up timer fire right after a release published (Persistent=true) read the
stale cached tag, mismatched, and kicked off a redundant multi-hour rebuild of
the just-published version. Read the manifest from the strongly-consistent
Spaces origin for the dedup so it always reflects the latest publish.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 17:49:47 -06:00
Roland
5497e1eb61
Fixing flaky domain search test. 2026-05-31 13:09:20 -06:00
Roland
345b69b7d7
chore: address Greptile P2 review comments on PR 449
- In get_effective_video_settings: on unparseable sleep_requests, delete
  the key instead of forcing 0.0. This lets the global config default
  (already present from _extract_global_inheritable) take effect, as
  suggested by Greptile.

- In handleInputEvent: add explicit `value !== ''` guard before the
  isNaN check. Prevents silent NaN in form state for empty number
  fields (isNaN('') === false in JS).

Both changes are strict improvements with no behavior change for
valid inputs.
2026-05-31 13:02:44 -06:00
Roland
9cc5c15e32 fix: coerce sleep_requests and numeric settings from string form values
`sleep_requests` (and other numeric inheritable video download settings)
could arrive as strings ('' or '0.75') from the frontend NumberInputForm
and DownloadRequest bodies. These were persisted into Download.settings
(JSONB) and later triggered:

    if effective['sleep_requests'] > 0:
    TypeError: '>' not supported between instances of 'str' and 'int'

in VideoDownloader.execute_download (and similar sites).

Root cause: DownloadSettings.__post_init__ only filtered None/[] and did
no type coercion, even though the dataclass declared Optional[float]/int.

Changes (minimal targeted fix):
- Add _coerce_optional_number helper and apply it to all numeric fields
  in DownloadSettings.__post_init__ (the existing sanitization boundary).
- Centralize legacy string coercion for persisted data in the single
  read chokepoint: get_effective_video_settings.
- Simplify VideoDownloaderConfig.sleep_requests getter.
- Frontend: use parseFloat for type=number inputs + null defaults for
  sleep_requests (prevents '' from ever being submitted).
- Add focused regression test for string coercion path.
- Revert unrelated change to string field filtering behavior.

No NaN/Inf speculation, no duplicated try/float guards at every call site,
no behavior change for non-numeric settings.
2026-05-31 13:02:31 -06:00
Roland
cd9fb4c6de
release-watch: don't auto-retry failed builds (default MAX_RETRIES=1)
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
Builds take hours and a failure almost always needs a code fix, not a blind
retry -- retrying 3x burned ~5h overnight. Default to a single attempt: on
failure, record the marker, fire the notify hook, and wait for a human to
investigate and clear it. The MAX_RETRIES override remains for N attempts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 12:21:41 -06:00
Roland
5625f17b5b
pi-gen: set HOME before git config --global in the chroot (systemd fix)
When the release runs under the wrolpi-release systemd service, HOME is unset,
so `git config --global --add safe.directory /opt/wrolpi` in the pi-gen chroot
aborts with "fatal: $HOME not set" and the build fails at stage 3. Manual
shell builds worked only because the shell provided HOME. Export HOME=/root in
the chroot hook (self-contained), and also set Environment=HOME=/root in the
service unit template.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 12:15:32 -06:00
Roland
dcfad8b4af
debian-live-config: copy built ISO by name, not a SCRIPT_DIR glob
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
The build ended with `mv "${SCRIPT_DIR}"/*.iso "${DEST}"`. A leftover ISO from
a previous build made the glob match two files, so `mv` treated the target as
a directory and failed ("No such file or directory") -- after a successful
37-minute lb build. Take the single ISO out of the freshly-wiped BUILD_DIR via
a nullglob array and copy it to the final name; robust to leftovers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 20:27:11 -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
03a2596799
Bumping version to 0.23.1-beta. 2026-05-30 14:32:06 -06:00
Roland
30550f4c5b
release: address Greptile review + add opaque notify hook
Greptile (PR #448):
- C1 (P1): release-watch.sh compared the GitHub tag (minus 'v') against the
  manifest's .version (from version.txt content). If a tag and its version.txt
  ever diverged, the guard never matched and it rebuilt every hour forever.
  Compare the manifest's .tag against the release tag directly -- exact and
  immune to version-string formatting. Drops the now-unused normalize().
- C2 (P2): document that .tag is the built git ref (release tag for automated
  builds, branch for manual) and that consumers use .version for display.
- C3 (P2): document that newest-by-publish-date is the intended trigger and
  that prereleases are deliberately included (every release is -beta), so we
  do not filter on .prerelease as suggested.

Also add an optional completion notify hook: release.sh invokes
$WROLPI_RELEASE_NOTIFY with a success/failure message (no-op if unset, skipped
on dry-run). The public script names no path or tool; the real notifier stays
on the host (commented Environment= placeholder in the service unit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 14:23:19 -06:00
Roland
0e3564b704 Automating build and upload of WROLPi release images. 2026-05-30 14:23:08 -06:00
Roland
f4d0468300
Force checkout in upgrade.sh so a dirty working tree can't abort the upgrade
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 runs `npm install`, which rewrites app/package-lock.json
and leaves the working tree dirty. The next upgrade's `git checkout ${BRANCH}`
then aborts with "Your local changes would be overwritten by checkout" before
the following `git reset --hard` can discard them, failing the upgrade with
exit 4.

Use `git checkout -f` so the branch switch discards local modifications; the
reset --hard immediately after already forces the tree to match origin, so the
intent (blow away local changes) is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 13:22:56 -06:00
Roland
e7591b16ad
Self-heal wrolpi-upgrade.service unit before delegating in upgrade.sh
upgrade.sh delegates to wrolpi-upgrade.service at the very start, but that
unit is only installed by repair.sh, which runs near the end of the upgrade.
On a box that has never run a repair since the unit was introduced, the
delegation fails immediately with "Unit wrolpi-upgrade.service not found",
the service never runs (so nothing is logged to the journal), and
/opt/wrolpi/upgrade.log is never created.

Install/refresh the unit from the checked-out tree and daemon-reload right
before starting the service. The delegation block already runs as root, so
no sudoers changes are needed. This is idempotent and also self-heals a
stale unit file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 13:16:07 -06:00
Roland
f245f25179
Fixing alembic migration that imported non-existent code.
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-29 11:15:32 -06:00
Roland
bae0e1b566
Bumping version to 0.23.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-05-28 21:44:16 -06:00
Roland
960eb81880
Documenting WROLPi Portable USB, splitting install docs into INSTALL.md.
- New INSTALL.md consolidates all install paths: WROLPi Portable (Live
  USB) as the headline option, plus Debian 12, Raspberry Pi, and Docker.
- README install sections collapsed into a single Install section that
  links to INSTALL.md.
- UPGRADE.md gains an "Upgrading a WROLPi Portable USB" section covering
  scripts/wrolpi-usb.sh upgrade, and documents that in-place conversion
  of an existing data drive is not supported (backup -> flash -> restore).
- wrolpi-usb.sh install now prints that supported procedure instead of a
  "not implemented" stub; debian-live-config/README.md documents the same.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 21:33:22 -06:00
Roland
1bc337bfb4
Addressing Greptile P2s on the Ventoy ephemeral fallback.
- Treat virtual disks (vd*/xvd*/hd*) as direct, not ephemeral.  A blank
  VM disk imaged with the ISO can safely hold a persistence partition;
  previously these fell through the wildcard to loop mode and never
  accumulated state.
- Move the ephemeral note_action inside the mountpoint guard so a manual
  wrolpi-bootstrap.service restart mid-session doesn't re-fire the "NOT
  writing to disk" first-boot dialog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 21:13:12 -06:00
Roland
82dd7782d3 Falling back to tmpfs when booted via Ventoy or other loop-mounted ISOs.
wrolpi-bootstrap.sh now classifies the live medium by device-node
pattern.  Real partitions (/dev/sd*, /dev/nvme*, /dev/mmcblk*) continue
to get a persistence partition appended.  Loop/device-mapper sources
(/dev/loop*, /dev/dm-*, /dev/mapper/*) — the marker for Ventoy/YUMI/
Easy2Boot/etc — mount a tmpfs at /media/wrolpi instead, so we never
sfdisk-append a partition onto the user's multiboot stick.

First-boot zenity dialog branches on mode and tells ephemeral users
their data won't persist; README documents the behaviour and the
deferred persistence.dat work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 21:12:52 -06:00
Roland
342374b97f
Guard preset lookup, drop dead Media import and volume*1 no-op
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
Address Greptile review on PR #447:
- Fall back to the minimum preset when an unrecognized/stale
  water_calculator_preset is read from localStorage, so a bad persisted
  value can no longer crash the component on mount.
- Remove the unused Media import.
- Simplify the metric branch of waterWeight (volume * 1 -> volume).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 18:07:01 -06:00
Roland
d998bb93a1 Adding realistic lifecycle test. 2026-05-28 18:06:48 -06:00
Roland
d3e57ea894 Water Storage calculator: rate guardrails, persisted duration, extra demand
Addresses calculator review feedback:
- Add an "Additional daily demand" (Extra) field for pets, livestock,
  guests, or sick household members not covered by the per-person rates.
- Make the preset choice explicit buttons with a note that switching
  resets rates to the preset defaults.
- Clamp rate and extra inputs to non-negative via a shared helper that
  preserves in-progress decimal entry (so "1.5" is typeable).
- Persist the duration slider to local storage (with an out-of-range
  guard), matching the people-count/metric/preset persistence.
- Relabel "Total Weight" as "Water Weight" to avoid implying it includes
  container tare weight.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 18:06:48 -06:00
Roland
c63fece967 Add Water Storage calculator (people → duration → containers)
New offline prepper calculator under Calculators:
- Men/women/children/pregnant-nursing counts with editable per-person
  daily rates (drinking + hygiene/dish use).
- Minimum/Comfortable presets grounded in CDC/FEMA (1 gal/person/day
  floor, store 2 weeks) and National Academies adequate-intake figures;
  Pregnant/Nursing adds the CDC/Ready.gov +0.5/+1 gal. Sources cited in
  the Daily Use info popup.
- Duration slider on a clean ladder (days → weeks → months → years) so it
  never lands on awkward values like "68 days".
- Containers Needed table listing every container (IBC tote → 500 mL
  bottle) with capacity and count + spare for the total volume.
- Total water and total weight (8.345 lb/gal, 1 kg/L).
- Imperial/metric, preset, and people counts persist via local storage.

Pure functions (dailyDemand, totalWater, containersNeeded, waterWeight,
formatDuration) covered by 21 unit tests. Registered in the calculator
list and global app search.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 18:06:48 -06:00
Roland
2e950b85ad
Drop spammy console.debug from Drive Ratio render/keystroke paths
Address Greptile review: remove the per-render and per-keystroke console.debug
calls (they fired on every React render and every field edit). Keep the single
strategic solveTrain log, which fires only on a settled (debounced) solve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 16:19:21 -06:00
Roland
4e25459d50 Add Drive Ratio calculator to global search apps
Add a SUGGESTED_APPS entry so the Drive Ratio calculator shows under "Apps"
in the global search, matching the other calculator entries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 15:17:50 -06:00
Roland
c38f90aae8 Add Drive Ratio calculator (pulleys, gears, sprockets)
A new "Drive Ratio" calculator under /more/calculators for belt, gear, and
chain-sprocket drives, supporting a compound train of 2–6 wheels on a single
shared belt/chain (constant size·rpm across the train).

- Pure, unit-tested math: size·rpm solver, ratio, torque, surface/belt speed,
  belt length, sprocket pitch diameter, chain length, gear pitch diameter.
- Mode toggle (Pulley/Gear/Sprocket) with imperial/metric units; torque in
  lb·ft or N·m.
- Add/remove wheels (2–6), each with a colorblind-friendly color shared between
  its inputs and the schematic SVG diagram (Paul Tol bright palette).
- Dependency-free inline-SVG diagram: colored wheels threaded by one belt/chain,
  or meshing gear teeth; auto-fitting viewBox.
- Per-mode details: belt/chain length and center distance (2-wheel only, with an
  info popup), per-wheel pitch diameters, and constant train speed.
- Guards against impossible geometry (wheels overlapping) and ideal-torque note.
- Integer teeth enforced in gear/sprocket modes; pulley diameters stay continuous.

Also fix Theme.Header dropping the theme's text color when a caller passes a
`style` prop (now merges {...t.style, ...props.style}).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 15:17:50 -06:00
Roland
9edfb0de25 Addressing Greptile P1s on wrolpi-usb.sh.
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
numfmt with a negative argument:
  Under `set -euo pipefail` the upgrade subcommand would die at
  `numfmt --to=iec ... $headroom_bytes` whenever the new ISO was larger
  than the headroom — numfmt treats the leading `-` as an option flag,
  exits non-zero, and the script aborts before printing the descriptive
  back-up-and-reflash error message just below.  Splitting positive
  and negative cases and feeding numfmt the absolute value fixes it;
  the eventual error message now actually shows up.

Silent umount during sanity check:
  `umount "$child" 2>/dev/null || true` was swallowing both stderr and
  the exit code.  A busy partition (open file handle from a daemon
  that auto-opened the USB) would survive the loop, and `cmd_upgrade`
  would proceed straight to `dd if=<iso> of=<dev>` — writing to a
  block device whose partition is still mounted, corrupting the
  filesystem on that partition.  Adds a post-loop assertion: re-read
  MOUNTPOINTS and die with an `lsof` hint if anything is still mounted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 20:16:02 -06:00