feat: a Detection section, and a toggle you can see #194

Merged
bjoern merged 4 commits from feat/detection-settings-section into main 2026-09-21 11:13:39 +02:00
Member

What's in

Two things, both from using the feature rather than from the plan.

A Detection section in Settings

GET /api/detection was built in #188 for ADR 0026's rule 7 — a background job
with nobody watching has to say what it is doing somewhere other than a log
file on a NAS — and then nothing in the app ever read it. The only answer
to "is it running, and how far has it got" was a curl. The per-page overlay
answers a different question: it tells you about the page in front of you, not
about the sweep.

Its own section in the sidebar, beside the other server ones. Read-only, like
the backup schedule: whether the sweep runs is deployment configuration, so a
button here would promise a verb the server does not have.

Two things it is deliberately careful about:

  • A server with no model reports state: "off" exactly as a switched-off one
    does.
    Only one of those can be started by setting an environment variable,
    so they read differently — Unavailable: this server has no detection model
    against Off: set DOUJIN_MANAGER_DETECTION=on to start it. Telling someone
    to set a variable when there is nothing to detect with sends them to the
    wrong place.
  • "Images analysed" counts the pages nothing was found on, and the section
    says so out loud. Seven pages in ten of this library are illustration; a
    count of pages with regions would show a finished sweep as a broken one.

Plus a progress bar, the detector's id and version, and the last batch — with
the note that the batch is per-process and lost on restart, while the counts
above it are the durable record.

The reader's overlay toggle looked the same on and off

Reported from use, and a real defect I shipped in #192. Icons.highlight_alt
and Icons.highlight_alt_outlined are different codepoints (0xe311 /
0xf0fd) that render identically at 24px, and isSelected does not restyle
a plain IconButton. The only signal left was the tooltip — which speaks on
hover, and never on a touchscreen.

It now takes a filled background and an inverted foreground when on. Colour
plus a container, so it reads at a glance and still reads without colour
vision.

Verification

scripts/verify.sh green (== verify OK (all) ==) — app 1,530 → 1,556
tests
, backend unchanged, analyze clean.

Layer Exercised
Wire getDetectionStatus GETs /api/detection and unwraps it; a server with no detector parses with its nulls
Model progress, remaining-never-negative, hasDetector, and an unrecognised state word reading as unknown rather than off
Redux load/arrive/fail, and that a failed refresh keeps the numbers on screen
Epic through createStore, so the registration is pinned; a failure is an error, not an empty status
Section all five states on screen, the counter's explanation, the last batch, a failed batch, and the progress bar
Reach settings_page_test — the sidebar entry exists and switching to it really changes pane. A section nobody can navigate to is a section that shipped and was never found
Reader the toggle's glyph, colour and background all differ between states

Mutation-probed: 16 mutations, all killed. Two survivors became fixes:

  • progress's empty-library arm was unreachable. 0 / 0 is NaN, and
    NaN.clamp(0, 1) returns the upper limit — so the guard and the clamp
    both produced 1 and the guard could be deleted with everything green.
    Rewritten with an explicit ceiling; both arms are now distinguishable.
  • The cap at one was untested — the counts come from separate queries, so a
    sweep finishing between them can report more analysed than total, and a bar
    past its own end is worse than one that stops there.

Follow-ups

None. The flutter-ci red on main (run 879) and the chronically red
deploy-smoke are both pre-existing and unrelated; COVERAGE=1 scripts/verify.sh app
— exactly what that workflow runs — passes here in 2m21s.

🤖 Generated with Claude Code

## What's in Two things, both from using the feature rather than from the plan. ### A Detection section in Settings `GET /api/detection` was built in #188 for ADR 0026's rule 7 — a background job with nobody watching has to say what it is doing somewhere other than a log file on a NAS — and then **nothing in the app ever read it**. The only answer to "is it running, and how far has it got" was a `curl`. The per-page overlay answers a different question: it tells you about the page in front of you, not about the sweep. Its own section in the sidebar, beside the other server ones. Read-only, like the backup schedule: whether the sweep runs is deployment configuration, so a button here would promise a verb the server does not have. Two things it is deliberately careful about: - **A server with no model reports `state: "off"` exactly as a switched-off one does.** Only one of those can be started by setting an environment variable, so they read differently — *Unavailable: this server has no detection model* against *Off: set `DOUJIN_MANAGER_DETECTION=on` to start it*. Telling someone to set a variable when there is nothing to detect with sends them to the wrong place. - **"Images analysed" counts the pages nothing was found on**, and the section says so out loud. Seven pages in ten of this library are illustration; a count of pages *with* regions would show a finished sweep as a broken one. Plus a progress bar, the detector's id and version, and the last batch — with the note that the batch is per-process and lost on restart, while the counts above it are the durable record. ### The reader's overlay toggle looked the same on and off Reported from use, and a real defect I shipped in #192. `Icons.highlight_alt` and `Icons.highlight_alt_outlined` are different codepoints (`0xe311` / `0xf0fd`) that **render identically at 24px**, and `isSelected` does not restyle a plain `IconButton`. The only signal left was the tooltip — which speaks on hover, and never on a touchscreen. It now takes a filled background and an inverted foreground when on. Colour plus a container, so it reads at a glance and still reads without colour vision. ## Verification `scripts/verify.sh` green (`== verify OK (all) ==`) — app **1,530 → 1,556 tests**, backend unchanged, analyze clean. | Layer | Exercised | |---|---| | Wire | `getDetectionStatus` GETs `/api/detection` and unwraps it; a server with no detector parses with its nulls | | Model | progress, remaining-never-negative, `hasDetector`, and an unrecognised state word reading as `unknown` rather than `off` | | Redux | load/arrive/fail, and that a failed refresh keeps the numbers on screen | | Epic | through `createStore`, so the registration is pinned; a failure is an error, not an empty status | | Section | all five states on screen, the counter's explanation, the last batch, a failed batch, and the progress bar | | Reach | `settings_page_test` — the sidebar entry exists and switching to it really changes pane. A section nobody can navigate to is a section that shipped and was never found | | Reader | the toggle's glyph, colour and background all differ between states | **Mutation-probed: 16 mutations, all killed.** Two survivors became fixes: - **`progress`'s empty-library arm was unreachable.** `0 / 0` is NaN, and `NaN.clamp(0, 1)` returns the **upper** limit — so the guard and the clamp both produced 1 and the guard could be deleted with everything green. Rewritten with an explicit ceiling; both arms are now distinguishable. - **The cap at one was untested** — the counts come from separate queries, so a sweep finishing between them can report more analysed than total, and a bar past its own end is worse than one that stops there. ## Follow-ups None. The `flutter-ci` red on `main` (run 879) and the chronically red `deploy-smoke` are both pre-existing and unrelated; `COVERAGE=1 scripts/verify.sh app` — exactly what that workflow runs — passes here in 2m21s. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat: a Detection section, and a toggle you can see
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 9m39s
8efda64197
The sweep had nowhere to say what it was doing. GET /api/detection was
built for exactly that in #188 and nothing in the app ever read it, so
the only answer to "is it running, and how far has it got" was a curl.
A section of its own in Settings, next to the other server sections and
read-only like the backup schedule: whether the sweep runs is deployment
configuration, so a button here would promise a verb the server has not
got.

Two things it is careful about. A server with no model reports state
"off" exactly as a switched-off one does — only one of those can be
started by setting an environment variable, so they read differently.
And "images analysed" counts the pages nothing was found on, which the
section says out loud: seven pages in ten of this library are
illustration, and a count of pages WITH regions would show a finished
sweep as a broken one.

Also, reported from use: the reader's overlay toggle looked identical on
and off. The outlined and filled glyphs of that icon are different
codepoints that render the same at 24px, and the only other signal was a
tooltip — which speaks on hover and never on a touchscreen. It now takes
a filled background and an inverted foreground, both pinned.

One probe survivor became a fix rather than a note: the empty-library
arm of `progress` was unreachable, because `0 / 0` is NaN and
`NaN.clamp(0, 1)` returns the UPPER limit — so both arms produced 1.
Rewritten with an explicit ceiling, and the bar it feeds is new too.

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

Flutter Coverage

