HTMX was snapshotting the post-JS mutated home DOM into history; on back-navigation
it restored the stale snapshot then re-ran the inline script, appending cards that
were already present. Fix by:
- hx-history="false" on the home root div so HTMX never caches the mutated DOM
- clearing #continue-reading-grid before each script run as a defensive guard
- Fix channel drain goroutine deadlock in handleRankingRefresh: replace
'for { ... continue ... if nil break }' with 'for A != nil || B != nil'
so the select never blocks on two nil channels
- Switch ScrapeRanking from urlClient (Browserless) to client (direct HTTP)
since novelfire.net/ranking is fully server-rendered — no JS needed
- Add ranking unit tests (single page, multi-page, empty page, write round-trip)
- Add .gitea/workflows/ci.yaml: lint, test, build jobs with commented-out
Docker image push step for when runner has Docker available
Each card in the Continue reading section now displays the saved chapter
number (e.g. 'Chapter 42') in amber below the author line, read from
localStorage.reading_progress.
Reading progress lives in localStorage so sorting is done client-side.
On page load, JS reads the reading_progress map, moves any matching book
cards into a 'Continue reading' grid above the main grid, and reveals an
'Available' heading for the remainder. Books with no saved progress are
unaffected and no server changes are required.
- Replace 'Load more chapters' append pattern with discrete page-number
pagination bar (« 1 2 3 … N ») on /books/{slug}
- Clicking a page number swaps #chapter-list via HTMX (innerHTML replace)
and updates the OOB #chapter-pagination bar; URL is pushed via hx-push-url
- applyProgress() is called after each swap to re-highlight saved chapter
- Fix empty bullet bug: progress-dot span no longer contains the '●'
character; replaced with a small amber circle via Tailwind (w-2 h-2
rounded-full bg-amber-400) so hidden spans are truly space-free
- Add template FuncMap (pages/prev/next helpers) to both handleBook and
handleBookChaptersPage; remove HasMore/NextPage from template data structs
- ScrapeRanking now accepts a maxPages int parameter (0 = all pages).
Each page is fetched strictly sequentially; the next page is only
requested after every entry from the current page has been sent,
so there is no pre-fetching or look-ahead.
Pagination stops automatically when no next-page link is present
or when the rank-novels container is absent/empty.
- The ranking URL pattern follows the existing catalogue convention:
/ranking?page=N (next-page link detection as the stop condition).
- Server: handleRankingRefresh reads an optional 'pages' form field
and passes it to ScrapeRanking. Timeout scales at 90 s/page.
- UI: Refresh Rankings button is now a small form with a numeric
'Pages' input (default 1), letting the user choose how many pages
to pull in one refresh without touching the server config.
Two bugs caused the 'Refresh Rankings' button to silently fail in production:
1. ScrapeRanking was using the plain HTTP client (s.client) instead of the
browserless content client (s.urlClient). The /ranking page requires
JavaScript rendering, so a plain fetch returned HTML without any novel
entries. Now uses s.urlClient so the page is fully rendered before scraping.
2. handleRankingRefresh was synchronous, holding the HTTP connection open for
up to 60 s while scraping. Reverse proxies and HTMX timeouts closed the
connection before the scrape finished. Rewritten to the same async pattern
used for book scraping: POST /ranking/refresh returns immediately with a
polling badge; the browser polls GET /ui/ranking/status every 3 s; when
the goroutine finishes the status endpoint sends HX-Redirect to /ranking.
Chapter list:
- Book page now shows the first 50 chapters only; a 'Load more' button
(HTMX, GET /books/{slug}/chapters-page?page=N) appends the next page
without a full navigation, replacing itself with the next load-more
button or disappearing when all chapters are loaded.
Reading progress:
- Visiting a chapter saves {slug: chapterN} to localStorage under
'reading_progress'.
- The book page reads that key on load and, if a saved chapter exists,
highlights the saved chapter row with an amber dot and shows a
'Resume — Chapter N' button that navigates directly to it.
- Progress dots are also re-applied after each Load More via
hx-on::after-request.
iOS Safari does not reliably support MediaSource for audio/mpeg streaming,
causing an 'audio decode error' when the MSE SourceBuffer path is used.
Detect iOS (and any browser without audio/mpeg MSE support) at runtime and
fall back to a single fetch(...).blob() download with stream:false. Playback
begins after the full chapter audio is received rather than mid-stream, but
it works on all devices. Desktop browsers keep the existing MSE streaming
path (stream:true) for faster time-to-first-audio.
The same detection applies to the next-chapter prefetch pipeline.
Scraped content is now stored in the 'static_books' Docker named volume
instead of a host bind mount, removing the dependency on STATIC_ROOT and
the need to pre-create ./static/books on the host.
The 3030:3000 port mapping only exposes port 3030 on the host. Container-to-container
traffic (scraper → browserless) and the healthcheck (which runs inside the browserless
container) must use the container's own port 3000. Only the host-side default in
main.go and the Dockerfile ENV remain on 3030.
- Add Kokoro-FastAPI TTS integration to the chapter reader UI:
- Browser-side MSE streaming with paragraph-level click-to-start
- Voice selector, speed slider, auto-next with prefetch of the next chapter
- New GET /ui/chapter-text endpoint that strips Markdown and serves plain text
- Add ranking page (novelfire /ranking scraper, WriteRanking/ReadRankingItems
in writer, GET /ranking + POST /ranking/refresh + GET /ranking/view routes)
with local-library annotation and one-click scrape buttons
- Add StrategyDirect (plain HTTP client) as a new browser strategy; the
default strategy is now 'direct' for chapter fetching and 'content'
for chapter-list URL retrieval (split via BROWSERLESS_URL_STRATEGY)
- Fix chapter numbering bug: numbers are now derived from the URL path
(/chapter-N) rather than list position, correcting newest-first ordering
- Add 'refresh <slug>' CLI sub-command to re-scrape a book from its saved
source_url without knowing the original URL
- Extend NovelScraper interface with RankingProvider (ScrapeRanking)
- Tune scraper timeouts: wait-for-selector reduced to 5 s, GotoOptions
timeout set to 60 s, content/scrape client defaults raised to 90 s
- Add cover extraction fix (figure.cover > img rather than bare img.cover)
- Add AGENTS.md and .aiignore for AI tooling context
- Add integration tests for browser client and novelfire scraper (build
tag: integration) and unit tests for chapterNumberFromURL and pagination