- Discover loading skeleton: add borders and book icon placeholder so
card shapes are visible on all dark themes (was invisible on forest)
- Library grid cards: match catalogue hover style (brand border, surface
lift, shadow, -translate-y-0.5) and remove hardcoded zinc-500 border
- Reading progress bar: increase height 2px→3px and add brand glow shadow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Lighten --color-muted from #6b9a77 to #a3c9a8 — prose body text and
secondary text were blending into the near-black green background
- Surface colors lifted slightly for card depth distinction
- Border color made more visible (#1e3a24 → #2c4e34)
- brand-dim updated to green-500 for slightly brighter hover states
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Issue: Two sequential 'docker compose up' commands caused race condition:
- First command (--no-deps) starts removing containers
- Second command (--remove-orphans) tries to remove same containers
- Result: 'removal of container is already in progress' error
Fix: Combine into single command with both flags:
docker compose up -d --no-deps --remove-orphans <services>
This ensures atomic operation without race conditions.
- Catalogue/cover: rewrite raw scraped cover URLs to /api/cover/{domain}/{slug}
in handleCatalogue so all covers route through the backend proxy; fix broken
cdn.novelfire.net fallback in handleGetCover to read stored URL from PocketBase
- Catalogue/profile: add Svelte 5 onerror handlers on cover <img> tags to show
letter-initial placeholder when image fails to load
- Library page: read ?status URL param to initialise activeShelf tab on load so
/books?status=reading correctly pre-selects the Reading tab
- Sessions: filter bot/tool user-agents (curl, python, wget, etc.) and debug-IP
sessions from listUserSessions display; also purge them in pruneStaleUserSessions
- Profile: show email under username, quick stats chips (streak/chapters/completed)
in header, reading count on Library row, dedicated Sign out row, history covers
routed through /api/cover proxy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously: 'docker compose up -d' recreated all services with changed images,
causing dependent services (pocketbase, minio, redis, etc.) to restart and
wait for healthchecks, leading to longer downtime.
Now: Use '--no-deps' flag to restart ONLY the services with updated images
(backend, runner, ui, caddy, pocketbase) without touching their dependencies.
Benefits:
- Faster deployments (~15-20s vs ~60s)
- No unnecessary restarts of infrastructure services
- Reduced downtime for the application
The final 'docker compose up -d --remove-orphans' ensures any orphaned
containers are cleaned up and all services are in the desired state.
Homelab is on private network (192.168.0.109), so we can safely disable
strict host key checking. This avoids the complexity of managing known_hosts
entries in Gitea secrets.
Changes:
- Remove HOMELAB_SSH_KNOWN_HOSTS requirement
- Add -o StrictHostKeyChecking=no to scp/ssh commands
- Add -o UserKnownHostsFile=/dev/null to avoid host key persistence
We use homelab/docker-compose.yml (full stack) for the homelab deployment,
not homelab/runner/docker-compose.yml (runner-only subset). Removing the
unused directory to prevent confusion.
CRITICAL FIX: The homelab server runs the full stack (runner + GlitchTip +
observability tools), not just the runner. Copying homelab/runner/docker-compose.yml
would have destroyed all other services.
Changed: homelab/runner/docker-compose.yml → homelab/docker-compose.yml
Content visibility:
- Add `visibility` field to books ("public" | "admin_only"); new migration
backfills all existing scraped books to admin_only
- Meilisearch: add visibility as filterable attribute; catalogue/search
endpoints filter to public-only for non-admin requests
- Admin users identified by bearer token bypass the filter and see all books
- All PocketBase discovery queries (trending, recommended, recently-updated,
audio shelf, discover, subscription feed) now filter to visibility=public
- New scraped books default to admin_only; WriteMetadata preserves existing
visibility on PATCH (never overwrites)
Author submission:
- POST /api/admin/books/submit — creates a public book with submitted_by
- PATCH /api/admin/books/{slug}/publish / unpublish — toggle visibility
- SvelteKit proxies: /api/admin/books/[slug]/publish|unpublish
- /api/books/[slug] endpoint for admin book lookup
Frontend:
- backendFetchAdmin() helper sends admin token on any path
- Catalogue server load uses admin fetch when user is admin
- /submit page: author submission form with genre picker and rights assertion
- "Publish" nav link shown to all logged-in users
- Admin catalogue-tools: visibility management panel (load book by slug, toggle)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add deploy-homelab job to sync homelab/runner/docker-compose.yml
- Rename deploy → deploy-prod for clarity
- Both deployments run in parallel after Docker images are pushed
- Homelab runner pulls only the runner image and restarts
Required secrets (to be added in Gitea):
- HOMELAB_HOST (192.168.0.109)
- HOMELAB_USER (root)
- HOMELAB_SSH_KEY (same as PROD_SSH_KEY or separate)
- HOMELAB_SSH_KNOWN_HOSTS (ssh-keyscan -H 192.168.0.109)
- Remove redundant Skip/Read Now/Like buttons from desktop right panel
- Main action buttons in center area remain visible on both mobile and desktop
- Keyboard shortcuts hint still available at bottom
- Cleaner UI with no repeated functionality
Book Info Page Improvements:
- Add quick stats row (chapters, rating, readers)
- Enhance author display with better typography
- Improve genre tags with amber-branded pills
- Add green badge for 'ongoing' status
- Wrap summary in card with better styling
- Enhance 'More' button with proper design
- Improve StarRating component to show rating more prominently
Discover Page Improvements:
- Add progress bar showing completion through deck
- Enhance keyboard shortcuts with visual kbd elements
- Improve empty state with better visuals and CTA hierarchy
- Enhance toast notifications with icons and color-coded backgrounds
- Add auto-dismiss for toast (4s timeout)
- Improve preferences modal button labels
- Add undo functionality for last vote
- Better stat display (X of Y remaining)
All changes maintain consistency with LibNovel's design system.
- Bump all section headings to text-lg for visual hierarchy
- Replace SVG book icon no-cover placeholders with first-letter avatars
across Completed, Ready to Listen, Trending, Recommendations, Recently
Updated, and From Following shelves
- Highlight user's top genre pill in Browse by Genre strip
- Add "View all → /catalogue?genre=…" link to Because You Read section
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Import: fix "1/1/1" date display (Go zero time.Time → show dash)
- AI Jobs: guard fmtDate against zero-time dates
- AI Jobs: add "Cancel all in-flight (N)" bulk action button
- AI Jobs sidebar: show live running+pending count badge from layout
- Notifications: add broadcast panel linking to push.libnovel.cc, relabel inbox section
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Persist audio filter in URL (?audio=1) via history.replaceState
- Show total novel count in browse mode subtitle
- Close filter panel automatically on Apply
- Replace missing-cover SVG placeholder with styled first-letter avatar
- Add forbidden scrape badge to list view (was missing, grid had it)
- Carry audio param through applyFilters() navigation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove UI build artifact upload/download steps (18 lines removed)
- Remove .dockerignore manipulation workaround
- Always build UI from source inside Docker (more reliable)
- Remove PREBUILT arg from ui/Dockerfile and docker-bake.hcl
This fixes the '/app/build not found' error in CI by eliminating the fragile
artifact-passing mechanism. UI now builds fresh in Docker using build cache,
same as local development.
The Gitea runner's docker buildx doesn't support HCL locals{} or function{}
blocks (added in buildx 0.12+). Replace with plain variables: VERSION and
MAJOR_MINOR are pre-computed in a CI step and passed as env vars to bake.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
docker-bake.hcl now owns semver tag generation via HCL locals + a reusable
img_tags() function: GIT_TAG="v4.1.5" → :4.1.5, :4.1, :latest on all images.
The Docker job shrinks from 13 steps to 6 — no docker/metadata-action needed.
CI simply passes GIT_TAG, COMMIT, BUILD_TIME as env vars to bake-action.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
docker compose pull without arguments pulls ALL services including
getmeili/meilisearch:latest — a recent Meilisearch version bump broke
compatibility with existing data on the server, causing meilisearch to
crash immediately on restart.
CI deploy now only pulls the 5 custom app images (backend, runner, ui,
caddy, pocketbase). Infrastructure images (meilisearch, valkey, minio,
redis, crowdsec) are updated deliberately via `just pull-infra`.
Also add pocketbase to `just pull-images` now that it's a custom image.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace 5 sequential build-push-action steps with a single docker/bake-action
call backed by docker-bake.hcl. BuildKit now builds all images in parallel:
backend/runner/pocketbase share one Go builder stage (compiled once), while
caddy and ui build concurrently alongside the Go targets.
Workflow YAML goes from ~130 lines of build steps to ~35. Adding a new image
now only requires a new target block in docker-bake.hcl.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove non-root user from pocketbase Docker image; the existing pb_data
volume was created by the previous root-running image so files are owned
by root — running as a non-root appuser caused an immediate permission
error and container exit
- Increase healthcheck retries to 10 and add start_period=30s so migrations
have time to run on first boot before liveness checks begin
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add wget to pocketbase Alpine image so the docker-compose healthcheck
(wget http://localhost:8090/api/health) can actually run
- Fix saveIfAbsent calling itself instead of app.Save(c) — was an
infinite recursion that would stack-overflow on a fresh install
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
release.yaml:
- Build and push kalekber/libnovel-pocketbase image on every release tag
- Add deploy job (runs after docker): copies docker-compose.yml from the
tagged commit to /opt/libnovel on prod, pulls new images, restarts
changed services with --remove-orphans (cleans up removed pb-init)
ci.yaml:
- Validate cmd/pocketbase builds on every branch push
Required new Gitea secrets: PROD_HOST, PROD_USER, PROD_SSH_KEY,
PROD_SSH_KNOWN_HOSTS (see deploy job comments for instructions).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each collection creator now skips creation if the collection already exists,
so deploying to an existing prod install no longer requires running
`migrate history-sync` manually. Migration 2 (missing fields) still applies
as normal since those fields genuinely don't exist yet.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a custom PocketBase binary (cmd/pocketbase) that embeds PocketBase as a
Go framework with version-controlled migrations, replacing the fragile 419-line
shell script. Migrations apply automatically on every `serve` startup.
Migration 1 (20260414000001): full baseline schema — all 21 collections, both
indexes on chapters_idx, and initial superuser creation from env vars.
Migration 2 (20260414000002): adds three fields found in code but missing from
the old script — books.rating, app_users.notify_new_chapters_push,
book_comments.chapter.
docker-compose: pocketbase service now uses the custom kalekber/libnovel-pocketbase
image; pb-init one-shot container removed (migrations replace it entirely).
Existing installs: run `migrate history-sync` once to mark migration 1 as done,
then restart — migration 2 will apply the three previously missing fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds BACKEND_ADMIN_TOKEN env var (set in Doppler) as a required Bearer
token for every admin route. Also fixes PocketBase filter injection in
notification queries and wires BACKEND_ADMIN_TOKEN through docker-compose
to both backend and ui services. Includes CLAUDE.md for AI assistant guidance.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Translation content is immutable once generated — add Cache-Control: public,
max-age=3600, stale-while-revalidate=86400 to handleTranslationRead so the
browser and any CDN reuse the response without hitting MinIO on repeat views.
Forward the header through the SvelteKit /api/translation/[slug]/[n] proxy
which previously stripped it by constructing a bare Content-Type-only Response.
Three bugs:
1. +page.server.ts returned an unawaited Promise — SvelteKit awaits it on
the server anyway so data.jobs arrived as a plain AIJob[] on the client,
not a Promise. The $effect calling .then() on an array silently failed,
leaving jobs=[] and the table empty. Fixed by awaiting in the load fn.
2. +page.svelte $effect updated to assign data.jobs directly (plain array)
instead of calling .then() on it.
3. handlers_textgen.go: final UpdateAIJob (payload+status write) used
r.Context() which is cancelled when the SSE client disconnects. If the
browser navigated away mid-job, results were silently dropped and the
payload stayed as the initial {pattern} stub with no results array.
Fixed by using context.Background() for the final write, matching the
pattern already used in handlers_image.go.
- Add bookstore.NotificationStore interface and wire it directly to *storage.Store
in Dependencies, bypassing the Asynq wrapper that caused Producer.(*storage.Store)
to fail with a 500 on every notification endpoint when Redis is configured
- Replace s.deps.Producer.(*storage.Store) type assertion in all 5 notification
handlers with s.deps.NotificationStore (nil-safe, always works)
- Fix PocketBase filter single-quote bug in ListNotifications, ClearAllNotifications,
and MarkAllNotificationsRead (PocketBase requires double quotes for string values)
The {#await ... then} + {@const} IIFE pattern for assigning to $state
variables stopped working reliably in Svelte 5.53+. Replaced with a
proper $effect that awaits both streamed promises and assigns to state,
which correctly triggers reactivity.
Also: switch library page selection mode entry from long-press to a
'Select' button in the page header.
Three compounding issues caused the 4+ second load:
1. getAllRatings() ran sequentially after the first Promise.all group,
adding it unnecessarily to the critical path. Now runs in parallel
with listBooks/getVotedSlugs/getSavedSlugs (all 4 concurrent).
2. discovery_votes was fetched twice on every page load — once inside
getBooksForDiscovery (via getVotedSlugs) and again by getVotedBooks.
Fixed by caching getVotedSlugs results with a 30s TTL so the second
call hits cache instead of PocketBase.
3. getVotedSlugs and getSavedSlugs were always uncached, hitting
PocketBase on every navigation. Added short-TTL per-user Valkey
cache entries (voted: 30s, saved: 60s). Cache is invalidated
immediately after each write (upsertDiscoveryVote, clearDiscoveryVotes,
undoDiscoveryVote, saveBook) so stale data is never served.
Removed the custom clear button (shown when query is non-empty) and
suppressed the browser-native webkit search cancel button via CSS.
Only the single Cancel button remains, avoiding the double/triple X
clutter on wider screens.
The SSE (non-async) chapter-names handler streamed results to the client
but never wrote them into the PocketBase job payload — only the initial
{pattern} stub was stored. The Review button then fetched the job and
found no results, showing 'No results found in this job's payload.'
Fix: accumulate allResults across batches (same as the async handler) and
write the full {pattern, slug, results:[...]} payload when marking done.
The wrapper div in +layout.svelte had pointer-events:none which blocked
all taps inside ListeningMode (chapter rows, buttons, scrolling). Removed
the wrapper div and moved the fly transition onto ListeningMode's own root
element so the slide-in animation works without stealing pointer events.
- Add notify_new_chapters_push field to AppUser, PATCH /api/profile, and profile loader
- Fix bell panel to reload notifications on every open (not just once on mount)
- Replace flat in-app + push toggles with structured category table (Category | In-app | Push)
- Add browser push master subscribe/unsubscribe row above the table
- Push column toggle disabled until browser is subscribed; shows — when unsupported/denied
- Update Notifications row hint to summarise active channels (In-app · Push / Off)