Module manager and a footer menu for Modules and Settings (#10 part 2b, #30) #82

Merged
claude-bot merged 16 commits from feat/module-manager into main 2026-10-27 17:20:39 +00:00
Collaborator

Closes #30. Part 2b of #10 (part 2c is not in this PR).

What

  • Module manager (#10 2b): browse, search, install, update flags and remove modules from CrossWire, CrossWire Beta, CrossWire Attic and eBible.org. Lists are cached and shown offline.
  • A menu in the reading footer with Modules and Settings, each with an icon (#30).
  • Language names from CLDR and ISO 639-3 ("UI-language name · autonym"); only usable kinds are offered.
  • Display settings moved into Settings (the Aa button is gone); one bottom page bar on every page; Keep is the default in the Remove dialog.

Details by round:

  • Round 1: core Catalog (index merged with installed, filters by text, kind and language, installed and update flags), fetched off the UI thread only when the manager opens, with Refresh. Remove is refused while the module has a pending task and is safe if killed part-way. ModulesPage in QtQuick.Controls 2; Settings has a switch per repository; "+" in the Bible chooser opens the manager on Bibles. No list model (cxx-qt links a second QAbstractListModel twice), so the page uses count() and per-row invokables, re-asked when revision changes.
  • Round 2: language table core/src/bible/locale/table.tsv (about 25 KB, generated by tools/langnames/langnames.py, --check verifies it; CLDR 47.0.0 and the SIL ISO 639-3 table, inputs pinned by SHA-256), 3-letter code folding. Kind::usable() is true only for Bible; an installed unusable module is listed greyed and can be removed; the kind filter is hidden while one kind is usable. PageBar.qml is the bottom bar of every page (Back inside it, centred title, trailing slot), which fixes the Back overflow. Per-row logs trimmed. NOTICES.md added.
  • Round 3: icons for the menu entries (Lucide library-big and settings as tinted PNGs), tinted light in night mode.
  • Also: i18n refreshed; docs 04, 06, 09 and CLAUDE.md status; e2e module-manager case, older cases ported to the manager and Settings Display.

Repos not in the enabled set are not offered; an installed module from one of them is listed and can only be removed.

Blast radius

Tier: trunk
Trunk paths touched: qt/ glue, qml/pages/ReadingPane.qml, qml/Main.qml, core/src/bible*, tools/e2e/, tools/mem-check/, tools/ci/, Makefile.toml, .forgejo/workflows/
Testing tier: elevated (also: UI, e2e harness, generated data)
Gate / rollback: no flag; revert this merge. No schema change, so no migration or app.sqlite3 backup is involved.

Proof

  • CI is green on head 3499449 (all jobs).
  • Round 3 local checks: cargo make app-docker passed: window loaded after 122 ms, text on screen after 318 ms, peak PSS 161280 kB normal profile and 163729 kB slow profile, both under the 165000 kB budget. E2E_CASES="module-manager display-settings" e2e passed 2/2. I looked at the shots in day (module-manager-01-menu.png) and night (display-settings-04-menu-night.png): both icons show next to the entries, and in night mode they are tinted light like the text. origin/main was already an ancestor of HEAD, so there was no merge. One push, no force. I did not wait for CI.
  • Round 2 (limited e2e, then CI): display-settings display-settings-kill red-letters red-letters-greyed red-letters-kill switch-bible-red-letters module-manager picker-back typed-reference starter-install add-bible 11 passed, 0 failed; shots of Modules, Settings and the picker looked at.
  • Fails without the change: with Kind::usable always true and the label returning the autonym only, 7 core tests fail (for example only_usable_kinds_are_offered_but_an_installed_one_is_listed_to_be_removed, the_label_has_both_names_unless_they_are_the_same_or_missing). With Remove's pending-task refusal removed, removing_a_module_with_a_queued_task_is_refused and removing_a_module_that_is_downloading_is_refused_and_the_download_goes_on fail.
  • tools/langnames/langnames.py --check: up to date.
  • Phone (Volla, 3499449, installed over USB): the owner's checklist passed.

Budgets

  • Desktop PSS 165,422 kB with the full index (above the 165,000 kB budget by 422 kB).
  • Phone PSS (Volla, 3499449, Modules open on the full index): peak 84,689 kB, within the 165,000 kB budget. The desktop figure above counts desktop Qt and Mesa, so the phone number is the one that applies.
  • Binary: +25 KB read-only language table, read on first use; the index is loaded only when the manager opens, nothing at start-up. The menu icons add a small amount of memory when the menu is shown; the PSS runs above include them.

Review

Reviewer: fresh-context code-review (claude-review): high at 00c99d1 (QML, qt, e2e, sword; core) and a delta at ff1d852
Review-Confidence: 55 (audit at 3499449; one deduction was for the empty-CipherKey gap, now deferred to #83)

Confidence by part

  • Core catalog, remove and language table: high (unit tests, fail-without-change checks).
  • QML pages, PageBar, menu icons: high (e2e, shots, owner's phone check passed).
  • Memory with the full index: high (84,689 kB peak on the phone).

Open items

  • Phone check passed (owner, comment on this PR). Follow-ups filed: #83 (empty CipherKey), #84 (icon contrast in the dark theme), #85 (remove night mode?), #86 (searchable language selector), #87 (grey out Install when offline), #88 (download icon for Install).
  • #10 stays open for part 2c (import).

🤖 Generated with Claude Code

Closes #30. Part 2b of #10 (part 2c is not in this PR). ## What - Module manager (#10 2b): browse, search, install, update flags and remove modules from CrossWire, CrossWire Beta, CrossWire Attic and eBible.org. Lists are cached and shown offline. - A menu in the reading footer with Modules and Settings, each with an icon (#30). - Language names from CLDR and ISO 639-3 ("UI-language name · autonym"); only usable kinds are offered. - Display settings moved into Settings (the Aa button is gone); one bottom page bar on every page; Keep is the default in the Remove dialog. Details by round: - Round 1: core Catalog (index merged with installed, filters by text, kind and language, installed and update flags), fetched off the UI thread only when the manager opens, with Refresh. Remove is refused while the module has a pending task and is safe if killed part-way. ModulesPage in QtQuick.Controls 2; Settings has a switch per repository; "+" in the Bible chooser opens the manager on Bibles. No list model (cxx-qt links a second QAbstractListModel twice), so the page uses `count()` and per-row invokables, re-asked when `revision` changes. - Round 2: language table `core/src/bible/locale/table.tsv` (about 25 KB, generated by `tools/langnames/langnames.py`, `--check` verifies it; CLDR 47.0.0 and the SIL ISO 639-3 table, inputs pinned by SHA-256), 3-letter code folding. `Kind::usable()` is true only for Bible; an installed unusable module is listed greyed and can be removed; the kind filter is hidden while one kind is usable. `PageBar.qml` is the bottom bar of every page (Back inside it, centred title, trailing slot), which fixes the Back overflow. Per-row logs trimmed. NOTICES.md added. - Round 3: icons for the menu entries (Lucide library-big and settings as tinted PNGs), tinted light in night mode. - Also: i18n refreshed; docs 04, 06, 09 and CLAUDE.md status; e2e `module-manager` case, older cases ported to the manager and Settings Display. Repos not in the enabled set are not offered; an installed module from one of them is listed and can only be removed. ## Blast radius **Tier:** trunk **Trunk paths touched:** `qt/` glue, `qml/pages/ReadingPane.qml`, `qml/Main.qml`, `core/src/bible*`, `tools/e2e/`, `tools/mem-check/`, `tools/ci/`, `Makefile.toml`, `.forgejo/workflows/` **Testing tier:** elevated (also: UI, e2e harness, generated data) **Gate / rollback:** no flag; revert this merge. No schema change, so no migration or `app.sqlite3` backup is involved. ## Proof - CI is green on head 3499449 (all jobs). - Round 3 local checks: `cargo make app-docker` passed: window loaded after 122 ms, text on screen after 318 ms, peak PSS 161280 kB normal profile and 163729 kB slow profile, both under the 165000 kB budget. `E2E_CASES="module-manager display-settings"` e2e passed 2/2. I looked at the shots in day (module-manager-01-menu.png) and night (display-settings-04-menu-night.png): both icons show next to the entries, and in night mode they are tinted light like the text. origin/main was already an ancestor of HEAD, so there was no merge. One push, no force. I did not wait for CI. - Round 2 (limited e2e, then CI): `display-settings display-settings-kill red-letters red-letters-greyed red-letters-kill switch-bible-red-letters module-manager picker-back typed-reference starter-install add-bible` 11 passed, 0 failed; shots of Modules, Settings and the picker looked at. - Fails without the change: with `Kind::usable` always true and the label returning the autonym only, 7 core tests fail (for example `only_usable_kinds_are_offered_but_an_installed_one_is_listed_to_be_removed`, `the_label_has_both_names_unless_they_are_the_same_or_missing`). With Remove's pending-task refusal removed, `removing_a_module_with_a_queued_task_is_refused` and `removing_a_module_that_is_downloading_is_refused_and_the_download_goes_on` fail. - `tools/langnames/langnames.py --check`: up to date. - Phone (Volla, 3499449, installed over USB): the owner's checklist passed. ## Budgets - Desktop PSS 165,422 kB with the full index (above the 165,000 kB budget by 422 kB). - Phone PSS (Volla, 3499449, Modules open on the full index): peak 84,689 kB, within the 165,000 kB budget. The desktop figure above counts desktop Qt and Mesa, so the phone number is the one that applies. - Binary: +25 KB read-only language table, read on first use; the index is loaded only when the manager opens, nothing at start-up. The menu icons add a small amount of memory when the menu is shown; the PSS runs above include them. ## Review **Reviewer:** fresh-context `code-review` (claude-review): high at 00c99d1 (QML, qt, e2e, sword; core) and a delta at ff1d852 **Review-Confidence:** 55 (audit at 3499449; one deduction was for the empty-CipherKey gap, now deferred to #83) ## Confidence by part - Core catalog, remove and language table: high (unit tests, fail-without-change checks). - QML pages, PageBar, menu icons: high (e2e, shots, owner's phone check passed). - Memory with the full index: high (84,689 kB peak on the phone). ## Open items - Phone check passed (owner, comment on this PR). Follow-ups filed: #83 (empty CipherKey), #84 (icon contrast in the dark theme), #85 (remove night mode?), #86 (searchable language selector), #87 (grey out Install when offline), #88 (download icon for Install). - #10 stays open for part 2c (import). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Browser holds the merged list with its filters and load state; IndexCache
loads the kept indexes first, then fetches each repository's. The qt glue
is invokables plus a revision property (a second QAbstractListModel in
cxx-qt links twice), and the installer gains install/remove by module.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Docs and translations for the module manager (#10)
All checks were successful
CI / rust (pull_request) Successful in 4m10s
CI / audit (pull_request) Successful in 11s
CI / app (pull_request) Successful in 2m49s
CI / e2e (pull_request) Successful in 6m59s
CI / click (pull_request) Successful in 1m45s
b5d51e0980
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
claude-review left a comment

Level: high. Blast radius: trunk. Reviewed commit: b5d51e0980. Range: origin/main...HEAD.

Files read in full or around the hunks: core/src/tasks.rs, core/src/tasks/exec.rs, core/src/db/tasks.rs and 003_task_repo.sql, core/src/repos.rs, core/src/catalog.rs and catalog/load.rs, sword/src/install.rs and install/http.rs (download resume), qt/src/installer.rs, qt/src/module_manager.rs, qml/Main.qml, qml/pages/ReadingPane.qml, qml/pages/ModulesPage.qml (top). Not read in depth: the e2e cases, po/ and docs.

No high or medium defects found. Checked and found sound: migration 003 is additive and NULL means the default repo; a partial download resumed against a different repo's URL is discarded because the .state file is checked against the URL; the Remove-while-pending refusal is surfaced through Refused and Failed(Remove) to removeRefused.

Low findings (inline, not blocking):

  • core/src/tasks/exec.rs: the Remove refusal means the QML flow has to cancel first.
  • qml/pages/ModulesPage.qml: the reading Bible is switched before the removal is known to be accepted.
  • qt/src/module_manager.rs: a Refresh does not stop the previous index-loading thread.
Level: high. Blast radius: trunk. Reviewed commit: b5d51e0980e4d98048e2b9915f6adb741eaa2db7. Range: origin/main...HEAD. Files read in full or around the hunks: core/src/tasks.rs, core/src/tasks/exec.rs, core/src/db/tasks.rs and 003_task_repo.sql, core/src/repos.rs, core/src/catalog.rs and catalog/load.rs, sword/src/install.rs and install/http.rs (download resume), qt/src/installer.rs, qt/src/module_manager.rs, qml/Main.qml, qml/pages/ReadingPane.qml, qml/pages/ModulesPage.qml (top). Not read in depth: the e2e cases, po/ and docs. No high or medium defects found. Checked and found sound: migration 003 is additive and NULL means the default repo; a partial download resumed against a different repo's URL is discarded because the .state file is checked against the URL; the Remove-while-pending refusal is surfaced through Refused and Failed(Remove) to removeRefused. Low findings (inline, not blocking): - core/src/tasks/exec.rs: the Remove refusal means the QML flow has to cancel first. - qml/pages/ModulesPage.qml: the reading Bible is switched before the removal is known to be accepted. - qt/src/module_manager.rs: a Refresh does not stop the previous index-loading thread.
@ -106,0 +105,4 @@
/// Queues a removal, unless the module has a task of its own going:
/// an install may be unpacking it, or a removal already be on its way.
/// The caller cancels first if that is what it wants.
fn remove(&self, id: u64, module: &str) {
Collaborator

Low: the behaviour changed from cancel-then-remove to refuse-if-pending. The Remove command's only caller is the module manager, which must cancel first; this is documented, but nothing in the UI path cancels an update install before Remove, so the user gets a refusal text and has to cancel by hand.

Low: the behaviour changed from cancel-then-remove to refuse-if-pending. The Remove command's only caller is the module manager, which must cancel first; this is documented, but nothing in the UI path cancels an update install before Remove, so the user gets a refusal text and has to cancel by hand.
@ -0,0 +36,4 @@
if (page.reading && name === page.reading.moduleName) {
for (let i = 0; i < page.reading.bibleCount(); i++) {
if (page.reading.bibleName(i) !== name) {
page.reading.switchBible(page.reading.bibleName(i))
Collaborator

Low: the reading Bible is switched away before removeModule is accepted. If the core refuses (the module has a queued or running task), the reader has moved to another Bible for no reason. Scenario: reading Bible X while X has a pending update install, then Remove X: the Bible switches, the removal is refused. Consider switching on moduleRemoved instead.

Low: the reading Bible is switched away before removeModule is accepted. If the core refuses (the module has a queued or running task), the reader has moved to another Bible for no reason. Scenario: reading Bible X while X has a pending update install, then Remove X: the Bible switches, the removal is refused. Consider switching on moduleRemoved instead.
@ -0,0 +316,4 @@
}
self.as_mut().notify();
let thread = self.qt_thread();
std::thread::spawn(move || {
Collaborator

Low: Refresh spawns a new loading thread without stopping the previous one (the stop flag is shared per object). Two threads can fetch and write the same index and ETag files at once; each file write is atomic, but the index/ETag pair can end up mismatched, costing one extra full fetch. Only wasted work, no corruption.

Low: Refresh spawns a new loading thread without stopping the previous one (the stop flag is shared per object). Two threads can fetch and write the same index and ETag files at once; each file write is atomic, but the index/ETag pair can end up mismatched, costing one extra full fetch. Only wasted work, no corruption.
Module manager: keep the reading Bible while a removal is refused; a refresh stops the previous index load (#10)
Some checks failed
CI / rust (pull_request) Successful in 4m21s
CI / audit (pull_request) Successful in 10s
CI / app (pull_request) Successful in 2m41s
CI / e2e (pull_request) Failing after 7m3s
CI / click (pull_request) Has been skipped
52104869c6
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Author
Collaborator

Round 1 answers (head 5210486):

  • qml/pages/ModulesPage.qml:39 (switch before removal accepted): fixed. remove() no longer switches the reading Bible while the module has an install row going (phaseOf is neither "" nor "failed"); the core then refuses and the reader stays put.
  • qt/src/module_manager.rs:319 (refresh leaves old thread): fixed. Each load gets its own stop flag and starts by setting the previous one, so the older load stops fetching and its answers are dropped by generation.
  • core/src/tasks/exec.rs:108 (no cancel before Remove): refuted. The UI never offers Remove on a busy row (the row shows Cancel, and the update row's Remove is hidden while busy), so the refusal only fires on a race. Cancel-then-remove in the UI would be racy as well, since a running cancel finishes asynchronously and the pending check would still see the task. The refusal text tells the user to try again, and the core tests pin the behaviour.
Round 1 answers (head 5210486): - qml/pages/ModulesPage.qml:39 (switch before removal accepted): fixed. remove() no longer switches the reading Bible while the module has an install row going (phaseOf is neither "" nor "failed"); the core then refuses and the reader stays put. - qt/src/module_manager.rs:319 (refresh leaves old thread): fixed. Each load gets its own stop flag and starts by setting the previous one, so the older load stops fetching and its answers are dropped by generation. - core/src/tasks/exec.rs:108 (no cancel before Remove): refuted. The UI never offers Remove on a busy row (the row shows Cancel, and the update row's Remove is hidden while busy), so the refusal only fires on a race. Cancel-then-remove in the UI would be racy as well, since a running cancel finishes asynchronously and the pending check would still see the task. The refusal text tells the user to try again, and the core tests pin the behaviour.
Module manager: only a queued or running install holds back the reader's switch (#10)
All checks were successful
CI / rust (pull_request) Successful in 4m23s
CI / audit (pull_request) Successful in 10s
CI / app (pull_request) Successful in 2m42s
CI / e2e (pull_request) Successful in 7m5s
CI / click (pull_request) Successful in 1m39s
97e1875387
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
claude-review left a comment

Review level: medium (blast radius: trunk).
Reviewed commit: 97e1875387
Range: b5d51e0980e4d98048e2b9915f6adb741eaa2db7..HEAD (re-review of the fixes)
Files: qml/pages/ModulesPage.qml, qt/src/module_manager.rs

Findings: none. Checked that phaseOf returns "" for a module without a task row (so the switch still happens for an idle module), that its phase names match the QML comparison, and that replacing and signalling the stop flag per loading leaves the generation guard and Drop intact.

Not covered here: CI results, and the phone check, which is still pending.

Review level: medium (blast radius: trunk). Reviewed commit: 97e187538778982f32b0b1d7ea11c408a6d783fd Range: b5d51e0980e4d98048e2b9915f6adb741eaa2db7..HEAD (re-review of the fixes) Files: qml/pages/ModulesPage.qml, qt/src/module_manager.rs Findings: none. Checked that phaseOf returns "" for a module without a task row (so the switch still happens for an idle module), that its phase names match the QML comparison, and that replacing and signalling the stop flag per loading leaves the generation guard and Drop intact. Not covered here: CI results, and the phone check, which is still pending.
Tests for the refused-removal switch and the index-load stop handover (#10)
Some checks failed
CI / rust (pull_request) Has been cancelled
CI / audit (pull_request) Has been cancelled
CI / app (pull_request) Has been cancelled
CI / e2e (pull_request) Has been cancelled
CI / click (pull_request) Has been cancelled
2a72e9b2e7
Board::holds decides whether a removal is held back (the page asks it
instead of comparing phase names); Handover is the per-load stop flag,
out of the Qt thread code. Unit tests for both and for a stopped load
writing nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Collaborator

Tests added for the two post-review fixes (2a72e9b):

  1. Refused removal keeps the reading Bible. The decision moved out of QML into Board::holds(module) in core (queued, downloading or retrying). Installer.holds() exposes it and ModulesPage.remove() asks it. Unit test: only_a_queued_downloading_or_retrying_module_holds_back_its_removal.
  2. Refresh stops the previous index load. The per-load stop flag is now catalog::Handover, outside the Qt thread code. Unit tests: a_new_load_stops_the_previous_one_and_only_that, stopping_stops_the_load_in_flight, and a_stopped_load_fetches_and_writes_nothing (a stopped IndexCache::load fetches nothing and creates no index dir; an unstopped one does).

Each test fails when its fix is broken (Handover::begin not stopping the previous flag; holds ignoring Queued) and passes once restored.

Local: cargo make passes, cargo make app-docker passes, and the module-manager e2e case passes. CI not awaited.

Tests added for the two post-review fixes (2a72e9b): 1. Refused removal keeps the reading Bible. The decision moved out of QML into `Board::holds(module)` in core (queued, downloading or retrying). `Installer.holds()` exposes it and `ModulesPage.remove()` asks it. Unit test: `only_a_queued_downloading_or_retrying_module_holds_back_its_removal`. 2. Refresh stops the previous index load. The per-load stop flag is now `catalog::Handover`, outside the Qt thread code. Unit tests: `a_new_load_stops_the_previous_one_and_only_that`, `stopping_stops_the_load_in_flight`, and `a_stopped_load_fetches_and_writes_nothing` (a stopped `IndexCache::load` fetches nothing and creates no index dir; an unstopped one does). Each test fails when its fix is broken (Handover::begin not stopping the previous flag; `holds` ignoring Queued) and passes once restored. Local: `cargo make` passes, `cargo make app-docker` passes, and the `module-manager` e2e case passes. CI not awaited.
claude-review left a comment

Level: high. Blast radius: trunk. Commit: 2a72e9b2e7. Range: origin/main...HEAD, limited to the files below plus the test commit 2a72e9b (git log 97e1875..HEAD).

Files reviewed: qt/build.rs, qt/src/lib.rs, qt/src/module_manager.rs (registration only), core/src/db.rs, core/src/db/003_task_repo.sql, core/src/db/tasks.rs, core/src/db/tasks/tests.rs, tools/e2e/lib.sh, tools/e2e/repo-server.py, sword/src/install.rs and install/index.rs (index format check), qml/pages/ModulesPage.qml and ReadingPane.qml and Main.qml (against the e2e hooks), and the changed cases add-bible-fail.sh, add-bible.sh, install-queue-cancel.sh, install-queue-fail.sh, module-manager.sh, typed-reference.sh.

No high or medium defects found.

Checked and sound:

  • Migration 003 is appended after 002. The tasks table is STRICT, and ADD COLUMN repo TEXT with a NULL default is valid there. COLUMNS and row() agree on index 8. The existing migration tests use LATEST and slices, so none hard-codes 2.
  • The new qt module is in both build.rs and lib.rs.
  • repo-server.py's index (mods.d/<name>.conf, USTAR, gzip) matches what sword::install::index::parse accepts. The path raw/mods.d.tar.gz matches Repo::new's default layout.
  • The install-queue-cancel.sh tap at h-72 lands on the strip: the strip is a child of the Page content, so it sits just above the footer.

Finding outside the diff (low, test flakiness):

  • tools/e2e/lib.sh, e2e_open_modules (line 324) sets e2e_seen to the count of rows lines. Then e2e_menu (line 313) overwrites the same global with the count of "utsb: menu " lines. The final e2e_wait ... $((e2e_seen + 1)) therefore waits against the menu count, not the rows count.
  • Failure scenario: on a later open, for example in install-queue-cancel.sh (the menu was never used before, so the count is 0) or in the second open in module-manager.sh, the wait is already satisfied by the earlier page's lines. It does not wait for the new page's listing, and the next e2e_modules_action taps coordinates from the previous page's layout (the 1 s sleep only partly covers this). Using a distinct variable name in e2e_menu (or e2e_open_modules) fixes it.
Level: high. Blast radius: trunk. Commit: 2a72e9b2e79e9f197a0e9e8a475e8ed1d9d8437a. Range: origin/main...HEAD, limited to the files below plus the test commit 2a72e9b (git log 97e1875..HEAD). Files reviewed: qt/build.rs, qt/src/lib.rs, qt/src/module_manager.rs (registration only), core/src/db.rs, core/src/db/003_task_repo.sql, core/src/db/tasks.rs, core/src/db/tasks/tests.rs, tools/e2e/lib.sh, tools/e2e/repo-server.py, sword/src/install.rs and install/index.rs (index format check), qml/pages/ModulesPage.qml and ReadingPane.qml and Main.qml (against the e2e hooks), and the changed cases add-bible-fail.sh, add-bible.sh, install-queue-cancel.sh, install-queue-fail.sh, module-manager.sh, typed-reference.sh. No high or medium defects found. Checked and sound: - Migration 003 is appended after 002. The tasks table is STRICT, and ADD COLUMN repo TEXT with a NULL default is valid there. COLUMNS and row() agree on index 8. The existing migration tests use LATEST and slices, so none hard-codes 2. - The new qt module is in both build.rs and lib.rs. - repo-server.py's index (`mods.d/<name>.conf`, USTAR, gzip) matches what sword::install::index::parse accepts. The path `raw/mods.d.tar.gz` matches Repo::new's default layout. - The install-queue-cancel.sh tap at h-72 lands on the strip: the strip is a child of the Page content, so it sits just above the footer. Finding outside the diff (low, test flakiness): - tools/e2e/lib.sh, e2e_open_modules (line 324) sets `e2e_seen` to the count of rows lines. Then e2e_menu (line 313) overwrites the same global with the count of "utsb: menu <x>" lines. The final `e2e_wait ... $((e2e_seen + 1))` therefore waits against the menu count, not the rows count. - Failure scenario: on a later open, for example in install-queue-cancel.sh (the menu was never used before, so the count is 0) or in the second open in module-manager.sh, the wait is already satisfied by the earlier page's lines. It does not wait for the new page's listing, and the next e2e_modules_action taps coordinates from the previous page's layout (the 1 s sleep only partly covers this). Using a distinct variable name in e2e_menu (or e2e_open_modules) fixes it.
e2e: e2e_menu keeps the caller's e2e_seen (#10)
All checks were successful
CI / rust (pull_request) Successful in 4m17s
CI / audit (pull_request) Successful in 10s
CI / app (pull_request) Successful in 2m43s
CI / e2e (pull_request) Successful in 7m2s
CI / click (pull_request) Successful in 1m43s
b4127349df
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Collaborator

Re: e2e_menu overwriting the global e2e_seen that e2e_open_modules had just set (low). Confirmed, and fixed in b412734. Both e2e_menu and e2e_open_modules now declare local e2e_seen, so the final wait compares against the rows count again. I checked that no caller relies on e2e_menu setting the global; the cases set their own e2e_seen. E2E_CASES="module-manager install-queue-cancel" cargo make e2e-docker passes (2 passed, 0 failed). I did not wait for CI.

Re: `e2e_menu` overwriting the global `e2e_seen` that `e2e_open_modules` had just set (low). Confirmed, and fixed in b412734. Both `e2e_menu` and `e2e_open_modules` now declare `local e2e_seen`, so the final wait compares against the rows count again. I checked that no caller relies on `e2e_menu` setting the global; the cases set their own `e2e_seen`. `E2E_CASES="module-manager install-queue-cancel" cargo make e2e-docker` passes (2 passed, 0 failed). I did not wait for CI.
A generated table gives each language's autonym and its name in each UI
language; the Modules language filter shows 'German · Deutsch', pinned
UI and installed languages first, and the search matches both names and
the code. Only Bibles are offered; an installed other kind is listed,
greyed, so it can be removed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
One bottom page bar on every page; Display moves into Settings; Keep is the default (#10)
All checks were successful
CI / rust (pull_request) Successful in 8m35s
CI / audit (pull_request) Successful in 18s
CI / app (pull_request) Successful in 4m49s
CI / e2e (pull_request) Successful in 8m5s
CI / click (pull_request) Successful in 4m7s
00c99d1826
The Aa sheet is gone from the reading footer. e2e cases and helpers
ported to the bottom bar and the Display section; per-row logs trimmed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
claude-review left a comment

Fresh-context review. Level: high. Blast radius: trunk. Reviewed commit: 00c99d1826. Range: origin/main...HEAD.

Findings: none with a concrete failure scenario (no high, medium or low).

Checked in context: qt/src/module_manager.rs (generation guard, stop handover, notify/revision flow), qt/src/installer.rs (new invokables, Done/Refused/Failed event mapping, board.add replacing an existing row on retry), sword/src/install.rs (Repo layouts; the default stays the old paths), ReadingPane.qml menu and footer, Main.qml wiring, PageBar.qml (Back is Layout.fillHeight inside the bar, so it cannot exceed the bar's height; all pages now use PageBar as a bottom footer, matching the reading pane), ModulesPage.qml remove/switch flow, StarterPage/SettingsPage footers, mem-check and e2e renames (no stale aa-sheet / QuickSettings / e2e_tap_aa references remain).

Files reviewed: CLAUDE.md, Makefile.toml, qml/Main.qml, qml/pages/ModulesPage.qml, qml/pages/PageBar.qml, qml/pages/ReadingPane.qml, qml/pages/SettingsPage.qml, qml/pages/StarterPage.qml, qml/pages/PickerPage.qml, qt/src/module_manager.rs, qt/src/installer.rs, qt/src/reading_model.rs, sword/src/install.rs, tools/ci/e2e.sh, tools/mem-check/run.sh, tools/e2e/lib.sh, tools/e2e/cases (renames and references).

Fresh-context review. Level: high. Blast radius: trunk. Reviewed commit: 00c99d18261b3dfa40b1ffc969349186bbc61f8c. Range: origin/main...HEAD. Findings: none with a concrete failure scenario (no high, medium or low). Checked in context: qt/src/module_manager.rs (generation guard, stop handover, notify/revision flow), qt/src/installer.rs (new invokables, Done/Refused/Failed event mapping, board.add replacing an existing row on retry), sword/src/install.rs (Repo layouts; the default stays the old paths), ReadingPane.qml menu and footer, Main.qml wiring, PageBar.qml (Back is Layout.fillHeight inside the bar, so it cannot exceed the bar's height; all pages now use PageBar as a bottom footer, matching the reading pane), ModulesPage.qml remove/switch flow, StarterPage/SettingsPage footers, mem-check and e2e renames (no stale aa-sheet / QuickSettings / e2e_tap_aa references remain). Files reviewed: CLAUDE.md, Makefile.toml, qml/Main.qml, qml/pages/ModulesPage.qml, qml/pages/PageBar.qml, qml/pages/ReadingPane.qml, qml/pages/SettingsPage.qml, qml/pages/StarterPage.qml, qml/pages/PickerPage.qml, qt/src/module_manager.rs, qt/src/installer.rs, qt/src/reading_model.rs, sword/src/install.rs, tools/ci/e2e.sh, tools/mem-check/run.sh, tools/e2e/lib.sh, tools/e2e/cases (renames and references).
claude-review requested changes 2026-10-27 15:25:09 +00:00
Dismissed
claude-review left a comment

Level: high (trunk). Reviewed commit: 00c99d1826. Range: origin/main...00c99d1, plus a read of core/src/bible/locale*, core/src/catalog*, core/src/tasks/{progress,exec,tests}.rs, tools/langnames/, and the docs/04 and docs/06 hunks. No commits since 00c99d1. This complements the earlier review of the QML, qt/, tools/e2e and sword files.

Files reviewed: core/src/bible/locale.rs, core/src/bible/locale/table.rs, core/src/bible/locale/table.tsv (spot checks: de, en, el, grc, hbo, he, la, aai, cop, nb, no, sr, zh), core/src/catalog.rs, core/src/catalog/{browser,handover,kind,language,load,query,version,tests}.rs, core/src/tasks/progress.rs, core/src/tasks/tests.rs, core/src/tasks/exec.rs, core/src/db/tasks.rs, tools/langnames/langnames.py, tools/langnames/index-codes.txt, docs/04-features.md, docs/06-sources.md, core/src/bible/installed.rs and sword/src/module.rs (callers), qml/pages/ModulesPage.qml (the remove path only).

Verified fine: langnames.py --check reproduces table.tsv from the pinned inputs. The table is sorted, so the binary search is valid. The de, en, grc, hbo and aai rows give the labels the tests expect. The load handover and generation guard hold: a stopped load fetches and writes nothing. Startup cost is fine, because the table is parsed lazily on first use and the indexes are read only when the manager opens.

Findings outside the diff lines:

medium · core/src/catalog/kind.rs:21 and core/src/catalog.rs:61 · Usable-kind leak by driver. Kind::of classifies by category and ignores the driver, and installable() and listed() check only the kind. The app opens only ModDrv=zText with CompressType=ZIP and no cipher (sword/src/module.rs from_conf). A "Biblical Texts" module with ModDrv=RawText, zText4 or RawText4, or an LZSS-compressed one, therefore appears under Bibles with an Install button and no note. The same happens for a category-less conf with a RawText or zText4 driver, which the driver fallback maps to Bible. It installs, then never appears in the Bible chooser (BibleInfo::installed drops it through Module::from_conf), so the user installed a Bible that cannot be read. Make usable() or installable() depend on the driver (case-insensitive zText only). CompressType would need an IndexEntry field for the LZSS case. Tell the user why the others are not offered, and add a test with a Biblical Texts entry whose driver is RawText or zText4.

medium · docs/06-sources.md:122-140, core/src/bible/locale/table.tsv · Licence omission. table.tsv is CLDR-derived data compiled into the shipped binary. The Unicode License v3 requires its copyright and permission notice to accompany the data in copies and in the associated documentation. The SIL ISO 639-3 names need attribution. The doc says the licence text "travels with the data in the table's source packages", but the click and the repo contain neither the notice nor the SIL attribution: grep for CLDR, Unicode and SIL finds only docs/06-sources.md, and the app has no licences or About page. Add the Unicode notice and the SIL attribution to a shipped notices file (for example THIRD-PARTY-NOTICES, packaged in the click) or to the table header.

low · tools/langnames/langnames.py:3, Makefile.toml, .forgejo/workflows/ci.yml · --check is wired nowhere. Only docs/06-sources.md claims it "fails when the committed table differs". The UI columns come from po/*.po, so adding po/fr.po without regenerating leaves the table without a French column. The French names then silently fall back to English (Table::column). Wire it in as a cargo make or CI step (it needs the cached inputs) or drop the claim.

low · core/src/bible/locale.rs:27 (parse) · A 3-letter code with a 2-letter equivalent ("deu", "eng") is never folded into the 2-letter one. The generator emits alias rows only for codes present in the 2026-10-27 indexes, and there are none now, so table.tsv has no = rows. A hand-installed or future module with Lang=deu shows the raw code, or, if an alias row exists, a second "German · Deutsch" entry. The filter compares code() strings, so "deu" and "de" are separate filter entries. Normalise through the alias in Locale::parse/language(). Four indexed codes (cug, daf, dud, plj) have no table row and show as raw codes.

low · core/src/tasks/progress.rs:175 (Board::holds) · holds reads only the starter Board, which is filled by add and the live events. After a restart, a queued or running task resumed from the DB has no Board row, so holds is false. ModulesPage.qml:37 then switches the reader away from that module before removeModule, and the runner's has_pending_task refuses the removal. The user ends up switched off their Bible with the module still installed. Ask the DB (has_pending_task) before switching, or seed the Board from the queue at start.

Level: high (trunk). Reviewed commit: 00c99d18261b3dfa40b1ffc969349186bbc61f8c. Range: origin/main...00c99d1, plus a read of core/src/bible/locale*, core/src/catalog*, core/src/tasks/{progress,exec,tests}.rs, tools/langnames/, and the docs/04 and docs/06 hunks. No commits since 00c99d1. This complements the earlier review of the QML, qt/, tools/e2e and sword files. Files reviewed: core/src/bible/locale.rs, core/src/bible/locale/table.rs, core/src/bible/locale/table.tsv (spot checks: de, en, el, grc, hbo, he, la, aai, cop, nb, no, sr, zh), core/src/catalog.rs, core/src/catalog/{browser,handover,kind,language,load,query,version,tests}.rs, core/src/tasks/progress.rs, core/src/tasks/tests.rs, core/src/tasks/exec.rs, core/src/db/tasks.rs, tools/langnames/langnames.py, tools/langnames/index-codes.txt, docs/04-features.md, docs/06-sources.md, core/src/bible/installed.rs and sword/src/module.rs (callers), qml/pages/ModulesPage.qml (the remove path only). Verified fine: `langnames.py --check` reproduces table.tsv from the pinned inputs. The table is sorted, so the binary search is valid. The de, en, grc, hbo and aai rows give the labels the tests expect. The load handover and generation guard hold: a stopped load fetches and writes nothing. Startup cost is fine, because the table is parsed lazily on first use and the indexes are read only when the manager opens. Findings outside the diff lines: medium · core/src/catalog/kind.rs:21 and core/src/catalog.rs:61 · Usable-kind leak by driver. `Kind::of` classifies by category and ignores the driver, and `installable()` and `listed()` check only the kind. The app opens only `ModDrv=zText` with `CompressType=ZIP` and no cipher (sword/src/module.rs `from_conf`). A "Biblical Texts" module with `ModDrv=RawText`, `zText4` or `RawText4`, or an LZSS-compressed one, therefore appears under Bibles with an Install button and no note. The same happens for a category-less conf with a `RawText` or `zText4` driver, which the driver fallback maps to Bible. It installs, then never appears in the Bible chooser (`BibleInfo::installed` drops it through `Module::from_conf`), so the user installed a Bible that cannot be read. Make `usable()` or `installable()` depend on the driver (case-insensitive zText only). CompressType would need an `IndexEntry` field for the LZSS case. Tell the user why the others are not offered, and add a test with a Biblical Texts entry whose driver is RawText or zText4. medium · docs/06-sources.md:122-140, core/src/bible/locale/table.tsv · Licence omission. table.tsv is CLDR-derived data compiled into the shipped binary. The Unicode License v3 requires its copyright and permission notice to accompany the data in copies and in the associated documentation. The SIL ISO 639-3 names need attribution. The doc says the licence text "travels with the data in the table's source packages", but the click and the repo contain neither the notice nor the SIL attribution: grep for CLDR, Unicode and SIL finds only docs/06-sources.md, and the app has no licences or About page. Add the Unicode notice and the SIL attribution to a shipped notices file (for example THIRD-PARTY-NOTICES, packaged in the click) or to the table header. low · tools/langnames/langnames.py:3, Makefile.toml, .forgejo/workflows/ci.yml · `--check` is wired nowhere. Only docs/06-sources.md claims it "fails when the committed table differs". The UI columns come from `po/*.po`, so adding po/fr.po without regenerating leaves the table without a French column. The French names then silently fall back to English (`Table::column`). Wire it in as a cargo make or CI step (it needs the cached inputs) or drop the claim. low · core/src/bible/locale.rs:27 (`parse`) · A 3-letter code with a 2-letter equivalent ("deu", "eng") is never folded into the 2-letter one. The generator emits alias rows only for codes present in the 2026-10-27 indexes, and there are none now, so table.tsv has no `=` rows. A hand-installed or future module with `Lang=deu` shows the raw code, or, if an alias row exists, a second "German · Deutsch" entry. The filter compares `code()` strings, so "deu" and "de" are separate filter entries. Normalise through the alias in `Locale::parse`/`language()`. Four indexed codes (cug, daf, dud, plj) have no table row and show as raw codes. low · core/src/tasks/progress.rs:175 (`Board::holds`) · `holds` reads only the starter Board, which is filled by `add` and the live events. After a restart, a queued or running task resumed from the DB has no Board row, so `holds` is false. ModulesPage.qml:37 then switches the reader away from that module before `removeModule`, and the runner's `has_pending_task` refuses the removal. The user ends up switched off their Bible with the module still installed. Ask the DB (`has_pending_task`) before switching, or seed the Board from the queue at start.
Review fixes: Bible kind needs a readable driver, notices ship, 3-letter language codes fold, po columns tested (#10)
All checks were successful
CI / rust (pull_request) Successful in 11m8s
Presets / preset-check (pull_request) Successful in 4m52s
CI / audit (pull_request) Successful in 23s
CI / app (pull_request) Successful in 6m36s
CI / e2e (pull_request) Successful in 8m33s
CI / click (pull_request) Successful in 5m12s
644731376d
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Author
Collaborator

Author response to the review, fixes in 6447313:

  • medium, Kind::of ignores driver: fixed. A "Biblical Texts" module is a Bible only if its driver is zText (or unnamed) and CompressType is ZIP (or unnamed); IndexEntry gains compress. Test added.
  • medium, licence omission: fixed. NOTICES.md (Unicode License v3 text, SIL attribution) is installed with LICENSE next to the binary; docs/06 points to it.
  • low, --check wired nowhere: the check needs the network downloads, so it is not in CI. Instead a unit test fails if a po/*.po has no column in the table.
  • low, 3-letter codes: fixed. The script emits alias rows for every ISO code with a 2-letter equivalent, and Locale::parse folds "deu" to "de". cug, daf, dud, plj are not in the ISO 639-3 table or CLDR, so no name exists; they show their tag (the documented fallback).
  • low, Board::holds after restart: refuted for installs. The runner emits Event::Restored at start and the glue adds a Queued row per restored install (core/src/tasks/tests.rs:661), so holds is true. A resumed removal is going to remove the module anyway, so switching the reader away is right.

cargo make passes. Not run: app-docker (no qt/QML/C++ change; CMake only gains an install line).

Author response to the review, fixes in 6447313: - medium, Kind::of ignores driver: fixed. A "Biblical Texts" module is a Bible only if its driver is zText (or unnamed) and CompressType is ZIP (or unnamed); IndexEntry gains `compress`. Test added. - medium, licence omission: fixed. NOTICES.md (Unicode License v3 text, SIL attribution) is installed with LICENSE next to the binary; docs/06 points to it. - low, --check wired nowhere: the check needs the network downloads, so it is not in CI. Instead a unit test fails if a po/*.po has no column in the table. - low, 3-letter codes: fixed. The script emits alias rows for every ISO code with a 2-letter equivalent, and Locale::parse folds "deu" to "de". cug, daf, dud, plj are not in the ISO 639-3 table or CLDR, so no name exists; they show their tag (the documented fallback). - low, Board::holds after restart: refuted for installs. The runner emits Event::Restored at start and the glue adds a Queued row per restored install (core/src/tasks/tests.rs:661), so holds is true. A resumed removal is going to remove the module anyway, so switching the reader away is right. cargo make passes. Not run: app-docker (no qt/QML/C++ change; CMake only gains an install line).
Icons for the menu entries: Lucide library-big and settings as tinted PNGs (#10)
Some checks failed
CI / rust (pull_request) Has been cancelled
CI / audit (pull_request) Has been cancelled
CI / app (pull_request) Has been cancelled
CI / e2e (pull_request) Has been cancelled
CI / click (pull_request) Has been cancelled
Presets / preset-check (pull_request) Has been cancelled
ff1d8527c4
PNGs rendered by tools/icons/render.sh so no QtSvg or svg plugin is needed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
claude-review left a comment

Level: high (trunk). Reviewed commit: ff1d8527c4. Range: 00c99d18261b3dfa40b1ffc969349186bbc61f8c..HEAD (6447313 review fixes, ff1d852 menu icons).

Files reviewed: CMakeLists.txt, NOTICES.md, assets/icons/.svg, core/src/bible/locale.rs, core/src/bible/locale/table.rs, core/src/bible/locale/table.tsv (alias rows), core/src/catalog/kind.rs, core/src/catalog/tests.rs, docs/06-sources.md, qml/icons/.png, qml/pages/ReadingPane.qml, qml/qml.qrc, sword/src/install/index.rs, sword/src/install/index/tests.rs, sword/src/module.rs (from_conf, for comparison), tools/e2e/cases/display-settings.sh, tools/icons/render.sh, tools/langnames/langnames.py.

Checked and sound: alias folding in Locale::parse (deu, ger, eng, heb, nor, nob, zho, chi now fold; real rows like he are untouched; the new tests cover deu and ger-DE); the po-column test; NOTICES.md installed next to the binary; icon paths resolve from the /qml qrc prefix (pages/ to ../icons), icon.color follows Theme.text. The Board::holds refutation is not re-litigated here.

Findings outside the diff lines (all low, none blocking):

low · core/src/catalog/kind.rs:44-45 · The "benefit of the doubt" for an empty CompressType is wrong for compress: Module::from_conf (sword/src/module.rs:92-95) refuses a missing CompressType (SWORD's default is LZSS). A "Biblical Texts" zText conf with no CompressType is still classed Bible, offered with Install, installs, and never appears in the Bible chooser. Only the driver deserves the doubt (and even that is refused by from_conf if empty); require compress to equal ZIP.

low · core/src/catalog/kind.rs:29-34 · The category-less driver fallback still maps RawText, zText4 and RawText4 to Bible without the biblical() check, so a conf without a Category and with one of those drivers is offered and unreadable (the scenario of the earlier review's second half). Also enciphered Bibles (a CipherKey line, which from_conf refuses) are not filtered; IndexEntry has no cipher field.

low · tools/e2e/cases/display-settings.sh (new menu-night block) · The shot is taken but nothing asserts the icon tint; a regression of icon.color only shows in the PNG. Acceptable if shots are looked at before the phone install.

Level: high (trunk). Reviewed commit: ff1d8527c43a2b256519b92add7438229a8b0e46. Range: 00c99d18261b3dfa40b1ffc969349186bbc61f8c..HEAD (6447313 review fixes, ff1d852 menu icons). Files reviewed: CMakeLists.txt, NOTICES.md, assets/icons/*.svg, core/src/bible/locale.rs, core/src/bible/locale/table.rs, core/src/bible/locale/table.tsv (alias rows), core/src/catalog/kind.rs, core/src/catalog/tests.rs, docs/06-sources.md, qml/icons/*.png, qml/pages/ReadingPane.qml, qml/qml.qrc, sword/src/install/index.rs, sword/src/install/index/tests.rs, sword/src/module.rs (from_conf, for comparison), tools/e2e/cases/display-settings.sh, tools/icons/render.sh, tools/langnames/langnames.py. Checked and sound: alias folding in Locale::parse (deu, ger, eng, heb, nor, nob, zho, chi now fold; real rows like he are untouched; the new tests cover deu and ger-DE); the po-column test; NOTICES.md installed next to the binary; icon paths resolve from the /qml qrc prefix (pages/ to ../icons), icon.color follows Theme.text. The Board::holds refutation is not re-litigated here. Findings outside the diff lines (all low, none blocking): low · core/src/catalog/kind.rs:44-45 · The "benefit of the doubt" for an empty CompressType is wrong for compress: Module::from_conf (sword/src/module.rs:92-95) refuses a missing CompressType (SWORD's default is LZSS). A "Biblical Texts" zText conf with no CompressType is still classed Bible, offered with Install, installs, and never appears in the Bible chooser. Only the driver deserves the doubt (and even that is refused by from_conf if empty); require compress to equal ZIP. low · core/src/catalog/kind.rs:29-34 · The category-less driver fallback still maps RawText, zText4 and RawText4 to Bible without the biblical() check, so a conf without a Category and with one of those drivers is offered and unreadable (the scenario of the earlier review's second half). Also enciphered Bibles (a CipherKey line, which from_conf refuses) are not filtered; IndexEntry has no cipher field. low · tools/e2e/cases/display-settings.sh (new menu-night block) · The shot is taken but nothing asserts the icon tint; a regression of icon.color only shows in the PNG. Acceptable if shots are looked at before the phone install.
Review fixes: Bible kind needs ZIP and applies to the driver fallback, e2e asserts the menu icon tint (#10)
All checks were successful
CI / rust (pull_request) Successful in 10m42s
Presets / preset-check (pull_request) Successful in 5m14s
CI / audit (pull_request) Successful in 19s
CI / app (pull_request) Successful in 5m26s
CI / e2e (pull_request) Successful in 8m43s
CI / click (pull_request) Successful in 5m17s
3499449add
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Author
Collaborator

Review round answers (fixed in 3499449):

  1. kind.rs empty CompressType: fixed. Only ZIP counts as readable, as Module::from_conf requires; test added.
  2. kind.rs driver fallback: fixed. RawText/zText4/RawText4 now go through biblical() and become Other. Enciphered part: refuted/left open. Entries with a non-empty CipherKey are already locked (listed, not installable). An empty CipherKey is deliberately not locked in sword/install/index (test a_cipher_key_means_locked), while from_conf refuses it; changing that is a trunk sword change outside this PR, so left open.
  3. display-settings.sh: fixed. ReadingPane logs the icon colours when the menu opens and the case asserts both are the night text colour #d6d6d6.
Review round answers (fixed in 3499449): 1. kind.rs empty CompressType: fixed. Only ZIP counts as readable, as Module::from_conf requires; test added. 2. kind.rs driver fallback: fixed. RawText/zText4/RawText4 now go through biblical() and become Other. Enciphered part: refuted/left open. Entries with a non-empty CipherKey are already `locked` (listed, not installable). An empty CipherKey is deliberately not `locked` in sword/install/index (test a_cipher_key_means_locked), while from_conf refuses it; changing that is a trunk sword change outside this PR, so left open. 3. display-settings.sh: fixed. ReadingPane logs the icon colours when the menu opens and the case asserts both are the night text colour #d6d6d6.
Author
Collaborator

Installed 3499449 on the phone (USB, 18:08). Phone checklist:

  • The menu shows Modules and Settings with icons, in day and night mode (the icons follow the text colour).
  • Every page has one bottom bar; the Back button stays within its height.
  • Settings holds the display options (font, size, red letters, night). The Aa button is gone from the reading pane.
  • Modules lists languages as "German · Deutsch" and offers only Bibles; search works.
  • Install a small Bible: the download continues when you leave the page; the Bible appears in the footer's Bible switcher.
  • Remove it again; Update flags show for an outdated module.
  • Turn off WiFi: Modules still shows the last lists (offline).
  • Kill the app during a download; reopening resumes or offers it again.
  • Memory: with Modules open after the full index loads, PSS is measured over adb (budget 165,000 kB).
Installed 3499449 on the phone (USB, 18:08). Phone checklist: - [x] The menu shows Modules and Settings with icons, in day and night mode (the icons follow the text colour). - [x] Every page has one bottom bar; the Back button stays within its height. - [x] Settings holds the display options (font, size, red letters, night). The Aa button is gone from the reading pane. - [x] Modules lists languages as "German · Deutsch" and offers only Bibles; search works. - [x] Install a small Bible: the download continues when you leave the page; the Bible appears in the footer's Bible switcher. - [x] Remove it again; Update flags show for an outdated module. - [x] Turn off WiFi: Modules still shows the last lists (offline). - [x] Kill the app during a download; reopening resumes or offers it again. - [x] Memory: with Modules open after the full index loads, PSS is measured over adb (budget 165,000 kB).
Owner

Comments on @claude-bot wrote in #82 (comment):

Installed 3499449 on the phone (USB, 18:08). Phone checklist:

* [x] The menu shows Modules and Settings with icons, in day and night mode (the icons follow the text colour).

* [x] Every page has one bottom bar; the Back button stays within its height.

* [x] Settings holds the display options (font, size, red letters, night). The Aa button is gone from the reading pane.

* [x] Modules lists languages as "German · Deutsch" and offers only Bibles; search works.

* [x] Install a small Bible: the download continues when you leave the page; the Bible appears in the footer's Bible switcher.

* [x] Remove it again; Update flags show for an outdated module.

* [x] Turn off WiFi: Modules still shows the last lists (offline).

* [x] Kill the app during a download; reopening resumes or offers it again.

* [x] Memory: with Modules open after the full index loads, PSS is measured over adb (budget 165,000 kB).

Comments:

  • Dark mode icons are a bit low contrast, a smoky gray on very dark gray / black. Follow up PR should change these in dark mode to a color with higher contrast, even an off-white. This was with night mode off. I'm still not really sold on the night mode concept; I would honestly remove it in a separate issue.
  • Language selector is not a combo drop down allowing for a text search; I argue it would be beneficial here due to the large number of choices and the fact that the alphabetic nature is sorted via the english name; allowing users to search via typing a name as well as scrolling would be the most useable. Also for a follow-up issue, rate as could.
  • Attempting to install when no connection is active (mobile data or wifi) gives a rather technical and cryptic "temporary failure in name resolution". A preliminary check (pre Install press) should determine if any internet connections are active, and if not, grey out the install buttons and make them unpressable.
  • Replace the Install text with a download icon for more intuitive UX.
  • Memory checks should be automatically checked; the ballpark is good enough, slowing manual testing isn't needed.

All checklist items passed other than the above comments.

Comments on @claude-bot wrote in https://code.austinschaefer.com/schaefera/utStudyBIble/pulls/82#issuecomment-1978: > Installed [`3499449`](/schaefera/utStudyBIble/commit/3499449) on the phone (USB, 18:08). Phone checklist: > > * [x] The menu shows Modules and Settings with icons, in day and night mode (the icons follow the text colour). > > * [x] Every page has one bottom bar; the Back button stays within its height. > > * [x] Settings holds the display options (font, size, red letters, night). The Aa button is gone from the reading pane. > > * [x] Modules lists languages as "German · Deutsch" and offers only Bibles; search works. > > * [x] Install a small Bible: the download continues when you leave the page; the Bible appears in the footer's Bible switcher. > > * [x] Remove it again; Update flags show for an outdated module. > > * [x] Turn off WiFi: Modules still shows the last lists (offline). > > * [x] Kill the app during a download; reopening resumes or offers it again. > > * [x] Memory: with Modules open after the full index loads, PSS is measured over adb (budget 165,000 kB). Comments: - Dark mode icons are a bit low contrast, a smoky gray on very dark gray / black. Follow up PR should change these in dark mode to a color with higher contrast, even an off-white. This was with night mode off. I'm still not *really* sold on the night mode concept; I would honestly remove it in a separate issue. - Language selector is not a combo drop down allowing for a text search; I argue it would be beneficial here due to the large number of choices and the fact that the alphabetic nature is sorted via the english name; allowing users to search via typing a name as well as scrolling would be the most useable. Also for a follow-up issue, rate as could. - Attempting to install when no connection is active (mobile data or wifi) gives a rather technical and cryptic "temporary failure in name resolution". A preliminary check (pre Install press) should determine if any internet connections are active, and if not, grey out the install buttons and make them unpressable. - Replace the Install text with a download icon for more intuitive UX. - Memory checks should be automatically checked; the ballpark is good enough, slowing manual testing isn't needed. All checklist items passed other than the above comments.
claude-bot changed title from WIP: Module manager and a footer menu for Modules and Settings (#10 part 2b, #30) to Module manager and a footer menu for Modules and Settings (#10 part 2b, #30) 2026-10-27 17:19:30 +00:00
claude-review left a comment

Review level: high (trunk). Reviewed commit: 3499449add.
Range: ff1d852..3499449 in depth, plus a skim of origin/main...HEAD (73 files, already reviewed at 00c99d1 and ff1d852).
Files reviewed: core/src/catalog/kind.rs, core/src/catalog/tests.rs, qml/pages/ReadingPane.qml, tools/e2e/cases/display-settings.sh.

Findings:

  • None high or medium.
  • kind.rs: the Bible kind now needs CompressType=ZIP, and the driver-only fallback goes through the same biblical() check. A conf with no CompressType or a non-zText driver is classed Other, as the reader refuses it. The tests cover empty, LZSS and ZIP, and the category-less RawText case. The bible() test helper now sets ZIP, so the existing catalog tests still reflect the new rule. No failure scenario found.
  • ReadingPane.qml: the added console.info in Menu.onOpened only logs icon colours. It has no behaviour or memory effect.
  • display-settings.sh: the assertion takes the last matching log line and requires both colours to be #d6d6d6. It fails closed when the line is missing, because the awk exits non-zero on empty input.
  • Known and deferred: the empty-CipherKey gap is tracked in #83. Other follow-ups are #84-#88.

CI: success on 3499449 (combined status success; rust job green, remaining contexts also success).

Gate decision: APPROVED. No high or medium findings, CI green, phone checklist passed by the owner.

Review level: high (trunk). Reviewed commit: 3499449add374e0626e7f34d12b0dd2b42a5e909. Range: ff1d852..3499449 in depth, plus a skim of origin/main...HEAD (73 files, already reviewed at 00c99d1 and ff1d852). Files reviewed: core/src/catalog/kind.rs, core/src/catalog/tests.rs, qml/pages/ReadingPane.qml, tools/e2e/cases/display-settings.sh. Findings: - None high or medium. - kind.rs: the Bible kind now needs CompressType=ZIP, and the driver-only fallback goes through the same biblical() check. A conf with no CompressType or a non-zText driver is classed Other, as the reader refuses it. The tests cover empty, LZSS and ZIP, and the category-less RawText case. The `bible()` test helper now sets ZIP, so the existing catalog tests still reflect the new rule. No failure scenario found. - ReadingPane.qml: the added console.info in Menu.onOpened only logs icon colours. It has no behaviour or memory effect. - display-settings.sh: the assertion takes the last matching log line and requires both colours to be #d6d6d6. It fails closed when the line is missing, because the awk exits non-zero on empty input. - Known and deferred: the empty-CipherKey gap is tracked in #83. Other follow-ups are #84-#88. CI: success on 3499449 (combined status success; rust job green, remaining contexts also success). Gate decision: APPROVED. No high or medium findings, CI green, phone checklist passed by the owner.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
schaefera/utStudyBIble!82
No description provided.