* feat(storygraph): add StoryGraph sync integration Sync reading status and progress from BookOrbit to StoryGraph using session-cookie authentication. Includes book matching with manual rematch and edition selection, per-user sync toggles, a throttled request queue, a storygraph_sync permission, and a StoryGraph settings page. Closes #274 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vi6e8ZJRAGMmRSmSj9sWXR * fix(storygraph): address automated review findings - clear stale cookie validation badge when inputs change - reject saves with only one cookie value instead of silently dropping it - handle linked-books load failures with a retryable error state so post-action reloads cannot misattribute failures to the action - add a 30s request timeout so a hung StoryGraph response cannot stall a user's sync queue indefinitely - reserve throttle slots synchronously so concurrent requests serialize - reject empty cookie strings that would overwrite stored credentials - report the retry response status when the rereading fallback fails - add component specs for the connection card and linked books, plus regression tests for the throttle, settings, and retry-status fixes * test(storygraph): fully restore setTimeout spies between queue tests * feat(storygraph): add sync-scope option Adds the book sync scope selector (all eligible books vs selected books only) and the per-book StoryGraph sync control on the book details page, mirroring the Hardcover integration's controls. All books with a syncable status are now eligible — the previous connected-at cutoff that skipped books finished before connecting is removed in favor of explicit scope and per-book selection. * fix(storygraph): stream live sync progress and surface failed books The manual sync progress stream emitted the same mutated status object, so distinctUntilChanged dropped every update after the first and the bar never moved. Progress snapshots are now immutable and track processed books (synced + skipped + failed), so the bar advances for every book. Adds live synced/skipped/failed counters, a persistent last-run summary from a terminal completed/cancelled event, and a failed-books list under Manual sync backed by GET /storygraph/sync/failures. Details-page sync state now refetches when the global sync scope changes. (cherry picked from commit 075eeaa0c8678d4ebbba5dcce7aa15df42d64f51) * feat(storygraph): show distinguishing identifiers in the editions list Edition rows previously showed only format, pages, and language, making identical-looking editions impossible to tell apart. The editions parser now also scrapes ISBN, publisher, publication date, and cover image when present, and each row shows the edition title, those identifiers, a cover thumbnail, and the StoryGraph edition id linking to the edition page for verification before switching. (cherry picked from commit 09ec111613e9be149b556f64eb242cd3d82b296c) * fix(storygraph): auto-refresh linked books after sync and add unmatched hint The Linked books list only loaded on mount, so a book linked during a manual sync run stayed on 'Not linked yet' until a manual page refresh. It now silently reloads when a run completes (no spinner flash, keeps expanded rows), guarded by a request id so overlapping reloads can't resolve out of order. Books StoryGraph couldn't match automatically now show a short hint pointing at 'Try auto-match' or pasting the StoryGraph URL. (cherry picked from commit 1d0cfa1e06f3e211258620f7ad5e61373df7a310) * feat(storygraph): compact edition link and wrap identifiers on mobile The editions list showed the raw StoryGraph edition UUID as a long monospace hyperlink, which was hard to read and cluttered the row. Replace it with a small icon-only 'Open on StoryGraph' button (aria-labelled) next to 'Use this', and let the publisher/date/ISBN line wrap instead of truncating so the ISBN is no longer cut off on narrow/mobile screens. (cherry picked from commit cdf4d64063534adeea9daafad11cd58a0cbc9a00) * fix(storygraph): re-sync books re-added after being marked unread, clarify labels A book synced while 'read' then marked 'unread' left its synced markers in place, so re-marking it read compared against a stale lastSyncedStatus and was silently skipped (shown as Linked but never re-pushed) until 'Try auto-match'. On the unread transition the synced markers are now cleared while keeping the match, so re-adding the book re-syncs. Also present match methods with real casing (ISBN/Title/Cached/Manual) and clarify that the manual-link field takes a StoryGraph URL or book ID (the code after /books/), not an ISBN. (cherry picked from commit 771e7e4fe793afbc30800717ae1c017ca307e5e1) * refactor(storygraph): compare skippedBooks explicitly in sync-status dedup The active-sync stream's equality check omitted skippedBooks, relying on processedBooks (synced+skipped+failed) to capture skip-only changes. That is correct but non-obvious; comparing every counter explicitly makes the intent clear. No behavior change. (cherry picked from commit f267088939f0d2493caadab8eb90f24ab4b28c0b) * fix(storygraph): encode manually-entered book id in the lookup path The manual-link id can be pasted directly, so a stray ?, #, or / would change which /books/ URL is requested. Encode it before interpolating; a bad id now 404s cleanly instead of being stored. * style(storygraph): fix prettier formatting and remove em dashes The CI Lint (Client) job runs prettier --check, which flagged StorygraphLinkedBooks.vue and StorygraphSyncProgress.vue. Reflow both to prettier style and replace em dashes in UI strings and comments with regular punctuation per project convention. * fix(storygraph): harden sync access and batching --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
BookOrbit - Server
NestJS 11 backend running on Fastify.
Running
From the repo root: pnpm dev (starts both server and client).
Server only: pnpm start:dev (from this directory).
The server loads environment variables from .env in this directory. See .env.example for all available options.
API docs
OpenAPI docs are disabled by default. Enable them with SWAGGER_ENABLED=true:
SWAGGER_ENABLED=true pnpm start:dev
Then open:
- Swagger UI: http://localhost:3000/api/docs
- OpenAPI JSON: http://localhost:3000/api/docs-json
Dev watch uses a Nest config without the Swagger metadata generator so the
server does not restart immediately after startup. Full build output still uses
the standard Nest config and generates src/metadata.ts for enriched docs.
Module structure
Each feature is a self-contained NestJS module under src/modules/:
src/modules/
├── auth/ Authentication (JWT, OIDC, password reset)
├── book/ Book CRUD, cover images, file serving
├── library/ Library management
├── scanner/ File system scanning, metadata extraction
├── collection/ User book collections
├── smart-scope/ Saved search/filter views
├── user/ User management
├── role/ Roles and permissions (RBAC)
├── metadata/ Book metadata providers
├── kobo/ Kobo device sync
├── annotation/ Highlights and annotations
├── bookmark/ Reading position bookmarks
├── reader-preferences/ Per-user reader settings
├── cbz/ CBZ archive handling
├── path/ File path utilities
├── seed/ Database seeding
└── app-settings/ Global application settings
Database
Schema files live in src/db/schema/, one per domain. Migrations are generated by Drizzle Kit.
pnpm db:generate add_some_column # generate migration from schema diff
pnpm db:migrate # apply pending migrations
pnpm db:studio # open Drizzle Studio (DB browser)
Testing
pnpm test # unit tests
pnpm test:watch # unit tests in watch mode
pnpm e2e:run # app-smoke end-to-end test from server package
pnpm test:cov # unit tests with coverage
For suite-based e2e runs, run from the repository root:
pnpm run e2e:list
pnpm run e2e:all
pnpm run e2e:run -- scanner-scenarios
Quality
pnpm type-check # TypeScript checks (no emit)
pnpm lint:check # Non-mutating lint checks
pnpm lint:fix # Auto-fix lint issues
pnpm format:check # Prettier check
pnpm format # Prettier write
Key conventions
- Constructor injection with
@Inject(DB)for the Drizzle instance. - Config via
@nestjs/confignamed configs (appConfig,dbConfig,authConfig,storageConfig,mailerConfig). Never readprocess.envdirectly in services. - DTOs use
class-validatordecorators. GlobalValidationPiperejects unknown fields. - Throw NestJS
HttpExceptionsubclasses (NotFoundException,ForbiddenException, etc.) - never rawError. - All user-owned data is scoped by
userId. Service methods check ownership.