feat: add Kindle-style reading time tracker - #1267
Open
BenjaminMichaelis wants to merge 2 commits into
Open
Conversation
- WordCountService: singleton that walks all SiteMappings at startup,
strips <pre>/<code>/<script>/<style> via HtmlAgilityPack, and caches
per-page, per-chapter, and book-wide prose word counts + prefix sums
- ReadingActivity + UserReadingProfile EF entities with migration
AddReadingTimeTracking; retention capped at 500 rows/user
- ReadingController (POST /session, GET /profile, GET /book-stats,
POST /reset); WPM algorithm: >900 WPM hard-discard, slow outlier
clamped at 0.25x referenceWpm, aggregate stored as
{totalWords, totalActiveSeconds}
- useReadingTracker.js composable: activity state machine (READING /
IDLE / HIDDEN), 5-min idle threshold, visibilitychange instant-pause,
localStorage for anonymous users, flush-on-navigate, sync local→server
on first authenticated page load
- ReadingTimeRemaining.vue: displays 'X min left in chapter / Y min left
in book' in header; live scroll-fraction update; only renders on
chapter pages; integrated into HeaderStatus.vue
- Reset button in Identity Manage Profile page
- Wired in HomeController (word-count ViewBag) and _Layout.cshtml
(window.* globals: CURRENT_PAGE_KEY, PAGE_WORD_COUNT, CHAPTER_WORD_COUNT,
WORDS_BEFORE_PAGE, CHAPTER_START_WORDS, BOOK_WORD_COUNT)
- Tests: WordCountServiceTests (prose-only counting, prefix sums,
chapter/book totals) and ReadingControllerTests (auth guards, WPM
algorithm constants, book-stats endpoint)
… deserialization) SiteMapping.PrimaryKey is not serialized in sitemap.json; after deserialization it is null, causing ArgumentNullException in WordCountService constructor and HomeController when calling Dictionary.ContainsKey(null). Use mapping.Keys.FirstOrDefault() ?? mapping.PrimaryKey consistently, matching the pattern already used in BookToolQueryService. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a Kindle-style "time left in chapter / book" indicator to the EssentialCSharp.Web header, adapting to each user's actual reading speed over time.
Features
Server-side
WordCountService— singleton that walks allSiteMappingentries at startup, strips<pre>/<code>/<script>/<style>via HtmlAgilityPack, and caches per-page/per-chapter/book-wide prose word counts + prefix sums (O(1)lookups at request time)ReadingActivity+UserReadingProfile— EF Core entities (migrationAddReadingTimeTracking); profile stores{totalWords, totalActiveSeconds}forO(1)WPM derivation; activity rows capped at 500/user for bounded storageReadingController— four endpoints:POST /api/reading/session— batch interval ingestion, WPM algorithm (>900 WPM hard-discard; slow outliers clamped at0.25 × referenceWpm), aggregate update + retention trimGET /api/reading/profile— current WPM for logged-in userGET /api/reading/book-stats— public word counts per chapter + totalPOST /api/reading/reset— clears aggregate + detail rowsClient-side
useReadingTracker.jscomposable — activity state machine (READING/IDLE/HIDDEN), 5-min idle threshold, instant-pause onvisibilitychange, flush-on-navigate, localStorage→server sync on first authenticated page loadReadingTimeRemaining.vue— displays"X min left in chapter · Y min left in book"in the header; recomputes every few seconds from live scroll fraction; only renders on chapter pages; integrated intoHeaderStatus.vuewindow.*globals —CURRENT_PAGE_KEY,PAGE_WORD_COUNT,CHAPTER_WORD_COUNT,WORDS_BEFORE_PAGE,CHAPTER_START_WORDS,BOOK_WORD_COUNTemitted from_Layout.cshtmlUX
Algorithm
Mirrors Kindle's
ReadingTimer:0.25 × referenceWpmare time-clamped (walked away mid-page)totalWords / (totalActiveSeconds / 60)— never stores a running average that needs re-weightingTesting
WordCountServiceTests: prose-only counting,<pre>/<code>exclusion, prefix sums, chapter/book totalsReadingControllerTests: auth guards (401 on all mutating endpoints for anonymous users), WPM algorithm constant verification,GET /api/reading/book-statsshape test