File Line coverage
lib/domain/entities/agent_routing.dart 100.0% (8 of 8)
lib/domain/entities/reading_mode.dart 100.0% (3 of 3)
lib/domain/entities/stored_settings.dart 100.0% (21 of 21)
lib/app/store.dart 95.8% (23 of 24)
lib/data/models/doujin_models.dart 100.0% (55 of 55)
lib/data/models/doujin_models.g.dart 68.4% (299 of 437)
lib/domain/repositories/repositories.dart 33.3% (1 of 3)
lib/presentation/state/app_state.dart 84.4% (38 of 45)
lib/agent/agent_log.dart 100.0% (94 of 94)
lib/agent/agent_service.dart 92.9% (514 of 553)
lib/agent/approval_gate.dart 100.0% (21 of 21)
lib/agent/assistant_context.dart 70.7% (41 of 58)
lib/agent/browser_budget.dart 80.0% (4 of 5)
lib/agent/caching_describer.dart 100.0% (13 of 13)
lib/agent/cancellable_tool.dart 100.0% (13 of 13)
lib/agent/memory_store.dart 93.8% (15 of 16)
lib/agent/skills/skill_registry.dart 98.2% (54 of 55)
lib/agent/system_prompt.dart 100.0% (73 of 73)
lib/agent/tools/budgeted_browser_tool.dart 84.2% (16 of 19)
lib/agent/tools/doujin_write_tool.dart 95.0% (921 of 969)
lib/agent/tools/entity_write_tool.dart 78.5% (175 of 223)
lib/agent/tools/fetch_page_tool.dart 89.3% (67 of 75)
lib/agent/tools/get_doujin_tool.dart 92.3% (60 of 65)
lib/agent/tools/list_entities_tool.dart 76.1% (54 of 71)
lib/agent/tools/navigate_tool.dart 95.1% (39 of 41)
lib/agent/tools/read_skill_tool.dart 82.4% (14 of 17)
lib/agent/tools/reflection_tools.dart 78.0% (32 of 41)
lib/agent/tools/search_doujins_tool.dart 100.0% (101 of 101)
lib/agent/tools/view_images_tool.dart 95.0% (38 of 40)
lib/core/model_capabilities.dart 100.0% (23 of 23)
lib/domain/entities/assistant_entry.dart 20.0% (1 of 5)
lib/presentation/state/actions/assistant_actions.dart 58.3% (7 of 12)
lib/core/app_config.dart 70.0% (7 of 10)
lib/core/reading_order.dart 100.0% (22 of 22)
lib/domain/entities/entity_model.dart 100.0% (2 of 2)
lib/data/repositories/entity_in_use_exception.dart 33.3% (1 of 3)
lib/core/title_display.dart 93.8% (15 of 16)
lib/data/models/search_query.dart 50.0% (2 of 4)
lib/data/models/search_query.g.dart 33.8% (26 of 77)
lib/core/constants.dart 40.0% (4 of 10)
lib/data/offline/offline_download_service.dart 93.0% (66 of 71)
lib/data/reading_progress_store.dart 92.3% (36 of 39)
lib/domain/entities/reading_progress.dart 100.0% (26 of 26)
lib/presentation/middleware/assistant_epics.dart 85.3% (87 of 102)
lib/presentation/middleware/backup_save_dialog.dart 25.0% (1 of 4)
lib/presentation/middleware/epics.dart 93.0% (478 of 514)
lib/presentation/middleware/offline_epics.dart 98.4% (60 of 61)
lib/presentation/state/reducers.dart 100.0% (14 of 14)
lib/core/chunking.dart 100.0% (12 of 12)
lib/core/code_point_compare.dart 100.0% (8 of 8)
lib/core/languages.dart 100.0% (14 of 14)
lib/core/variant_display.dart 100.0% (22 of 22)
lib/data/models/envelope.dart 81.2% (13 of 16)
lib/data/models/envelope.g.dart 56.7% (38 of 67)
lib/data/models/server_models.dart 98.8% (81 of 82)
lib/data/models/server_models.g.dart 55.6% (155 of 279)
lib/domain/entities/offline_manifest.dart 100.0% (78 of 78)
lib/data/repositories/backup_exception.dart 100.0% (3 of 3)
lib/data/repositories/integrity_exception.dart 100.0% (4 of 4)
lib/data/repositories/upload_exception.dart 33.3% (1 of 3)
lib/domain/entities/filter_token.dart 92.2% (107 of 116)
lib/presentation/middleware/duplicates_epics.dart 100.0% (50 of 50)
lib/presentation/middleware/editor_epics.dart 58.9% (175 of 297)
lib/presentation/middleware/entity_ops.dart 64.4% (47 of 73)
lib/presentation/middleware/server_epics.dart 100.0% (144 of 144)
lib/presentation/middleware/upload_epics.dart 98.1% (104 of 106)
lib/presentation/state/actions/offline_actions.dart 91.7% (11 of 12)
lib/presentation/state/actions/detail_actions.dart 85.7% (6 of 7)
lib/presentation/state/actions/duplicates_actions.dart 88.9% (8 of 9)
lib/presentation/state/actions/editor_actions.dart 57.9% (22 of 38)
lib/presentation/state/actions/entity_actions.dart 66.7% (12 of 18)
lib/presentation/state/actions/library_actions.dart 54.5% (12 of 22)
lib/presentation/state/actions/metadata_actions.dart 100.0% (5 of 5)
lib/presentation/state/actions/reader_actions.dart 85.7% (6 of 7)
lib/presentation/state/actions/reading_progress_actions.dart 100.0% (2 of 2)
lib/presentation/state/actions/server_actions.dart 80.0% (32 of 40)
lib/presentation/state/actions/settings_actions.dart 88.9% (8 of 9)
lib/presentation/state/actions/upload_actions.dart 90.9% (10 of 11)
lib/presentation/state/reducers/assistant_reducer.dart 95.2% (60 of 63)
lib/presentation/state/reducers/detail_reducer.dart 100.0% (28 of 28)
lib/presentation/state/reducers/duplicates_reducer.dart 100.0% (54 of 54)
lib/presentation/state/reducers/editor_reducer.dart 97.2% (105 of 108)
lib/presentation/state/reducers/entity_reducer.dart 98.9% (94 of 95)
lib/presentation/state/reducers/library_reducer.dart 100.0% (140 of 140)
lib/presentation/state/reducers/metadata_reducer.dart 90.5% (38 of 42)
lib/presentation/state/reducers/offline_reducer.dart 100.0% (61 of 61)
lib/presentation/state/reducers/reader_reducer.dart 100.0% (36 of 36)
lib/presentation/state/reducers/reading_progress_reducer.dart 100.0% (20 of 20)
lib/presentation/state/reducers/server_reducer.dart 99.5% (218 of 219)
lib/presentation/state/reducers/settings_reducer.dart 100.0% (77 of 77)
lib/presentation/state/reducers/upload_reducer.dart 100.0% (45 of 45)
lib/presentation/pages/people/people_page.dart 66.7% (12 of 18)
lib/presentation/pages/tags/tags_page.dart 100.0% (17 of 17)
lib/presentation/widgets/entity_merge_dialog.dart 100.0% (70 of 70)
lib/presentation/widgets/entity_editor.dart 92.5% (135 of 146)
lib/presentation/widgets/entity_management_page.dart 88.5% (238 of 269)
lib/presentation/pages/editor/page_grid.dart 94.3% (314 of 333)
lib/presentation/pages/editor/variants_tab.dart 55.3% (52 of 94)
lib/core/natural_sort.dart 100.0% (27 of 27)
lib/core/url_utils.dart 100.0% (4 of 4)
lib/presentation/pages/editor/chapter_panel.dart 100.0% (64 of 64)
lib/presentation/pages/editor/chapter_reorder_dialog.dart 100.0% (51 of 51)
lib/presentation/pages/editor/chapter_title_dialog.dart 100.0% (27 of 27)
lib/presentation/widgets/cover_thumbnail.dart 83.3% (30 of 36)
lib/presentation/pages/editor/upload_panel.dart 38.6% (61 of 158)
lib/presentation/pages/editor/variant_dialog.dart 0.0% (0 of 61)
lib/presentation/widgets/language_dropdown.dart 84.6% (11 of 13)
lib/presentation/pages/reader/reader_overlay.dart 97.2% (69 of 71)
lib/presentation/pages/reader/reader_page.dart 95.0% (497 of 523)
lib/presentation/pages/reader/region_overlay.dart 100.0% (61 of 61)
lib/presentation/pages/reader/reader_sequence.dart 100.0% (31 of 31)
lib/presentation/widgets/star_rating.dart 100.0% (72 of 72)
lib/app/di.dart 48.3% (14 of 29)
lib/presentation/assistant/assistant_panel.dart 91.7% (88 of 96)
lib/presentation/layout/main_layout.dart 86.3% (44 of 51)
lib/data/api_client.dart 93.6% (73 of 78)
lib/data/repositories/doujin_api_repository.dart 46.9% (231 of 493)
lib/data/repositories/health_repository.dart 87.0% (47 of 54)
lib/data/secure_storage.dart 97.8% (45 of 46)
lib/core/theme.dart 96.9% (31 of 32)
lib/presentation/assistant/approval_card.dart 95.0% (38 of 40)
lib/presentation/assistant/assistant_markdown.dart 100.0% (4 of 4)
lib/presentation/assistant/chat_entries.dart 87.5% (35 of 40)
lib/presentation/pages/editor/editor_page.dart 77.9% (60 of 77)
lib/presentation/pages/editor/editor_pane.dart 92.6% (63 of 68)
lib/core/person_display.dart 100.0% (7 of 7)
lib/presentation/pages/editor/association_picker.dart 95.5% (106 of 111)
lib/presentation/pages/editor/associations_tab.dart 72.2% (83 of 115)
lib/presentation/pages/editor/doujin_list_pane.dart 67.7% (44 of 65)
lib/presentation/pages/editor/edit_title_dialog.dart 91.7% (55 of 60)
lib/presentation/pages/editor/new_doujin_dialog.dart 66.7% (30 of 45)
lib/presentation/pages/editor/metadata_tab.dart 70.5% (93 of 132)
lib/presentation/pages/characters/characters_page.dart 57.9% (11 of 19)
lib/presentation/pages/detail/detail_page.dart 88.9% (192 of 216)
lib/presentation/pages/detail/variant_tabs_panel.dart 97.2% (242 of 249)
lib/data/offline/filesystem_offline_store.dart 94.1% (144 of 153)
lib/core/offline_paths.dart 100.0% (3 of 3)
lib/presentation/pages/offline/offline_page.dart 98.2% (164 of 167)
lib/app/app.dart 67.1% (47 of 70)
lib/presentation/pages/settings/settings_page.dart 99.2% (389 of 392)
lib/presentation/pages/circles/circles_page.dart 52.6% (10 of 19)
lib/presentation/pages/duplicates/duplicates_page.dart 100.0% (265 of 265)
lib/presentation/pages/library/library_page.dart 93.3% (224 of 240)
lib/presentation/pages/series/series_page.dart 50.0% (9 of 18)
lib/presentation/widgets/smart_filter_bar.dart 84.1% (207 of 246)
lib/presentation/pages/settings/server_sections.dart 99.2% (751 of 757)
lib/presentation/pages/settings/settings_layout.dart 100.0% (78 of 78)
lib/presentation/widgets/model_combo_field.dart 66.2% (98 of 148)
lib/app/skill_assets.dart 92.9% (13 of 14)

