Commit graph

765 commits

Author SHA1 Message Date
Roland
8044fd1240
File Browser: add filter input and collapse-all button
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Controller Tests / controller-tests (push) Waiting to run
Jest Tests / jest-tests (push) Waiting to run
Add a client-side filter above the File Browser table. Typing reduces
the visible rows to files/directories whose name matches, plus their
parent folders. A folder whose own name matches keeps its entire
subtree, and opening a matching folder shows its contents unfiltered.

* 'f' focuses the filter input, like the search inputs on other pages.
* An attached X button clears the filter; disabled while empty.
* A collapse-all (angle double up) button closes all open folders,
  clearing the ?folders= query param and deselecting hidden children.
* The filter row is right-aligned and double-width on desktop, and
  fills a single row on mobile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 11:31:41 -06:00
Roland
2162396212
Module pages: always show the deep-search reminder during fast searches
The DeepSearchHint only appeared on zero results, so a fast search with
matches gave no indication that captions/contents were not searched.
Module searches have no deep estimate, so show a generic reminder
whenever a fast search is active: "More results may be available in
deep search." with results, "Not finding what you need?" without.

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

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

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

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 10:32:16 -06:00
Roland
a035945033
Flasher: show connect error next to the detect button too
A failed "Filter files by detecting device" only surfaced the download-mode
reminder down in the Connect segment, off-screen. Extract the message into a
shared renderer and show it both places, so the error appears right where the
user clicked on the "Choose from your WROLPi" tab.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 17:26:45 -06:00
Roland
c7e77f3a4d Address Greptile review: fix chip-id map (17=C5, 18=P4) and drop stale mock
- ESP_CHIP_ID_NAMES disagreed with the backend ESP_CHIP_IDS: it mapped 18 to
  ESP32-C5 (18 is ESP32-P4) and omitted 17 entirely, so C5 firmware showed as
  "chip id 17" and P4 firmware was mislabeled "ESP32-C5". Now matches the
  backend (17=ESP32-C5, 18=ESP32-P4), with a test guarding it.
- Removed the stale filesSearch import/mock/beforeEach from the test; the
  flasher no longer calls it.

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 17:26:37 -06:00
Roland
6740507e57
Moving flasher config save button so that it is visible on all tabs. 2026-07-02 16:11:09 -06:00
Roland
2acdc2e16d
Expanding FileBrowser button widths when page is wide enough.
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Controller Tests / controller-tests (push) Waiting to run
Jest Tests / jest-tests (push) Waiting to run
2026-07-01 19:52:15 -06:00
Roland
05bb65fc03
Moving OTP and Vin decoder to calculators to save Navbar space. Reworking calculators to purpose groups, and adding colors. 2026-07-01 18:48:06 -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
e513e36496
Fix daily download limits not displaying in Settings UI
getSettings() in api.js remaps the /settings response through a fixed field
whitelist, which dropped download_daily_limit_global/per_domain before they
reached the SettingsContext. The values saved correctly but always rendered
empty. Add both fields to the mapping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 17:30:33 -06:00
Roland
706e22bd8f
Address review: clear daily limits via 0; document soft-cap semantics
- Settings.js: send 0 (not null) when a daily-limit field is cleared, so the
  settings API actually clears a previously-saved limit instead of dropping the
  null and keeping the old value.
- downloader.py: document that the daily limit is a best-effort soft cap; minor
  bounded overshoot is possible when create_downloads' background dispatch overlaps
  the perpetual cycle, and self-corrects the next cycle. A strict cross-process cap
  is intentionally not implemented.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 17:18:01 -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
