Skip to content

Post-2.1.5 hardening: search UX, RTL completion, stats integrity & background TTS reconciliation - #23

Merged
bizzkoot merged 16 commits into
masterfrom
dev
Sep 5, 2026
Merged

bizzkoot merged 16 commits into
masterfrom
dev

Conversation

@bizzkoot

@bizzkoot bizzkoot commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Repository: bizzkoot/lnreader
Source Branch: dev (26348b2c1)
Target Branch: master (be0106d47, v2.1.5)
Commits Ahead: 16 commits · Files Changed: 53 files (+2,814 / −297 lines)
Verification Status:tsc --noEmit 0 errors · ESLint clean · Prettier clean · 1,688 Jest tests passing (119 suites) · 14 native unit tests passing · release build succeeds · zero console.log/TODO leftovers in diff


1. Executive Summary

This PR stacks 16 hardening and polish commits on top of v2.1.5 (be0106d47). No new top-level features — it takes the features already merged into master (in-chapter search, RTL support, reading-time tracking, stats overhaul, scheduled updates) and fixes the defects found by follow-up adversarial audits plus one real-world data-loss bug:

  • In-chapter search goes from functional to polished: configurable close-return behavior with position preservation, animated return-anchor banner with hardware-back dismissal, DOM-preserving highlight engine (no more cloneContents transplant bugs), and virtualized match navigation capped at 200 rendered marks.
  • RTL goes from partial to complete: WebView paging/gesture mirroring, seekbar inversion, navigation icon mirroring, forced layout on locale switch with restart prompt, and a scheduler fix so category-only updates no longer postpone global library updates.
  • Reading-time stats become trustworthy: 12h duration caps, NTP-skew guards, Doze-drift capping, 60s crash-safe checkpoints, seconds resolution, focus refetch, velocity computed from active chapters — and a native monotonic speaking clock so hours of background TTS listening are no longer recorded as ~zero.
  • Slider gestures stop fighting the UI: deferred responder claim unblocks vertical scroll, a global drag flag interlocks bottom-sheet TabView swipes, tap-to-seek is preserved, and progress saves can't be clobbered to 0% before initial scroll completes.

2. What Changed (by Theme)

2.1. In-Chapter Search UX & Engine Fixes (4 commits)

Commit Change
74404ed43 feat Configurable search-close return behavior (SearchReturnBehaviorModal + NavigationTab setting + MMKV persistence), position preservation, animated return banner, improved match counters/animation/accessibility, new en strings
fbf73b4e3 fix Pass statusBarHeight from NovelContext instead of StatusBar.currentHeight for consistent searchbar layout; explicit return-timer cleanup; searchbar elevation
dd53eeb3e fix Hardware back press dismisses the return banner first (priority-ordered handler); missing clearReturnTimer dep added
a11e42bf1 fix Search engine rewrite (audit AUD-SRCH-01…06): bounded DOM tree-walk replaces cloneContents bottleneck; per-text-node wrapping (no cross-tag transplant, no inline-node deletion); lazy rendering beyond MAX_RENDERED_MATCHES (200) with wrap-consistent focus; cyclic stepper in ReaderSearchbar; return-to-anchor TTS priority restricted to actively-reading state; paragraph-indexed saves allowed during search

Files: ReaderScreen.tsx, ReaderSearchbar.tsx, WebViewReader.tsx, search.js, SearchReturnBehaviorModal.tsx (new), NavigationTab.tsx, useSettings.ts, en/strings.json

2.2. RTL Hardening & Completion (2 commits + part of §2.3)

Commit Change
111fa901a fix WebView RTL mirroring (AUD-RTL-01…03): mirror pageReader translateX, drag preview, tap zones and swipe thresholds under dir=rtl; invert continuous-chapter swipe next/prev mapping; invert seekbar ratio, anchor thumb via right:100% with right-fill progress, clamp 0–1, mirror off-screen transition position. Also fixes swipe this.post TypeError (closure vars, AUD-TIME-01) and blocks 0% progress save before initial scroll (AUD-PERS-01). LTR unchanged
a8895e63c fix Native nav-icon mirroring (I18nManager.isRTLarrow-right in SearchbarV2, ReaderAppbar, ReaderSearchbar); scheduler isolation — category-only updates no longer advance the global LAST_UPDATE_TIME timestamp
cf76883ae (part) Force RTL layout when an RTL locale is picked (LanguagePickerModal) with app-restart prompt