Total: 86.5% (11732 of 13559)

<!-- flutter-coverage-comment --> ## Flutter Coverage | File | Line coverage | |:---|---:| | lib/domain/entities/agent_routing.dart | 100.0% (8 of 8) | | lib/domain/entities/reading_mode.dart | 100.0% (3 of 3) | | lib/domain/entities/stored_settings.dart | 100.0% (21 of 21) | | lib/app/store.dart | 95.8% (23 of 24) | | lib/data/models/doujin_models.dart | 100.0% (55 of 55) | | lib/data/models/doujin_models.g.dart | 68.4% (299 of 437) | | lib/domain/repositories/repositories.dart | 33.3% (1 of 3) | | lib/presentation/state/app_state.dart | 84.4% (38 of 45) | | lib/agent/agent_log.dart | 100.0% (94 of 94) | | lib/agent/agent_service.dart | 92.9% (514 of 553) | | lib/agent/approval_gate.dart | 100.0% (21 of 21) | | lib/agent/assistant_context.dart | 70.7% (41 of 58) | | lib/agent/browser_budget.dart | 80.0% (4 of 5) | | lib/agent/caching_describer.dart | 100.0% (13 of 13) | | lib/agent/cancellable_tool.dart | 100.0% (13 of 13) | | lib/agent/memory_store.dart | 93.8% (15 of 16) | | lib/agent/skills/skill_registry.dart | 98.2% (54 of 55) | | lib/agent/system_prompt.dart | 100.0% (73 of 73) | | lib/agent/tools/budgeted_browser_tool.dart | 84.2% (16 of 19) | | lib/agent/tools/doujin_write_tool.dart | 95.0% (921 of 969) | | lib/agent/tools/entity_write_tool.dart | 78.5% (175 of 223) | | lib/agent/tools/fetch_page_tool.dart | 89.3% (67 of 75) | | lib/agent/tools/get_doujin_tool.dart | 92.3% (60 of 65) | | lib/agent/tools/list_entities_tool.dart | 76.1% (54 of 71) | | lib/agent/tools/navigate_tool.dart | 95.1% (39 of 41) | | lib/agent/tools/read_skill_tool.dart | 82.4% (14 of 17) | | lib/agent/tools/reflection_tools.dart | 78.0% (32 of 41) | | lib/agent/tools/search_doujins_tool.dart | 100.0% (101 of 101) | | lib/agent/tools/view_images_tool.dart | 95.0% (38 of 40) | | lib/core/model_capabilities.dart | 100.0% (23 of 23) | | lib/domain/entities/assistant_entry.dart | 20.0% (1 of 5) | | lib/presentation/state/actions/assistant_actions.dart | 58.3% (7 of 12) | | lib/core/app_config.dart | 70.0% (7 of 10) | | lib/core/reading_order.dart | 100.0% (22 of 22) | | lib/domain/entities/entity_model.dart | 100.0% (2 of 2) | | lib/data/repositories/entity_in_use_exception.dart | 33.3% (1 of 3) | | lib/core/title_display.dart | 93.8% (15 of 16) | | lib/data/models/search_query.dart | 50.0% (2 of 4) | | lib/data/models/search_query.g.dart | 33.8% (26 of 77) | | lib/core/constants.dart | 40.0% (4 of 10) | | lib/data/offline/offline_download_service.dart | 93.0% (66 of 71) | | lib/data/reading_progress_store.dart | 92.3% (36 of 39) | | lib/domain/entities/reading_progress.dart | 100.0% (26 of 26) | | lib/presentation/middleware/assistant_epics.dart | 85.3% (87 of 102) | | lib/presentation/middleware/backup_save_dialog.dart | 25.0% (1 of 4) | | lib/presentation/middleware/epics.dart | 93.0% (478 of 514) | | lib/presentation/middleware/offline_epics.dart | 98.4% (60 of 61) | | lib/presentation/state/reducers.dart | 100.0% (14 of 14) | | lib/core/chunking.dart | 100.0% (12 of 12) | | lib/core/code_point_compare.dart | 100.0% (8 of 8) | | lib/core/languages.dart | 100.0% (14 of 14) | | lib/core/variant_display.dart | 100.0% (22 of 22) | | lib/data/models/envelope.dart | 81.2% (13 of 16) | | lib/data/models/envelope.g.dart | 56.7% (38 of 67) | | lib/data/models/server_models.dart | 98.8% (81 of 82) | | lib/data/models/server_models.g.dart | 55.6% (155 of 279) | | lib/domain/entities/offline_manifest.dart | 100.0% (78 of 78) | | lib/data/repositories/backup_exception.dart | 100.0% (3 of 3) | | lib/data/repositories/integrity_exception.dart | 100.0% (4 of 4) | | lib/data/repositories/upload_exception.dart | 33.3% (1 of 3) | | lib/domain/entities/filter_token.dart | 92.2% (107 of 116) | | lib/presentation/middleware/duplicates_epics.dart | 100.0% (50 of 50) | | lib/presentation/middleware/editor_epics.dart | 58.9% (175 of 297) | | lib/presentation/middleware/entity_ops.dart | 64.4% (47 of 73) | | lib/presentation/middleware/server_epics.dart | 100.0% (144 of 144) | | lib/presentation/middleware/upload_epics.dart | 98.1% (104 of 106) | | lib/presentation/state/actions/offline_actions.dart | 91.7% (11 of 12) | | lib/presentation/state/actions/detail_actions.dart | 85.7% (6 of 7) | | lib/presentation/state/actions/duplicates_actions.dart | 88.9% (8 of 9) | | lib/presentation/state/actions/editor_actions.dart | 57.9% (22 of 38) | | lib/presentation/state/actions/entity_actions.dart | 66.7% (12 of 18) | | lib/presentation/state/actions/library_actions.dart | 54.5% (12 of 22) | | lib/presentation/state/actions/metadata_actions.dart | 100.0% (5 of 5) | | lib/presentation/state/actions/reader_actions.dart | 85.7% (6 of 7) | | lib/presentation/state/actions/reading_progress_actions.dart | 100.0% (2 of 2) | | lib/presentation/state/actions/server_actions.dart | 80.0% (32 of 40) | | lib/presentation/state/actions/settings_actions.dart | 88.9% (8 of 9) | | lib/presentation/state/actions/upload_actions.dart | 90.9% (10 of 11) | | lib/presentation/state/reducers/assistant_reducer.dart | 95.2% (60 of 63) | | lib/presentation/state/reducers/detail_reducer.dart | 100.0% (28 of 28) | | lib/presentation/state/reducers/duplicates_reducer.dart | 100.0% (54 of 54) | | lib/presentation/state/reducers/editor_reducer.dart | 97.2% (105 of 108) | | lib/presentation/state/reducers/entity_reducer.dart | 98.9% (94 of 95) | | lib/presentation/state/reducers/library_reducer.dart | 100.0% (140 of 140) | | lib/presentation/state/reducers/metadata_reducer.dart | 90.5% (38 of 42) | | lib/presentation/state/reducers/offline_reducer.dart | 100.0% (61 of 61) | | lib/presentation/state/reducers/reader_reducer.dart | 100.0% (36 of 36) | | lib/presentation/state/reducers/reading_progress_reducer.dart | 100.0% (20 of 20) | | lib/presentation/state/reducers/server_reducer.dart | 99.5% (218 of 219) | | lib/presentation/state/reducers/settings_reducer.dart | 100.0% (77 of 77) | | lib/presentation/state/reducers/upload_reducer.dart | 100.0% (45 of 45) | | lib/presentation/pages/people/people_page.dart | 66.7% (12 of 18) | | lib/presentation/pages/tags/tags_page.dart | 100.0% (17 of 17) | | lib/presentation/widgets/entity_merge_dialog.dart | 100.0% (70 of 70) | | lib/presentation/widgets/entity_editor.dart | 92.5% (135 of 146) | | lib/presentation/widgets/entity_management_page.dart | 88.5% (238 of 269) | | lib/presentation/pages/editor/page_grid.dart | 94.3% (314 of 333) | | lib/presentation/pages/editor/variants_tab.dart | 55.3% (52 of 94) | | lib/core/natural_sort.dart | 100.0% (27 of 27) | | lib/core/url_utils.dart | 100.0% (4 of 4) | | lib/presentation/pages/editor/chapter_panel.dart | 100.0% (64 of 64) | | lib/presentation/pages/editor/chapter_reorder_dialog.dart | 100.0% (51 of 51) | | lib/presentation/pages/editor/chapter_title_dialog.dart | 100.0% (27 of 27) | | lib/presentation/widgets/cover_thumbnail.dart | 83.3% (30 of 36) | | lib/presentation/pages/editor/upload_panel.dart | 38.6% (61 of 158) | | lib/presentation/pages/editor/variant_dialog.dart | 0.0% (0 of 61) | | lib/presentation/widgets/language_dropdown.dart | 84.6% (11 of 13) | | lib/presentation/pages/reader/reader_overlay.dart | 97.2% (69 of 71) | | lib/presentation/pages/reader/reader_page.dart | 95.0% (497 of 523) | | lib/presentation/pages/reader/region_overlay.dart | 100.0% (61 of 61) | | lib/presentation/pages/reader/reader_sequence.dart | 100.0% (31 of 31) | | lib/presentation/widgets/star_rating.dart | 100.0% (72 of 72) | | lib/app/di.dart | 48.3% (14 of 29) | | lib/presentation/assistant/assistant_panel.dart | 91.7% (88 of 96) | | lib/presentation/layout/main_layout.dart | 86.3% (44 of 51) | | lib/data/api_client.dart | 93.6% (73 of 78) | | lib/data/repositories/doujin_api_repository.dart | 46.9% (231 of 493) | | lib/data/repositories/health_repository.dart | 87.0% (47 of 54) | | lib/data/secure_storage.dart | 97.8% (45 of 46) | | lib/core/theme.dart | 96.9% (31 of 32) | | lib/presentation/assistant/approval_card.dart | 95.0% (38 of 40) | | lib/presentation/assistant/assistant_markdown.dart | 100.0% (4 of 4) | | lib/presentation/assistant/chat_entries.dart | 87.5% (35 of 40) | | lib/presentation/pages/editor/editor_page.dart | 77.9% (60 of 77) | | lib/presentation/pages/editor/editor_pane.dart | 92.6% (63 of 68) | | lib/core/person_display.dart | 100.0% (7 of 7) | | lib/presentation/pages/editor/association_picker.dart | 95.5% (106 of 111) | | lib/presentation/pages/editor/associations_tab.dart | 72.2% (83 of 115) | | lib/presentation/pages/editor/doujin_list_pane.dart | 67.7% (44 of 65) | | lib/presentation/pages/editor/edit_title_dialog.dart | 91.7% (55 of 60) | | lib/presentation/pages/editor/new_doujin_dialog.dart | 66.7% (30 of 45) | | lib/presentation/pages/editor/metadata_tab.dart | 70.5% (93 of 132) | | lib/presentation/pages/characters/characters_page.dart | 57.9% (11 of 19) | | lib/presentation/pages/detail/detail_page.dart | 88.9% (192 of 216) | | lib/presentation/pages/detail/variant_tabs_panel.dart | 97.2% (242 of 249) | | lib/data/offline/filesystem_offline_store.dart | 94.1% (144 of 153) | | lib/core/offline_paths.dart | 100.0% (3 of 3) | | lib/presentation/pages/offline/offline_page.dart | 98.2% (164 of 167) | | lib/app/app.dart | 67.1% (47 of 70) | | lib/presentation/pages/settings/settings_page.dart | 99.2% (389 of 392) | | lib/presentation/pages/circles/circles_page.dart | 52.6% (10 of 19) | | lib/presentation/pages/duplicates/duplicates_page.dart | 100.0% (265 of 265) | | lib/presentation/pages/library/library_page.dart | 93.3% (224 of 240) | | lib/presentation/pages/series/series_page.dart | 50.0% (9 of 18) | | lib/presentation/widgets/smart_filter_bar.dart | 84.1% (207 of 246) | | lib/presentation/pages/settings/server_sections.dart | 99.2% (751 of 757) | | lib/presentation/pages/settings/settings_layout.dart | 100.0% (78 of 78) | | lib/presentation/widgets/model_combo_field.dart | 66.2% (98 of 148) | | lib/app/skill_assets.dart | 92.9% (13 of 14) | **Total: 86.5% (11732 of 13559)**
fix: the Detection section's spacing and its Detector rows
Some checks failed
Flutter CI / analyze-and-test (pull_request) Has been cancelled
2699413f0d
Margins first. Card children in this shell pad themselves — SettingsRow
16, _ScheduleRow 12, _StatTiles 16 — and _HintLine does not, because its
other two callers sit loose in a column with their own spacing above
them. Used as the last child of a card it left the text four pixels off
the bottom edge, which is what cramped looks like. _CardFootnote is that
hint with the padding its siblings have; the loose callers are untouched.

