- 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