Commit graph

589 commits

Author SHA1 Message Date
Roland
cdd28d0a39
Videos: a Channel's directory beats source_id when assigning a Video.
Some checks failed
CodeQL / Analyze (push) Has been cancelled
Controller Tests / controller-tests (push) Has been cancelled
Jest Tests / jest-tests (push) Has been cancelled
A user reported videos placed in a Channel's directory did not appear on
that Channel's page. Their info json named another existing Channel, and
get_channel preferred the source_id match over the directory, so the
videos were assigned to the other Channel.

Reorder get_channel so the directory match (with its parent-directory
walk) runs before the source_id/url matches. A Video's files must live
in their Channel's directory, so the directory reflects the user's
intent and beats yt-dlp metadata. This also aligns validation with
VideoDownloader._get_channel, which already prefers the destination
directory, and stops validation from clobbering that decision.

channel_id still wins over all other params; source_id/url still apply
when the file is not in any Channel's directory.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 18:33:15 -06:00
Roland
f65f0aca6b
Restore "Most Views" download order
yt-dlp 2026.7.4 (via upstream PR #16965) again extracts view_count from
YouTube's lockupViewModel channel/playlist listings, so the 'views'
download order deprecated in fcb64fed can come back — verified live
against a real channel listing.

Unlike the old implementation, the sort degrades gracefully:

- Entries missing view_count sort last instead of raising KeyError.
- If NO entry has a view_count (yt-dlp older than ~2026.07), the
  download fails cleanly with UnrecoverableDownloadError telling the
  user to update yt-dlp or switch to Newest/Oldest — no retry loop.

Listing counts are approximate ("3K" -> 3000), which is fine for
ordering. No requirements.txt change needed; the >=2026.03.17 range
picks up 2026.7.4 automatically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 16:34:50 -06:00
Roland
68fab29a4b
Channels: log directory video counts after a refresh
After a Channel refresh, log how many video/audio FileGroups are in the
Channel's directory, and how many are assigned to this Channel, other
Channels, or no Channel. Helps debug Videos missing from their Channel
after files are moved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 17:45:10 -06:00
Roland
64b1ca8029
Channels: fix statistics inflated by tagged videos
The tag_file join in Channel.get_statistics() produced one row per tag,
so a Video with N Tags was counted N times in video_count, size, and
length. Count tagged Videos in a subquery instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 17:29:24 -06:00
Roland
73fafcd645
Search: fast ABC mode by default, opt-in deep search
The file_group.textsearch tsvector indexes all four text columns;
d_text (captions, document text, zip contents) is ~90% of the data,
making ts_rank searches take seconds on large libraries.  Add a
textsearch_abc generated column (title/author/description only, ~8x
smaller) and default all searches to it, with deep=true opting into the
full column.

Database:
- Migration adds the generated column and a GIN index (rewrites the
  table; a few minutes on large libraries).

Backend:
- FilesSearchRequest, VideoSearchRequest, ArchiveSearchRequest, and
  DocSearchRequest accept `deep`; each search function picks the column.
- /api/search_file_estimates returns both counts in one index-bound
  query (COUNT FILTER on the abc subset): `file_groups` (fast) and
  `file_groups_deep`.

UI (one coherent story across the count surfaces):
- The typing dropdown always suggests the deep count (what is findable).
- The tab badge shows the active mode's count so it matches the result
  list.
- The gap is reconciled by a hint offering deep search: count-aware on
  the global search ("No results in fast search. N results available in
  deep search." / "N more results available..."), generic on the module
  pages (no deep estimate there).  The hint resets pagination.
- "Search Depth" toggle in the search filter modals, persisted as
  ?deep=true.

Tests that relied on d_text matches now pass deep=True, with fast-mode
assertions alongside.  Also skip flaky test_download_channel on
CircleCI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 10:32:16 -06:00
Roland
c0fb9f95e1 Flasher: chip-mismatch guard, WROLPi firmware table, colorblind chip colors
Prevents the wrong-chip flash that bricks a board (e.g. an ESP32-S2 bootloader
in an ESP32 set): after connecting, each selected file's ESP image header is
checked against the device's chip_id and the offending row is flagged (a
warning, not a block). Changing the firmware selection now disconnects so the
check is always re-run against a freshly detected chip.

- "Choose from your WROLPi" is now a table (File / Path / Size / Chip / Kind
  / add), served by a single /flasher/search call
- search_esp_firmware returns every .bin annotated when unfiltered (non-ESP
  parts like partition tables, boot_app0, and littlefs stay pickable); a chip
  filter still narrows to that chip's ESP images
- Detect-device filter now searches by the canonical chip name (CHIP_NAME,
  e.g. "ESP32") instead of the variant description ("ESP32-D0WD-V3"), which
  matched nothing
- Chip column uses deterministic, colorblind-safe colors; same-width chip
  names always get distinct colors, and the palette has headroom to grow

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 17:26:37 -06:00
Roland
c95afc7f62 Add ESP32 firmware flasher
Browser-based ESP32/ESP8266 flasher (esptool-js + Web Serial); nothing is
uploaded to WROLPi, the flash is written straight to the device over USB.

Firmware sources (themed, tabbed): a saved set, the WROLPi media directory,
or a local file. Local files can be dropped anywhere on the page, which jumps
to the "Add from computer" dropzone. The media picker searches .bin files by
path and can detect the connected chip to filter firmware to matching images.

- Indexed file_group.suffix + path filter for fast .bin search (alembic
  migration backfills via split_path_stem_and_suffix)
- modules/flasher: chip detection from ESP image headers, /search endpoint,
  and flasher.yaml saved configurations (/configs CRUD)
- Flash progress modal with ETA, connect timeout, boot-mode hint, auto-
  scrolling logs

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 18:34:04 -06:00
Roland
fcb64fed13
Deprecate "Most Views" download order
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
YouTube's new listing layout means yt-dlp no longer returns `view_count`
in flat channel/playlist entries, so sorting by views raised KeyError and
the download failed and retried forever.

Sorting by views now requires per-video extraction (hundreds of requests
per channel), so the feature is deprecated rather than degraded:

- downloader: the 'views' branch raises UnrecoverableDownloadError so the
  download fails cleanly (no retry loop) with a message to switch to
  Newest/Oldest. May be revisited via the YouTube Data API.
- schema: download_order validation drops 'views' (newest/oldest/null only).
- UI: removed "Most Views" from the download-order dropdown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 18:06:34 -06:00
Roland
d95249218b Add per-domain and global daily download limits
Adds two optional DownloadManager settings:
- download_daily_limit_per_domain: max non-recurring downloads per domain per day
- download_daily_limit_global: max non-recurring downloads across all domains per day

Recurring Channel/RSS catalog updates are never gated and never counted; only the
videos/archives they spawn and one-off downloads count. Both successful and failed
attempts consume a slot (counted once per download per day). Limits reset at local
midnight (configured timezone). Over-limit downloads stay "new" and resume the next day.

Implementation:
- New Download.last_download_attempt column (+ index, migration) set at dispatch time so
  the count survives restarts and is correct across workers.
- dispatch_downloads gates non-recurring downloads: per-domain skips that domain, global
  stops dispatching counted downloads for the cycle. daily_download_counts derives the
  day's totals from the DB.
- Domain grouping uses normalized URLs via a new Downloader.normalize_domain method
  (base strips www/m/mobile + port); VideoDownloader/ChannelDownloader override it to
  collapse youtu.be, /shorts/, and tracking params to youtube.com. ArchiveDownloader is
  the seam for future archive URL normalization.
- Settings wired through WROLPiConfig, the /settings API (GET/PATCH, non-negative
  validation, 0 = unlimited), and the React Settings page.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 17:17:42 -06:00
lrnselfreliance
cafc6514cb
Restore the censored-video search filter (#499)
* Restore the censored-video search filter

Censored videos (no longer publicly available — detected when a channel
refresh drops them from the channel list, or when comment downloads fail
with "video unavailable/private/removed") are already flagged on
FileGroup.censored, but there was no way to find them since the search
rework dropped the old filter.

Add a "Censored" toggle to the reworked search filter modal, shown only on
the Videos page (showCensored).  When enabled the video search returns only
videos with FileGroup.censored = TRUE, tracked via a ?censored=true query
param (useSearchCensored).
* Move Availability filter below the Tags/Results row

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 09:48:36 -06:00
Roland
6a46cfd17d
Add manual import + backup Restore/Merge/Overwrite for inventory configs
Inventories are a config-only MultiFileConfig (config/inventory/<slug>.yaml) and
were not reachable by the admin config Restore UI, so there was no way to re-import
a hand-edited/copied-in file or restore an inventory from its daily backup.

Backend:
- MultiFileConfig gains generic helpers: get_backup_dates(slug)/_get_backup_file
  (surface the daily config/backup/<subdir>/<slug>-DATE.yaml files) and reimport()
  (rebuild the in-memory store from disk; picks up hand-edits and copied-in files,
  drops files deleted on disk).  backup_date is constrained to YYYYMMDD so it cannot
  traverse out of the backup directory.
- InventoriesConfig.preview_restore/apply_restore: overwrite replaces the whole
  inventory; merge unions items by id (keeps current fields and existing items).
  The pre-restore state is backed up automatically, so a restore is undoable.
- API: POST /inventory/reimport, GET /inventory/<slug>/backups,
  POST /inventory/<slug>/restore/preview, POST /inventory/<slug>/restore.

Frontend:
- New InventoryImportModal (opened from a Restore button in the Inventory toolbar):
  Re-import from disk, plus a dated-backup table with per-row Merge/Overwrite that
  previews the change before applying.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 16:28:11 -06:00
Roland
305c73921e
Add "Count by Weight" to estimate item counts from weighing
Counting bulk hardware (screws, nails) by hand is slow.  This adds a workflow
to estimate the count from weight instead: weigh one item, weigh the lot, and
the count is filled in automatically.

- The Fields modal gains a "Count by Weight" button that adds two quantity
  fields, Unit Weight and Total Weight (default grams), and tags the Count
  field as computed.  An existing `count` field is linked in place rather than
  duplicated, so hand-counted items keep their values.
- The link is plain metadata on the Count field
  (`compute: {kind: 'count_by_weight', total, unit}`), so it round-trips
  through the config/API with no backend change (covered by a backend test).
- During item entry, Count = round(Total Weight / Unit Weight), converting
  mismatched units first (g/kg/oz) and refusing to divide when only one side
  has a unit.  It rounds to a whole number (no 403.7 nails).
- When a weight changes, the count always reflects the new reality: it is set
  to the computed value (0 for a sub-unit total) when both weights are present,
  or cleared when they can't produce one (a weight blanked, incompatible
  units) — so a stale count can never persist against edited weights.  A
  hand-typed count is only touched when a weight actually changes.

countByWeight lives in units.js; the field preset, config detection, and the
entry-time apply hook live in a new computeFields.js.  Covered by unit tests
(countByWeight, computeFields), InventoryTable auto-fill / clear / sub-unit on
draft and inline edit, the FieldSchemaEditor preset, and a backend
field-metadata round-trip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 14:36:56 -06:00
Roland
ee0b827c71
Add a recommended Food Storage example, a Ration tab, and a supply planner
Build on the rebuilt config-only Inventory with three related additions.

Recommended example inventory:
- A fresh install now seeds the "Food Storage" example with the recommended
  one-adult / one-year (~3,000 kcal/day) emergency food supply (60 items,
  ~1.12M kcal), instead of an empty inventory.
- The counts are layered on top of the shipped food catalog (single source of
  per-package nutrition) via RECOMMENDED_ONE_YEAR_FOOD_COUNTS, joined at seed
  time by recommended_food_storage_items().
- Genericized the two remaining brand names in the catalog defaults
  (Velveeta -> Processed Cheese, Spam -> Canned Lunch Meat) so both the catalog
  and the seeded inventory stay brand-free.

Ration tab (split out of Summary):
- Moved the Ration Estimate (People / Daily Calories / estimate table) out of
  the increasingly long Summary tab into its own "Ration" tab, shown only for
  inventories that have a calories field.

Supply planner ("Plan More Supply"):
- A slider picks a target duration longer than the current estimate; the whole
  inventory is scaled up proportionally (every item by the same factor, mix
  kept balanced) into a sortable shopping list of what to buy.
- Any split package rounds UP to a whole one (ceil), matching the "1.6 buckets
  -> buy 1 more" rule.
- The shopping list is exportable as CSV (reusing toCSV) and PDF (reusing the
  print-to-PDF path, scoped via body.printing-shopping so only the list prints
  -- no inventory or summary).

Search filter:
- Moved the items search out of the top toolbar (which implied it filtered
  every tab) into the Items tab, so it clearly narrows Items only;
  Summary/Ration/Export/print always use the full inventory.

Aggregation/role helpers (findNameKey, planSupplyPurchase) live in summarize.js,
the single source of truth shared by Summary, export, and the ration views.
All new pure functions and UI flows are covered by tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 12:40:30 -06:00
Roland
5427beb5fb Fix swapped arguments in download_info_json Video.get_by_url call
`download_info_json` called `Video.get_by_url(url, session)`, but the method
signature is `get_by_url(session, url)`. This passed the URL string where the
session was expected, raising `AttributeError: 'str' object has no attribute
'query'` whenever a Video metadata refresh ran.

Swap the arguments at both call sites and add a regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 12:01:42 -06:00
Roland
cc5afe3fbc
Add responsive mobile view for Inventory with configurable columns
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
Portrait mobile shows a condensed, read-only table; landscape/tablet+ keeps
the full editable spreadsheet.  The columns shown on mobile are configurable
per field via a new "Mobile" toggle in the Customize Fields editor.

- defaults.py: seed `mobile` on each type's default columns (food: Name,
  Subcategory, Size, Count; fuel/tool: sensible analogs).