Groups also had nothing between them, so every small-caps label sat on
the card above it. Twenty pixels, which is what the statistics section
already uses. The progress bar now has room above it and a radius, and
shares a child with the tiles so no divider splits the two halves of one
thought.

Then the Detector card, which said "regions f3bf7701aebb0185" as the
value of a row called Model: two different things concatenated, neither
labelled, and no hint what the hex was. Now two rows — Model, and
Version described as a fingerprint of the model file that changes when
the model does, which is how the server knows to examine every page
again. A server without a model shows no version row at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix: align the readout values, and load on opening the tab
Some checks failed
Flutter CI / analyze-and-test (pull_request) Failing after 11m24s
61a220dfaa
The values sat eight pixels above their labels, and the cause is in
SettingsRow: it nudges the LABEL down eight so the label sits level with
the centre of a form control. Beside a TextField that is right. Beside a
line of text it is eight pixels of nothing — invisible until a
description makes the row tall enough to see the two columns start at
different heights, which the Detector card does.

SettingsReadout is that row's sibling for values nobody edits: same
label column, same stacking breakpoint, both columns starting on the
same line. Reusable — the statistics section's server-version row has
the same shape and could take it later.

And the section no longer greets you with "not loaded". It joins the
other server sections' first load on opening the page, and — alone among
them — refreshes when its own tab is selected. The others show what
changes when a person changes it; the sweep advances every third of a
second, so a figure fetched when the page opened is stale before anyone
navigates to it.

That made every Settings test reach a repository call, so both fakes now
answer it. A throw there failed tests that were about something else
entirely.

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

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A settings section that answers "is the background job alive" without a curl — ADR 0026 rule 7 finally has a face~ And the toggle fix made me giggle: two different codepoints that draw the same glyph at 24px is exactly the kind of sneaky little defect I live to hunt. ♪

I probed everything. The model layer is excellent — every guard I mutated died on its own test. But the newest commit added UI branches that no test can see, darling. You wouldn't leave those unpinned, would you? ♡

Verdict: ⛔ I can't let this pass~ ♡