Files: core.js, index.js, pageReader.css, toolWrapper.css, SearchbarV2.tsx, ReaderAppbar.tsx, updates/index.ts, LanguagePickerModal.tsx

2.3. Stats & Time-Tracking Integrity (5 commits)

Commit Change
09a966a35 fix Tracking hardening (AUD-TIME-02…06, AUD-STAT-01…06): idle window excluded from inactivity flush; 12h duration caps + non-monotonic-negative drops (NTP/zone skew); 60s periodic checkpoints (30s min, SIGKILL/LMK-safe, no duplicate sessions); TTS inactive-poll capped at background-enter time (Doze-drift guard); chapter-change starter timer isolated from unrelated re-renders. Chart fixes: DistributionBar single-full-circle fallback (no 360° SVG collapse), velocity guard (cph > 50), Unicode-aware genre normalization (CJK/Cyrillic/accented preserved), memoized PluginsTab, reduce-based max (no spread-stack risk). Migration 007: idx_novel_inLibrary index for WHERE inLibrary=1 seeks
cf76883ae feat readingChapters aggregate (distinct chapter IDs from sessions of library novels); velocity computed from active chapters instead of lifetime chaptersRead, implausible-velocity heuristics dropped; manual sessions flush on inactive checkpoint ticks
317e6e1ce feat Sub-minute times render as seconds ("20s" not "<1m", new statsScreen.secs string); stats refetch on screen focus via useFocusEffect (no full-loader flash on silent refresh); end-to-end reading-time simulation tests (manual scroll, TTS incl. background, multi-checkpoint)
b30abcc12 fix Heartbeat-based background check replaces flawed background-enter truncation — lockscreen/Bluetooth-paused background TTS keeps its full time; periodic checkpoints and inactive flushes bounded to confirmed heartbeat window (Doze-drift cap); simulation + unit tests for background pause, screen-off starts, Doze suspension
26348b2c1 fix Background TTS reconciliation via native monotonic clock (the big one): TTSForegroundService gains a speaking-time clock (segment opens on utterance onStart, closes on drain/stop/pause; speak/speakBatch boundaries flush stale segments), bridged via TTSHighlightModule.getTtsPlaybackClock() as {spokenMs, speaking}. useTimeTracking snapshots the clock on app-background and tops up unattested time on foreground/unmount (wall-clock capped, 12h sanitized, <1s dropped). Post-foreground restart requires native speaking (no phantom sessions); fail-open when clock is null/unbound (identical to old behavior). Native TTSSpeakingClockTest 4/4 via Robolectric

Files: useTimeTracking.ts, StatsQueries.ts, TimeTab.tsx, StatsScreen.tsx, utils.ts, DistributionBar.tsx, GenreSection.tsx, PluginsTab.tsx, migration 007_*, TTSForegroundService.kt, TTSHighlightModule.kt, TTSSpeakingClockTest.kt (new), useTimeTracking.reconcile.test.ts (new), readingTime.simulation.test.ts

2.4. Slider Gestures & Progress Persistence (3 commits)