- FieldSchemaEditor: per-field Mobile toggle (persists through PUT /<slug>/fields).
- InventoryItemsMobile: read-only condensed view driven by the `mobile` flag,
  falling back to a default column set for schemas predating the flag.
- InventoryRoute: split Items tab via <Media> — mobile vs tablet+.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:08:34 -06:00
Roland
3d6aead072
Rebuild Inventory as a config-only system with food catalog and calculators
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
Replace the DB-backed inventory module with a config-only design and build out
food-storage tooling around it.

Inventory (config-only, no database)
- Each inventory is a YAML file at config/inventory/<slug>.yaml, loaded once
  into memory and the single source of truth. New reusable MultiFileConfig
  (wrolpi/common.py) manages a directory of per-entity files.
- Inventory types (food/fuel/tool) seed a customizable field schema
  (text/number/quantity/date/select/location/calories). Unit math/aggregation
  moved to the frontend (mathjs); Pint dropped from the backend.
- Minimal API: GET / (all inventories full), POST /, PUT /<slug> (whole-
  inventory save with version-based optimistic concurrency; 404 for missing),
  DELETE. Legacy inventories.yaml / old tables migrate to per-inventory files
  (pound->lb), and an alembic migration drops the tables.

Frontend
- New app/src/components/inventory/ package: spreadsheet-style table with
  Tab/Enter keyboard entry, sortable columns, expired-row highlighting,
  customizable field editor, and pooled cross-inventory location autocomplete.