⛔ These need fixing before I'm satisfied~

  1. app/lib/presentation/pages/settings_page.dart:286-292 — the headline of commit 61a220d — "load on opening the tab" — is not pinned. I deleted the LoadDetectionStatusAction dispatch from _selectSection and ran the entire suite: 1,558 tests, all green. The existing assertion in settings_page_test.dart (isLoadingDetection is true after tapping Detection) is satisfied by _loadServerSections' page-open dispatch alone — it cannot tell whether the per-entry refresh exists. Mutating the other arm (removing the page-open dispatch instead) also survives. Two new dispatch sites, zero individual pins.
    Fix: after the page-open load settles, reset the fake's detectionCalls, tap Detection, and assert a second getDetectionStatus call — FakeServerRepo already counts them.

  2. app/lib/presentation/pages/settings_layout.dart:181 — SettingsReadout's narrow-pane arm is untested. I changed constraints.maxWidth < _kRowStackBelow to < 0 (stacked arm unreachable) and settings_page_test stayed green, 129/129. Its sibling SettingsRow has exactly this pin — 'a pane too narrow for two columns stacks the row' — so the repo's own standard already demands it; the new widget must not be the one row that escapes it.
    Fix: mirror the sibling test — pump a SettingsReadout (the Detection section works) in a pane under 620px and assert the label stacks above the value.

  3. app/lib/presentation/pages/settings/server_sections.dart:378-384 — the failed-refresh warning banner is untested. I deleted the whole if (server.detectionError != null) ...[InlineBanner...] block and both test files stayed green. No test ever builds the state it exists for: a status on screen and a failed refresh — which is precisely the state the comment above it documents. The reducer half IS pinned (a failed refresh keeps the numbers — my probe there died ♡); the UI half is invisible.
    Fix: dispatch DetectionStatusLoadedAction(status) then DetectionStatusErrorAction('boom') into the store, pump, assert the warning banner renders above the still-visible numbers.

💡 Little ideas (non-blocking)~

  1. app/lib/presentation/pages/settings/server_sections.dart:602 — /// One line of subdued explanation under the toolbar. is _HintLine's doc summary, but it now sits atop _CardFootnote's doc block, and _HintLine below is left doc-less. The _CardFootnote doc itself is lovely (the 4-pixel-cramped explanation!) — just give the stray summary line back to its owner.

✅ What I liked~

  • progress's explicit ceiling — documenting in the getter why clamp made the empty-library arm unprobeable (NaN.clamp returns the upper limit!) is knowledge-preservation at its finest. Both arms now die on their own tests — I checked, twice. ♪
  • off vs Unavailable from hasDetector — telling someone to set DOUJIN_MANAGER_DETECTION on a server with no model sends them to the wrong place; distinguishing the two is correct product thinking, and both arms are tested.
  • unknown, not off for an unrecognized wire word — "this build doesn't know" and "it's switched off" are different answers. Probe-killed.
  • The epic test through createStore so registration itself is pinned — I unwired it and the test went red exactly as it should. ♡
  • SettingsReadout as a sibling for read-only rows (label baselines aligning beside text, not form controls) — right abstraction, right file, documented reason.
  • FakeServerRepo's detection member follows the file's scripted-answer pattern exactly, and throwing by default in the noop mixin catches accidental calls. ♪

Automated review by Jibril · 2026-09-21
CI/CD: run 889 reports failure for head 61a220d, but the same workflow flips success/failure on identical SHAs across this runner's history and run 887 was green on the first commit — treated as inconclusive (likely runner-side; worth a re-run). Coverage bot comment covers 8efda64 only. · Local checks: flutter analyze clean; 273 tests across the four touched files + full suite 1,558/1,582 green; 12 mutation probes — 8 killed, 4 survived (3 became the blockers above).

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A settings section that answers "is the background job alive" without a `curl` — ADR 0026 rule 7 finally has a face~ And the toggle fix made me giggle: two different codepoints that draw the same glyph at 24px is exactly the kind of sneaky little defect I live to hunt. ♪ I probed everything. The model layer is **excellent** — every guard I mutated died on its own test. But the newest commit added UI branches that no test can see, darling. You wouldn't leave those unpinned, would you? ♡ ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **app/lib/presentation/pages/settings_page.dart:286-292** — the headline of commit `61a220d` — *"load on opening the tab"* — is not pinned. I deleted the `LoadDetectionStatusAction` dispatch from `_selectSection` and ran the **entire suite: 1,558 tests, all green**. The existing assertion in `settings_page_test.dart` (`isLoadingDetection` is true after tapping Detection) is satisfied by `_loadServerSections`' page-open dispatch alone — it cannot tell whether the per-entry refresh exists. Mutating the other arm (removing the page-open dispatch instead) also survives. Two new dispatch sites, zero individual pins. Fix: after the page-open load settles, reset the fake's `detectionCalls`, tap Detection, and assert a *second* `getDetectionStatus` call — `FakeServerRepo` already counts them. 2. **app/lib/presentation/pages/settings_layout.dart:181** — `SettingsReadout`'s narrow-pane arm is untested. I changed `constraints.maxWidth < _kRowStackBelow` to `< 0` (stacked arm unreachable) and `settings_page_test` stayed green, 129/129. Its sibling `SettingsRow` has exactly this pin — *'a pane too narrow for two columns stacks the row'* — so the repo's own standard already demands it; the new widget must not be the one row that escapes it. Fix: mirror the sibling test — pump a `SettingsReadout` (the Detection section works) in a pane under 620px and assert the label stacks above the value. 3. **app/lib/presentation/pages/settings/server_sections.dart:378-384** — the failed-refresh warning banner is untested. I deleted the whole `if (server.detectionError != null) ...[InlineBanner...]` block and both test files stayed green. No test ever builds the state it exists for: a status on screen *and* a failed refresh — which is precisely the state the comment above it documents. The reducer half IS pinned (a failed refresh keeps the numbers — my probe there died ♡); the UI half is invisible. Fix: dispatch `DetectionStatusLoadedAction(status)` then `DetectionStatusErrorAction('boom')` into the store, pump, assert the warning banner renders *above* the still-visible numbers. #### 💡 Little ideas (non-blocking)~ 1. **app/lib/presentation/pages/settings/server_sections.dart:602** — `/// One line of subdued explanation under the toolbar.` is `_HintLine`'s doc summary, but it now sits atop `_CardFootnote`'s doc block, and `_HintLine` below is left doc-less. The `_CardFootnote` doc itself is lovely (the 4-pixel-cramped explanation!) — just give the stray summary line back to its owner. #### ✅ What I liked~ - **`progress`'s explicit ceiling** — documenting *in the getter* why `clamp` made the empty-library arm unprobeable (NaN.clamp returns the upper limit!) is knowledge-preservation at its finest. Both arms now die on their own tests — I checked, twice. ♪ - **`off` vs `Unavailable`** from `hasDetector` — telling someone to set `DOUJIN_MANAGER_DETECTION` on a server with no model sends them to the wrong place; distinguishing the two is correct product thinking, and both arms are tested. - **`unknown`, not `off`** for an unrecognized wire word — "this build doesn't know" and "it's switched off" are different answers. Probe-killed. - The epic test **through `createStore`** so registration itself is pinned — I unwired it and the test went red exactly as it should. ♡ - `SettingsReadout` as a sibling for read-only rows (label baselines aligning beside *text*, not form controls) — right abstraction, right file, documented reason. - `FakeServerRepo`'s detection member follows the file's scripted-answer pattern exactly, and throwing by default in the noop mixin catches accidental calls. ♪ --- *Automated review by Jibril · 2026-09-21* *CI/CD: run 889 reports failure for head `61a220d`, but the same workflow flips success/failure on identical SHAs across this runner's history and run 887 was green on the first commit — treated as inconclusive (likely runner-side; worth a re-run). Coverage bot comment covers `8efda64` only. · Local checks: `flutter analyze` clean; 273 tests across the four touched files + full suite 1,558/1,582 green; 12 mutation probes — 8 killed, 4 survived (3 became the blockers above).*
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A settings section that answers "is the background job alive" without ssh-ing into a NAS~ The off-vs-unavailable split, the unrecognised-word-reads-as-unknown, the analysed-counts-empty-pages explanation written right where the number could be misread... I read this diff twice just to watch the reasoning in the comments. ♡

(Reviewed at 61a220d — the head moved twice while I read; everything below is probed at the current head. The coverage comment 8856 covers only 8efda64, so I ran scripts/verify.sh app myself: green, +1558 tests, analyze clean, codegen checksums intact.)

And then I probed it. Fufu~ you know what happens next.

Verdict: ⛔ I can't let this pass~ ♡

⛔ These need fixing before I'm satisfied~

  1. server_sections.dart:377 — the failed-refresh banner is a rendering branch no test exercises. if (server.detectionError != null) puts a warning banner above the last good numbers instead of replacing them — a deliberate, documented behavior of this PR. I deleted the InlineBanner (kept the SizedBox) and ran the detection + settings suites: all 153 green. The branch is decorative as far as the suite knows. And you built the ammunition but never loaded it: FakeServerRepo.throwOnDetection and detectionCalls (server_stub_repo.dart:26-32) exist and are asserted nowhere.
    Fix: a widget test that seeds a status, fails the refresh (throwOnDetection = true), taps Refresh, and asserts the banner's message renders and the old numbers stay on screen.
  2. settings_page.dart:275 and :291 — two new dispatch sites, zero pins. First I deleted the page-open dispatch(const LoadDetectionStatusAction()) from _loadServerSections — the site whose five sibling loads are pinned by "load statistics and backups once on entry": all green. Then, separately, I deleted _selectSection's dispatch and restored the page-open one: green again — the reach-test's isLoadingDetection isTrue is satisfied by the still-in-flight page-open load, so it pins neither site. Each of your two loads can be silently deleted and nothing complains.
    Fix: assert detectionCalls (that is what it is for~): once after the page opens on a configured server, and again that tapping the Detection tab re-asks the server (a second call after the tap).

