mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
41fd6f1881
* feat(books): add moving books between libraries Allow moving books to another library without re-uploading, so a staging library (e.g. book-dock imports) can feed curated genre libraries. Re-parent the existing book row instead of delete+recreate so reading progress, annotations, collections, and Kobo sync state survive the move. Relocate files by rebasing library-relative paths onto the target folder with rollback on failure and a copy+delete fallback for cross-filesystem moves, then schedule the target library's rename pattern. Exposed as a bulk endpoint plus UI actions in the selection bar and book detail page. Closes #257 * ci: allow container image dispatch from any branch Enable building a GHCR image from a feature branch via workflow_dispatch to test changes in a container before they land on main. The main tag is still only updated by the CI-gated workflow_run path. Ref #257 * fix(books): update book row before files when moving between libraries book_files_book_folder_consistency_fk references books(id, library_folder_id) with ON UPDATE CASCADE, so updating a file's library_folder_id while the book still points at the source folder violates the constraint and the move fails with a 500. Update the books row first so the new folder id cascades to the files, then rewrite the file paths. Verified against a real Postgres instance in addition to the reordered unit test expectations. Ref #257 * test(books): add book move e2e suite Unit tests mock the database and cannot catch constraint-order bugs like the book_files_book_folder_consistency_fk violation fixed in b1c430df. Cover the move endpoint against a real Postgres and filesystem: successful cross-library move with row re-parenting, file relocation, and preserved user state, plus disk-collision skip, foreign-folder rejection, and format-restriction skip. Reuses the metadata-write e2e harness. Ref #257 * fix(books): emit transfer event so library views refresh after a move Moving books via the API updated the database and files but never told connected clients, so the source and target library views kept stale book lists and counts until a manual reload. Emit the existing book:transferred websocket event (grouped per source library) after successful moves; the library and book detail views already subscribe to it and refresh their lists, counts, and buckets. Ref #257 * fix(books): refresh views deterministically after moving books The initiating library view only removed rows locally and relied on the book:transferred websocket round-trip for counts, so deployments where websockets are unavailable kept stale headers and sidebar badges until a reload. Refresh the view's list and buckets directly after a successful move, refresh the sidebar library counts on transfer events like scans and uploads already do, and surface an error toast when a move is confirmed with an empty selection instead of silently doing nothing. Promote the move action from the overflow menu to a dedicated, count-gated button in the selection action bar. Ref #257 * fix(books): show move action for demo-restricted accounts like delete The move button was additionally gated on the demo-restriction flag, so accounts carrying the explicit demo_restricted permission saw the delete button but no way to move books. Gate it on library_delete_books alone, mirroring the delete action next to it; demo instances withhold that permission anyway. Ref #257 * style(server): align union type formatting with upstream main The criss-cross merges of two upstream states left seven files with the older prettier union-type layout, which fails lint against the current formatter output. Restore the files byte-identical to upstream main. * i18n(client): translate the move-to-library UI The move action, dialog, and detail menu entries carried hardcoded English strings from before the vue-i18n migration. Move them to translation keys following the existing catalog structure and add entries to all locale catalogs so the parity validator passes; Crowdin can refine the non-English translations later. Ref #257 * security(deps): update brace-expansion and engine.io past new advisories Trivy fails the container build on CVE-2026-13149 (brace-expansion DoS) and CVE-2026-59725 (engine.io DoS). Both fixes are semver-compatible patch releases, so refreshing the lockfile resolves them without manifest changes. * security(deps): patch fastify static and find-my-way CVEs Trivy blocks image builds on CVE-2026-15074 (route guard bypass in fastify/static 9.x) and CVE-2026-47219 (HTTP2 DDoS in find-my-way 9.6.0). Bump the direct fastify/static dependency to 10.1.2 and add a find-my-way override because nestjs/platform-fastify pins it to an exact vulnerable version. The static plugin usage (register with root and prefix plus sendFile fallback) is unchanged in v10; the peer range warning from NestJS is expected until it allows v10. * i18n(client): add BookBulkMove audit action label to all catalogs Upstream's audit filtering feature derives its action options from the AuditAction enum and requires a label for every member. Add the label for the move feature's BookBulkMove action to all nine locale catalogs. * ci: restore main-only guard for container image dispatch Revert the fork-local change that allowed workflow_dispatch container builds from any branch. The guard matches upstream again so the branch only carries the move feature. Fork builds continue from the fork-build branch which keeps the relaxed dispatch. This reverts commit b13d276a4becbba7be80723ff6708df23132b3fb. * style(client): restore upstream formatting in untouched files Merge conflict resolution left four files with stale prettier formatting that differs from upstream while carrying no functional change. Restore them byte-identical to upstream/main so the move feature diff stays clean. * fix(book-move): harden error paths raised in PR review Address review feedback on bookorbit#818: - Guard the applyMove call so a database error fails only that book instead of aborting the batch, which discarded collected results and skipped transfer events for books that had already moved - Replace the bare catch around the source access check with one that maps ForbiddenException to the access reason and logs unexpected errors with a distinct failure reason - Report an incomplete rollback in the outcome reason so callers know a book may need manual verification - Ignore backdrop and cancel clicks in the move dialog while a move is running - Catch thrown move requests in handleBulkMove, surface an error toast, and keep the dialog open on transport errors so the user can retry - Wire onBooksMoved in SmartScopeView so scope totals and jump buckets refresh deterministically after a move - Extend the book-move e2e suite changedPaths with the library and scanner modules and the metadata-write harness it depends on * i18n(client): fill move keys with English source for Crowdin Crowdin owns the target catalogs, so hand-written translations get overwritten or conflict on the next export. Replace the translated move dialog, menu, tooltip, and audit label values in all fifteen target catalogs with the English source strings, keeping the locale-specific ICU plural categories the parity validator requires. Real translations happen in Crowdin. * fix(book-move): harden access, filesystem, and audit behavior from review Address the maintainer review on bookorbit#818, server side: - Require editor-level access on the target library via a new LibraryService.verifyUserAccessLevel backed by a repository lookup, mirroring the LibraryAccessGuard ranking; viewer access could previously write files into a library through this endpoint - Skip books whose files are missing before touching the database - Assert resolved target paths stay inside the target folder so a bad relPath cannot escape the library via mkdir and rename - Bound empty-directory cleanup at the library folder root and walk nested directories upward so flat libraries keep their configured folder and Author/Series trees do not orphan - Bump books.updatedAt in applyMove so Kobo sync sees moved books, and cancel the source library's pending unavailable-books notification like the scanner's transfer path does - Suppress file-watcher events for all touched paths through SelfWriteRegistry while files move, closing the race against missing-marking and duplicate detection - Replace rename with link+unlink (COPYFILE_EXCL on cross-device fallback) so concurrent moves resolving the same target fail with EEXIST instead of silently clobbering each other - Record the actual moved/skipped/failed outcome in the audit entry instead of the requested count * fix(book-move): use stable outcome codes and an accessible move dialog Address the client-facing maintainer review items on bookorbit#818: - Replace the free-form English outcome reasons with a stable code union in MoveBookOutcome; the prose stays in server log lines and the client maps codes to translated copy, so the UI no longer renders raw server strings - Localize the single-book move toasts on the detail page through the new outcome keys (English source in all catalogs, translation via Crowdin) - Rebuild MoveBooksDialog on reka-ui DialogRoot/Content/Title like ConfirmDialog, gaining role=dialog, focus trap and restore, Escape handling, and aria wiring; closing stays blocked while a move runs - Filter libraries without folders out of the target picker since confirming one is a guaranteed 400 * test(book-move): assert database and disk state in no-op scenarios The rejected and skipped e2e cases only checked the response before; a partial move could have passed. Assert the 400 error message, the unchanged libraryId and libraryFolderId, the source file remaining, the destination staying absent, and that a colliding target file keeps its original content. * feat(book-move): stream bulk moves with per-book progress The move endpoint processed an unbounded selection serially inside one request, which cannot finish within proxy timeouts on target-scale libraries and gave no progress. Convert it to the same SSE pattern as bulk metadata refresh: - The endpoint streams one event per book plus a final done summary and stops processing when the client disconnects - Target validation runs before headers are written so user errors stay regular JSON error responses - Audit is emitted manually with the actual outcome because the interceptor never sees a response body on streaming handlers - The bulk composable consumes the stream, reports progress through the shared in-flight indicator, and removes moved books from the view as their events arrive; the detail page reads the single outcome from the stream - The e2e suite parses the event stream and additionally asserts the done summary and content type * fix(book-move): explain skipped books in multi-library selections Collection and smart-scope selections can span libraries, so the books' own library legitimately appears as a move target and picking it skips them with already_in_target. Surface that in the dialog with a hint that is only shown when the view has no single current library to filter out. Exact target filtering would need a library id on every book card, which collapsed series cards cannot provide unambiguously. --------- Co-authored-by: ohoeltke <hoeltke@oho-dt.de>