Roland
2cdb2a4b3e Fix dark-theme Accordion background mismatch outside modals
The dark-theme rule hardcoded the inverted Accordion's title/content
background to #171616, which is the Modal content color. Inside a Modal
this matched, but on the /admin/status page (and any Segment, which
renders at #1B1C1D) the Accordion showed up as a darker band.

Make the inverted Accordion transparent so it inherits whatever
container it sits in, matching how the light theme already renders
accordions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 16:43:50 -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
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
Roland
16070e98b3
Fixing error where focusing on an empty Directory search would throw an error. 2026-06-24 15:23:32 -06:00
lrnselfreliance
264cc8cd58
Consolidate search options into a single Filter modal (#497)
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
* Consolidate search options into a single Filter modal

Reworks the file-search UI on the Dashboard, Videos, Archives, and Docs
pages. The crowded row of controls (sort, tags, limit, file-type filter,
date selector) is replaced with a single "Filter" button that opens one
comprehensive modal.

- Add SearchFilterModal + SearchFilterButton (violet + count badge when
  active). Sections (Sort, File Type, Tags, Results Per Page, Published
  Date) are shown per-page via props; layout uses a Semantic Grid.
- Selections are drafted locally and applied in a single updateQuery only
  on close/Done, so the search isn't re-run on every field change.
- Results Per Page and Sort use buttons (fewer clicks, no clipped dropdown).
- Extract shared SearchControlBar (view toggle + input + Filter) for the
  three results pages; move the global-search view toggle onto the tab row.
- Rework the Dashboard search line into one adaptive flex row; clear button
  only shows when there's an active search.
- Remove now-dead FilesView limit/tag returns and SearchLimitDropdown.

* Address review: reset limit on Clear All; use replace over replaceAll

- handleClearAll now resets Results Per Page to the default page size
  (DEFAULT_SEARCH_LIMIT = 24) so "Clear All" actually clears every setting.
- Replace start-anchored replaceAll(/^-/g) with replace(/^-/) for the sort key.
- Add a test covering the Clear All limit reset.

* Remove now-unused Media import from Files.js

The FilesSearchView refactor dropped the last <Media> usage in this file,
leaving an unused import (a build warning). Remove it.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 19:49:25 -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
e7c737fe0c
Allow arithmetic expressions in inventory number fields
A user can type a calculation into any numeric inventory field (number,
quantity magnitude, calories) and have it evaluated in place — e.g. weigh a
400 g box of nails, enter '400 - 20', and Tab/Enter swaps in 380.  The result
feeds dependent logic like count-by-weight automatically.

- evaluateExpression() (units.js) reuses the scoped mathjs instance, guarded by
  a strict regex so only bare arithmetic (digits, + - * / ( ) and spaces) is
  evaluated; anything with a letter, a plain number, or unparseable input is
  left untouched.  Float noise is rounded off.
- FieldCell evaluates on blur (Tab/click-away) and on Enter: in a numeric field
  the first Enter resolves a pending expression, a second submits the row.
- NUMERIC_TYPES is exported once from units.js and shared by the table and cell
  editors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 15:14:10 -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
447e4a40df
Select an item's value when its cell is clicked for editing
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
Clicking a cell in the Items table now focuses that field's input and selects
its contents, so the user can type a replacement value immediately. FieldCell
gains an `autoFocus` prop (focus + select on mount via a merged ref that still
supports the draft row's first-field ref); InventoryTable tracks which cell was
clicked and flags the matching FieldCell.

Also add a FieldSchemaEditor test covering the select-options comma fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 13:47:32 -06:00
Roland
6dd8e43635
Inventory select options can be typed (commas were ignored). 2026-06-19 13:19:56 -06:00
Roland
8101961749
Applying theme style to paragraphs in inventory. 2026-06-19 12:52:27 -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
947ee01b25
Add CSV/PDF export and a search filter to Inventory
New Export tab with two client-side formats (no new dependencies):
- CSV: the raw items table (RFC-4180 quoting, UTF-8 BOM for Excel), generated
  entirely in the browser.  The download anchor is attached to the DOM and the
  object URL is revoked on the next tick for cross-browser robustness.
- PDF: a printable full-inventory table followed by a group-by summary table,
  via the browser's print dialog ("Save as PDF").  The summary's grouping is
  chosen in the Export tab.

Search filter: a box on its own line under the inventory selector filters the
active inventory across every column's displayed value (case-insensitive;
whitespace-separated terms are AND-ed).  It narrows the whole view — Items
table, Summary aggregation, and Export/PDF — while edits/adds in the table
still operate on the full inventory, so filtering never drops data.

Summary/export grouping:
- The group-by aggregation is extracted into a shared summarize.js (field-role
  detection + count-aware totals via units.js + calories), now the single
  source of truth for the Summary tab, the PDF summary, and the ration
  calculator.  findCaloriesKey/findCountKey moved here from RationCalculator
  (re-exported for back-compat).
- The Sum selector now offers number and calories fields in addition to
  quantity: number/calories are summed as a plain column total, while quantity
  stays count-aware and unit-compacted and the dedicated Calories column stays
  count-aware.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:06:38 -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
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
bfbfa0710b
Playlists can be tagged on creation. Playlist destination is no longer empty by default. 2026-06-11 19:46:35 -06:00
Roland
898da37d47
Fixing bug where zim articles were not aligned in modals. 2026-06-11 19:46:34 -06:00
lrnselfreliance
9a5d1f1630
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-11 19:46:34 -06:00
Roland
8e5654a661
Removing extremely flaky test. 2026-06-11 19:46:34 -06:00
lrnselfreliance
e018616daf
Warn in the navbar when a drive's SMART health is failing. (#471)
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-11 19:46:33 -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
dependabot[bot]
512813d41e
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-11 19:46:33 -06:00
dependabot[bot]
2604985dda
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-11 19:46:32 -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
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
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