- Make overlay scroll within the viewport instead of being clipped (overflow-y-auto on backdrop, items-start alignment)
- Split summary text into paragraphs in the zoom overlay (newline-split with sentence-boundary fallback)
- Add Summary heading in overlay for context
- Remove old merge-poller JS; simplify generateAudio to use data.url directly (Kokoro backend rewrite cleanup)
Embed the complete ranking JSON into the page and replace the DOM-only
filter with a client-side search over ALL_ITEMS. When a filter is active
the grid is re-rendered from the full dataset; clearing filters restores
the server-paginated view. Also adds summary zoom overlay on the book page.
- Home page book cards show downloaded chapter count (amber badge) via new CountChapters method
- Book page cover image is clickable to zoom into a fullscreen overlay; click anywhere to dismiss
- Book page shows a 'Latest Chapter' button for unstarted books linking to the last downloaded chapter
- Continue-reading cards on home page have a dismiss (×) button that removes progress from localStorage and slides the card out to the left
- Remove Scrape form/button and Source text link from non-local ranking cards
- First click on a card highlights it amber and shows 'Click again to scrape' hint
- Second click POSTs to /ui/scrape/book and replaces card with polling status badge
- Pending state auto-cancels after 3 s or when clicking outside
- Source icon (external link SVG) retained but dimmed; stops click propagation
- Extract scrape form and autocomplete JS from homeTmpl into new scrapeTmpl
- Add handleScrape GET handler serving /scrape with ranking autocomplete
- Register GET /scrape route in server.go
- Replace inline scrape form on home page with '+ Add' flat button in header
- handleHome no longer loads ranking items (only needed on /scrape)
rankingPageNums now takes a current page parameter and always renders
first 2, last 2, and current±2 — so navigating to page 3 shows
1 2 3 4 5 … 80 81 instead of only the fixed quarter-mark anchors.
- Server collects distinct genres and statuses from all cached items and
passes them as AllGenres/AllStatuses slices to the template
- Facet chips rendered as pill toggles below the text search bar
- Cards gain data-status, data-genres (pipe-delimited), data-local attributes
- JS filter engine: AND between facets, OR within status facet (a book has
one status), AND within genre facet (all selected genres must be present)
- Pagination row hidden while any filter is active; restored on clear
- 'Clear filters' button appears when any filter is active
- sortedKeys helper added to produce stable sorted facet chip order
All filled/pill buttons across home, ranking, book detail, and chapter
pages now use transparent backgrounds with colored text only, consistent
with the chapter reader nav bar style:
- Home: Browse Rankings (amber text), Scrape (amber text)
- Ranking: View Markdown, page numbers, fetch buttons, Scrape/Source on cards
- Book detail: Source, Refresh, Resume, chapter pagination
- Chapter: bottom Prev/Next nav, article-end nav links
An <a> element nested inside the card's outer <a> is invalid HTML — browsers
auto-close the outer anchor before the inner one, causing the card to be split
across two grid cells visually. Replace with a <button> that calls window.open()
with event.preventDefault/stopPropagation to avoid interfering with card navigation.
- Replace bare ← back-to-book link with ☰ (hamburger) icon to eliminate
the visual double-arrow confusion with the ← Prev chapter button
- Export SplitChapterTitle in writer package and call it in handleChapter
to split the raw heading into (title, date) — fixes concatenated title+date bug
- Display ChapterDate as a smaller muted line below the chapter title
in the center nav button
- Bump nav bar height from h-12 to h-14 to accommodate two-line center button
The previous selectors were based on a hypothetical structure that does not
match the actual site. The real novelfire.net popular listing uses:
<li class="novel-item"> (not <div>)
<a href="/book/slug" title="Title">
<figure class="novel-cover"><img data-src="/path.jpg"></figure>
<h4 class="novel-title text2row">Title</h4>
</a>
</li>
And pagination uses <a rel="next"> (not <a class="next">).
Changes:
- ScrapeRanking: use li.novel-item, h4.novel-title, figure.novel-cover,
img[data-src]; strip base64 placeholder covers
- hasNextPageLink(): new helper walking all <a> nodes for rel="next"
- Import golang.org/x/net/html for Node.Attr access
- Test fixtures rewritten to match real structure (li/h4/rel=next)
- Status and genres removed from ranking items (not present on listing page)
Verified end-to-end: ranking.json written with 24 items on first fetch
Previously all cached ranking items were dumped into one page and the
page buttons only triggered refetches from novelfire — not navigation.
- handleRanking now reads ?page=N query param (default 1)
- Items sliced server-side at 20 per page (rankingPageSize constant)
- Template receives DisplayNums/CurrentPage/TotalPages for browse nav
- Display pagination bar renders GET /ranking?page=N HTMX links with
the active page highlighted in amber; hidden when only 1 page
- Fetch section collapsed into a <details> to reduce visual noise
- FetchNums replaces old PageNums field name in template data
- selectItem() now sets the visible input to source_url (not the title), so the user can see and copy the link before scraping
- Keyboard navigation (ArrowUp/Down) also previews the URL in the input
- Added URL hint line inside each dropdown item showing the full source_url
- Genres included in search haystack (was missing before)
- Deduplicated filter logic into filterRanking() helper
- Raised max results from 8 to 10
- Switch ScrapeRanking to novelfire.net/genre-all/sort-popular URL and updated DOM selectors (div.novel-item, h3.novel-title, div.genres)
- Replace 5 hardcoded refresh buttons with dynamic 100-page paginator (smart ellipsis via rankingPageNums)
- Add RankingPageCacher interface and writer methods to cache raw HTML per page under static/books/_ranking_cache/page-N.html
- ScrapeRanking serves from disk cache on hit and writes to cache on miss, skipping Browserless round-trip
- Thread writer as PageCacher through novelfire.New and main.go
- Add TestScrapeRanking_CacheHit and TestScrapeRanking_CacheMiss tests
- Cards now use flex layout with break-words/min-w-0 so long titles wrap
instead of stretching the column; grid uses gap-3 for tighter density
- Added filter bar (title/author/genre/status) with live result count,
consistent with the home page filter
- Replaced four big refresh buttons with compact pagination buttons
(p.1, p.1-3, p.1-5, p.1-10, All) labelled with page ranges; source
link points to novelfire.net/genre-all/sort-popular/status-all/all-novel
- Template data field renamed from RefreshPages to Pages; pagination
entries updated to reflect 100 novels per page
- Split chapter TTS into up to 10 paragraph-aligned parts; part 0 is
generated synchronously so playback starts immediately, parts 1-9 and
the final merge happen in a background goroutine
- New routes: GET /ui/audio/{slug}/{n}/status (merge poll) and
GET /ui/audio-file/{slug}/{n}/part/{p} (serve individual part)
- JS polls status every 3 s and seamlessly swaps audio.src to the merged
file once ready, preserving playback position proportionally
- Home page scrape form replaced with a ranking-search autocomplete:
type a title/author to see matching ranking items (cover + metadata),
click or keyboard-select to inject the source URL, or paste a raw URL
directly; ranking data is embedded as JSON at page render time
- Add POST /ui/audio/{slug}/{n} endpoint: calls Kokoro-FastAPI server-side,
writes MP3 atomically to disk, deduplicates concurrent requests via
in-flight channel map, wraps route with 10-min TimeoutHandler
- Add GET /ui/audio-file/{slug}/{n} endpoint: serves cached MP3 with 1-day
cache headers
- Add AudioDir/AudioPath helpers to writer.go
- Rewrite JS TTS: remove all MSE/blob/stream code; use plain fetch to
generate endpoint then set audio.src to returned URL
- Add AbortController to generateAudio; abort on stop() and navigation
- Keep voice/speed controls disabled until setPlaying() fires
- Reset prefetchFired on voice/speed change
- Upgrade prefetch from fire-and-forget to promise chain updating queue badge
- Add sticky bottom audio queue panel: always-visible scrubber with timestamps,
expandable rows for Now/Next/Dbg with color-coded state badges
- Fix iOS Reader Mode: header->nav aria-hidden, audio outside nav, role=main,
visible h1 in content area, hover-only paragraph highlight
- Fix home screen Available books hidden: stop hiding cards in #books-grid
when they appear in Continue Reading; Available always shows all books
- Remove the duplicate <h1> that appeared in the content area below the
sticky bar (title is already shown in the header)
- Replace the static title span with a clickable button (title + ▼ caret)
that opens a full-width chapter list drawer below the sticky bar
- Drawer is server-rendered with all chapters; current chapter is highlighted
in amber and scrolled into view on open; clicking any row navigates to it
and closes the drawer
- Opening the chapter list closes the settings panel and vice versa
- Both panels dismiss on outside click
- Pass AllChapters to the chapter template data struct
The previous approach cloned the card DOM node and used removeChild to move it,
which meant the source node was gone on re-runs and a stale clone could persist
across history restores. Replace with: clear continueGrid, build a fresh <a>
element from the source card's attributes/innerHTML on every run, and hide
(not remove) the original in #books-grid so it is always available as a clean
source. Also scope the data-slug selector to #books-grid to avoid accidental
matches against the continue-reading cards themselves.
Replace the separate top nav bar and TTS player bar with a single compact
sticky header that stays visible while scrolling. Navigation (← back, Prev,
Next) and the Listen/Pause button are always accessible. Voice, speed and
auto-next settings are moved into a ⚙ dropdown that opens from the header,
keeping the reading area clean. A thin status strip below the toolbar shows
Buffering/Playing/Error states only when active.
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