- Full-bleed cover fills top ~52% of screen with top+bottom gradient
overlays for header and track info legibility; eliminates the large
dead space between cover and seek bar
- Chapter number shown as brand-coloured label above the chapter title
- Remaining time (−m:ss) displayed in the centre of the seek bar row
- Transport row uses justify-between; chapter-skip buttons are smaller
(w-5, muted/60 opacity) vs time-skip (w-7, full muted) to clearly
distinguish secondary from primary seek controls
- Speed / Auto-next / Sleep now sit on a single tidy row — no more
wrapping or mixed visual styles between the segmented speed control
and the two pill buttons
- Header buttons use frosted-glass style (bg-black/25 backdrop-blur)
so they remain legible over the cover image
- Hide the global site footer on chapter pages (not useful mid-reading)
- Merge the three separate floating nav pills into a single unified pill
with dividers, removing the visual clutter of multiple bordered bubbles
- Float the pill lower (bottom-6) when the mini-player is not active
Adds a PageLines preference (Few/Normal/Many) that adjusts the paginated
container height via a rem offset on the existing calc(). The setting row
appears in Reader Settings → Layout only when Pages mode is active, matching
the style of all other setting rows. Persisted in localStorage (reader_layout_v1).
Use a Svelte action on each chapter button that calls scrollIntoView with
behavior:'instant' so the list opens centred on the active chapter with
no visible scroll animation.
Reading audioStore.isPlaying inside the toggleRequest $effect caused Svelte 5
to subscribe to it, so the effect re-ran on every isPlaying change. When
resuming from ListeningMode, play() would fire onplay → isPlaying=true →
effect re-ran → called pause() → onpause → isPlaying=false → effect re-ran
→ called play() → infinite loop. Wrapping the isPlaying read in untrack()
limits the effect's subscription to toggleRequest only.
Cycles through all in-progress books every 6s; prev/next arrow buttons
overlay the card edges; active dot stretches to a pill; cover fades in
on slide change via {#key} + animate-fade-in; shelf excludes the current
hero to avoid duplication.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- justify-between on scrollable body so cover art sits top, controls
sit bottom — no more half-empty screen on tall phones
- Move ListeningMode outside {#if audioStore.active} so pausing never
tears down the overlay and loses resume context
- Mini-bar time/track click now opens ListeningMode with chapter picker
pre-shown (same view as the Chapters button inside ListeningMode)
- Remove the old chapterDrawerOpen mini-bar drawer (replaced by above)
- Add openChapters prop to ListeningMode for pre-opening chapter modal
Replaces the bottom drawer + backdrop with a fixed full-screen overlay
matching the voice/chapter picker style in ListeningMode — chevron header,
tab bar with brand-color active state, scrollable content. Escape closes it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both ListeningMode and the standard AudioPlayer now open chapters via a
full-screen overlay (same UX as the voice selector) — header + search bar +
rows with circular chapter-number badge, title, and active indicator.
Removes the cramped inline card from the bottom of ListeningMode.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents the silly "2:01 / 0:00" display when audio src is being swapped
from preview to full audio and duration hasn't loaded yet.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Strip leading digit prefix (e.g. "6Chapter 6 → Chapter 6") and
content after first newline (scraped date artifacts) from chapter titles
- Add "CHAPTER N" eyebrow label above the h1 for clear hierarchy
- Show date_label as small muted text in the meta row
- Remove double-border / mt-6 gap from standard AudioPlayer inside the
chapter page's collapsible panel (was rendering two nested boxes)
- Remove redundant "Audio Narration" label (toggle already says "Listen")
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- deviceFingerprint now hashes only User-Agent (not UA+IP) so switching
networks (VPN, mobile data, wifi) no longer creates a new session row
- On re-login with same device, also refresh the stored IP field so the
sessions page shows the current network address
- feat(library): bulk remove and bulk shelf-change actions on /books
Long-press any card to enter selection mode; sticky action bar with
Move to shelf dropdown and Remove button; POST /api/library/bulk-remove
and POST /api/library/bulk-shelf endpoints
- fix(catalogue): make Scrape button visible with solid amber-500 fill
and dark text instead of low-opacity ghost style that blended into card
The description was crammed into the narrow right column beside the cover,
creating a wall of text on mobile. Now it renders full-width below the
cover+title row with better line-height, 5-line collapse, gradient fade,
and a chevron-annotated show-more button.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add POST /api/admin/text-gen/chapter-names/async backend endpoint: fire-and-forget,
returns job_id immediately (HTTP 202), runs batch generation in background goroutine,
persists proposed titles in ai_job payload when done
- Register new route in server.go alongside existing SSE endpoint (backward compat)
- Add SvelteKit proxy at /api/admin/text-gen/chapter-names/async
- Add SvelteKit proxy for GET /api/admin/ai-jobs/[id] (job detail with payload)
- Add Review button on ai-jobs page for done chapter-names jobs; inline panel shows
editable title table (old to new) with Apply All button that POSTs to chapter-names/apply
- Backend: add GET /api/audio-preview/{slug}/{n} — generates first ~1800-char
chunk via CF AI so playback starts immediately; full chapter cached in MinIO
- Frontend: replace CF AI spinner with preview blob URL + background swap to
full presigned URL when runner finishes, preserving currentTime
- AudioPlayer: isPreview state + 'preview' badge in mini-bar during swap
- pocketbase.ts: fix 403 on stale token — reduce TTL to 50 min + retry once
on 401/403 with forced re-auth (was cached 12 h, PB tokens expire in 1 h)
- Footer build time now rendered in user's local timezone via toLocaleString()
When Valkey is unreachable, ioredis was holding cache.get() calls in
the offline queue for the default 10s connectTimeout before failing.
This caused admin/image-gen and text-gen pages to stall on every load.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Speed and auto-next are already available in the full listening mode
overlay — no need to clutter the compact bottom bar with them.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When PREBUILT=1 the pre-built artifact is downloaded into ui/build/ but
.dockerignore excludes 'build', so Docker never sees it and /app/build
doesn't exist in the builder stage — causing the runtime COPY to fail.
Fix: rewrite ui/.dockerignore on the CI runner (grep -v '^build$') so the
pre-built directory is included in the Docker context.
Also in this commit:
- book page: gate EPUB download on isPro (UI upsell + server 403 guard)
- book page: chapter names default pattern changed to '{scene}'
When the mini-player is active on the current chapter, the collapsible
'Listen to this chapter' panel now shows a brief note instead of rendering
a full second AudioPlayer.
Bottom bar track info column: removed chapter title and book title lines
so the time display fits on one line without crowding the controls.
The docker-ui job was rebuilding the UI from scratch inside Docker, producing
chunk hashes and JS files that didn't match the source maps uploaded to
GlitchTip, causing all stack traces to appear minified.
Fix:
- ui/Dockerfile: add PREBUILT=1 ARG; skip npm run build when set
- release.yaml upload-sourcemaps: re-upload artifact after sentry-cli inject
- release.yaml docker-ui: download injected artifact into ui/build/ and pass
PREBUILT=1 so Docker reuses the exact same JS files whose debug IDs are in
GlitchTip
- Add /admin/ai-jobs page with live-polling jobs table, status badges, progress bars, and cancel action
- Add listAIJobs() helper in pocketbase.ts; AIJob type
- Add POST /api/admin/ai-jobs/[id]/cancel SvelteKit proxy
- Add ai-jobs link to admin sidebar nav (all 5 locales)
- Cache image-gen and text-gen model lists in Valkey (10 min TTL) via scraper.ts helpers
- Cache changelog Gitea API response (5 min TTL)
- Improve 502/504 error message on image-gen page with CF AI timeout hint
- Show CF AI timeout warning in advanced options when a Cloudflare/FLUX model is selected
Pass build output from check-ui to upload-sourcemaps via artifact
instead of rebuilding from scratch. Saves ~4-5 min (npm ci + npm run build)
from the upload-sourcemaps job.
- Consolidate top nav into a single row: ← back | ← → chapter arrows | settings gear
Removes the duplicate prev/next buttons that appeared at both top and bottom
- Language switcher moved inline into chapter meta line (after word count);
lock icons made smaller and less distracting for free users; removes the
separate "Upgrade to Pro" text link
- Audio player wrapped in a collapsible "Listen to this chapter" panel;
auto-expands if audio is already playing for the chapter, collapsed otherwise
so content is immediately visible on page load
- Bottom nav redesigned: next chapter is a full-width card CTA with hover
accent, previous is a small secondary text link — clear visual hierarchy
- Remove floating gear button (settings now triggered from top nav settings icon)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
glitchtip-cli v0.1.0 uploads chunks but never calls the assemble endpoint,
leaving releases with 0 files. sentry-cli correctly calls assemble after
chunk upload — confirmed working in manual test (2.5.84-test3 = 211 files).
- Add homelab/glitchtip/files_api.py with GzipChunk fallback for sentry-cli 3.x
raw zip uploads; bind-mount into both glitchtip-web and glitchtip-worker
- Add release.yaml prune step to delete all but the 10 newest GlitchTip releases
- Reader page: remove dead code and simplify layout
Adds a full-screen listening mode accessible via the headphones button in the
mini-player bar. Moves voice selector, speed, auto-next, and sleep timer out of
the reader settings panel into the new overlay. Voices are stored in AudioStore
so ListeningMode can read them without prop drilling.
- Mount glitchtip_uploads named volume to /code/uploads on both
glitchtip-web and glitchtip-worker so chunk blobs written by the
web container are readable by the worker during assembly
- Set MEDIA_ROOT=/code/uploads explicitly on both services so the
Django storage path matches the volume mount and DB blob records
- New `ai_jobs` PocketBase collection tracks all long-running AI tasks
(batch-covers, chapter-names) with status, progress, and cancellation
- `handlers_aijobs.go`: GET/cancel endpoints for ai_jobs; centralised
cancel registry (moved from handlers_catalogue)
- Batch-covers and chapter-names SSE handlers now create/resume ai_job
records, support from_item/to_item ranges, and resume from items_done
on restart via job_id
- New `POST /api/admin/image-gen/auto-prompt`: generates an image prompt
from book description (cover) or chapter title (chapter) via LLM
- image-gen page: "Auto-prompt" button calls auto-prompt API when a slug
is selected; falls back gracefully if TextGen not configured
- text-gen chapter-names: from/to chapter range inputs + job ID display
- catalogue-tools batch-covers: from/to item range + resume job ID input
- pb-init-v3.sh: adds ai_jobs collection (idempotent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add 'releases new' and 'releases finalize' steps around sourcemaps
upload in release.yaml — without an explicit 'releases new' call,
GlitchTip creates the release entry but associates 0 files.
Add root AGENTS.md (picked up by Claude, Cursor, Copilot, etc.) with
full project context: stack, repo layout, Gitea CI conventions,
GlitchTip DSN/upload flow, infra, and iOS notes.
On md+ screens the drawer is now right-aligned and 320px wide (w-80)
instead of full-viewport-width. The sticky header is pulled out of the
scroll container so it never scrolls away, and overflow-y-auto is
applied only to the chapter list itself so both mobile and desktop can
scroll through long chapter lists.
- Add @grafana/faro-web-sdk to UI; wire initializeFaro in hooks.client.ts
gated on PUBLIC_FARO_COLLECTOR_URL (no-op in dev)
- Add Grafana Alloy service (faro.receiver) to homelab compose;
Faro endpoint → alloy:12347 (faro.libnovel.cc via cloudflared)
- Add PUBLIC_FARO_COLLECTOR_URL env var to docker-compose.yml UI service
- Add Web Vitals dashboard (web-vitals.json): LCP/INP/CLS/TTFB/FCP p75
stats + LCP/TTFB time-series + Faro exception logs from Loki
- Fix runner.json: strip libnovel_ prefix from all metric names
- Fix backend.json: replace 5 dead http_client_* panels with
spanmetrics-based equivalents (Request Rate by Span Name + Latency
by Span Name p95)
- Fix OTel collector: add service.telemetry.metrics.address: 0.0.0.0:8888
so Prometheus can scrape collector self-metrics
- Add Grafana link to admin nav external tools; add admin_nav_grafana
message key to all 5 locale files; recompile paraglide
CF Workers AI has a ~4MB JSON body limit. Large cover images base64-encoded
can easily exceed this, causing Cloudflare to return a 502 Bad Gateway.
Added resizeRefImage() which scales the reference down so its longest side
≤ 768px (nearest-neighbour, re-encoded as JPEG) before passing it to the
GenerateImageFromReference call. Images already within the limit pass through
unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Llama 4 Scout returns `result.response` as an array of objects
[{"generated_text":"..."}] instead of a plain string. Decode into
json.RawMessage and try both shapes; fall back to generated_text[0].
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Cover generation: editable prompt (pre-filled from title+summary),
img2img toggle to use existing cover as reference, Full editor link
- Chapter cover: editable prompt textarea
- Description: instructions input field, Full editor link
- Chapter names: editable pattern field, SSE streaming with live batch
progress, inline-editable title proposals, batch warning display
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Split chapter-name LLM requests into 100-chapter batches and stream
results back as SSE so large books (e.g. Shadow Slave: 2916 chapters)
never time out or truncate. Frontend shows live batch progress inline
and accumulates proposals as they arrive.
chapters_idx was missing created/updated columns (never defined in the
PocketBase schema), causing PocketBase to return 400 for any query
sorted by -created. recentlyUpdatedBooks() uses this sort.
- Add created date field to chapters_idx schema in pb-init-v3.sh
(also added via add_field for existing installations)
- Add idx_chapters_idx_created index for sort performance
- Set created timestamp on first insert in upsertChapterIdx so new
chapters are immediately sortable; existing records retain empty created
and will sort to the back (acceptable — only affects home page recency)
Without com.centurylinklabs.watchtower.enable=true the homelab watchtower
(running with --label-enable) silently skipped the runner container,
leaving it stuck on v2.5.60 while fixes accumulated on newer tags.
- Replace ?? with || when reading font_size so 0 falls back to 1.0
(affects GET /api/settings, layout.server.ts, +layout.svelte)
- Remove the explicit 'body.fontSize !== 0' exception in PUT /api/settings
validation so 0 is now correctly rejected as an invalid font size
- Add add_index helper + idx_chapters_idx_slug_number declaration to
scripts/pb-init-v3.sh (idempotent UNIQUE INDEX on chapters_idx)