The production code itself I could not break: progress's empty-library and above-one arms, imagesRemaining's floor, unknown-vs-off, the wire unwrap against DetectionStatusDto field-for-field, the reducer keeping numbers on a failed refresh, the epic through createStore — all pinned, all doing exactly what their tests say. It is only those three behaviors above that shipped unpinned. Fufu~ you wouldn't leave THIS in production, would you? ♡

✅ What I liked~

  • The NaN.clamp(0, 1) discovery — that an unreachable guard can hide behind NaN returning the upper limit — is written into progress where the next person will trip over the same thing. Documentation doing real work.
  • Unavailable vs Off vs Unrecognised: three honest answers to "is it running", where a lesser client prints the string and shrugs.
  • The reader toggle fix — colour plus container, so it reads at a glance, on a touchscreen, and without colour vision; the test asserts exactly those three things.
  • Loading on opening the tab, with the every-third-of-a-second rationale in the doc comment. Correct instinct, correctly justified — it just needs its pin.

Automated review by Jibril · 2026-09-21
CI/CD: coverage comment 8856 covers 8efda64 only (stale for 61a220d) · Local checks: verify.sh app green at 61a220d (+1558); 3 mutation probes survived (reported above), everything else red under its own mutation

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A settings section that answers "is the background job alive" without ssh-ing into a NAS~ The off-vs-unavailable split, the unrecognised-word-reads-as-unknown, the analysed-counts-empty-pages explanation written right where the number could be misread... I read this diff twice just to watch the reasoning in the comments. ♡ (Reviewed at `61a220d` — the head moved twice while I read; everything below is probed at the current head. The coverage comment 8856 covers only `8efda64`, so I ran `scripts/verify.sh app` myself: **green, +1558 tests, analyze clean, codegen checksums intact**.) And then I probed it. Fufu~ you know what happens next. ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **`server_sections.dart:377` — the failed-refresh banner is a rendering branch no test exercises.** `if (server.detectionError != null)` puts a warning banner above the last good numbers instead of replacing them — a deliberate, documented behavior of this PR. I deleted the `InlineBanner` (kept the `SizedBox`) and ran the detection + settings suites: **all 153 green**. The branch is decorative as far as the suite knows. And you built the ammunition but never loaded it: `FakeServerRepo.throwOnDetection` and `detectionCalls` (`server_stub_repo.dart:26-32`) exist and are asserted **nowhere**. Fix: a widget test that seeds a status, fails the refresh (`throwOnDetection = true`), taps Refresh, and asserts the banner's message renders *and* the old numbers stay on screen. 2. **`settings_page.dart:275` and `:291` — two new dispatch sites, zero pins.** First I deleted the page-open `dispatch(const LoadDetectionStatusAction())` from `_loadServerSections` — the site whose five sibling loads *are* pinned by "load statistics and backups once on entry": all green. Then, separately, I deleted `_selectSection`'s dispatch and restored the page-open one: green again — the reach-test's `isLoadingDetection isTrue` is satisfied by the still-in-flight page-open load, so it pins **neither** site. Each of your two loads can be silently deleted and nothing complains. Fix: assert `detectionCalls` (that is what it is for~): once after the page opens on a configured server, and again that tapping the Detection tab re-asks the server (a second call after the tap). The production code itself I could not break: `progress`'s empty-library and above-one arms, `imagesRemaining`'s floor, unknown-vs-off, the wire unwrap against `DetectionStatusDto` field-for-field, the reducer keeping numbers on a failed refresh, the epic through `createStore` — all pinned, all doing exactly what their tests say. It is *only* those three behaviors above that shipped unpinned. Fufu~ you wouldn't leave THIS in production, would you? ♡ #### ✅ What I liked~ - The `NaN.clamp(0, 1)` discovery — that an unreachable guard can hide behind NaN returning the *upper* limit — is written into `progress` where the next person will trip over the same thing. Documentation doing real work. - `Unavailable` vs `Off` vs `Unrecognised`: three honest answers to "is it running", where a lesser client prints the string and shrugs. - The reader toggle fix — colour *plus* container, so it reads at a glance, on a touchscreen, and without colour vision; the test asserts exactly those three things. - Loading on opening the tab, with the every-third-of-a-second rationale in the doc comment. Correct instinct, correctly justified — it just needs its pin. --- *Automated review by Jibril · 2026-09-21* *CI/CD: coverage comment 8856 covers 8efda64 only (stale for 61a220d) · Local checks: verify.sh app green at 61a220d (+1558); 3 mutation probes survived (reported above), everything else red under its own mutation*
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A settings section that tells the truth about a background job — off-because-no-model versus off-because-not-switched-on, "analysed" counting the pages nothing was found on, NaN.clamp evaded with an explicit ceiling… this is my favourite kind of PR, scarlet. The layering is flawless: wire model mirrors DetectionStatusDto key-for-key, the repository method is a carbon copy of the getImageRegions sibling, the Redux trio mirrors the stats trio arm-for-arm, and the epic is pinned through createStore so registration can't rot. I ran the whole thing locally and probed it with mutations, and the core claims all held — 7 of my 11 probes died exactly on their own tests~ ♡

But four probes survived, my dear. Four. You wrote tests that cannot fail for branches you wrote on purpose. You wouldn't leave those in production… would you? ♡

Verdict: ⛔ I can't let this pass~ ♡

⛔ These need fixing before I'm satisfied~

  1. The section's error story and the load-on-tab dispatch are unpinned — three surviving mutations. I deleted each of these from the head tree and the entire suite stayed green (153/153 in the affected files):

    • settings_page.dart:289 — the LoadDetectionStatusAction dispatch in _selectSection (the entire point of commit 61a220d, "load on opening the tab") can be deleted with every test green. the sidebar reaches the Detection section asserts isLoadingDetection is true after the tap, but the page-open load from _loadServerSections already set it — the assertion is tautological. The behavior ships on trust, not on a test.
    • server_sections.dart, DetectionSection — the InlineBanner over stale data (if (server.detectionError != null) …). Deleting the whole block: green. Your PR body promises "a failed refresh keeps the numbers on screen", and the only test of that is the reducer keeping detection non-null — nothing pins the banner rendering.
    • server_sections.dart, DetectionSection — _PlaceholderCard(error: server.detectionError, …). Passing error: null instead: green. A first-load failure shows nothing distinctive.

    The sibling sections hold the standard: statistics has a failed statistics load shows the error and can retry and a failed statistics refresh keeps the stale values; backups has the matching pair. Detection has neither — and most suspicious of all~ FakeServerRepo.throwOnDetection and detectionCalls were added in this very PR and have zero consumers. You built the test tools and then walked away without using them. Fufu~ that's just cruel to the tools.
    Fix: three widget tests in settings_page_test.dart beside the stats/backups ones — (a) throwOnDetection = true, open the tab, assert detection down shows and Load status retries (detectionCalls == 2); (b) stale status on screen, throwOnDetection = true, refresh, assert both the stale figures and the error; (c) after settling the page-open load, switch to the tab and assert detectionCalls increments — that pins the per-tab dispatch.

  2. settings_layout.dart:256 — SettingsReadout's narrow-window stack arm is dead code as far as the suite knows. I killed the arm (< _kRowStackBelow → < 0): all green. This is a new public widget, and its stack arm is reachable — the pane drops below the 620px threshold on a narrow window. The identical arm on its sibling SettingsRow is tested (a pane too narrow for two columns stacks the row, 600×900). Same widget, same threshold, same file — one arm pinned, one arm not. Fix: mirror that test against the Detection section (600×900, open Detection, assert the Model value stacks below its label).

💡 Little ideas (non-blocking)~

  1. settings_page.dart:75 — navKey's doc comment still enumerates the sections as "connection, assistant, … backups"; detection never arrived. One word.

✅ What I liked~

  • The off/unavailable split is genuinely thoughtful UX: state: "off" from a model-less server would send someone to set DOUJIN_MANAGER_DETECTION for nothing. Pinned twice (unit + widget), and my reorder probe died on exactly those tests~
  • The progress NaN story — documenting why not clamp (NaN returns the upper limit, making the empty-library arm unprobe-deletable) and rewriting with an explicit ceiling so both arms are distinguishable. My probes 1 and 2 both went red exactly where you said they would. Delicious~ ♪
  • The reader toggle fix is properly multi-channel: codepoint + colour + filled container. I checked it against the actual palette (0xFFE94560/0xFFEAEAEA on the black87 scrim) — it reads with and without colour vision, and my style-deletion probe went red on exactly the new test.
  • unknown ≠ off for unrecognised wire words — forward-compatible honesty, and the when !detection.hasDetector guard-arm is precisely cased.
  • Epic pinned through createStore ("an epic written and never wired up passes a direct test and does nothing") — you even wrote the moral in the comment. Correct, obviously~

Automated review by Jibril · 2026-09-21
CI/CD: failed for head 61a220d (run 889; also 888 for the prior head) — logs unretrievable (404), cause unknown; PR body flags main's flutter-ci as chronically red and claims the exact CI command passes locally · Local checks: full app suite 1,558/1,556+ passed, analyze 0 issues, 11 mutation probes (7 red-on-target, 4 survivors → the blockers above)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A settings section that tells the truth about a background job — off-because-no-model versus off-because-not-switched-on, "analysed" counting the pages nothing was found on, NaN.clamp evaded with an explicit ceiling… this is my *favourite* kind of PR, scarlet. The layering is flawless: wire model mirrors `DetectionStatusDto` key-for-key, the repository method is a carbon copy of the `getImageRegions` sibling, the Redux trio mirrors the stats trio arm-for-arm, and the epic is pinned *through `createStore`* so registration can't rot. I ran the whole thing locally and probed it with mutations, and the core claims all held — 7 of my 11 probes died exactly on their own tests~ ♡ But four probes **survived**, my dear. Four. You wrote tests that cannot fail for branches you wrote on purpose. You wouldn't leave those in production… would you? ♡ ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **The section's error story and the load-on-tab dispatch are unpinned — three surviving mutations.** I deleted each of these from the head tree and the entire suite stayed green (153/153 in the affected files): - **`settings_page.dart:289`** — the `LoadDetectionStatusAction` dispatch in `_selectSection` (the entire point of commit 61a220d, "load on opening the tab") can be deleted with every test green. `the sidebar reaches the Detection section` asserts `isLoadingDetection` is true after the tap, but the page-open load from `_loadServerSections` already set it — the assertion is tautological. The behavior ships on trust, not on a test. - **`server_sections.dart`, DetectionSection — the `InlineBanner` over stale data** (`if (server.detectionError != null) …`). Deleting the whole block: green. Your PR body promises "a failed refresh keeps the numbers on screen", and the only test of that is the *reducer* keeping `detection` non-null — nothing pins the banner rendering. - **`server_sections.dart`, DetectionSection — `_PlaceholderCard(error: server.detectionError, …)`**. Passing `error: null` instead: green. A first-load failure shows nothing distinctive. The sibling sections hold the standard: statistics has `a failed statistics load shows the error and can retry` and `a failed statistics refresh keeps the stale values`; backups has the matching pair. Detection has neither — and most suspicious of all~ **`FakeServerRepo.throwOnDetection` and `detectionCalls` were added in this very PR and have zero consumers.** You built the test tools and then walked away without using them. Fufu~ that's just cruel to the tools. Fix: three widget tests in `settings_page_test.dart` beside the stats/backups ones — (a) `throwOnDetection = true`, open the tab, assert `detection down` shows and `Load status` retries (`detectionCalls == 2`); (b) stale status on screen, `throwOnDetection = true`, refresh, assert both the stale figures *and* the error; (c) after settling the page-open load, switch to the tab and assert `detectionCalls` increments — that pins the per-tab dispatch. 2. **`settings_layout.dart:256` — `SettingsReadout`'s narrow-window stack arm is dead code as far as the suite knows.** I killed the arm (`< _kRowStackBelow` → `< 0`): all green. This is a new *public* widget, and its stack arm is reachable — the pane drops below the 620px threshold on a narrow window. The identical arm on its sibling `SettingsRow` **is** tested (`a pane too narrow for two columns stacks the row`, 600×900). Same widget, same threshold, same file — one arm pinned, one arm not. Fix: mirror that test against the Detection section (600×900, open Detection, assert the Model value stacks below its label). #### 💡 Little ideas (non-blocking)~ 1. **`settings_page.dart:75`** — `navKey`'s doc comment still enumerates the sections as "`connection`, `assistant`, … `backups`"; `detection` never arrived. One word. #### ✅ What I liked~ - The **off/unavailable split** is genuinely thoughtful UX: `state: "off"` from a model-less server would send someone to set `DOUJIN_MANAGER_DETECTION` for nothing. Pinned twice (unit + widget), and my reorder probe died on exactly those tests~ - The **`progress` NaN story** — documenting *why* not `clamp` (NaN returns the upper limit, making the empty-library arm unprobe-deletable) and rewriting with an explicit ceiling so both arms are distinguishable. My probes 1 and 2 both went red exactly where you said they would. Delicious~ ♪ - **The reader toggle fix** is properly multi-channel: codepoint + colour + filled container. I checked it against the actual palette (`0xFFE94560`/`0xFFEAEAEA` on the black87 scrim) — it reads with and without colour vision, and my style-deletion probe went red on exactly the new test. - `unknown ≠ off` for unrecognised wire words — forward-compatible honesty, and the `when !detection.hasDetector` guard-arm is precisely cased. - Epic pinned through `createStore` ("an epic written and never wired up passes a direct test and does nothing") — you even wrote the moral in the comment. Correct, obviously~ --- *Automated review by Jibril · 2026-09-21* *CI/CD: failed for head 61a220d (run 889; also 888 for the prior head) — logs unretrievable (404), cause unknown; PR body flags main's flutter-ci as chronically red and claims the exact CI command passes locally · Local checks: full app suite 1,558/1,556+ passed, analyze 0 issues, 11 mutation probes (7 red-on-target, 4 survivors → the blockers above)*
test: pin the five branches three reviews found unpinned
Some checks failed
Flutter CI / analyze-and-test (pull_request) Failing after 24m9s
e35d853c94
Every one was real, and the load-on-tab pair is the embarrassing one:
the reach test asserted isLoadingDetection was true after the tap, which
the page-open load had already made true. Tautological — either dispatch
could be deleted in silence. FakeServerRepo counts calls, so the new
test counts them: one when the page opens, two after the tab is picked.
That pins both sites with one assertion each.

The error story was worse than untested, it was tooling built and then
abandoned: throwOnDetection and detectionCalls were added in the same PR
and asserted nowhere. Now the first-load failure shows its message and
retries, and a failed refresh puts the banner over the last good numbers
rather than replacing them — which is what the PR body promised and only
the reducer half was pinning.

SettingsReadout's narrow-pane arm gets the test its sibling SettingsRow
already has; a new public widget should not be the one row that escapes
the standard.

Both doc slips too: _CardFootnote had taken _HintLine's summary line on
the way in, and navKey's list of section names never gained detection.

All five mutations from the reviews re-run and now red.

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

Fixed at e35d853. Three passes, one list — all five were real, and one of them is the worst kind.

The tautological test

the sidebar reaches the Detection section asserted isLoadingDetection was
true after tapping the tab. The page-open load had already made it true, so
the assertion could not fail whichever dispatch you deleted. I wrote a test that
looked like it pinned the headline behaviour of the commit and pinned nothing.

FakeServerRepo counts calls, so the new test counts them:

await pumpPage(tester, storeWith(repo));
expect(repo.detectionCalls, 1);   // the page-open load
await openSection(tester, 'detection');
expect(repo.detectionCalls, 2);   // and the per-tab refresh

Two sites, two assertions, one test. Both your mutations go red.

The tooling I built and abandoned

You are right and it is worse than untested: throwOnDetection and
detectionCalls were added in this PR and asserted nowhere.
I built the
ammunition, described the behaviour in the PR body, and never loaded it.

Three tests now, beside the statistics and backups pairs they mirror:

Test Pins
a failed detection load shows the error and can retry the placeholder's error: — passing null now goes red
a failed detection refresh keeps the stale numbers the InlineBanner over the last good figures, which the PR body promised and only the reducer half was pinning
both detection loads ask the server the two dispatch sites above

SettingsReadout's narrow arm

Taken. A new public widget should not be the one row that escapes a
standard its own sibling already meets — a pane too narrow for two columns stacks a readout, at 600×900, mirroring SettingsRow's.

Both doc slips

_CardFootnote took _HintLine's summary line on the way in, leaving the
owner doc-less; returned. And navKey's enumeration of section names never
gained detection; added.

Verification

All five mutations from the reviews re-run against the new head — 5/5 red,
each on its own test. scripts/verify.sh green (== verify OK (all) ==),
1,562 tests, analyze clean.

On the red CI

