- Remove play button and gear icon from chapter nav bar
- Show bottom mini-player bar always (not slide-up on play)
- Remove duplicate collapsed-bar seek track; keep only expanded panel scrub bar
- Move gear/settings button into the bottom mini-player bar
- Move settings panel into mini-player aside, opening upward
- Clean up JS: remove navBtn/navIcon refs, showPlayer/hidePlayer, playerSeekTrack refs
html/template's context-aware parser was treating the backslash-escaped
quote in the inline onclick regex literal as an HTML attribute terminator,
causing template execution to produce an empty/broken output.
Move the summary paragraph-splitting logic into a named window.openSummary
function in the script block, and call it via a clean onclick=openSummary(this)
attribute with no inline regex literals.
- 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
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.