Commit Change
d16880e17 fix Gesture arbitration (AUD-GEST-01…03): slider defers responder claim to move (unblocks vertical scroll), termination allowed when not dragging, sliderDragState global flag interlocks bottom-sheet TabView swipe while dragging, 400ms fallback keeps dragValue until controlled value catches up (no permanent thumb freeze). Persistence (AUD-PERS-02/03): MMKV-first synchronous write in saveProgress, .catch on all updateChapterProgress/MarkRead paths (incl. useNovel, useChapter, useTTSController)
05c5e24f4 fix Lint follow-up for the above (single-line)
31e0f1d54 fix Tap-to-seek preserved: onTouchStart/onTouchEnd tap detection (panGestureRef + HORIZONTAL_CLAIM_THRESHOLD) fires onSlidingComplete for taps that never become pan gestures; flush-guard condition corrected to `

Files: Slider.tsx, sliderDragState.ts (new), ReaderBottomSheet.tsx, WebViewReader.tsx, useChapter.ts, useTTSController.ts, useNovel.ts, Slider.test.tsx, WebViewReader.backgroundFlush.test.tsx

2.5. Build & Docs (2 commits)

Commit Change
9e737c9f4 build CommonJS Metro bundle config in android/app/build.gradle (release packaging fix, 1 line)
b7bd96318 docs AGENTS.md / GEMINI.md context refresh (test counts, post-merge hardening status)

3. Branch Health Check (this review)

  • ✅ All 16 commits are clean, scoped, and conventional (feat/fix/build/docs); audit-tagged fixes reference their finding IDs (AUD-SRCH/GEST/TIME/STAT/PERS/RTL).
  • tsc --noEmit: 0 errors (re-verified at 26348b2c1 on 2026-09-05).
  • ✅ No console.log/debugger/TODO/FIXME in added lines (excluding __tests__).
  • en/strings.json gains: search return-behavior keys, statsScreen.secs, RTL restart notes. Secondary locales intentionally deferred to a downstream localization pass.
  • ✅ Two fixups (fbf73b4e3, dd53eeb3e, 05c5e24f4, 31e0f1d54) directly follow their parents — no squashing needed, history reads chronologically.
  • ⚠️ Heads-up for reviewers: 26348b2c1 touches native Kotlin + adds a Robolectric test — needs an Android release-build sanity check if CI doesn't cover it (commit message attests release build succeeds).

4. Verification & Test Coverage

Gate Status
Jest suites ✅ 119 passed / 119 total
Jest tests ✅ 1,688 passed (zero regressions; +4 reconcile, +simulation/focus-refetch/seconds coverage in this range)
Native unit tests TTSSpeakingClockTest 4/4 (Robolectric) + 10 existing media-session tests
TypeScript (tsc --noEmit) ✅ 0 errors
ESLint / Prettier ✅ clean
Android release build ✅ succeeds (per 26348b2c1 validation)

Manual QA smoke checklist

  • Search UX: open chapter → search → close via back/X → verify return-to-position banner appears, countdown works, back dismisses banner first, setting in Reader → Navigation changes close behavior.
  • Search engine: search a term with 200+ matches → verify no DOM corruption (inline styles intact), stepper wraps around, TTS paragraph indexing unaffected.
  • RTL: switch to Arabic → verify reader paging direction, tap zones, seekbar direction, back arrows mirror; category update doesn't reset the global update timer.
  • Stats: short (<1m) session shows seconds; leave app and return → stats refresh without flash; background TTS for N minutes → Time tab gains ≈N minutes.
  • Slider: drag chapter slider inside bottom sheet → no TabView swipe conflict; tap track → jumps; thumb never freezes.
  • Progress: background the app mid-chapter before first scroll settles → progress never resets to 0%.

5. Chronological Commit Log (16 Commits, be0106d47..26348b2c1)

74404ed43 feat(reader): enhance in-chapter search UX
fbf73b4e3 fix(reader): pass status bar height to searchbar
dd53eeb3e fix(reader): handle back press for return banner
a11e42bf1 fix(reader-search): preserve DOM on highlight, virtual navigation and anchor fixes
111fa901a fix(reader): harden RTL paging/gestures, touch binding and early progress flush
09a966a35 fix(stats): harden time tracking and statistics integrity
d16880e17 fix(reader): harden slider gestures and progress persistence
05c5e24f4 fix(reader): satisfy slider lint after gesture hardening
a8895e63c fix(ui): complete RTL navigation and update scheduling fixes
31e0f1d54 fix(reader): preserve slider tap behavior and flush guard coverage
cf76883ae feat(stats): add reading chapters stat for velocity
317e6e1ce feat(stats): show seconds and refresh stats on focus
b30abcc12 fix(stats): preserve background tts reading time and cap doze drift
9e737c9f4 build(android): configure metro bundle config for cjs in build gradle
b7bd96318 docs(context): update test counts and post-merge hardening context
26348b2c1 fix(tts): reconcile background listening time via native clock

6. How to Test & Review This PR

# Checkout and setup
git checkout dev
pnpm install

# Static quality checks
pnpm run type-check
pnpm run lint
pnpm run format:check

# Full test suite (expect 119 suites / 1,688 tests passing)
pnpm run test

# Focused suites for this PR's risk areas
pnpm run test -- --testPathPattern="useTimeTracking|readingTime|StatsScreen|TimeTab|Slider|backgroundFlush|ReaderSearchbar"

# Specialized TTS invariant suites (unchanged, regression guard)
pnpm run test -- --testPathPattern="TTSAudioManager.wakeCycle|TTSAudioManager.refill"

Supersedes: the previous version of this doc described 110 commits / 346 files — that was accurate when master was at v2.1.4-era history. Since then those commits merged (through v2.1.5), so this rewrite covers only the 16 commits still ahead of origin/master.

- Add configurable search-close return behavior and position preservation.
- Improve search bar controls, animation, accessibility, and match counters.
- Validate formatting before local commit.
- Inject `statusBarHeight` from `NovelContext` to `ChapterContent` and `ReaderSearchbar` to ensure consistent layout.
- Replace native `StatusBar.currentHeight` usage with the injected prop value.
- Refactor return banner dismissal to explicitly clear timer and reset countdown state.
- Add elevation to the search bar container for visual layering.
- intercept back press when return banner is visible to dismiss it
- update back handler priority comment to reflect new logic
- add missing clearReturnTimer dependency
… anchor fixes

- search.js: replace cloneContents bottleneck with bounded DOM walk (AUD-SRCH-06)
- search.js: per-text-node wrapping to avoid cross-tag transplant and inline node deletion (AUD-SRCH-02)
- search.js: store all match positions and lazily render beyond MAX_RENDERED_MATCHES, wrap-consistent focus (AUD-SRCH-05)
- ReaderSearchbar: enable cyclic stepper navigation to match WebView wrapping (AUD-SRCH-05)
- ReaderScreen: restrict return-to-anchor TTS priority to actively reading state (AUD-SRCH-01)
- ReaderScreen+WebViewReader: allow paragraph-indexed saves during search and bypass Stay Here race (AUD-SRCH-03/04)
…ress flush

- fix AUD-TIME-01: replace window-bound this.post/initialX in swipe IIFE with reader.post and closure vars to prevent TypeError on touchstart

- fix AUD-PERS-01: guard flushPendingProgressSave with hasPerformedInitialScroll OR suppressSaveOnScroll to block 0% save before initial scroll/calculatePages completes

- fix AUD-RTL-01: mirror pageReader translateX, drag preview, tap zones and swipe thresholds when dir=rtl so pages no longer shift off-screen

- fix AUD-RTL-02: invert continuous chapter swipe next/prev mapping when dir=rtl

- fix AUD-RTL-03: invert horizontal seekbar ratio, anchor thumb via right:100% and right-fill progress under dir=rtl, clamp ratio 0-1; mirror off-screen transition chapter position

- keep LTR behavior unchanged; validated type-check and existing tests
- useTimeTracking: exclude idle window from inactivity flush via lastActivityAt (AUD-TIME-02)
- cap durations to 12h and drop non-monotonic negatives to guard NTP/zone skew (AUD-TIME-05)
- periodic 60s checkpoint with 30s min to survive SIGKILL/LMK without duplicate sessions (AUD-TIME-04)
- cap TTS inactive-poll to background-enter time to avoid Doze drift counting paused hours (AUD-TIME-03)
- isolate chapter-change starter timer from unrelated re-renders to prevent cleared starter (AUD-TIME-06)
- DistributionBar: use visibleEntries single-full Circle fallback to avoid 360° SVG arc collapse (AUD-STAT-01)
- TimeTab: guard velocity cph >50 or minsPerChapter <0.5 to avoid lifetime/post-migration distortion (AUD-STAT-02)
- normalizeGenre: Unicode-aware \p{L}\p{N} preserve CJK/Cyrillic/accented genres (AUD-STAT-03)
- PluginsTab: memoize entries and palette, reduce-based max to avoid stack/extra recompute (AUD-STAT-04/05)
- GenreSection: reduce-based max to avoid spread stack (AUD-STAT-05)
- add migration 007 idx_novel_inLibrary for WHERE inLibrary=1 seek (AUD-STAT-06)
- update useTimeTracking and migration tests to match new semantics

Validation: pnpm run type-check passed, pnpm run format passed, jest useTimeTracking/StatsQueries/migration tests passed (1666+ passed, 7 migration version expectations updated to 7)
- defer slider responder claim to move to unblock vertical scroll, allow termination when not actively dragging, interlock ViewPager swipe while dragging to avoid half-commits, fallback-clear dragValue when parent rejects value (AUD-GEST-01/02/03)
- gate progress DB writes with rejection handling and document best-effort MMKV fallback for background process freeze without ForegroundService (AUD-PERS-02/03)

- AUD-GEST-01: onStartShouldSetPanResponder false, shouldClaimPanResponder on move, terminationRequest dynamic

- AUD-GEST-02: sliderDragState global flag, ReaderBottomSheet disables TabView swipe while dragging, terminate reverts without committing

- AUD-GEST-03: keep dragValue until controlled catches up with 400ms fallback to avoid permanent thumb freeze

- AUD-PERS-02: document multi-hop IPC limitation, MMKV synchronous write first in saveProgress, WebView flush note

- AUD-PERS-03: attach .catch to updateChapterProgress/MarkRead via useNovel and direct ChapterQueries callers

- tests: update Slider start-claim expectation, keep released-value-visible behavior

Validation: pnpm run type-check passed, pnpm jest Slider.test 16 passed
- Add readingChapters aggregate counting distinct chapter IDs from reading sessions of library novels
- Compute reading velocity from readingChapters instead of lifetime chaptersRead and drop implausible-velocity heuristic guards
- Flush manual reading sessions when the user is inactive during checkpoint ticks
- Cache lazily rendered search marks and return the last mark to avoid duplicate wrapping
- Force RTL layout when an RTL locale is selected and prompt an app restart
- Remove search save bypass window from reader anchor clearing
- Brace single-statement ifs and use const in search script for consistency
- Format sub-minute reading time as seconds ("20s") instead of "<1m" so short sessions are visible in the Time tab
- Add a seconds field to formatTotalTimeParts and a seconds unit box to TimeTab, backed by a new statsScreen.secs string
- Refetch stats on screen focus via useFocusEffect so returning from the reader picks up newly recorded ReadingSession rows
- Skip the full-screen loader on refocus after the first load to avoid a UI flash during silent refreshes
- Add reading-time simulation tests covering manual scroll, TTS (including background), and multi-checkpoint sessions end-to-end
- Add StatsScreen focus-refetch tests and update TimeTab/utils tests for seconds precision
- replace flawed AUD-TIME-03 background-enter truncation with heartbeat-based polling check in useTimeTracking

- preserve full background TTS playback time when paused via lockscreen notification or Bluetooth headset

- protect periodic checkpoints and inactive flushes against Doze drift by bounding to confirmed heartbeat window

- add simulation and unit tests covering background pause, screen-off starts, and Doze suspension drift capping

- validate with type-check, linter, Prettier, and all 91 Stats and reading-time tests passing
- specify bundleConfig in react block to explicitly resolve metro.config.cjs

- fix createBundleReleaseJsAndAssets SHA-1 failure with pnpm symlink resolution

- validate with successful assembleRelease generating app-release.apk
- Update test metrics to 1,684 passing tests across 118 test suites in AGENTS.md and GEMINI.md.

- Document post-merge hardening for in-chapter search UX, RTL inversion, reading time tracking, gesture arbitration, and Android packaging.

- Note translation key additions in English strings and queued downstream sync for secondary locales.

- Confirm branch health and zero regressions across all ahead commits.

- Validation: passed Prettier formatting and TypeScript type-check with zero errors.
- Add monotonic speaking-time clock to TTSForegroundService (segment per utterance-active span) with getTtsPlaybackClock bridge.

- Snapshot the native clock on app-background entry and top up unattested time on foreground/unmount, capped by wall-clock and native attestation.

- Gate post-foreground TTS restart on native speaking state to prevent phantom sessions; fail open when the clock is unavailable.

- Validation: 1688 jest tests pass (+4 reconcile), 14 native unit tests pass, type-check and lint clean, release build succeeds.
@bizzkoot

bizzkoot commented Sep 5, 2026

Copy link
Copy Markdown
Owner Author

Close #22

@bizzkoot
bizzkoot merged commit 02cd2d2 into master Sep 5, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant