Commit Graph

157 Commits

Author SHA1 Message Date
Admin
0d7b985469 fix(audio): return 404 from presign when audio object not yet uploaded
handlePresignAudio now checks AudioExists before presigning; previously it
would generate a valid-looking signed URL for a non-existent object, causing
the browser to get 404 from MinIO directly.

- Add AudioExists to Store interface and HybridStore
- Guard handlePresignAudio with AudioExists check, return 404 if missing
- Propagate 404 through presignAudio() in minio.ts (typed error.status=404)
- SvelteKit presign route forwards 404 to the browser instead of 500
2026-03-04 15:21:17 +05:00
Admin
53af7515a3 feat(ui): replace two-button audio UI with single smart play button and pseudo progress bar
- Single 'Play narration' button: checks presign first, plays immediately if audio exists, otherwise triggers generation
- During generation shows an animated pseudo progress bar: slow start (4%/s) → accelerates (12%/s at 30%) → slows near 80% (4%/s) → crawls to 99% (0.3%/s)
- When generation completes, bar jumps to 100% then transitions to audio player
- Auto-plays audio after generation completes
2026-03-04 15:15:23 +05:00
Admin
11a846d043 feat(scraper): rewrite browse storage to domain/html+assets structure, populate ranking from snapshot
- Replace BrowsePageKey(genre/sort/status/type/page) with BrowseHTMLKey(domain, page) -> {domain}/html/page-{n}.html
- Add BrowseCoverKey(domain, slug) -> {domain}/assets/book-covers/{slug}.jpg
- Add SaveBrowseAsset/GetBrowseAsset for binary assets in browse bucket
- Rewrite triggerBrowseSnapshot: after storing HTML, parse it, upsert ranking records with MinIO cover keys, fire per-novel cover download goroutines
- Add handleGetCover endpoint (GET /api/cover/{domain}/{slug}) to proxy cover images from MinIO
- handleGetRanking rewrites MinIO cover keys to /api/cover/... proxy URLs
- Update save-browse CLI to use BrowseHTMLKey, populate ranking, and download covers
2026-03-04 15:13:09 +05:00
Admin
bf2ffa54db fix(scraper): add EnsureMigrations to patch progress.user_id on startup
On every scraper startup, EnsureMigrations fetches each collection schema
and PATCHes in any missing fields. This repairs the progress collection on
cloud deploys where pb-init ran before user_id was added to the schema.
No-ops when the field already exists.
2026-03-04 14:59:09 +05:00
Admin
fe204598a2 fix(pb-init): rewrite ensure_field with pure sh/awk, fix HTTP status parsing
The previous ensure_fields helper used python3 which is not available in
alpine:3.19, causing exit 127 in the cloud init container.

Replaced with ensure_field (per-field, no python/jq) that uses only
busybox sh, wget, sed, and awk. Also fixed the HTTP status grep pattern
in create_collection and ensure_field to match only the response header
line (^  HTTP/) instead of the wget error line, eliminating the spurious
'unexpected status server' warnings.
2026-03-04 14:00:31 +05:00
Admin
9906c7d862 fix(pb-init): add user_id field migration for progress collection
The progress collection was created before user_id was added to the schema,
causing all user-keyed progress queries to return 400. Add an ensure_fields
helper that idempotently patches existing collections with missing fields,
and run it for progress.user_id on every init.
2026-03-04 13:49:55 +05:00
Admin
06feb91f4f feat(ui): add homepage with Continue Reading, Recently Updated, and stats
Replace the single-button placeholder with a full homepage:
- Stats bar: total books, total chapters, books in progress
- Continue Reading grid (up to 6): links directly to last chapter read
- Recently Updated grid (up to 6): recent books not already in progress
- Empty state with Discover Novels CTA when library is empty

New pocketbase.ts helpers: recentlyAddedBooks(), getHomeStats(),
listN(), countCollection().
2026-03-04 12:55:03 +05:00
Admin
5a7751e6d1 fix(scraper): replace glibc binary with npm single-file-cli on node:22-alpine
The pre-compiled single-file binary requires glibc symbols (__res_init)
that Alpine's gcompat shim does not provide, causing exit status 127.
Switch runtime base to node:22-alpine and install single-file-cli via npm.
Also add .dockerignore to exclude bin/ and static/ from build context.
2026-03-04 12:54:51 +05:00
Admin
555973c053 feat(browse): add SingleFile browse-page snapshot cache via MinIO
- New MinIO bucket 'libnovel-browse' (MINIO_BUCKET_BROWSE env) for storing
  self-contained HTML snapshots of novelfire browse pages
