Show a clear too-many-attempts message on the login page when the server
throttles the request. Remove the nova and golden-ratio background options,
whose CSS definitions were already deleted.
In book_per_file mode the scanner identifies books by the file's absolute
path (not its parent directory). The finalize service was always using
dirname(destPath) regardless of organization mode, so the scanner would
see the freshly-moved file as unknown, create a second book record, and
then steal the book_file from the first record via the onConflictDoUpdate
upsert - leaving the original book as a ghost with a stale primary_file_id.
Fix: check library.organizationMode after fetching the library and pass
destPath directly as bookFolderPath when the mode is book_per_file.
Add a dedicated test case that asserts createBookRecord receives the file
path (not its parent dir) in book_per_file mode.
When APP_DATA_PATH is set to a relative path (e.g. ../local/data), the
stored absolutePath in book_dock_files was relative. The file watcher
(@parcel/watcher) always reports events with fully resolved absolute
paths, so findByAbsolutePath never matched an uploaded file, causing the
watcher to ingest it again as a duplicate record.
- Replace music-metadata and mediainfo with @ffprobe-installer for all audio metadata extraction; remove mediainfo dependency
- Show BookCoverPlaceholder in all missing-cover locations: Similar/Series/Author Books carousel, cover editor panel, metadata search result cards, and metadata diff panel
- Extend BookRecommendation and SeriesBookRecommendation shared types with hasCover and authors; update recommendation and book repositories to supply them via parallel queries
- Fix cover refresh not updating in grid: BookCoverCard now merges refreshed BookDetail into the local BookCard and emits update:book; VirtualBookGrid forwards the event; Home, Series, Author, and Collection views wire the handler so covers update without a page reload
- Add audiobook covers checkbox to the Online Search drawer so users can toggle between square and portrait cover searches; result thumbnails and loading skeletons adapt their aspect ratio to match the selection
Replace the live self-join scan (O(N^2), times out at 100K+ authors) with a
background-computed candidates table served instantly at read time.
Key changes:
- Add entity_duplicate_candidates and entity_duplicate_scan_status tables
- Background job batches 1K authors at a time (~58s total for 200K authors)
- Scan endpoint reads from the indexed table (<10ms regardless of scale)
- Candidates are invalidated after merge/delete, not recomputed from scratch
- Add GET /duplicates/status and POST /duplicates/refresh endpoints
- Frontend updates clusters locally after merge/dismiss (no re-scan)
- Status bar shows last computed time and recompute button with progress
- Author browse in entity manager now shows all authors, not just those with books
- overhaul EPUB/CBZ/PDF mobile header, footer, toolbar, sidebar, and settings interactions
- add EPUB search minimum query length enforcement with updated unit tests
- improve PDF rendering stability by drawing from an offscreen render canvas
- add app-shell level horizontal overflow/touch constraints to prevent sideways pan and double-tap zoom issues
- make statistics header controls and configure drawer responsive on mobile
- refactor ViewHeader into reusable desktop/mobile subcomponents (desktop search, display controls, mobile menu, mobile search sheet)
- standardize mobile search/sort/filter behavior across Home, Collection, Smart Scope, Authors, and Series
- fix duplicate search clear icons by suppressing native search cancel controls where custom clear buttons are used
- move Authors and Series into contained scroll regions and tighten mobile paddings/margins for better header alignment
- fix author card count/kebab z-index overlap with sticky headers
- clamp author bio to 2 lines on mobile with show more/less toggle
- simplify Series filters by removing separate author filter and support searching series by title or author name
- update series repository query logic to include author-name matching in `q` search
- improve mobile sheet sizing for Customize Dashboard and Notifications; add compact personalized dashboard greeting
- remove sidebar trigger tooltip and prevent action-bar icon/button shrink issues
- add local provider icon assets and a shared providerIconPath helper for metadata providers
- switch provider links in book details and quick view to use bundled SVG icons
- proxy external metadata cover URLs through /api/v1/books/cover/proxy for display while keeping same-origin/data/blob URLs untouched
- apply proxied display cover URLs in metadata diff/result UI and Book Dock surfaces
- keep raw provider cover URLs in metadata patches and add composable tests for proxy behavior
- add optional CSP_ALLOW_CLOUDFLARE_INSIGHTS env support and document it in server/root env examples and README
- wire conditional Cloudflare Insights script/connect sources into CSP directives
- remove pino HTTP customProps context injection to avoid duplicated context labels
- reduce file-write log noise by using debug level for write start and auto-skipped completions
- tune entity-manager duplicate scans by setting pg_trgm similarity_threshold per query/transaction to align with minSimilarity and prevent scan blowups
Handle existing upload folder paths without creating duplicate books, tolerate partial RAR 1.5 archive reads, and clean up related production defaults and dashboard type safety.
- Switch EPUB provider identifiers to opf:scheme style (Calibre-compatible)
and teach the OPF parser to read both opf:scheme and legacy urn: formats
- Extract all provider IDs, rating, pageCount, and tags from EPUB on scan
- Validate publication years (1000-2200) across all metadata providers
- Guard SmartScope service against null filter to prevent runtime crash
- Replace window.confirm() in EditCollectionDialog with inline confirm state
- Fix useLiveScanBooks to apply book updates, not just prepend new arrivals
- Add toast feedback and fix route name on SmartScope/Collection delete
- Streamline onboarding tour order (left-to-right, no page jumps)
- Remove author insights and duplicate suggestions panels, composables, DTOs, and all backend support
- Add genre and tag duplicate detection, merge, and dismiss with server-side paginated scan results
- Consolidate per-entity-type tools into a single entity-manager module backed by the Strategy pattern
- Support browse, merge, rename, delete, split, and dismiss across all entity types (author, genre, tag, narrator, publisher, language, series)
- Replace old tools module with unified entity-manager controller, service, and repository; add 85+ unit tests
Core performance overhaul - new library_dir_scan_state schema table persists per-directory
mtimes so unchanged directories are skipped entirely on subsequent scans, reducing filesystem
stat() calls from O(all-files) to O(changed-dirs) for large libraries.
Incremental scan (walk.ts / scanner.service.ts):
- New library_dir_scan_state table (bigint mtime, FK to library_folders ON DELETE CASCADE)
- findBookCandidates / findLooseFileCandidates now accept knownDirMtimes and return WalkResult
(candidates + skippedDirs + unchangedDirs + dirMtimes) instead of bare BookCandidate[]
- Per-dir mtime skip with 3 safety rules: never skip disc dirs, never skip dirs with disc
subdirs, never skip non-root dirs whose parent mtime changed (guards stem-named merging)
- Root directory now also skippable - parent-mtime check conditionally skipped for root since
root-level files are never stem-merged (explicitly excluded in buildBookCandidates)
- Settings hash invalidation: SHA-256 of {allowedFormats, formatPriority, excludePatterns,
organizationMode} forces full scan when settings change or on first scan after server restart
- isInUnchangedDir guard prevents false book-missing marks for skipped dirs
- Math.round() on stat().mtimeMs before storing - fixes bigint rejection of macOS fractional
milliseconds (e.g. 1776138191093.9556) in PostgreSQL
Walk improvements:
- Bounded concurrency: subdirs processed in chunks of 50 to avoid EMFILE on deep trees
- format + role cached on FileStat to avoid re-classifying the same file twice
- statFilesIntoAcc helper extracted to deduplicate parallel stat logic
- skippedDirs set propagated up from permission-denied dirs
ScanJobStore:
- acquireStartLock / releaseStartLock prevent concurrent scan starts for the same library
- markPendingRescan / consumePendingRescan queue a follow-up scan if one arrives while running
waitForStability:
- Accepts optional knownMtimeMs to skip a redundant stat() call when mtime is already known
File event processor (Issue 25, 11, 20):
- Own-book restore: check file's own book before searching all missing books, fixing false
book-moved results when an editor saves with a new inode (atomic-save pattern)
- Settings cache per library avoids repeated DB lookups during reconcile of many missing books
- Partial move guard: verify old file is gone before reassigning inode (multi-file books)
Live notifications (scanner.service.ts / file-watcher.service.ts):
- scanBookFolder now emits scan:books:added via existing WebSocket infrastructure so the
frontend grid updates live when the file watcher adds a book (all 3 code paths covered)
- bufferWatcherNotification() with 30s debounce fires one ScanCompleted notification per
library summarising watcher-driven additions and removals ('Novels updated - 3 added')
- Removals wired in FileWatcherService.process() and reconcile() on book-missing events
- findLibraryName() added to ScannerRepository for notification title lookup
- Add series browser page featuring browse and detail views
- Implement card deck cover design with horizontal spread and square aspect ratio
- Fix series-related bugs in decoding, state management, and data types
- Optimize responsive grid layout for series items