feat(books): rebuild duplicate books as a ledger

Sort groups by reclaimable space, pre-pick a keeper with a rule that states
its reasoning, and expand any row into a field-by-field comparison that shows
only what differs. Persist "not duplicates" so a judgement survives a rescan,
and review every copy before deleting. Reading progress no longer renders 0%.
This commit is contained in:
neon
2026-09-20 20:11:34 +00:00
parent 0007a9d84f
commit c239cfa2dd
57 changed files with 24922 additions and 1139 deletions
@@ -1,5 +1,5 @@
{
"totalRoutes": 595,
"totalRoutes": 598,
"byPermission": {
"(none)": 259,
"Permission.ManageMetadataConfig": 34,
@@ -17,7 +17,7 @@
"Permission.OpdsAccess": 4,
"Permission.ViewAuditLog": 1,
"Permission.ViewUserActivity": 6,
"Permission.LibraryDeleteBooks": 11,
"Permission.LibraryDeleteBooks": 14,
"Permission.LibraryDownload": 4,
"Permission.LibraryUpload": 2,
"Permission.ManageBookDock": 3,
@@ -1,4 +1,40 @@
[
{
"file": "server/src/modules/book-duplicates/book-duplicates.controller.ts",
"className": "BookDuplicatesController",
"methodName": "listDismissals",
"httpMethod": "GET",
"path": "/book-duplicates/dismissals",
"permissions": ["Permission.LibraryDeleteBooks"],
"libraryAccess": [],
"isPublic": false,
"allowDefault": false,
"useGuards": []
},
{
"file": "server/src/modules/book-duplicates/book-duplicates.controller.ts",
"className": "BookDuplicatesController",
"methodName": "dismissGroup",
"httpMethod": "POST",
"path": "/book-duplicates/dismissals",
"permissions": ["Permission.LibraryDeleteBooks"],
"libraryAccess": [],
"isPublic": false,
"allowDefault": false,
"useGuards": []
},
{
"file": "server/src/modules/book-duplicates/book-duplicates.controller.ts",
"className": "BookDuplicatesController",
"methodName": "restoreDismissal",
"httpMethod": "DELETE",
"path": "/book-duplicates/dismissals/:bookIdA/:bookIdB",
"permissions": ["Permission.LibraryDeleteBooks"],
"libraryAccess": [],
"isPublic": false,
"allowDefault": false,
"useGuards": []
},
{
"file": "server/src/modules/book-dock/book-dock.controller.ts",
"className": "BookDockController",