- Store interface gains SaveBrowsePage / GetBrowsePage / BrowsePageKey methods
- handleBrowse is now cache-first: serves from MinIO snapshot when available,
  then fires a background triggerBrowseSnapshot goroutine to populate cache
  on live-fetch (de-duplicated, 90s timeout)
- New 'save-browse' CLI subcommand to bulk-capture pages via SingleFile CLI
- Dockerfile: downloads pinned single-file-x86_64-linux binary (v2.0.83),
  adds gcompat + libstdc++ to Alpine runtime for glibc compatibility
- docker-compose: adds libnovel-browse bucket init and SINGLEFILE_PATH env
- .gitignore: exclude scraper/scraper build artifact
2026-03-04 10:42:26 +05:00
Admin
c2d6ce1c5b feat(ui): merge Browse and Ranking into a unified Discover page
The /ranking route is removed. /browse becomes the Discover page with:

- Sort=Ranking option: fetches from /api/ranking (richer metadata — author,
  genres, source_url) and renders the full pre-computed ranked list
- Sort=Popular/New/Updated: fetches from /api/browse as before, paginated
- Grid / List view toggle: grid is the default for browse; list auto-selects
  for ranking and gives the information-dense ranked layout (was /ranking)
- Admin Refresh catalogue button (moved from /ranking) and per-novel Scrape
  button work in both views
- Genre and status filters are disabled (visually dimmed) when sort=rank
  since the ranking endpoint does not support per-page filtering
- Nav: 'Browse' renamed to 'Discover', 'Ranking' link removed
2026-03-04 10:11:22 +05:00
Admin
8edad54b10 feat(progress): associate progress with logged-in user for cross-device sync
Add optional user_id field to the progress collection. When a user is
authenticated, progress is keyed by user_id instead of session_id, making
it portable across devices and browsers. Anonymous reading still works via
session_id as before.

On login or registration, any progress accumulated under the anonymous
session is merged into the user's account (most-recent timestamp wins),
so chapters read before logging in are not lost.

- scripts/pb-init.sh: add user_id field to progress collection schema
- scraper/internal/storage/pocketbase.go: add user_id to EnsureCollections
- ui/src/lib/server/pocketbase.ts: Progress interface gains user_id;
  getProgress/allProgress/setProgress accept optional userId and query/write
  by user_id when present; add mergeSessionProgress() helper
- ui/src/routes/login/+page.server.ts: call mergeSessionProgress after
  successful login and registration (fire-and-forget, non-fatal)
- ui/src/routes/api/progress/+server.ts: pass locals.user?.id to setProgress
- ui/src/routes/books/+page.server.ts: pass locals.user?.id to allProgress
- ui/src/routes/books/[slug]/+page.server.ts: pass locals.user?.id to getProgress
2026-03-04 09:51:11 +05:00
Admin
48d8fdb6b9 feat: udpate minio endpoint env name 2026-03-04 01:19:31 +05:00
Admin
1b05b6ebc6 fix(audio): sign presigned audio URLs with public MinIO endpoint
Add a second MinIO client (pub) initialized with MINIO_PUBLIC_ENDPOINT so
presigned audio URLs are signed against the public hostname from the start,
rather than signed internally and then rewritten. This avoids AWS4 signature
mismatch (SignatureDoesNotMatch 403) that occurred when the signed host was
substituted after signing.

- storage/minio.go: add PublicEndpoint/PublicUseSSL to MinioConfig; add pub
  client field; NewMinioClient creates pub client when public endpoint differs;
  PresignAudio uses pub, PresignChapter keeps internal client
- cmd/scraper/main.go: wire MINIO_PUBLIC_ENDPOINT and MINIO_PUBLIC_USE_SSL env vars
- docker-compose.yml: expose MINIO_PUBLIC_ENDPOINT and MINIO_PUBLIC_USE_SSL to scraper service
- ui/src/lib/server/minio.ts: remove rewriteHost() call from presignAudio
2026-03-04 01:15:46 +05:00
Admin
cabdd3ffdd fix(ui/minio): don't rewrite presigned URL host for server-side chapter fetches
The SvelteKit server fetches chapter markdown server-side using the presigned
URL. rewriteHost() was replacing the internal minio:9000 host with the public
PUBLIC_MINIO_PUBLIC_URL (localhost:9000), which is unreachable from inside
Docker, causing MinIO 403/connection errors.

presignChapter now takes an optional rewrite=false parameter — the server-side
load function gets the raw internal URL, while any future browser-facing use
can pass rewrite=true to get the public-facing URL.
2026-03-04 01:06:52 +05:00
Admin
f80b83309a fix(ui): install prod deps in Docker runtime; add reindex endpoint for chapters_idx
- ui/Dockerfile: copy package-lock.json and run npm ci --omit=dev in the
  runtime stage so marked (and other runtime deps) are available to
  adapter-node at startup — fixes ERR_MODULE_NOT_FOUND for 'marked'
- storage: add ReindexChapters to Store interface and HybridStore — walks
  MinIO objects for a slug, reads chapter titles, upserts chapters_idx
- server: add POST /api/reindex/{slug} to rebuild chapters_idx from MinIO
2026-03-04 00:59:36 +05:00
Admin
49ba2c27c2 fix(ui/books): handle null genres field in parseGenres 2026-03-04 00:51:58 +05:00
Admin
353d7397eb fix(ui/books): guard null book fields in template; log null titles from PocketBase 2026-03-04 00:49:50 +05:00
Admin
89ff90629f fix(ui/books): guard against null items from PocketBase listAll 2026-03-04 00:46:40 +05:00
Admin
f6febfdb5e fix(ui/browse): use value prop on select elements to fix filter state
Replace selected={data.x === opt.value} on individual <option> elements
with value={data.x} on the <select> — the idiomatic Svelte approach that
ensures correct hydration and form submission of the current filter values.
2026-03-04 00:40:46 +05:00
Admin
2c43907e34 feat(ui): add Ranking page with admin refresh action and nav link
Adds /ranking route showing the cached ranking list with cover, title,
author, status, and genres. Admin users get a Refresh button that
triggers a full catalogue scrape. Adds Ranking to the nav.
2026-03-04 00:40:39 +05:00
Admin
0e868506ca feat(server): track scrape jobs in scraping_tasks and expose GET /api/scrape/tasks
runAsync creates a scraping_tasks record on job start, flushes progress
counters via OnProgress, and finalizes status (done/failed/cancelled) on
completion. Adds GET /api/scrape/tasks to list all historical jobs.
Also fixes relative cover URLs in parseBrowsePage.
2026-03-04 00:40:32 +05:00
Admin
1b234754e8 feat(orchestrator): add Progress type and OnProgress callback
Adds atomic counters for books_found, chapters_scraped, chapters_skipped,
and errors. The new OnProgress callback fires after each counter update
and once more at the end of Run, giving callers a live progress feed.
2026-03-04 00:40:24 +05:00
Admin
041099598b feat(storage): add scraping_tasks collection and Store interface methods
Adds a scraping_tasks PocketBase collection with fields for kind, status,
progress counters, timestamps, and error info. Exposes CreateScrapeTask,
UpdateScrapeTask, and ListScrapeTasks on the Store interface with
implementations in HybridStore and PocketBaseStore.
2026-03-04 00:40:17 +05:00
Admin
333c8ad868 fix(scraper): prepend base URL for relative cover images in metadata scrape 2026-03-04 00:40:04 +05:00
Admin
d16ae00537 fix(config): align env var names with docker-compose (POCKETBASE_ADMIN_EMAIL/PASSWORD) 2026-03-03 23:56:06 +05:00
Admin
d16313bb6c feat(observability): log pocketbase_url and pocketbase_email at startup 2026-03-03 23:53:55 +05:00
Admin
1bab7028c6 fix: remove name on libnovel ui 2026-03-03 23:07:01 +05:00
Admin
6520fb9a50 chore(infra): remove static_books volume from scraper service
No longer needed — chapters and audio are stored in MinIO, metadata in
PocketBase. The legacy filesystem writer is not used in production.
2026-03-03 22:49:05 +05:00
Admin
7acf04fb9f fix(storage): surface all silent errors with structured logging
- Inject *slog.Logger into HybridStore, PocketBaseStore, and pbClient
- Fix credential defaults in main.go (changeme123 / admin) to match docker-compose
- listOne/listAll/upsert/deleteWhere now return errors on non-2xx HTTP status
- WriteChapter: log warn instead of discarding UpsertChapterIdx error
- MetadataMtime, GetAudioCache, GetProgress: log warn on PocketBase failures
- EnsureCollections: log info/debug/warn per outcome instead of _ = err
- CountChapterIdx: log warn on failure instead of silently returning 0
- server: log warn when SetAudioCache fails after audio generation
- NewHybridStore: add explicit Ping() before EnsureCollections for fast-fail on bad credentials
2026-03-03 22:31:14 +05:00
Admin
c2bcb2b0a6 feat: update port on broswerless deployment 2026-03-03 20:49:42 +05:00
Admin
cfd893d24b perf(scraper): switch chapter list to direct HTTP, remove Browserless dependency
novelfire.net chapter-list pages (/chapters?page=N) are server-rendered —
verified via curl. Switch urlClient to NewDirectHTTPClient alongside the
existing chapterClient. Remove BROWSERLESS_URL_STRATEGY env var and clean
up the now-irrelevant WaitFor/GotoOptions fields from both ScrapeChapterList
and ScrapeChapterListPage ContentRequests.
2026-03-03 20:46:22 +05:00
Admin
cff0c78b4f perf(scraper): use direct HTTP for chapter text fetching, bypass Browserless
novelfire.net chapter content is server-rendered, so Browserless is not
needed. Add a dedicated chapterClient (always StrategyDirect) to Scraper
and use it in ScrapeChapterText, removing the now-irrelevant WaitFor /
RejectResourceTypes / GotoOptions fields from the ContentRequest.
2026-03-03 20:40:01 +05:00
Admin
d89cefe975 fix(auth): add Bearer prefix to PocketBase Authorization header
PocketBase v0.23+ requires 'Bearer <token>' — sending the raw JWT without
the prefix results in 403 'Only superusers can perform this action' on all
collection record endpoints. Fix applied in three places:
- ui/src/lib/server/pocketbase.ts (pbGet, pbPost, pbPatch helpers)
- scraper/internal/storage/pocketbase.go (pbClient.do)
- scripts/pb-init.sh (wget --header in create_collection)
2026-03-03 20:15:44 +05:00
Admin
a0344b36d7 feat(infra): add pb-init sidecar to create PocketBase collections on startup
Add scripts/pb-init.sh — an idempotent alpine sh script that authenticates
with the PocketBase admin API and POSTs all required collection schemas
(books, chapters_idx, ranking, progress, audio_cache, app_users) before
any application service starts. 400/422 responses are treated as success so
it is safe to run on every docker compose up.

Wire pb-init into docker-compose.yml:
- pb-init service: alpine:3.19, depends on pocketbase healthy, mounts script
- scraper and ui both depend on pb-init via service_completed_successfully,
  guaranteeing collections exist before the first request hits app_users
2026-03-03 20:09:25 +05:00
Admin
af3c487afb feat(e2e): use direct HTTP for chapter scraping, cap TTS at 200 chars
- e2e fixture: replace single contentClient with directClient (plain HTTP)
  for chapter/metadata/ranking + contentClient (Browserless) for urlClient
  only — matches production wiring and is significantly faster
- server: add max_chars field to audio request body; truncates stripped text
  to N runes before sending to Kokoro (used by e2e for quick TTS tests)
- fix: move RankingItem to scraper package to break novelfire→storage import
  cycle; storage.RankingItem is now a type alias for backward compat
- fix: update stale New() call in novelfire integration test (missing args)
- fix: replace removed blob-ranking methods in storage integration test with
  current per-item API (UpsertRankingItem/ListRankingItems/RankingLastUpdated)
- justfile: add test-e2e and e2e tasks
2026-03-03 20:02:15 +05:00
Admin
b8d4d94b18 refactor(ranking): replace blob cache with per-item PocketBase storage
- Replace SetRanking/GetRanking/SetRankingPageHTML/GetRankingPageHTML blob methods
  with WriteRankingItem/ReadRankingItems/RankingFreshEnough per-item operations
- Add 24h staleness gate in ScrapeRanking to skip re-scraping fresh data
- Add GET /api/ranking endpoint returning []RankingItem sorted by rank
- Remove RankingPageCacher interface and rankingCacheAdapter adapter
- Update integration tests to use new per-item upsert semantics
- Include e2e test suite (scraper/internal/e2e/)
2026-03-03 19:37:49 +05:00
Admin
56bf4dde22 fix(auth): update PocketBase v0.23+ auth endpoint and rename users collection
- Replace deprecated /api/admins/auth-with-password with
  /api/collections/_superusers/auth-with-password in both the
  SvelteKit UI (pocketbase.ts) and Go scraper (pocketbase.go)
- Rename custom users collection to app_users to avoid name clash
  with PocketBase's built-in users auth collection
- Fix docker-compose volume mount path pb/pb_data -> pb_data so
  persisted data matches the entrypoint --dir flag
- Add PB_ADMIN_EMAIL/PB_ADMIN_PASSWORD env vars to pocketbase service
  so the superuser is auto-created on first boot
