- Admin changelog page now fetches releases live from the public Gitea
API (https://gitea.kalekber.cc) instead of a baked releases.json file
- Remove /static/releases.json from ui/.gitignore (no longer generated)
- Add Gitea to admin sidebar external tools (admin_nav_gitea i18n key,
all 5 locales, compiled paraglide JS force-added)
- release.yaml: remove 'Fetch releases from Gitea API' step; extract
release title and body from the tagged commit message for Gitea releases
New feed_*.js and nav_feed.js outputs from npm run paraglide were not tracked
because src/lib/paraglide/.gitignore ignores everything by default. Force-add
them so CI can run svelte-check without running paraglide compile first.
Also include recentlyUpdatedBooks fallback improvements from working tree.
Fix "Recently Updated" showing stale books: replace meta_updated
sorting (only changes on metadata writes) with chapters_idx sorted
by -created, so the section now reflects actual chapter activity.
Add per-section show/hide toggles on the home page, persisted in
localStorage via Svelte 5 $state. Each section header gets a small
hide button; hidden sections appear as restore chips above the footer.
Toggleable: Recently Updated, Browse by Genre, From Following.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mobile action area was cluttered with Continue, Start ch.1, bookmark,
stars, and shelf dropdown all competing in a single unstructured row.
- Split mobile CTAs into two clear rows:
Row 1: primary read button(s) only (Continue / Start from ch.1)
Row 2: bookmark icon + shelf dropdown + star rating inline
- 'Start from ch.1' no longer stretches to flex-1 when Continue is
present — it's a compact secondary button instead
- Stars and shelf dropdown moved out of the CTA row into their own line
Genre tags were plain <span> elements with no interaction. Tapping
'fantasy' or 'action' now navigates to /catalogue?genre=fantasy,
pre-selecting the genre filter on the catalogue page.
The save-settings $effect was firing on the initial data load because
settingsApplied was set to true synchronously in the apply effect, then
currentTheme/fontFamily/fontSize were written in the same tick — causing
the save effect to immediately fire with uninitialized default values
(theme: "", fontFamily: "", fontSize: 0), producing a 400 error.
- Add settingsDirty flag, set via setTimeout(0) after initial apply so
the save effect is blocked for the first load and only runs on real
user-driven changes
- Also accept empty string / 0 as 'not provided' in PUT /api/settings
validation as a defensive backstop
Cache home stats (10 min) and recently added books (5 min) to avoid
hitting PocketBase on every homepage load. Cache all ratings for
discovery ranking (5 min) with invalidation on setBookRating.
invalidateBooksCache now clears all related keys atomically.
Fix discover card pop-to-full-size bug: new card now transitions from
scale(0.95) to scale(1.0) matching its back-card position, instead of
snapping to full size instantly after each swipe.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
**Profile stats tab**
- New Stats tab on /profile page (Profile / Stats switcher)
- Reading overview: chapters read, completed, reading, plan-to-read counts
- Activity cards: day streak + avg rating given
- Favourite genres (top 3 by frequency across library/progress)
- getUserStats() in pocketbase.ts — computes streak, shelf counts, genre freq
**Discover history tab**
- New History tab on /discover with full voted-book list
- Per-entry: cover thumbnail, title link, author, action label (Liked/Skipped/etc.)
- Undo button: optimistic update + DELETE /api/discover/vote?slug=...
- Clear all history button; tab shows vote count badge
- getVotedBooks(), undoDiscoveryVote() in pocketbase.ts
**Rating-ranked discovery deck**
- getBooksForDiscovery now sorts by community avg rating before returning
- Tier-based shuffle: books within the same ±0.5 star bucket are still randomised
- Higher-rated books surface earlier without making the deck fully deterministic
**End-of-chapter sleep timer**
- New cycle option: Off → End of Chapter → 15m → 30m → 45m → 60m → Off
- sleepAfterChapter flag in AudioStore; layout handles it in onended (skips auto-next)
- Button shows "End Ch." label when active in this mode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
OAuth callbacks were creating a new session record on every login from
the same device because user-agent/IP were hardcoded as empty strings,
producing a pile-up of 6+ identical 'Unknown browser' sessions.
- Add upsertUserSession(): looks up existing session by user_id +
device_fingerprint (SHA-256 of ua::ip, first 16 hex chars); reuses
and touches it (returning the same authSessionId) if found, creates
a new record otherwise
- Add device_fingerprint field to UserSession interface
- Fix OAuth callback to read real user-agent/IP from request headers
(they are available in RequestHandler via request.headers)
- Switch both OAuth and password login to upsertUserSession so the
returned authSessionId is used for the auth token
- Extend pruneStaleUserSessions to also cap sessions at 10 per user
- Keep createUserSession as a deprecated shim for gradual migration
**Ratings (1–5 stars)**
- New `book_ratings` PB collection (session_id, user_id, slug, rating)
- `getBookRating`, `getBookAvgRating`, `setBookRating` in pocketbase.ts
- GET/POST /api/ratings/[slug] API route
- StarRating.svelte component with hover, animated stars, avg display
- Star rating shown on book detail page (desktop + mobile)
**Plan-to-Read shelf**
- `shelf` field added to `user_library` (reading/plan_to_read/completed/dropped)
- `updateBookShelf`, `getShelfMap` in pocketbase.ts
- PATCH /api/library/[slug] for shelf updates
- Shelf selector dropdown on book detail page (only when saved)
- Shelf tabs on library page to filter by category
**Sleep timer**
- `sleepUntil` state added to AudioStore
- Layout handles timer lifecycle (survives chapter navigation)
- Cycles Off → 15m → 30m → 45m → 60m → Off
- Shows live countdown in AudioPlayer when active
**EPUB export**
- Go backend: GET /api/export/{slug}?from=N&to=N
- Generates valid EPUB2 zip (mimetype uncompressed, OPF, NCX, XHTML chapters)
- Markdown → HTML via goldmark
- SvelteKit proxy at /api/export/[slug]
- Download button on book detail page (only when in library)
**Fix TS errors**
- discover/+page.svelte: currentBook possibly undefined (use {@const book})
- cardEl now $state for reactive binding
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the mini-player chapter drawer is opened, the current chapter is
now immediately scrolled into the center of the list instead of always
starting from the top. Uses a Svelte action (setIfActive) to track the
active chapter element and a $effect to call scrollIntoView on open.
- New /discover page with swipe UI: left=skip, right=like, up=read now, down=nope
- Onboarding modal to collect genre/status preferences (persisted in localStorage)
- 3-card stack with pointer-event drag, CSS fly-out animation, 5 action buttons
- Tap card for preview modal; empty state with deck reset
- Like/read-now auto-saves book to user library
- POST /api/discover/vote + DELETE for deck reset
- Discovery vote persistence via PocketBase discovery_votes collection
- Fix duplicate books: dedup by slug in getBooksBySlugs
- Fix WriteMetadata TOCTOU race: conflict-retry on concurrent insert
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace static Polar checkout links with a server-side POST /api/checkout
route that creates a checkout session with customer_external_id = user ID
and customer_email locked (not editable). Adds loading/error states and
a post-checkout success banner on the profile page.
- Fix customer email path: was data.customer_email, is actually
data.customer.email per Polar v1 API schema
- Add resolveUser() helper: tries polar_customer_id → email → external_id
- Add subscription.active and subscription.canceled event handling
- Handle order.created for fast-path pro upgrade on purchase
- Profile page: fetch user email + polarCustomerId from PocketBase
- Profile page: pre-fill ?customer_email= on checkout links
- Profile page: link to polar.sh/purchases for existing customers
- Add +layout.server.ts to enforce admin role check at layout level,
preventing 404 on /admin and protecting all sub-routes centrally
- Add +page.server.ts to redirect /admin → /admin/scrape (was 404)
The fixed top bar was hidden behind the main site navbar (z-50).
Replace with an inline 'Admin menu' button at the top of the content.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The admin_nav_* message files generated by paraglide used 0-param inner
functions but called them with inputs, causing 50 svelte-check type errors.
Add _inputs = {} to each inner locale function to match the call signature.
Also adds backend/backend and backend/runner to .gitignore — binaries are
built inside Dockerfile and should never be committed.
The paraglideVitePlugin runs at build time (buildStart hook) and fetches
the inlang plugin from cdn.jsdelivr.net to recompile messages. This:
1. Overwrites messages.js with 'export * as m from ...' unconditionally
2. Causes Rollup SSR tree-shaking to replace all m.*() calls with (void 0)
3. Crashes every page server-side with 'TypeError: (void 0) is not a function'
The plugin is no longer needed: compiled paraglide output is committed to
git and updated via 'npm run paraglide' when messages change. Removing the
plugin lets Vite treat messages.js as a plain static module, keeping all
exports intact through the SSR bundle.
- ui/src/error.html: custom SvelteKit last-resort fallback (replaces
the bare '500 | Internal Error' shown when +error.svelte itself fails)
— branded, auto-refreshes in 20s, book+lightning SVG illustration
- ui/src/routes/+error.svelte: improved with context-aware SVG
illustrations (question mark book for 404, lightning bolt for 5xx),
larger status watermark, and a Retry button on non-404 errors
- caddy/errors/500.html: new static error page matching the 502/503/504
design — served by Caddy when a gateway-level 500 occurs
- Caddyfile: add handle_errors 500 block pointing at /srv/errors/500.html
- caddy/Dockerfile: COPY errors/ into image so static pages are baked in
These files are needed at CI check time. paraglide fetches its plugin
from cdn.jsdelivr.net which is unavailable in the CI environment,
causing compile to produce empty output. Committing the generated
output means CI never needs to recompile them.
paraglide-js fetches its plugin from cdn.jsdelivr.net at compile time.
In CI (no outbound internet access), this silently produces an empty
_index.js, causing svelte-check to fail with 'not a module' errors.
Fix:
- Commit all generated src/lib/paraglide/ files to git (removing
the auto-generated .gitignore that was hiding them)
- Simplify prepare script to only run svelte-kit sync
- Add separate 'npm run paraglide' script for developers to
regenerate when messages/*.json source files change
paraglide-js unconditionally emits 'export * as m from ...' in messages.js
which causes Vite/Rollup SSR to tree-shake all named message imports,
replacing every m.*() call with (void 0)() and crashing every page.
Strip the two offending lines via a Node.js one-liner in the prepare script
so the fix survives every npm ci run in CI.
Also stop tracking messages.js in git since it is always regenerated.
The paraglide messages.js had an extra 'export * as m from ...' line which
caused Rollup/Vite to tree-shake all actual message function imports in the
SSR bundle. Every m.* call compiled to (void 0)(), crashing every page
server-side with TypeError. Removed the duplicate namespace re-export.
Collections without an 'updated' field (books, user_sessions, user_settings,
user_library) were returning 400 because listOne always sent sort=-updated.
Changed default to empty string since we only fetch 1 record (no ordering needed).
- CommentsSection now accepts a chapter prop and scopes comments to that chapter
- Chapter reader page mounts CommentsSection with current chapter number
- Book detail page shows rolling 7-day unique reader count badge
- API GET/POST pass chapter param; pocketbase listComments filters by chapter
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- First continue-reading book becomes a wide hero card with title,
description, genre tags, and a prominent Resume ch.N CTA
- Remaining in-progress books move to a horizontal scroll shelf
- Recently Updated deduplicates by slug; books with multiple new
chapters show a green "+N ch." badge
- Genre discovery strip (horizontal scroll) links to /catalogue?genre=X
- Stats demoted to a subtle two-number footer bar
- All rows use horizontal scroll instead of fixed grids
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three light variants mirroring the existing dark set. All use CSS custom
properties so no component changes are needed. Theme dots in the header
and mobile drawer show a separator between dark/light groups; light-theme
swatches get a subtle ring so they're visible on light backgrounds.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove both "Save settings" buttons; all settings now auto-save with
800ms debounce and show a transient "✓ Saved" indicator
- Apply theme, font family, and font size to context immediately on
change so the preview is live without waiting for the save
- Merge Appearance + Reading settings into a single Preferences card
with dividers — fewer sections, less visual noise
- Pro users see a compact subscription row; free users see upgrade CTAs
- Speed label splits value and units (shows "1.5x" separately in brand
color) for cleaner readout
- Auto-advance toggle gains a subtitle description
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: user_settings table was missing theme, locale, font_family,
font_size columns — PocketBase silently dropped them on every save.
Added the four columns via PocketBase API.
Also:
- listOne now sorts by -updated so the most-recent settings record wins
- PARAGLIDE_LOCALE cookie is now cleared when switching back to English
- pt-BR renamed to pt throughout (messages, inlang settings, validLocales)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sessions not seen in 30+ days are deleted in the background each time
a new session is created. No cron job needed — self-cleaning on login.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Desktop header before: logo · Library · Catalogue · Feedback · ●●● · EN RU ID PT-BR FR · Admin · username · Sign out
Desktop header after: logo · Library · Catalogue · Feedback · ●●● · [🌐 EN ▾] · [avatar ▾]
Changes:
- Theme dots kept but made slightly smaller (3.5 → 3.5, less gap)
- 5 bare language codes replaced with a compact globe + current locale
dropdown (click to expand all 5 options)
- Admin link, username, and Sign out collapsed into a single user-initial
avatar dropdown (Profile / Admin panel / Sign out)
- Click-outside overlay closes any open dropdown
- Mobile drawer: added Theme and Language rows so users can switch both
without opening the desktop header
- Footer locale switcher removed (redundant with header and mobile drawer)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reading content is now publicly accessible without login:
- /catalogue, /books/[slug], /books/[slug]/chapters, /books/[slug]/chapters/[n]
are all public — no login redirect
- /books (personal library), /profile, /admin remain protected
Unauthenticated UX:
- Chapter page: "Audio narration available — Sign in to listen" banner
replaces the audio player for logged-out visitors
- Book detail: bookmark icon links to /login instead of triggering
the save action (both desktop and mobile CTAs)
SEO:
- robots.txt updated: Allow /books/ and /catalogue, Disallow /books (library)
- sitemap.xml now includes /catalogue as primary crawl entry point
i18n: added reader_signin_for_audio, reader_signin_audio_desc,
book_detail_signin_to_save in all 5 languages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix language not persisting after refresh: save locale in user_settings,
set PARAGLIDE_LOCALE cookie from DB preference on server load
- Fix theme change not applying: context setter was mismatched (setTheme vs current)
- Add font family (system/serif/mono) and text size (sm/md/lg/xl) user settings
stored in DB and applied via CSS custom properties
- Add theme color dots and language picker to desktop header for quick access
- Footer locale switcher now saves preference to DB before switching
- Remove change password section (OAuth-only, no password login)
- Fix active sessions piling up: reuse existing session on re-login via OAuth
- Extend speed step cycle to include 2.5× and 3.0× (matching profile slider)
- Replace plain checkbox with modern toggle switch for auto-next setting
- Fix catalogue status labels (ongoing/completed) to use translation keys
- Add font family and text size translation keys to all 5 locale files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Webhook handler verifies HMAC-SHA256 sig and updates user role on
subscription.created / subscription.updated / subscription.revoked
- Audio endpoint gated: free users limited to 3 chapters/day via Valkey
counter; returns 402 {error:'pro_required'} when limit reached
- Translation proxy endpoint enforces 402 for non-pro users
- AudioPlayer.svelte surfaces 402 via onProRequired callback + upgrade banner
- Chapter page shows lock icon + upgrade prompts for gated translation langs
- Profile page: subscription section shows Pro badge + manage link (active)
or monthly/annual checkout buttons (free); isPro resolved fresh from DB
- i18n: 13 new profile_subscription_* keys across all 5 locales
The paraglide output dir has its own .gitignore (ignores everything), so
generated files are never committed. CI ran `npm ci` then `svelte-check`
without ever invoking vite, so $lib/paraglide/server was missing at
type-check time.
Adding `paraglide-js compile` to the prepare script ensures the output is
regenerated during `npm ci` before svelte-kit sync and svelte-check run.
Also adds translation_jobs collection to pb-init-v3.sh.