Your three reports disagree about it and I cannot settle it either: run 887 was
green on the first commit, 888 and 889 red on the next two, and the jobs API
404s for this token so neither of us can read a log. I did reproduce what the
workflow actually runs — COVERAGE=1 bash scripts/verify.sh app, which is
--coverage --concurrency=1 — and it passes here in 2m21s, well inside the
12-minute attempt budget. flutter-ci has also been red on main since run
879, before this branch existed. I have flagged it to the owner rather than
guess further.

Fixed at `e35d853`. Three passes, one list — all five were real, and one of them is the worst kind. ## The tautological test `the sidebar reaches the Detection section` asserted `isLoadingDetection` was true after tapping the tab. The page-open load had **already** made it true, so the assertion could not fail whichever dispatch you deleted. I wrote a test that looked like it pinned the headline behaviour of the commit and pinned nothing. `FakeServerRepo` counts calls, so the new test counts them: ```dart await pumpPage(tester, storeWith(repo)); expect(repo.detectionCalls, 1); // the page-open load await openSection(tester, 'detection'); expect(repo.detectionCalls, 2); // and the per-tab refresh ``` Two sites, two assertions, one test. Both your mutations go red. ## The tooling I built and abandoned You are right and it is worse than untested: **`throwOnDetection` and `detectionCalls` were added in this PR and asserted nowhere.** I built the ammunition, described the behaviour in the PR body, and never loaded it. Three tests now, beside the statistics and backups pairs they mirror: | Test | Pins | |---|---| | `a failed detection load shows the error and can retry` | the placeholder's `error:` — passing `null` now goes red | | `a failed detection refresh keeps the stale numbers` | the `InlineBanner` over the last good figures, which the PR body promised and only the reducer half was pinning | | `both detection loads ask the server` | the two dispatch sites above | ## `SettingsReadout`'s narrow arm Taken. A new **public** widget should not be the one row that escapes a standard its own sibling already meets — `a pane too narrow for two columns stacks a readout`, at 600×900, mirroring `SettingsRow`'s. ## Both doc slips `_CardFootnote` took `_HintLine`'s summary line on the way in, leaving the owner doc-less; returned. And `navKey`'s enumeration of section names never gained `detection`; added. ## Verification All five mutations from the reviews re-run against the new head — **5/5 red**, each on its own test. `scripts/verify.sh` green (`== verify OK (all) ==`), **1,562 tests**, analyze clean. ## On the red CI Your three reports disagree about it and I cannot settle it either: run 887 was green on the first commit, 888 and 889 red on the next two, and the jobs API 404s for this token so neither of us can read a log. I did reproduce what the workflow actually runs — `COVERAGE=1 bash scripts/verify.sh app`, which is `--coverage --concurrency=1` — and it passes here in 2m21s, well inside the 12-minute attempt budget. `flutter-ci` has also been red on `main` since run 879, before this branch existed. I have flagged it to the owner rather than guess further.
Member

🔮 fufu~ Jibril reviewed your code!

Round 2~ I came armed with the same five mutations that survived round 1, and this time every single one died on its own test. You even quoted my tautology diagnosis back at me and wrote the counting test I wanted — detectionCalls == 1 after page-open, == 2 after the tap. Two sites, two assertions, one test. Fufu~ that's the pin I was dreaming of~ ♡

Verdict: ✅ Looks good to me~

✅ What I liked~

  • The honest post-mortem. "I wrote a test that looked like it pinned the headline behaviour of the commit and pinned nothing" — admitting a tautology out loud is rarer than fixing it, and the comment above the counting test now explains the trap for the next person.
  • The ammunition is loaded. throwOnDetection/detectionCalls finally have consumers — three tests, sitting beside the statistics/backups pairs they mirror, exactly where the calibration demanded.
  • Probes, independently re-run at e35d853 (each mutation applied alone, run, restored — tree left pristine):
    1. _selectSection's dispatch deleted → both detection loads ask the server RED.
    2. Page-open dispatch deleted instead → same test RED. Each site is individually pinned.
    3. The InlineBanner failed-refresh block deleted → a failed detection refresh keeps the stale numbers RED.
    4. error: null in the placeholder → a failed detection load shows the error and can retry RED.
    5. SettingsReadout's stack arm disabled (< _kRowStackBelow → < 0) → a pane too narrow for two columns stacks a readout RED. (A confession~: my first attempt at this probe killed SettingsRow's lookalike arm by mistake — the two builders are near-twins, height: 10 vs height: 8 — and the suite stayed green because that arm genuinely is tested. Corrected to the right widget: dead. Both twins pinned, exactly as it should be ♪)
  • The two doc slips — _HintLine's summary line returned to its owner, navKey's enumeration gained detection. One word each, both landed.
  • Increment discipline: +107/−3 across exactly 3 files; the production delta is two doc-comment lines, everything else is additive test. This is what a fix commit should look like.

scripts/verify.sh app green end-to-end at e35d853: 1,562/1,562, analyze clean, codegen checksums intact — your PR-body count, reproduced exactly. ♡

The red flutter-ci remains unexplained (runs 888/889 red on the prior heads, logs 404 for this token, main red since run 879 — before this branch existed). You flagged it to the owner, which is the right call; it's the one loose thread and it isn't this PR's.

Fufu~ all five fixed, all five verified dead. I'm satisfied~ Merge it. ♪


Automated review by Jibril · 2026-09-21
CI/CD: inconclusive for e35d853 (no bot comment yet; prior runs 888/889 red with unretrievable logs, pre-existing main redness flagged to owner) · Local checks: verify.sh app green — 1,562/1,562, analyze 0 issues, codegen checksums OK; settings suite 133/133; 5/5 mutation probes RED on their own tests

## 🔮 fufu~ Jibril reviewed your code! Round 2~ I came armed with the same five mutations that survived round 1, and this time **every single one died on its own test**. You even quoted my tautology diagnosis back at me and wrote the counting test I wanted — `detectionCalls == 1` after page-open, `== 2` after the tap. Two sites, two assertions, one test. Fufu~ that's the pin I was dreaming of~ ♡ ### Verdict: ✅ Looks good to me~ #### ✅ What I liked~ - **The honest post-mortem.** *"I wrote a test that looked like it pinned the headline behaviour of the commit and pinned nothing"* — admitting a tautology out loud is rarer than fixing it, and the comment above the counting test now explains the trap for the next person. - **The ammunition is loaded.** `throwOnDetection`/`detectionCalls` finally have consumers — three tests, sitting beside the statistics/backups pairs they mirror, exactly where the calibration demanded. - **Probes, independently re-run at `e35d853`** (each mutation applied alone, run, restored — tree left pristine): 1. `_selectSection`'s dispatch deleted → `both detection loads ask the server` RED. 2. Page-open dispatch deleted instead → same test RED. Each site is *individually* pinned. 3. The `InlineBanner` failed-refresh block deleted → `a failed detection refresh keeps the stale numbers` RED. 4. `error: null` in the placeholder → `a failed detection load shows the error and can retry` RED. 5. `SettingsReadout`'s stack arm disabled (`< _kRowStackBelow` → `< 0`) → `a pane too narrow for two columns stacks a readout` RED. (A confession~: my *first* attempt at this probe killed `SettingsRow`'s lookalike arm by mistake — the two builders are near-twins, `height: 10` vs `height: 8` — and the suite stayed green because that arm genuinely is tested. Corrected to the right widget: dead. Both twins pinned, exactly as it should be ♪) - **The two doc slips** — `_HintLine`'s summary line returned to its owner, `navKey`'s enumeration gained `detection`. One word each, both landed. - **Increment discipline**: +107/−3 across exactly 3 files; the production delta is *two doc-comment lines*, everything else is additive test. This is what a fix commit should look like. `scripts/verify.sh app` green end-to-end at `e35d853`: **1,562/1,562**, analyze clean, codegen checksums intact — your PR-body count, reproduced exactly. ♡ The red `flutter-ci` remains unexplained (runs 888/889 red on the prior heads, logs 404 for this token, `main` red since run 879 — before this branch existed). You flagged it to the owner, which is the right call; it's the one loose thread and it isn't this PR's. Fufu~ all five fixed, all five verified dead. I'm satisfied~ Merge it. ♪ --- *Automated review by Jibril · 2026-09-21* *CI/CD: inconclusive for `e35d853` (no bot comment yet; prior runs 888/889 red with unretrievable logs, pre-existing main redness flagged to owner) · Local checks: verify.sh app green — 1,562/1,562, analyze 0 issues, codegen checksums OK; settings suite 133/133; 5/5 mutation probes RED on their own tests*
bjoern merged commit 94a498a2e3 into main 2026-09-21 11:13:39 +02:00
bjoern deleted branch feat/detection-settings-section 2026-09-21 11:13:39 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
4 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
TeamAI/doujin-manager!194
No description provided.