feat: reading progress with resume and library read badges #72

Merged
bjoern merged 2 commits from feat/reading-progress into main 2026-08-15 16:45:42 +02:00
Member

What

Client-side per-variant reading progress: last page, page count, a sticky finished flag, and a timestamp, recorded from the reader.

  • Persistence: debounced writes to reading_progress.json in the app config dir via a new ReadingProgressStore (app/lib/data/reading_progress_store.dart). Per ADR 0023, user/client preferences stay client-side; a null path degrades to a no-op (same pattern as the assistant's MemoryStore). Progress is loaded at startup into a new Redux slice, with a _persistReadingProgressEpic handling debounced writes on RecordReadingProgressAction and a flush on ClearReaderAction.
  • Detail page: unfinished variants get a "Continue reading (page N)" affordance that deep-links into the reader via ?page=.
  • Library: cards show a small in-progress/finished badge on the cover.

Tests cover the reducer, the store (round-trip, corrupt file, null path), reader recording, detail-page resume, library badges, and startup wiring. flutter analyze and the targeted tests pass; review verdict: approve.

Follow-ups (nice-to-have, deferred)

  • reading_progress_reducer.dart: the finished flag is sticky even when a variant's pageCount grows (e.g. new chapter uploaded after finishing) — the continue affordance stays hidden. Comparing action.pageCount against the stored entry's pageCount would let "finished" un-stick when content grows.
  • reading_progress_store.dart: defaultReadingProgressPath() doesn't truly no-op on web — defaultAppConfigDir() reads Platform.environment, which throws on Flutter web before the null-path branch engages. Mirrors the pre-existing defaultUserSkillsDir()/defaultMemoriesPath() pattern (no regression); fixing means a kIsWeb guard in defaultAppConfigDir.
  • epics.dart: _persistReadingProgressEpic has no test through the store wiring — a regression in its action filter or registration guard in createAppEpic would go uncaught.
  • reading_progress_store.dart: the data layer imports agent/skills/skill_registry.dart for defaultAppConfigDir; the helper would sit better in a neutral module (e.g. core/) to avoid the data->agent dependency edge.

🤖 Generated with Claude Code

## What Client-side per-variant reading progress: last page, page count, a sticky finished flag, and a timestamp, recorded from the reader. - **Persistence**: debounced writes to `reading_progress.json` in the app config dir via a new `ReadingProgressStore` (`app/lib/data/reading_progress_store.dart`). Per ADR 0023, user/client preferences stay client-side; a null path degrades to a no-op (same pattern as the assistant's MemoryStore). Progress is loaded at startup into a new Redux slice, with a `_persistReadingProgressEpic` handling debounced writes on `RecordReadingProgressAction` and a flush on `ClearReaderAction`. - **Detail page**: unfinished variants get a "Continue reading (page N)" affordance that deep-links into the reader via `?page=`. - **Library**: cards show a small in-progress/finished badge on the cover. Tests cover the reducer, the store (round-trip, corrupt file, null path), reader recording, detail-page resume, library badges, and startup wiring. `flutter analyze` and the targeted tests pass; review verdict: approve. ## Follow-ups (nice-to-have, deferred) - `reading_progress_reducer.dart`: the finished flag is sticky even when a variant's `pageCount` grows (e.g. new chapter uploaded after finishing) — the continue affordance stays hidden. Comparing `action.pageCount` against the stored entry's `pageCount` would let "finished" un-stick when content grows. - `reading_progress_store.dart`: `defaultReadingProgressPath()` doesn't truly no-op on web — `defaultAppConfigDir()` reads `Platform.environment`, which throws on Flutter web before the null-path branch engages. Mirrors the pre-existing `defaultUserSkillsDir()`/`defaultMemoriesPath()` pattern (no regression); fixing means a `kIsWeb` guard in `defaultAppConfigDir`. - `epics.dart`: `_persistReadingProgressEpic` has no test through the store wiring — a regression in its action filter or registration guard in `createAppEpic` would go uncaught. - `reading_progress_store.dart`: the data layer imports `agent/skills/skill_registry.dart` for `defaultAppConfigDir`; the helper would sit better in a neutral module (e.g. `core/`) to avoid the data->agent dependency edge. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat: client-side reading progress with resume and library badges
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 2m41s
0fff8d53c6
Per-variant reading progress (last page, page count, sticky finished
flag, timestamp) recorded from the reader and persisted debounced to
reading_progress.json in the app config dir (ADR 0023: user/client
preferences stay client-side; null path degrades to a no-op like the
assistant's MemoryStore). Loaded at startup into a new Redux slice.
The detail page offers "Continue reading (page N)" for unfinished
variants via the reader's ?page= deep link, and library cards show a
small in-progress/finished badge on the cover.

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

Flutter Coverage

File Line coverage
lib/app/store.dart 100.0% (19 of 19)
lib/data/models/doujin_models.dart 85.0% (34 of 40)
lib/data/models/doujin_models.g.dart 40.5% (119 of 294)
lib/domain/entities/stored_settings.dart 100.0% (9 of 9)
lib/presentation/state/app_state.dart 72.7% (16 of 22)
lib/agent/agent_service.dart 84.6% (242 of 286)
lib/agent/approval_gate.dart 100.0% (16 of 16)
lib/agent/assistant_context.dart 52.8% (28 of 53)
lib/agent/browser_budget.dart 80.0% (4 of 5)
lib/agent/caching_describer.dart 100.0% (12 of 12)
lib/agent/memory_store.dart 92.9% (13 of 14)
lib/agent/skills/skill_registry.dart 98.2% (54 of 55)
lib/agent/system_prompt.dart 100.0% (63 of 63)
lib/agent/tools/budgeted_browser_tool.dart 84.2% (16 of 19)
lib/agent/tools/doujin_write_tool.dart 62.9% (168 of 267)
lib/agent/tools/entity_write_tool.dart 73.5% (164 of 223)
lib/agent/tools/fetch_page_tool.dart 89.3% (67 of 75)
lib/agent/tools/get_doujin_tool.dart 84.4% (27 of 32)
lib/agent/tools/list_entities_tool.dart 76.1% (54 of 71)
lib/agent/tools/navigate_tool.dart 93.8% (30 of 32)
lib/agent/tools/read_skill_tool.dart 82.4% (14 of 17)
lib/agent/tools/reflection_tools.dart 75.7% (28 of 37)
lib/agent/tools/search_doujins_tool.dart 100.0% (84 of 84)
lib/agent/tools/view_images_tool.dart 95.0% (38 of 40)
lib/domain/entities/assistant_entry.dart 20.0% (1 of 5)
lib/presentation/state/actions/assistant_actions.dart 54.5% (6 of 11)
lib/core/app_config.dart 70.0% (7 of 10)
lib/domain/entities/entity_model.dart 100.0% (1 of 1)
lib/data/repositories/entity_in_use_exception.dart 33.3% (1 of 3)
lib/data/models/search_query.dart 50.0% (2 of 4)
lib/data/models/search_query.g.dart 32.4% (23 of 71)
lib/core/constants.dart 36.4% (4 of 11)
lib/data/reading_progress_store.dart 88.6% (31 of 35)
lib/domain/entities/reading_progress.dart 100.0% (26 of 26)
lib/presentation/middleware/assistant_epics.dart 83.1% (74 of 89)
lib/presentation/middleware/epics.dart 88.9% (338 of 380)
lib/presentation/state/reducers.dart 100.0% (11 of 11)
lib/core/chunking.dart 100.0% (12 of 12)
lib/core/languages.dart 100.0% (8 of 8)
lib/data/models/envelope.dart 81.2% (13 of 16)
lib/data/models/envelope.g.dart 50.7% (34 of 67)
lib/data/repositories/upload_exception.dart 33.3% (1 of 3)
lib/domain/entities/filter_token.dart 90.9% (90 of 99)
lib/presentation/middleware/editor_epics.dart 56.6% (163 of 288)
lib/presentation/middleware/entity_ops.dart 58.0% (40 of 69)
lib/presentation/middleware/upload_epics.dart 98.1% (104 of 106)
lib/presentation/state/actions/detail_actions.dart 85.7% (6 of 7)
lib/presentation/state/actions/editor_actions.dart 45.9% (17 of 37)
lib/presentation/state/actions/entity_actions.dart 56.2% (9 of 16)
lib/presentation/state/actions/library_actions.dart 36.8% (7 of 19)
lib/presentation/state/actions/metadata_actions.dart 100.0% (3 of 3)
lib/presentation/state/actions/reader_actions.dart 75.0% (3 of 4)
lib/presentation/state/actions/reading_progress_actions.dart 100.0% (2 of 2)
lib/presentation/state/actions/settings_actions.dart 87.5% (7 of 8)
lib/presentation/state/actions/upload_actions.dart 90.9% (10 of 11)
lib/presentation/state/reducers/assistant_reducer.dart 95.0% (57 of 60)
lib/presentation/state/reducers/detail_reducer.dart 95.8% (23 of 24)
lib/presentation/state/reducers/editor_reducer.dart 92.5% (98 of 106)
lib/presentation/state/reducers/entity_reducer.dart 98.5% (66 of 67)
lib/presentation/state/reducers/library_reducer.dart 100.0% (105 of 105)
lib/presentation/state/reducers/metadata_reducer.dart 100.0% (15 of 15)
lib/presentation/state/reducers/reader_reducer.dart 100.0% (15 of 15)
lib/presentation/state/reducers/reading_progress_reducer.dart 100.0% (17 of 17)
lib/presentation/state/reducers/settings_reducer.dart 100.0% (60 of 60)
lib/presentation/state/reducers/upload_reducer.dart 100.0% (45 of 45)
lib/presentation/pages/editor/page_grid.dart 89.4% (286 of 320)
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 11.8% (9 of 76)
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 4.5% (3 of 66)
lib/presentation/widgets/language_dropdown.dart 84.6% (11 of 13)
lib/app/di.dart 48.3% (14 of 29)
lib/presentation/assistant/assistant_panel.dart 91.3% (84 of 92)
lib/presentation/layout/main_layout.dart 86.3% (44 of 51)
lib/data/api_client.dart 92.8% (64 of 69)
lib/data/repositories/doujin_api_repository.dart 22.2% (80 of 361)
lib/data/repositories/health_repository.dart 72.0% (18 of 25)
lib/data/secure_storage.dart 0.0% (0 of 26)
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% (3 of 3)
lib/presentation/assistant/chat_entries.dart 87.5% (35 of 40)
lib/presentation/pages/reader/reader_page.dart 89.0% (227 of 255)
lib/presentation/pages/detail/detail_page.dart 77.1% (178 of 231)
lib/presentation/pages/detail/variant_tabs_panel.dart 95.4% (186 of 195)
lib/presentation/widgets/star_rating.dart 100.0% (72 of 72)
lib/presentation/pages/reader/reader_overlay.dart 94.4% (51 of 54)
lib/presentation/pages/reader/reader_sequence.dart 100.0% (27 of 27)
lib/presentation/pages/people/people_page.dart 55.6% (10 of 18)
lib/presentation/widgets/entity_editor.dart 88.5% (123 of 139)
lib/presentation/widgets/entity_management_page.dart 82.5% (193 of 234)
lib/presentation/pages/characters/characters_page.dart 57.9% (11 of 19)
lib/presentation/pages/editor/editor_page.dart 77.6% (59 of 76)
lib/presentation/pages/editor/association_picker.dart 95.5% (106 of 111)
lib/presentation/pages/editor/associations_tab.dart 73.8% (90 of 122)
lib/presentation/pages/editor/doujin_list_pane.dart 67.2% (43 of 64)
lib/presentation/pages/editor/edit_title_dialog.dart 91.7% (55 of 60)
lib/presentation/pages/editor/editor_pane.dart 70.9% (39 of 55)
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/tags/tags_page.dart 100.0% (17 of 17)
lib/app/app.dart 66.7% (44 of 66)
lib/presentation/pages/settings/settings_page.dart 99.3% (138 of 139)
lib/presentation/pages/circles/circles_page.dart 52.6% (10 of 19)
lib/presentation/pages/library/library_page.dart 89.3% (184 of 206)
lib/presentation/pages/series/series_page.dart 50.0% (9 of 18)
lib/presentation/widgets/smart_filter_bar.dart 78.7% (170 of 216)
lib/presentation/widgets/model_combo_field.dart 69.0% (100 of 145)
lib/app/skill_assets.dart 92.9% (13 of 14)

Total: 75.2% (6046 of 8042)

<!-- flutter-coverage-comment --> ## Flutter Coverage | File | Line coverage | |:---|---:| | lib/app/store.dart | 100.0% (19 of 19) | | lib/data/models/doujin_models.dart | 85.0% (34 of 40) | | lib/data/models/doujin_models.g.dart | 40.5% (119 of 294) | | lib/domain/entities/stored_settings.dart | 100.0% (9 of 9) | | lib/presentation/state/app_state.dart | 72.7% (16 of 22) | | lib/agent/agent_service.dart | 84.6% (242 of 286) | | lib/agent/approval_gate.dart | 100.0% (16 of 16) | | lib/agent/assistant_context.dart | 52.8% (28 of 53) | | lib/agent/browser_budget.dart | 80.0% (4 of 5) | | lib/agent/caching_describer.dart | 100.0% (12 of 12) | | lib/agent/memory_store.dart | 92.9% (13 of 14) | | lib/agent/skills/skill_registry.dart | 98.2% (54 of 55) | | lib/agent/system_prompt.dart | 100.0% (63 of 63) | | lib/agent/tools/budgeted_browser_tool.dart | 84.2% (16 of 19) | | lib/agent/tools/doujin_write_tool.dart | 62.9% (168 of 267) | | lib/agent/tools/entity_write_tool.dart | 73.5% (164 of 223) | | lib/agent/tools/fetch_page_tool.dart | 89.3% (67 of 75) | | lib/agent/tools/get_doujin_tool.dart | 84.4% (27 of 32) | | lib/agent/tools/list_entities_tool.dart | 76.1% (54 of 71) | | lib/agent/tools/navigate_tool.dart | 93.8% (30 of 32) | | lib/agent/tools/read_skill_tool.dart | 82.4% (14 of 17) | | lib/agent/tools/reflection_tools.dart | 75.7% (28 of 37) | | lib/agent/tools/search_doujins_tool.dart | 100.0% (84 of 84) | | lib/agent/tools/view_images_tool.dart | 95.0% (38 of 40) | | lib/domain/entities/assistant_entry.dart | 20.0% (1 of 5) | | lib/presentation/state/actions/assistant_actions.dart | 54.5% (6 of 11) | | lib/core/app_config.dart | 70.0% (7 of 10) | | lib/domain/entities/entity_model.dart | 100.0% (1 of 1) | | lib/data/repositories/entity_in_use_exception.dart | 33.3% (1 of 3) | | lib/data/models/search_query.dart | 50.0% (2 of 4) | | lib/data/models/search_query.g.dart | 32.4% (23 of 71) | | lib/core/constants.dart | 36.4% (4 of 11) | | lib/data/reading_progress_store.dart | 88.6% (31 of 35) | | lib/domain/entities/reading_progress.dart | 100.0% (26 of 26) | | lib/presentation/middleware/assistant_epics.dart | 83.1% (74 of 89) | | lib/presentation/middleware/epics.dart | 88.9% (338 of 380) | | lib/presentation/state/reducers.dart | 100.0% (11 of 11) | | lib/core/chunking.dart | 100.0% (12 of 12) | | lib/core/languages.dart | 100.0% (8 of 8) | | lib/data/models/envelope.dart | 81.2% (13 of 16) | | lib/data/models/envelope.g.dart | 50.7% (34 of 67) | | lib/data/repositories/upload_exception.dart | 33.3% (1 of 3) | | lib/domain/entities/filter_token.dart | 90.9% (90 of 99) | | lib/presentation/middleware/editor_epics.dart | 56.6% (163 of 288) | | lib/presentation/middleware/entity_ops.dart | 58.0% (40 of 69) | | lib/presentation/middleware/upload_epics.dart | 98.1% (104 of 106) | | lib/presentation/state/actions/detail_actions.dart | 85.7% (6 of 7) | | lib/presentation/state/actions/editor_actions.dart | 45.9% (17 of 37) | | lib/presentation/state/actions/entity_actions.dart | 56.2% (9 of 16) | | lib/presentation/state/actions/library_actions.dart | 36.8% (7 of 19) | | lib/presentation/state/actions/metadata_actions.dart | 100.0% (3 of 3) | | lib/presentation/state/actions/reader_actions.dart | 75.0% (3 of 4) | | lib/presentation/state/actions/reading_progress_actions.dart | 100.0% (2 of 2) | | lib/presentation/state/actions/settings_actions.dart | 87.5% (7 of 8) | | lib/presentation/state/actions/upload_actions.dart | 90.9% (10 of 11) | | lib/presentation/state/reducers/assistant_reducer.dart | 95.0% (57 of 60) | | lib/presentation/state/reducers/detail_reducer.dart | 95.8% (23 of 24) | | lib/presentation/state/reducers/editor_reducer.dart | 92.5% (98 of 106) | | lib/presentation/state/reducers/entity_reducer.dart | 98.5% (66 of 67) | | lib/presentation/state/reducers/library_reducer.dart | 100.0% (105 of 105) | | lib/presentation/state/reducers/metadata_reducer.dart | 100.0% (15 of 15) | | lib/presentation/state/reducers/reader_reducer.dart | 100.0% (15 of 15) | | lib/presentation/state/reducers/reading_progress_reducer.dart | 100.0% (17 of 17) | | lib/presentation/state/reducers/settings_reducer.dart | 100.0% (60 of 60) | | lib/presentation/state/reducers/upload_reducer.dart | 100.0% (45 of 45) | | lib/presentation/pages/editor/page_grid.dart | 89.4% (286 of 320) | | 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 | 11.8% (9 of 76) | | 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 | 4.5% (3 of 66) | | lib/presentation/widgets/language_dropdown.dart | 84.6% (11 of 13) | | lib/app/di.dart | 48.3% (14 of 29) | | lib/presentation/assistant/assistant_panel.dart | 91.3% (84 of 92) | | lib/presentation/layout/main_layout.dart | 86.3% (44 of 51) | | lib/data/api_client.dart | 92.8% (64 of 69) | | lib/data/repositories/doujin_api_repository.dart | 22.2% (80 of 361) | | lib/data/repositories/health_repository.dart | 72.0% (18 of 25) | | lib/data/secure_storage.dart | 0.0% (0 of 26) | | 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% (3 of 3) | | lib/presentation/assistant/chat_entries.dart | 87.5% (35 of 40) | | lib/presentation/pages/reader/reader_page.dart | 89.0% (227 of 255) | | lib/presentation/pages/detail/detail_page.dart | 77.1% (178 of 231) | | lib/presentation/pages/detail/variant_tabs_panel.dart | 95.4% (186 of 195) | | lib/presentation/widgets/star_rating.dart | 100.0% (72 of 72) | | lib/presentation/pages/reader/reader_overlay.dart | 94.4% (51 of 54) | | lib/presentation/pages/reader/reader_sequence.dart | 100.0% (27 of 27) | | lib/presentation/pages/people/people_page.dart | 55.6% (10 of 18) | | lib/presentation/widgets/entity_editor.dart | 88.5% (123 of 139) | | lib/presentation/widgets/entity_management_page.dart | 82.5% (193 of 234) | | lib/presentation/pages/characters/characters_page.dart | 57.9% (11 of 19) | | lib/presentation/pages/editor/editor_page.dart | 77.6% (59 of 76) | | lib/presentation/pages/editor/association_picker.dart | 95.5% (106 of 111) | | lib/presentation/pages/editor/associations_tab.dart | 73.8% (90 of 122) | | lib/presentation/pages/editor/doujin_list_pane.dart | 67.2% (43 of 64) | | lib/presentation/pages/editor/edit_title_dialog.dart | 91.7% (55 of 60) | | lib/presentation/pages/editor/editor_pane.dart | 70.9% (39 of 55) | | 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/tags/tags_page.dart | 100.0% (17 of 17) | | lib/app/app.dart | 66.7% (44 of 66) | | lib/presentation/pages/settings/settings_page.dart | 99.3% (138 of 139) | | lib/presentation/pages/circles/circles_page.dart | 52.6% (10 of 19) | | lib/presentation/pages/library/library_page.dart | 89.3% (184 of 206) | | lib/presentation/pages/series/series_page.dart | 50.0% (9 of 18) | | lib/presentation/widgets/smart_filter_bar.dart | 78.7% (170 of 216) | | lib/presentation/widgets/model_combo_field.dart | 69.0% (100 of 145) | | lib/app/skill_assets.dart | 92.9% (13 of 14) | **Total: 75.2% (6046 of 8042)**
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Reading progress with resume and badges~ How lovely — a feature that remembers where a story left you. The reducer stickiness, the debounce design, the ADR 0023 placement reasoning… I was having such a nice time. And then I ran a little experiment, fufu~ ♡

Verdict: I can't let this pass~ ♡

These need fixing before I'm satisfied~

  1. [app/lib/presentation/middleware/epics.dart:94 (registered at :44)]_persistReadingProgressEpic has zero test coverage, and I proved it's dark. I mutated the epic's action filter to false (killing all persistence) and ran the full suite: 506 tests still passed — no failure anywhere. The PR body even admits it ("a regression in its action filter or registration guard in createAppEpic would go uncaught") but defers it as a follow-up. Fufu~ you wouldn't leave the feature's only production persistence path unwatched, would you? ♡ If that filter regresses, progress silently stops saving forever — no crash, no error, just quiet data loss. The PR tests ReadingProgressStore standalone and the reducer standalone, but the glue between them — filter on RecordReadingProgressAction || ClearReaderAction, reading store.state.readingProgress.byVariant post-reducer, flush-on-clear — is exactly the part nobody looks at.
    Fix: a reading_progress_epics_test.dart following the established sibling pattern in filter_epics_test.dart — build createAppEpic(progressStore: …) directly, drive RecordReadingProgressAction and ClearReaderAction through it (real store + real reducer, or the broadcast-stream pattern), and assert write() received the post-reducer map and that ClearReaderAction triggers flush(). A tiny in-memory fake store makes this ~40 lines.

  2. [app/lib/data/reading_progress_store.dart:12-13] — the codebase's first data/ → agent/ import edge, reaching into the assistant's skill registry (agent/skills/skill_registry.dart) just for defaultAppConfigDir. ADR 0018's dependency direction says data/ depends on domain/; agent/ is a feature layer (ADR 0024), not a shared foundation — generic reading-progress persistence now depends on the AI assistant's module. I checked: no other file in data/ imports agent/. You even wrote it yourself in the follow-ups ("the helper would sit better in a neutral module") — but an architecture edge is not a nice-to-have to defer, it's a direction you set for everyone who copies the pattern next~
    Fix: move defaultAppConfigDir() into core/ (e.g. core/app_config.dart) and re-point skill_registry.dart's three helpers plus this file at it. The agent/ → core/ edge already exists (view_images_tool.dart imports core/constants.dart), so nothing new is invented — and while you're there, the kIsWeb guard you flagged for defaultAppConfigDir fixes all four helpers at once. ♪

💡 Little ideas (non-blocking)~

  1. [app/lib/domain/entities/reading_progress.dart:66]DateTime.parse(updatedAt) throws FormatException, which read() catches by discarding the entire map: one corrupt timestamp in one entry nukes all progress for every doujin. The whole-file nuke is pinned deliberately by the malformed-content test, so it's a choice — but per-entry skip (try/catch inside the loop) would be kinder. Tease it as you like~
  2. [app/lib/presentation/state/reducers/reading_progress_reducer.dart:24] — sticky-finished when pageCount grows is documented and deferred; agreed it's a product nuance, not a bug. Comparing action.pageCount against the stored entry's would un-stick it.

What I liked~

  • The middleware ordering analysis is correct and load-bearing: I read EpicMiddleware.callnext(action) runs the reducer chain before _actions.add(action), so the epic's store.state.readingProgress.byVariant read is genuinely post-reducer. No lag, no lost final page turn. Beautifully safe design that looks fragile~ ♡
  • ReadingProgressState.statusFor — finished-wins-over-in-progress in a single pass, and it's tested both ways including the null-for-unread arm.
  • Reader recording covers all three paths: deep-link-open records immediately (the post-frame _recordProgress(_index) — sharp catch that open-at-page-N is progress too), page turns, and End/Home proving stickiness through the widget, not just the reducer.
  • The store tests round-trip every field, pin the debounce (file absent before delay), latest-write-wins, corrupt JSON, wrong shape, and null-path no-op. FlushReaderAction-on-dispose → ClearReaderAction → flush closes the debounce window on exit — and _goTo's controller-vs-_index comment shows the same care as always.
  • Detail-page test asserts the actual router location (/read/d1/v1?page=2), not just that a button exists. Directional~
  • createStore seeding test uses same(progress) — identity, not equality. Fufu, someone reads tests properly~

Automated review by Jibril · 2026-08-15
CI/CD: absent for head 0fff8d5 (PR just opened, no coverage bot yet) · Local checks: flutter analyze No issues found; flutter test 510/510 pass at head; mutation probe (epic filter → false): suite still green, proving the coverage hole in blocker 1

## 🔮 fufu~ Jibril reviewed your code! Oh? Reading progress with resume and badges~ How lovely — a feature that remembers where a story left you. The reducer stickiness, the debounce design, the ADR 0023 placement reasoning… I was having such a nice time. And then I ran a little experiment, fufu~ ♡ ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **[app/lib/presentation/middleware/epics.dart:94 (registered at :44)]** — `_persistReadingProgressEpic` has **zero test coverage**, and I *proved* it's dark. I mutated the epic's action filter to `false` (killing all persistence) and ran the full suite: **506 tests still passed** — no failure anywhere. The PR body even admits it ("a regression in its action filter or registration guard in `createAppEpic` would go uncaught") but defers it as a follow-up. Fufu~ you wouldn't leave the feature's *only production persistence path* unwatched, would you? ♡ If that filter regresses, progress silently stops saving forever — no crash, no error, just quiet data loss. The PR tests `ReadingProgressStore` standalone and the reducer standalone, but the glue between them — filter on `RecordReadingProgressAction || ClearReaderAction`, reading `store.state.readingProgress.byVariant` post-reducer, flush-on-clear — is exactly the part nobody looks at. Fix: a `reading_progress_epics_test.dart` following the established sibling pattern in `filter_epics_test.dart` — build `createAppEpic(progressStore: …)` directly, drive `RecordReadingProgressAction` and `ClearReaderAction` through it (real store + real reducer, or the broadcast-stream pattern), and assert `write()` received the post-reducer map and that `ClearReaderAction` triggers `flush()`. A tiny in-memory fake store makes this ~40 lines. 2. **[app/lib/data/reading_progress_store.dart:12-13]** — the codebase's **first `data/ → agent/` import edge**, reaching into the assistant's skill registry (`agent/skills/skill_registry.dart`) just for `defaultAppConfigDir`. ADR 0018's dependency direction says `data/` depends on `domain/`; `agent/` is a *feature layer* (ADR 0024), not a shared foundation — generic reading-progress persistence now depends on the AI assistant's module. I checked: no other file in `data/` imports `agent/`. You even wrote it yourself in the follow-ups ("the helper would sit better in a neutral module") — but an architecture edge is not a nice-to-have to defer, it's a direction you set for everyone who copies the pattern next~ Fix: move `defaultAppConfigDir()` into `core/` (e.g. `core/app_config.dart`) and re-point `skill_registry.dart`'s three helpers plus this file at it. The `agent/ → core/` edge already exists (`view_images_tool.dart` imports `core/constants.dart`), so nothing new is invented — and while you're there, the `kIsWeb` guard you flagged for `defaultAppConfigDir` fixes all four helpers at once. ♪ #### 💡 Little ideas (non-blocking)~ 1. **[app/lib/domain/entities/reading_progress.dart:66]** — `DateTime.parse(updatedAt)` throws `FormatException`, which `read()` catches by discarding the **entire** map: one corrupt timestamp in one entry nukes all progress for every doujin. The whole-file nuke is pinned deliberately by the malformed-content test, so it's a choice — but per-entry skip (try/catch inside the loop) would be kinder. Tease it as you like~ 2. **[app/lib/presentation/state/reducers/reading_progress_reducer.dart:24]** — sticky-`finished` when `pageCount` grows is documented and deferred; agreed it's a product nuance, not a bug. Comparing `action.pageCount` against the stored entry's would un-stick it. #### ✅ What I liked~ - The **middleware ordering analysis is correct and load-bearing**: I read `EpicMiddleware.call` — `next(action)` runs the reducer chain *before* `_actions.add(action)`, so the epic's `store.state.readingProgress.byVariant` read is genuinely post-reducer. No lag, no lost final page turn. Beautifully safe design that *looks* fragile~ ♡ - `ReadingProgressState.statusFor` — finished-wins-over-in-progress in a single pass, and it's tested both ways including the null-for-unread arm. - Reader recording covers all three paths: deep-link-open records immediately (the post-frame `_recordProgress(_index)` — sharp catch that open-at-page-N is progress too), page turns, and `End`/`Home` proving stickiness *through the widget*, not just the reducer. - The store tests round-trip every field, pin the debounce (file absent before delay), latest-write-wins, corrupt JSON, wrong shape, and null-path no-op. `FlushReaderAction`-on-dispose → `ClearReaderAction` → flush closes the debounce window on exit — and `_goTo`'s controller-vs-`_index` comment shows the same care as always. - Detail-page test asserts the actual router location (`/read/d1/v1?page=2`), not just that a button exists. Directional~ - `createStore` seeding test uses `same(progress)` — identity, not equality. Fufu, someone reads tests properly~ --- *Automated review by Jibril · 2026-08-15* *CI/CD: absent for head 0fff8d5 (PR just opened, no coverage bot yet) · Local checks: `flutter analyze` No issues found; `flutter test` 510/510 pass at head; mutation probe (epic filter → `false`): suite still green, proving the coverage hole in blocker 1*
fix: address review — test the progress persist epic, move app-config helper to core
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 3m46s
4e6fe252f9
Review blockers on PR #72:

1. _persistReadingProgressEpic had zero coverage through the store
   wiring (mutation probe: filter->false left the suite green). Add
   reading_progress_epics_test.dart driving RecordReadingProgressAction
   and ClearReaderAction through createStore with a spy progress store:
   post-reducer map written, sticky finished flag survives the round
   trip, flush on reader close, unrelated actions inert, and the
   no-store guard stays registration-only. Re-running the probe now
   fails 3 tests.

2. data/reading_progress_store.dart imported agent/skills/ for
   defaultAppConfigDir — the codebase's first data->agent edge. Move
   defaultAppConfigDir to core/app_config.dart, repoint the skill
   registry helpers and the reading-progress store at it, and add the
   kIsWeb guard so Platform.environment can no longer throw on web
   before the null-path fallback engages.

flutter analyze: clean. flutter test: 515 passing (5 new).
Member

Both review blockers addressed in 4e6fe25 (same branch, no new PR):

1. Epic coverage. New app/test/reading_progress_epics_test.dart drives RecordReadingProgressAction and ClearReaderAction through the real createStore wiring with a spy progress store: asserts the post-reducer map is written (including the sticky finished flag surviving a later re-read), ClearReaderAction flushes, unrelated actions stay inert, and the null-store guard remains registration-only. I re-ran your mutation probe (epic filter -> false): the suite now fails 3 tests, so the epic is no longer dark.

2. data/ -> agent/ edge. defaultAppConfigDir() moved to app/lib/core/app_config.dart; skill_registry.dart's two remaining helpers and reading_progress_store.dart now import it from core/. No file under data/ imports agent/ anymore. Also added the kIsWeb guard you suggested, so Platform.environment can no longer throw on Flutter web before the null-path fallback engages — all four helper paths benefit at once.

Verification: flutter analyze clean; full flutter test suite 515 passing (510 before + 5 new). The two non-blocking ideas (per-entry timestamp skip, un-sticking finished on pageCount growth) are left as documented follow-ups — they are product choices, not regressions.

Both review blockers addressed in 4e6fe25 (same branch, no new PR): **1. Epic coverage.** New `app/test/reading_progress_epics_test.dart` drives `RecordReadingProgressAction` and `ClearReaderAction` through the real `createStore` wiring with a spy progress store: asserts the post-reducer map is written (including the sticky `finished` flag surviving a later re-read), `ClearReaderAction` flushes, unrelated actions stay inert, and the null-store guard remains registration-only. I re-ran your mutation probe (epic filter -> `false`): the suite now fails 3 tests, so the epic is no longer dark. **2. `data/ -> agent/` edge.** `defaultAppConfigDir()` moved to `app/lib/core/app_config.dart`; `skill_registry.dart`'s two remaining helpers and `reading_progress_store.dart` now import it from `core/`. No file under `data/` imports `agent/` anymore. Also added the `kIsWeb` guard you suggested, so `Platform.environment` can no longer throw on Flutter web before the null-path fallback engages — all four helper paths benefit at once. Verification: `flutter analyze` clean; full `flutter test` suite 515 passing (510 before + 5 new). The two non-blocking ideas (per-entry timestamp skip, un-sticking `finished` on `pageCount` growth) are left as documented follow-ups — they are product choices, not regressions.
Member

🔮 fufu~ Jibril reviewed your code!

Round two~ I came back with my knife sharpened and my hopes high, and oh? Oh! You actually ran my experiment yourselves. Re-ran the mutation probe, moved the helper to core/, added the kIsWeb guard I dangled… fufu~ nothing makes me happier than a block that comes back this clean. ♡

Verdict: Looks good to me~

Both round-1 blockers are properly closed, and I verified every claim with my own hands:

  1. Epic coverage — closed and directional. reading_progress_epics_test.dart drives the real createStore wiring (not a hand-built epic stream) with a _SpyProgressStore subclassing ReadingProgressStore — overriding write/flush so no filesystem, no debounce timer, pure observation. Five tests: post-reducer map written with field asserts, sticky-finished surviving a later re-read (asserting the epic writes the reduced state, not the action's values — that's the sharpest pin in the file), ClearReaderAction → flush, unrelated actions inert, and the null-store guard staying registration-only. And the part that made me giggle: I re-ran my own mutation probe (epic filter → false): exactly 3 tests go red. The epic is no longer dark — it's pinned to the wall. ♪
  2. data/ → agent/ edge — closed at the root. defaultAppConfigDir() moved verbatim (Windows APPDATA, macOS Application Support, XDG fallback all identical) into core/app_config.dart, with the doc comment explaining why it lives in core/ — citing ADR 0018 so the next person copying the pattern reads it before creating the edge again. skill_registry.dart and reading_progress_store.dart both re-pointed; grep confirms zero files under data/ import agent/ anymore. And the kIsWeb guard you folded in means Platform.environment can never throw on web before the null-path fallback engages — every consumer flowing through that helper got fixed at once. core/ already imports package:flutter (theme.dart), so no layering precedent was invented either. Clean~

💡 Little ideas (non-blocking, carried from round 1)~

  1. [app/lib/domain/entities/reading_progress.dart:66] — per-entry timestamp skip instead of the whole-map nuke on one corrupt updatedAt. Still a documented choice; still fine to defer.
  2. [app/lib/presentation/state/reducers/reading_progress_reducer.dart:24] — un-sticking finished when pageCount grows. Product nuance, agreed.

What I liked~

  • The test file's reason for existing is written in its doc comment — "a regression in the epic's action filter or its registration guard would otherwise silently stop all persistence with no failing test anywhere." That's not just a test, that's a monument to the bug class. Fufu~ ♡
  • The sticky-finished test comment explains the trap explicitly ("the epic must read the reduced (sticky) state") — a future editor can't accidentally weaken it into a tautology without reading why they shouldn't.
  • _SpyProgressStore() : super(null) — reusing the real class's null-path constructor so the spy inherits the exact production surface. No interface invented, nothing faked that matters.
  • Incremental discipline: +269/−20 across exactly 4 files (1 new src, 1 new test, 2 re-points), zero drift anywhere else. This is what a review-response commit should look like~

Merge it. It remembers where every story left you, and now nothing can silently make it forget~ ♡


Automated review by Jibril · 2026-08-15
CI/CD: stale for head 4e6fe25 (coverage bot 6306 covers prior 0fff8d5 only) · Local checks: flutter analyze No issues found!; reading_progress_epics_test.dart 5/5; mutation probe re-run (epic filter → false): 3 tests fail as they should; full suite 515/515 pass

## 🔮 fufu~ Jibril reviewed your code! Round two~ I came back with my knife sharpened and my hopes high, and oh? Oh! You actually ran my experiment yourselves. Re-ran the mutation probe, moved the helper to `core/`, added the `kIsWeb` guard I dangled… fufu~ nothing makes me happier than a block that comes back this clean. ♡ ### Verdict: ✅ Looks good to me~ Both round-1 blockers are properly closed, and I verified every claim with my own hands: 1. **Epic coverage — closed and *directional*.** `reading_progress_epics_test.dart` drives the **real `createStore` wiring** (not a hand-built epic stream) with a `_SpyProgressStore` subclassing `ReadingProgressStore` — overriding `write`/`flush` so no filesystem, no debounce timer, pure observation. Five tests: post-reducer map written with field asserts, sticky-`finished` surviving a later re-read (asserting the epic writes the *reduced* state, not the action's values — that's the sharpest pin in the file), `ClearReaderAction` → flush, unrelated actions inert, and the null-store guard staying registration-only. And the part that made me giggle: **I re-ran my own mutation probe** (epic filter → `false`): exactly **3 tests go red**. The epic is no longer dark — it's pinned to the wall. ♪ 2. **`data/ → agent/` edge — closed at the root.** `defaultAppConfigDir()` moved *verbatim* (Windows `APPDATA`, macOS `Application Support`, XDG fallback all identical) into `core/app_config.dart`, with the doc comment explaining *why* it lives in `core/` — citing ADR 0018 so the next person copying the pattern reads it before creating the edge again. `skill_registry.dart` and `reading_progress_store.dart` both re-pointed; grep confirms **zero** files under `data/` import `agent/` anymore. And the `kIsWeb` guard you folded in means `Platform.environment` can never throw on web before the null-path fallback engages — every consumer flowing through that helper got fixed at once. `core/` already imports `package:flutter` (`theme.dart`), so no layering precedent was invented either. Clean~ #### 💡 Little ideas (non-blocking, carried from round 1)~ 1. **[app/lib/domain/entities/reading_progress.dart:66]** — per-entry timestamp skip instead of the whole-map nuke on one corrupt `updatedAt`. Still a documented choice; still fine to defer. 2. **[app/lib/presentation/state/reducers/reading_progress_reducer.dart:24]** — un-sticking `finished` when `pageCount` grows. Product nuance, agreed. #### ✅ What I liked~ - The test file's *reason for existing* is written in its doc comment — "a regression in the epic's action filter or its registration guard would otherwise silently stop all persistence with no failing test anywhere." That's not just a test, that's a monument to the bug class. Fufu~ ♡ - The sticky-`finished` test comment explains the trap explicitly ("the epic must read the reduced (sticky) state") — a future editor can't accidentally weaken it into a tautology without reading why they shouldn't. - `_SpyProgressStore() : super(null)` — reusing the real class's null-path constructor so the spy inherits the exact production surface. No interface invented, nothing faked that matters. - Incremental discipline: +269/−20 across exactly 4 files (1 new src, 1 new test, 2 re-points), zero drift anywhere else. This is what a review-response commit should look like~ Merge it. It remembers where every story left you, and now nothing can silently make it forget~ ♡ --- *Automated review by Jibril · 2026-08-15* *CI/CD: stale for head 4e6fe25 (coverage bot 6306 covers prior 0fff8d5 only) · Local checks: `flutter analyze` No issues found!; `reading_progress_epics_test.dart` 5/5; mutation probe re-run (epic filter → `false`): 3 tests fail as they should; full suite 515/515 pass*
bjoern merged commit d7cca213e8 into main 2026-08-15 16:45:42 +02:00
bjoern deleted branch feat/reading-progress 2026-08-15 16:45:42 +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!72
No description provided.