2026-03-03 16:59:25 +05:00
Admin
2f0857be45 test: add integration tests for storage, server, and scrape+store flows
- scraper/internal/storage/integration_test.go: MinioClient and PocketBaseStore
  integration tests covering chapter/audio round-trips, presign URLs, book
  metadata, chapter index, ranking, progress, and audio cache CRUD
- scraper/internal/storage/hybrid_integration_test.go: HybridStore end-to-end
  tests for metadata, chapters, ranking, progress, presign, and audio cache
- scraper/internal/storage/scrape_integration_test.go: live Browserless + storage
  tests that scrape book metadata and first 3 chapters, store them, and verify
  the round-trip via HybridStore
- scraper/internal/server/integration_test.go: HTTP server functional tests for
  health, scrape status, presign chapter, reading progress, and chapter-text
  endpoints against real MinIO + PocketBase backends
- justfile: task runner at repo root with recipes for build, test, lint, UI,
  docker-compose, and individual service management
2026-03-03 14:54:43 +05:00
Admin
bf5774d8d0 feat(ui): add structured JSON logging to all server-side routes and lib
Introduces a logger.ts module emitting slog-compatible JSON lines to stderr.
Replaces silent catch blocks and console.error calls throughout minio.ts,
pocketbase.ts, hooks.server.ts, login, books, browse, and all API routes so
auth/registration failures, MinIO presign errors, and scraper proxy failures
are now visible in container logs.
2026-03-03 14:34:48 +05:00
Admin
5131ae0bc4 feat(ui): update nav — show Browse link, username, and Sign out when logged in 2026-03-03 14:03:32 +05:00
Admin
9fa0776258 feat(ui): add /browse page and /api/scrape proxy route
- /browse calls GET /api/browse on the scraper and renders a novel grid mirroring the
  novelfire layout: cover, rank/rating badges, chapter count, genre/sort/status filters,
  and pagation controls
- Scrape buttons are shown only to admin users; clicking enqueues the book via /api/scrape
- /api/scrape is an admin-only SvelteKit server route that proxies POST requests to the
  Go scraper's /scrape/book or /scrape endpoints; returns 403 for non-admins
2026-03-03 14:03:27 +05:00
Admin
f265d9d020 feat(scraper): add GET /api/browse and GET /api/scrape/status endpoints
- GET /api/browse fetches novelfire.net catalogue page and parses it with golang.org/x/net/html;
  returns JSON {novels, page, hasNext} with per-novel slug/title/cover/rank/rating/chapters/url.
  Supports page, genre, sort, status query params.
- GET /api/scrape/status returns {"running": bool} for polling job state from the UI
2026-03-03 14:03:19 +05:00
Admin
3c26dfe2c0 feat(auth): add user authentication with roles, HMAC-signed cookies, and login/register UI
- Add `users` PocketBase collection (username, password_hash, role, created)
- Implement HMAC-SHA256 signed cookie auth in hooks.server.ts; token payload is userId:username:role
- Add User type, getUserByUsername, createUser (scrypt), loginUser (timing-safe) to pocketbase.ts
- Add login/register page with tabbed form UI and server actions
- Add logout route that clears the auth cookie
- Add layout.server.ts auth guard: redirect unauthenticated users to /login
- Extend App.Locals and App.PageData with role field
- Add AUTH_SECRET, POCKETBASE_ADMIN_EMAIL/PASSWORD to .env.example
- Install @types/node for Node crypto/scrypt types
2026-03-03 14:03:11 +05:00
Admin
1820fa7303 chore: make browserless port and UI port configurable via env vars 2026-03-03 13:32:05 +05:00
Admin
38e400a4c7 feat(scraper): remove HTMX UI — delete ui.go, extract helpers.go, drop goldmark dependency 2026-03-02 22:03:36 +05:00
Admin
cb90771248 refactor(scraper): rename /ui/audio*, /ui/chapter-text routes to /api/ namespace 2026-03-02 22:01:52 +05:00
Admin
59b1cfab1d chore: add all missing port and URL env vars to .env.example 2026-03-02 22:00:45 +05:00
Admin
f95ad3ed29 feat(ui): proxy audio generation and streaming through SvelteKit, fix hardcoded scraper URL in AudioPlayer 2026-03-02 22:00:13 +05:00
Admin
e4c4f8de66 feat(ui): wire SvelteKit into docker-compose with ui service and env vars 2026-03-02 21:43:32 +05:00
Admin
4f84bd29c9 feat(ui): add audio player component, presign API route, and reading progress tracking 2026-03-02 21:41:20 +05:00