- A single useInventories store loads everything once and persists whole
  inventories optimistically; list/items/summary/locations derive client-side.
- Summary groups items (default by Category), totals size x count, shows a
  calories column, and embeds a ration estimate (how long the stock lasts).

Food catalog
- Shared, editable catalog (config/inventory/catalog.yaml) seeded from
  catalog_defaults.yaml (~60 generic items, total calories per package) with
  merge-keep-edits + tombstones. GET/PUT /api/inventory/catalog, a management
  modal, and entry-row pre-fill of category/size/calories from a name match.

Calculators
- Generic Food Storage planner (like the Water calculator): household +
  editable per-person/year amounts -> total to store over a chosen duration,
  with women/children weighted by DGA calorie shares and an imperial/metric
  toggle.

Also fixes ConfigFile.save() to accept nested file_names. Backend pytest and
frontend jest suites updated/added throughout; alembic check clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 17:14:54 -06:00
lrnselfreliance
657a5bd561
Move One-Time Pad encryption to the browser (#479)
* Move One-Time Pad encryption to the browser; delete backend OTP module

OTP encryption/decryption is now performed entirely in the browser so
messages, keys, and generated pads never leave the originating device.

- Add app/src/components/otp.js: a JS port of the former modules/otp/lib.py
  (encrypt/decrypt, message formatting, validation, and secure pad
  generation via crypto.getRandomValues with rejection sampling).
- Encrypt/Decrypt now run live as the user types or pastes, with inline
  error messages instead of submit buttons.
- Generate New Pad builds the printable HTML locally (blob URL) instead of
  hitting /api/otp/html.
- Migrate all backend OTP tests to app/src/components/otp.test.js with
  identical data.
- Delete the modules/otp backend module and its blueprint registration.

* Revoke the One-Time Pad blob URL after the new tab loads

Addresses a minor blob-handle leak: handleGenerateNewPad created an object
URL on every click but never released it.

* Let users choose the One-Time Pad character set

Add an "Advanced" accordion under the buttons with three options: A-Z,
A-Z/0-9 (default, unchanged), and a custom character set. The chosen set is
shared by encrypt, decrypt, and pad generation.

- otp.js: parameterize encrypt/decrypt/generate on the alphabet (default
  OTP_CHARS for back-compat); add OTP_CHARS_ALPHA and validateCharset; input
  is auto-uppercased only when the set is all-uppercase, so custom sets with
  lowercase are case-sensitive; the printable pad is labeled with its set.
- Apps.js: lift charset state into OTP(); gate encrypt/decrypt/generate on a
  valid set with inline validation errors.
- otp.test.js: add A-Z, custom round-trip, auto-uppercase, validateCharset,
  and pad-label tests.

* Scope inverted-accordion dark background to direct children

The rule forcing #171616 on .ui.inverted.accordion .title/.content used a
descendant selector, so it leaked onto any nested element using the `content`
class — e.g. an ErrorMessage's Message.Content rendered inside SimpleAccordion
got a dark band behind its text. Scope to direct children so the accordion
still styles its own title/content but not nested components.

* Add optional error-detection checksum to the One-Time Pad

Adapts the position-weighted checksum from otp-checksum.md to the
frontend-only OTP. checksum = sum(position * value) mod N, parameterized on
the selected character set (N = chars.length), so it follows whichever pad
alphabet is chosen. Position weighting catches transposition errors.

- otp.js: add calculateChecksum/appendChecksum/verifyChecksum.
- Apps.js: opt-in "Append error-detection checksum" toggle in Advanced
  (default off). When on, encrypt appends a checksum char; decrypt verifies
  it, warns on mismatch, and still shows the decrypted text.
- otp.test.js: checksum unit tests, charset-variation, and end-to-end vectors.

* Show compact checksum status instead of a large warning box

Replace the bulky semantic-ui WarningMessage on decrypt with a small inline
status line: a green check + "Checksum is valid" or a red x + "Checksum is
invalid", shown whenever the checksum option is on and a message is being
decrypted.

* Fix checksum-mode decrypt truncating messages without a checksum

When the "Append error-detection checksum" option was on, Decrypt always
stripped the last ciphertext character before decrypting, so a message that
carried no trailing checksum (wrong sender setting, body pasted alone, etc.)
lost its final character and decrypted to garbage.

Add stripChecksum(received, chars), which removes the trailing character only
when it actually verifies as the checksum of the preceding characters; a
message without a valid checksum is returned intact and flagged invalid. The
decrypt UI now uses it.

Also broaden test coverage:
- stripChecksum tests, including a regression for the truncation bug.
- validation error classes (InvalidOTP/Plaintext/Ciphertext) and the
  longer-than-OTP length guards.
- deterministic encrypt/decrypt roundtrip fuzz across several character sets.
- generateMessage character-count and charset-membership checks.
- end-to-end checksummed encrypt -> format -> strip -> decrypt flow.

Use a dedicated InvalidMessage error for the checksum helpers instead of the
misleading InvalidPlaintext.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 13:52:34 -06:00
Roland
fb23fe1f61
HTML indexer is binary-safe; it indexes the page inside a compressed singlefile.
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 archive modeler claims WROLPi's own singlefiles, but a foreign
compressed (SingleFileZ) HTML file (e.g. saved by the browser extension and
dropped into the media directory) falls through to the generic HTML
indexer, which crashed reading the binary ZIP tail as text.  The indexer
now reads bytes and indexes the index.html inside the ZIP.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 17:24:02 -06:00
lrnselfreliance
1acb148c90
Single file deno (#478)
* Singlefile runs under Deno 2.8.3; browser leftovers are reaped.

single-file-cli is designed for Deno.  Under node 18 (Debian 12/RPi OS) it
cannot reach the browser CDP on IPv6-enabled hosts ("localhost" resolves to
::1 first with no IPv4 fallback) and it never exits after a successful save,
blocking the download worker forever.

The native path and the Archive docker container now run single-file with
"deno run".  Deno is pinned at v2.8.3 everywhere because yt-dlp 2026.06.09
requires at least 2.3.0 (our node is Debian's v18, below yt-dlp's v22
minimum).  Deno >= 2.3 kills the browser single-file spawns (piped stderr
that single-file never reads), so the browser is run through
singlefile_browser_wrapper.sh which discards its stderr.

Commands are run in their own process group which is killed when they
finish, reaping the browser single-file leaves behind.  The Archive
downloader now has a ten minute timeout.  Install scripts install Deno
(previously only upgrade.sh and the image builds did).

* Singlefile writes to a file instead of dumping to stdout.

Deno >= 2.3 truncates large --dump-content output at exit; compressed
(SingleFileZ) singlefiles of large pages lost their ZIP tail, failed the
compression check in write_archive_files, and were destroyed by the HTML
prettifier (blank white archives).  single-file now writes its result to a
temporary file which WROLPi reads back.

As insurance, a singlefile marked compressed (<html data-sfz) whose ZIP is
unreadable is now stored verbatim instead of being prettified.

* Compressed Archive screenshots and readability images are no longer broken.

The index.html inside a compressed (SingleFileZ) singlefile references its
resources by relative ZIP paths.  Screenshots are now taken from the fully
extracted ZIP so those references resolve.  Readability images are inlined
as data URIs read from the ZIP (readability-extractor absolutizes relative
srcs against the page URL, so both forms are matched).

* Compressed Archives are created by converting the uncompressed capture.

The page is always captured uncompressed; readability, screenshot, and
title come from that canonical page (its images are inline data URIs, so
no ZIP extraction or image inlining is needed).  When compress_singlefile
is set, a second single-file run converts the captured file into a
compressed (SingleFileZ) self-extracting file.  The conversion is a local
file:// operation - the page is not fetched again, and single-file
preserves the original URL header when re-saving its own pages.  If the
conversion fails, the uncompressed singlefile is stored instead.

* Consolidating duplicate compressed-singlefile tests.

The write_archive_files tests (verbatim write, title fallback, broken-ZIP
guard, readability inlining) are combined into one test; the helper probes
and the setting-plumbing tests are each merged into one.  The do_singlefile
compression-failure scenario and the timeout constant are folded into the
main do_singlefile test, which shared their setup.

* Addressing review comments: removeprefix, PermissionError when killing.

* Bumping version to 0.24.1-beta.

* Install scripts assert the deno.zip hash to avoid malicious or invalid files.

The hashes are the official sha256sum assets of the Deno v2.8.3 release,
per architecture.

* Archive Dockerfile asserts the deno.zip hash to avoid malicious or invalid files.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 17:07:28 -06:00
lrnselfreliance
0a776d030f
Archives can be compressed (SingleFileZ self-extracting HTML) (#477)
* Archives can be compressed (SingleFileZ self-extracting HTML).

A new compress_singlefile download setting runs single-file with
--compress-content, creating a self-extracting HTML file which is also a
valid ZIP.  Only the singlefile .html is compressed; readability files are
always written uncompressed.  Readability, screenshots, titles, and URLs
are extracted from the index.html inside the ZIP.  A Compress checkbox in
the Archive and RSS download forms remembers the user's choice in local
storage.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 19:46:35 -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
e691f4db1e Fixing kiwix restart cmd: missing comma silently concatenated two argv entries.
`cmd = ('sudo', '/usr/bin/systemctl', 'restart' 'wrolpi-kiwix.service')`
                                              ^ missing comma here

Python implicit string-literal concatenation joined the two strings into
'restartwrolpi-kiwix.service', so systemctl saw one argument it didn't
recognise and exited 2 (INVALIDARGUMENT).  Net effect: kiwix never
restarted after a Zim download, the user still had to manually
`sudo systemctl restart wrolpi-kiwix` to see the new Zim.

Adds a unit test that pins the argv shape so an adjacent-literal typo
here surfaces in CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 20:16:02 -06:00
Roland
1951ca548d
Using stubs to simplify tests.
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-07 20:07:46 -06:00
Roland
63d768bdc3 Reporting download progress using download context. 2026-05-07 20:07:31 -06:00
Roland
8f0bb78722 Converting RSS and Video Downloaders to new testable strategy. 2026-05-07 20:07:31 -06:00
Roland
8f49f33ba2 Passing download down archive downloader.
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-07 15:54:53 -06:00
Roland
c644d83a5b Migrating map downloaders. 2026-05-07 15:54:53 -06:00
Roland
9a8ad7c3de Migrating Kiwix downloaders. 2026-05-07 15:54:53 -06:00
Roland
a29df682ef Migrating FileDownloader. 2026-05-07 15:54:53 -06:00
Roland
f80b3ea769 Fixing bug where Channel Downloader would create empty videos.xml channel if RSS feed was sent to it. 2026-05-07 15:54:53 -06:00
Roland
4080b7a641 Refactoring ArchiveDownloader so that it is more testable. 2026-05-07 15:54:53 -06:00
Roland
ba63170e55
Consolidating deletion endpoints.
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-03 19:14:30 -06:00
Roland
09a967535b Models like Archive/Video/Doc can be deleted, even if they are tagged. User is shown a warning and must confirm deletion of tagged files. 2026-05-03 19:14:15 -06:00
Roland
a9a460c54c
Creating browser extension to facilitate starting downloads. Chrome/Brave and Firefox supported.
The extension is hosted in a sibling repo and
is distributed off-store. Each WROLPi release ships the prebuilt artifacts
under wrolpi/static/extensions/ and serves them at /api/extensions/<file>:

  - wrolpi-chrome.zip       Load-Unpacked sideload for Chrome / Brave / Edge
  - wrolpi-firefox.xpi      Mozilla-signed (unlisted self-distribution),
                            one-click install in any Firefox
  - versions.json           Per-browser version metadata for the install page

Backend (modules/archive/api.py, wrolpi/root_api.py):

  - POST /api/archive/upload accepts an optional repeated `tagNames`
    multipart form field; the upload switch handler applies tags to the
    new FileGroup via FileGroup.set_tags(). Unknown tag names are auto-
    created inside a per-name SAVEPOINT so concurrent inserts on the same
    novel name don't abort the transaction (Tag.name has unique=True).
  - GET /api/extensions returns metadata (per-file availability + size +
    versions.json contents) for the install page.
  - GET /api/extensions/<filename> serves the binary, whitelisted by
    filename. Firefox .xpi is served as application/x-xpinstall WITHOUT a
    Content-Disposition: attachment header, so Firefox triggers its
    add-on install handler instead of saving the file.

Frontend (app/src/):

  - New /admin/extension page (linked from AdminRoute) detects the user's
    browser via navigator.userAgent and renders an install card per
    Firefox / Chromium / Other. Pulls metadata from /api/extensions to
    show file size and version, and gracefully reports 'Not yet
    available' when binaries are missing.
  - Dashboard's Getters component supports a deep link from the
    extension's 'Configure Download' button:
      /?downloader=<archive|video|...>&download_url=<u1>&download_url=<u2>
    On mount, the matching downloader form auto-opens with URLs pre-
    filled. Query params are cleared on close so a refresh doesn't
    re-open the modal.
  - DownloadMenu and the per-downloader forms accept an `initialUrls`
    seed via the existing `download` prop pattern, joined with newlines
    for the `urls` textarea (and first URL for single-URL forms).

Behavior is fully backwards compatible: pre-existing API callers can
still POST to /api/archive/upload without `tagNames`, and the
DashboardPage modal continues to open via the existing button click
when no deep-link query params are present.
2026-05-03 11:48:50 -06:00
Roland
9468448a54
Modifying Video/Channel/RSS Downloaders so that they can download videos, but audio only. These audio files are grouped under their channel directories. 2026-04-14 15:33:01 -06:00
Roland
cefa29e67c
Adding simple search to epub viewer. 2026-04-13 17:23:20 -06:00
Roland
8a75cf9057 Linking to specific page or section of an EPUB/PDF when user searches. 2026-04-13 16:47:02 -06:00
Roland
c726654de9
Map search added to global search. Preview cards are displayed for all results in new Map tab.
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
Map viewer has simple search.

Map search databases can be downloaded from CDN, or built locally. Map searches can be built offline, but will not have enriched things like state names downloaded from Wikidata.
2026-04-13 12:17:08 -06:00
Roland
9e5a8b8057
Adding some vertical video formats to downloader. 2026-04-09 15:03:44 -06:00
Roland
e0324ce0fc Migrating Controller from port 8087 to 80 so that it is more discoverable. Reformatting Controller dashboard to make header at the top. Reformatting navigation bar to handle the new links which prevented Admin from being visible in some screens. 2026-04-09 15:01:05 -06:00
Roland
250575ef87
Delete duplicate video on download, even if it has tags.
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-04-04 19:05:16 -06:00
Roland
105ae377f9
Video download destination can download to the incorrect Channel.
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-04-04 15:52:47 -06:00
Roland
bbc23dbf90
Adding map default view option to the map context menu. 2026-04-04 14:56:25 -06:00
Roland
b8a532c8d9
Key changes:
- Add MapLibre GL JS viewer with light/dark themes, terrain layer,
  and embedded static map page for coordinate sharing
- Add map pin support with persistent map_pins.yaml config
- Add map region subscriptions and downloader with progress tracking,
  sourcing pre-extracted PMTiles from a cold CDN
- Add map export to PNG and GPS coordinate copy via context menu
- Add MapViewer React component alongside reworked Map page
- Add map catalog and region-building script (build_map_regions.py)
- Remove OSM import/render scripts, leaflet files, Apache configs,
  renderd service, and osm-map-dumper Docker container
- Update Caddyfile to serve PMTiles and map static assets
- Update build/install/repair scripts for Protomaps dependencies
- Drop map_file table, add/drop map_subscription table (alembic)
- Adding button to skip primary drive mounting during onboarding.
2026-04-04 13:48:19 -06:00
Roland
0ceac364ec Hiding traceback when zim file does not match kiwix naming scheme. 2026-03-29 10:19:28 -06:00
Roland
755b133fa4
Fixing bug where configs could be overwritten with empty configs on first refresh. Fixing bug where configs were displayed as JSON in admin settings. 2026-03-22 12:56:37 -06:00
Roland
282b0f63e8
Adding links next to video captions to allow scrubbing of video by the captions, adding filter to make finding quick. 2026-03-21 16:50:27 -06:00
Roland
1c4f57226f
Adding skip_viewed to archive and video fetching endpoints.
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-03-20 20:17:27 -06:00
Roland
00cfdc14c6
Trying to get existing channel using more variations of Youtube URLs.
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-03-20 17:26:51 -06:00