Skip to content

feat(storygraph): add StoryGraph sync integration#568

Merged
neonsolstice merged 16 commits into
bookorbit:mainfrom
blinkidy:BO-274-storygraph-sync
Jul 8, 2026
Merged

feat(storygraph): add StoryGraph sync integration#568
neonsolstice merged 16 commits into
bookorbit:mainfrom
blinkidy:BO-274-storygraph-sync

Conversation

@blinkidy

@blinkidy blinkidy commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Per our discussion in #274 — please review @thomasplevy

Adds a StoryGraph integration that syncs reading status and progress from BookOrbit to StoryGraph.

  • Connect a StoryGraph account via session-cookie authentication (no official API exists), managed from a new StoryGraph settings page.
  • Automatic sync on read-status changes and progress updates (each individually toggleable), plus a manual "sync now" action with per-book results.
  • Book matching against StoryGraph search (ISBN and title/author), with manual match/re-match from the settings page for books the matcher gets wrong, and an edition picker.
  • New storygraph_sync permission wired into the existing permission groups, admin user form, settings navigation, and authorization matrix.
  • One new migration (0037_add-storygraph-sync) creating storygraph_user_settings and storygraph_book_state.

No new dependencies (HTML parsing uses cheerio, already a server dependency).

Closes #274

How did you test this?

  • pnpm verify passes (lint, typecheck, full server and client unit suites).
  • Server unit tests cover the client service (cookie auth, HTML parsing), matching service, sync service, queue, scheduler, event listener, settings service, controller, and repository.
  • Client unit tests cover the settings/sync composables and the settings header tab gating.
  • /storygraph/settings added to the authorization-matrix e2e spec for the new permission.
  • Manually validated end-to-end on my own instance for several weeks: connected a real StoryGraph account, synced reading progress and status changes across my library, exercised manual rematch and edition selection.

Screenshots

image image image

Anything non-obvious in the diff?

  • StoryGraph has no public API, so the integration authenticates with the user's session cookie and parses HTML (cheerio). Sync is deliberately one-way (BookOrbit → StoryGraph) and rate-limited through a queue to be polite to their servers.
  • Session cookies are stored per-user in storygraph_user_settings; disconnecting deletes them.
  • Sync work runs through a queue service with a scheduler so status/progress events don't block request paths.

AI tools used: Claude (Claude Code)
Extent: assisted throughout development of the feature on my personal fork (scaffolding, implementation, and tests) and extracted this contribution from my fork onto a clean branch, including regenerating the database migration. I have been running the integration daily on my own instance and have reviewed the diff.

Checklist

  • I've read through my own diff
  • This PR was discussed in an issue and has maintainer approval (for new features) — [Feature] Storygraph Sync #274, status:approved
  • Lint and tests pass locally
  • No unintended files included (build artifacts, .env, personal configs)

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added a StoryGraph settings experience with cookie-based connection, validation status, and disconnect.
    • Added StoryGraph sync progress with manual “sync now”, pending counts, last-run details, and failure reporting.
    • Added linked books management, edition switching, and per-book “Sync now” controls.
    • Added a permission-gated StoryGraph tab in Settings with correct routing and active styling.
  • Bug Fixes

    • Updated the permissions preset to include the new StoryGraph sync permission.
  • Tests

    • Added/updated unit, integration, and component tests covering StoryGraph settings, sync lifecycle, linked books/editions UI, and tab gating behavior.

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 bookorbit#274

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vi6e8ZJRAGMmRSmSj9sWXR
@github-actions github-actions Bot added server Changes affecting server-side code, APIs, or backend behavior. client Changes affecting the client application or frontend behavior. packages labels Jul 6, 2026
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds StoryGraph sync support across shared types, database schema, server services, controller/module wiring, and client API, state, and UI.

Changes

StoryGraph Sync Feature

Layer / File(s) Summary
Shared types, permissions, and UI gating
packages/types/src/storygraph.ts, packages/types/src/permissions.ts, packages/types/src/index.ts, client/src/features/admin/UserFormDrawer.vue, client/src/features/settings/SettingsHeader.vue, client/src/features/settings/__tests__/SettingsHeader.spec.ts, client/src/router/index.ts
Adds StoryGraph sync types, the StorygraphSync permission, and navigation or tab entries that expose StoryGraph settings in the client.
Schema, migration, and repository
server/src/db/migrations/0037_add-storygraph-sync.sql, server/src/db/migrations/meta/_journal.json, server/src/db/schema/storygraph.ts, server/src/db/schema/index.ts, server/src/modules/storygraph/storygraph.repository.ts, server/src/modules/storygraph/storygraph.repository.test.ts
Adds StoryGraph persistence tables, migration metadata, and repository methods for settings, book state, permissions, and sync queries.
HTTP client, queue, and settings service
server/src/modules/storygraph/storygraph.constants.ts, server/src/modules/storygraph/storygraph-queue.service.ts, server/src/modules/storygraph/storygraph-client.service.ts, server/src/modules/storygraph/storygraph-settings.service.ts, server/src/modules/storygraph/dto/storygraph.dto.ts, server/src/modules/storygraph/dto/index.ts, and tests
Adds StoryGraph request constants, throttled and retrying HTTP access, settings lifecycle handling, DTO validation, and tests for those flows.
Book matching and editions
server/src/modules/storygraph/storygraph-book-match.service.ts, server/src/modules/storygraph/storygraph-book-match.service.test.ts, client/src/features/storygraph/composables/useStorygraphBookSyncState.ts
Adds StoryGraph book matching, manual resolution, edition parsing, edition switching, and client-side per-book sync state logic.
Sync orchestration
server/src/modules/storygraph/storygraph-sync.service.ts, server/src/modules/storygraph/storygraph-sync.service.test.ts, server/src/modules/storygraph/storygraph-sync-policy.ts, server/src/modules/storygraph/storygraph-sync-policy.test.ts
Adds single-book sync, bulk sync runs, cancellation, pending-summary/status APIs, and sync eligibility policy logic with tests.
Auto-sync scheduler and event listener
server/src/modules/storygraph/storygraph-auto-sync-scheduler.service.ts, server/src/modules/storygraph/storygraph-auto-sync-scheduler.service.test.ts, server/src/modules/storygraph/storygraph-event-listener.service.ts, server/src/modules/storygraph/storygraph-event-listener.service.test.ts
Adds debounced per-book auto-sync scheduling and achievement-event wiring into sync requests.
Controller, module, and app wiring
server/src/modules/storygraph/storygraph.controller.ts, server/src/modules/storygraph/storygraph.controller.test.ts, server/src/modules/storygraph/storygraph.module.ts, server/src/app.module.ts, server/test/authorization-matrix.e2e-spec.ts
Adds the StoryGraph controller, module registration, app import, and authorization coverage for the new permission route.
Client API and composables
client/src/features/storygraph/api/storygraph.api.ts, client/src/features/storygraph/composables/useStorygraphSettings.ts, client/src/features/storygraph/composables/useStorygraphSync.ts, and tests
Adds the StoryGraph client API wrapper plus settings, sync, and per-book composables with polling, streaming, and validation tests.
Client components and detail tab wiring
client/src/features/storygraph/components/*.vue, client/src/features/storygraph/components/__tests__/*.spec.ts, client/src/features/book/components/detail/tabs/DetailsTab.vue, client/src/features/book/components/detail/tabs/__tests__/DetailsTab.spec.ts
Adds the StoryGraph settings page, connection card, linked-books view, sync progress panel, book sync grid item, and updated book detail tab coverage.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant StorygraphConnectionCard
  participant StorygraphSettingsService
  participant StorygraphClientService
  participant StoryGraphSite

  StorygraphConnectionCard->>StorygraphSettingsService: validateCookies(sessionCookie, rememberToken)
  StorygraphSettingsService->>StorygraphClientService: GET /journal
  StorygraphClientService->>StoryGraphSite: fetch with cookies
  StoryGraphSite-->>StorygraphClientService: response
  StorygraphClientService-->>StorygraphSettingsService: redirectedToSignIn / html
  StorygraphSettingsService-->>StorygraphConnectionCard: { valid }
  StorygraphConnectionCard->>StorygraphSettingsService: saveSettings(payload)
Loading
sequenceDiagram
  participant StorygraphSyncProgress
  participant StorygraphSyncService
  participant StorygraphBookMatchService
  participant StorygraphClientService
  participant StorygraphRepository

  StorygraphSyncProgress->>StorygraphSyncService: startSync()
  StorygraphSyncService->>StorygraphRepository: findSyncableBooks(userId)
  StorygraphSyncService->>StorygraphBookMatchService: matchBook(book)
  StorygraphBookMatchService->>StorygraphClientService: fetch StoryGraph pages
  StorygraphSyncService->>StorygraphClientService: updateStatus / updateProgress
  StorygraphSyncService->>StorygraphRepository: upsertBookState(result)
  StorygraphSyncService-->>StorygraphSyncProgress: status stream updates
Loading

Suggested labels: server, client, packages

Poem

A rabbit hops through cookies and threads,
Syncing my shelf while I nap in my bed,
StoryGraph whispers, “your books are all read!”
Editions and matches all neatly aligned,
Hop, sync, repeat — reading peace of mind. 🐇📚

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding StoryGraph sync integration.
Description check ✅ Passed The description matches the template well, includes the issue reference, testing notes, screenshots, and non-obvious details.
Linked Issues check ✅ Passed The changes implement the requested StoryGraph sync integration, including login via cookies, syncing, matching, and the new permission.
Out of Scope Changes check ✅ Passed The diff stays focused on StoryGraph integration work and related UI, API, server, and migration support.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (9)
packages/types/src/storygraph.ts (1)

49-56: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider narrowing matchMethod/matchError to string unions.

matchMethod and matchError are typed as loose string | null. Since these values are almost certainly produced from a fixed set of internal match strategies/error codes (e.g. 'isbn' | 'title_author' | 'manual'), a string-literal union would give consumers (client components rendering match status) better type safety and autocomplete.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/types/src/storygraph.ts` around lines 49 - 56,
`StorygraphLinkedBook` currently exposes `matchMethod` and `matchError` as broad
`string | null`, which weakens type safety for consumers. Narrow these fields in
the `StorygraphLinkedBook` interface to string-literal unions that reflect the
fixed internal match strategies/error codes used by the matching logic, and
update any related types/usages so components consuming this model get accurate
autocomplete and compile-time validation.
server/src/db/schema/storygraph.ts (1)

28-52: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider a standalone index on book_id for cascade-delete performance.

The only index covering book_id is the composite (user_id, book_id) unique constraint, with user_id as the leading column. Deleting a row from books (ON DELETE CASCADE) will need to locate matching storygraph_book_state rows by book_id alone, which this composite index cannot efficiently serve.

♻️ Suggested addition
   (t) => [unique('storygraph_book_state_user_book_uidx').on(t.userId, t.bookId)],
+  // plus a separate index() on t.bookId for efficient cascade deletes
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/db/schema/storygraph.ts` around lines 28 - 52, Add a standalone
index on bookId in storygraphBookState to support efficient ON DELETE CASCADE
lookups from books. The existing unique constraint
storygraph_book_state_user_book_uidx only helps when userId is part of the
filter, so update the pgTable definition in storygraphBookState to include a
separate index on the bookId column alongside the current unique constraint.
server/src/modules/storygraph/storygraph-queue.service.test.ts (1)

26-48: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a concurrent-call test and stronger timing assertions.

None of these tests invoke throttle concurrently for the same userId (e.g. via Promise.all), so the race condition in the underlying implementation (see comment on storygraph-queue.service.ts) wouldn't be caught by this suite. Also, "should throttle within the interval" and "should not wait when interval has passed" don't assert the actual wait duration (e.g. via setTimeout spy args), only that the call resolves.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/modules/storygraph/storygraph-queue.service.test.ts` around lines
26 - 48, The current StorygraphQueueService tests only verify that throttle
resolves, so they miss same-user concurrency races and do not validate the
actual delay. Update the throttle specs in StorygraphQueueService tests to
include a concurrent same-user case using Promise.all on throttle for the same
userId, and add stronger timing assertions by spying on setTimeout (or
equivalent timer usage) to assert the expected wait duration in the throttled
path and no wait once STORYGRAPH_MIN_INTERVAL_MS has elapsed.
server/src/modules/storygraph/storygraph-client.service.ts (1)

79-91: 🩺 Stability & Availability | 🔵 Trivial | ⚖️ Poor tradeoff

Network-level errors aren't retried, only HTTP status failures are.

A thrown fetch error (DNS failure, connection reset, etc.) is rethrown immediately with no retry, while 429/5xx responses do get the retry/backoff treatment. For an external, occasionally-flaky scraping target, retrying transient network errors the same way would improve resiliency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/modules/storygraph/storygraph-client.service.ts` around lines 79 -
91, The retry logic in executeWithRetry only handles HTTP 429/5xx responses,
while thrown fetch/network errors are logged and rethrown immediately. Update
the catch block to treat transient network failures like other retryable cases:
log the error, then if attempt is still below STORYGRAPH_MAX_RETRIES, apply the
same exponential backoff and recurse to executeWithRetry instead of throwing
right away. Keep the existing errorClass/error sanitization and use the same
retry path as the response.status branch.
server/src/modules/storygraph/storygraph-book-match.service.ts (1)

38-76: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Fallback matching attempts continue after a likely session failure.

If searchForBook fails due to redirectedToSignIn (invalid/expired cookies) on the ISBN13 attempt, matchBook still proceeds to try ISBN10 and title search — each of which will fail identically since the session is invalid, wasting throttled requests to the external site for every book being synced.

Consider short-circuiting (e.g., returning a distinct "invalid session" signal) so callers can abort the whole sync run early rather than retrying with different search terms.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/modules/storygraph/storygraph-book-match.service.ts` around lines
38 - 76, The fallback flow in matchBook continues calling searchForBook after a
redirectedToSignIn failure, which wastes requests when cookies are invalid.
Update matchBook and the searchForBook result handling so a sign-in redirect
returns a distinct invalid-session signal that short-circuits the ISBN10/title
fallbacks. Ensure callers can detect this condition and abort the sync run early
instead of continuing retries.
server/src/modules/storygraph/dto/storygraph.dto.ts (1)

39-51: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

No format constraint on input/editionId beyond length.

LinkStorygraphBookDto.input accepts any non-empty string up to 2048 chars, which is then interpolated directly into a StoryGraph request path in StorygraphBookMatchService.resolveManualInput/extractBookIdFromInput without further validation or encoding (see server/src/modules/storygraph/storygraph-book-match.service.ts lines 171-195). Consider adding a pattern/regex constraint here (e.g., restrict to safe URL/id characters) as defense-in-depth, since this is the entry point for user-supplied StoryGraph identifiers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/modules/storygraph/dto/storygraph.dto.ts` around lines 39 - 51,
The StoryGraph DTOs currently only enforce non-empty length on
LinkStorygraphBookDto.input and SetStorygraphEditionDto.editionId, leaving
user-supplied identifiers too permissive before they reach
StorygraphBookMatchService.resolveManualInput and extractBookIdFromInput.
Tighten validation in storygraph.dto.ts by adding a pattern/regex constraint to
both fields to allow only safe StoryGraph URL/id characters, keeping the
existing length limits, so malformed or unexpected inputs are rejected at the
boundary.
server/src/modules/storygraph/storygraph-settings.service.test.ts (1)

126-181: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing regression test for empty-string cookie overwrite on update.

Given the empty-string overwrite bug flagged in storygraph-settings.service.ts (lines 43-55), consider adding a test case where upsertSettings is called on an existing row with sessionCookie: '' (or whitespace) and asserting it either throws or preserves the existing cookie.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/modules/storygraph/storygraph-settings.service.test.ts` around
lines 126 - 181, The upsertSettings tests do not cover the empty-string cookie
overwrite regression in Storygraph settings updates. Add a case in
storygraph-settings.service.test.ts for makeService().upsertSettings when an
existing row is present and sessionCookie is passed as an empty string or
whitespace, and assert the behavior matches the intended fix in upsertSettings
(either reject it with BadRequestException or preserve the existing stored
cookie). Use the existing upsertSettings and
mockRepo.findSettings/mockRepo.upsertSettings setup to verify the cookie field
is not overwritten incorrectly.
server/src/modules/storygraph/storygraph-sync.service.test.ts (1)

1-505: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Solid coverage overall; consider adding regression tests for the two issues raised on the service file.

Neither the response.status vs retry.status mismatch in updateStatus's retry-fallback path nor the cancelSync-then-immediate-syncAll race is currently exercised. A test asserting the thrown error message uses the retry's status when the retry also fails, and a test simulating cancelSync() followed immediately by a second syncAll() call (verifying only one runSyncAll proceeds), would guard these paths going forward.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/modules/storygraph/storygraph-sync.service.test.ts` around lines 1
- 505, The StorygraphSyncService test suite is missing regression coverage for
the retry-fallback status handling in updateStatus and the cancelSync/syncAll
race condition. Add a test around updateStatus that forces both the initial
status update and the retry to fail, and assert the thrown error
message/reporting uses the retry response’s status from the retry path rather
than the original response. Also add a syncAll/cancelSync race test on
StorygraphSyncService that calls cancelSync() and then immediately triggers
syncAll() again, verifying only one runSyncAll execution proceeds and the
cancelled run does not continue.
server/src/modules/storygraph/storygraph.controller.test.ts (1)

13-24: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing test coverage for cancelSync.

mockSyncService.cancelSync is defined but never exercised by a test, even though the sync orchestration layer description explicitly calls out cancellation support. Consider adding a delegation test for the controller's cancel-sync endpoint for parity with the other mocked methods.

Also applies to: 32-122

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/modules/storygraph/storygraph.controller.test.ts` around lines 13
- 24, The controller test suite defines mockSyncService.cancelSync but never
verifies it is used, leaving the cancel-sync path untested. Add a delegation
test in storygraph.controller.test.ts for the StorygraphController cancel-sync
endpoint that asserts the controller calls mockSyncService.cancelSync with the
expected arguments, matching the existing coverage pattern used for syncAll,
getSyncStatus, and the other service methods.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@client/src/features/storygraph/components/StorygraphConnectionCard.vue`:
- Line 13: The “Valid” badge in StorygraphConnectionCard is stale because
validationResult is only reset on disconnect, so editing sessionCookieInput or
rememberTokenInput still shows the previous success state. Update the component
logic around StorygraphConnectionCard’s validation flow to clear
validationResult whenever either cookie input changes, either by watching those
refs or resetting it in the corresponding input handlers, so the badge reflects
only the current values.
- Around line 39-54: handleSave in StorygraphConnectionCard.vue currently drops
a partially entered cookie update because hasNewCookies only passes values when
both inputs are filled, which can make Save appear successful even though the
new cookie was ignored. Mirror the validation used in handleValidateCookies by
checking for a partial cookie state before calling saveSettings, showing a toast
error and aborting when only one of sessionCookieInput or rememberTokenInput has
a value. Keep the existing save flow intact for the valid cases and only clear
inputs/show the success toast after a real cookie update or settings save
succeeds.

In `@client/src/features/storygraph/components/StorygraphLinkedBooks.vue`:
- Around line 28-35: loadBooks() in StorygraphLinkedBooks.vue currently lets
fetchStorygraphLinkedBooks() failures bubble up, causing an unhandled rejection
on mount and misleading the action handlers’ catch blocks. Update loadBooks() to
handle its own reload errors (or make the callers explicitly swallow best-effort
reload failures) so onMounted and the handleLink, handleRematch, and
handleSetEdition flows don’t turn a post-success refresh failure into a generic
failure toast. Keep the loading state cleanup in finally, but ensure loadBooks()
no longer propagates errors into those outer try/catch blocks.

In `@server/src/db/schema/storygraph.ts`:
- Around line 6-26: The StoryGraph credentials in storygraphUserSettings are
being persisted in plaintext, so update the storage flow for sessionCookie and
rememberToken to encrypt them before saving and decrypt them when reading.
Implement this in the StoryGraph settings model/DB access path that uses
storygraphUserSettings, ideally with application-level envelope encryption and a
managed key, and keep the public field names unchanged so existing callers
continue to work.

In `@server/src/modules/storygraph/storygraph-client.service.ts`:
- Around line 76-84: Add a request timeout to the StoryGraph fetch path in
StorygraphClientService by updating the existing fetch call in the retry block
to pass an abort signal. Use AbortSignal.timeout(...) when available, or create
an AbortController-based timeout fallback, and ensure the timeout is applied to
the same fetch invocation that currently logs failures in the catch path.

In `@server/src/modules/storygraph/storygraph-queue.service.ts`:
- Around line 10-20: The throttle logic in StorygraphQueueService.throttle is
vulnerable to concurrent calls because lastRequestAt is only updated after the
await, letting multiple requests for the same user compute the same delay and
run together. Reserve the slot synchronously before waiting by recording the
next allowed time immediately when throttle() starts, then base the sleep
calculation on that reserved timestamp so concurrent callers serialize correctly
and still respect STORYGRAPH_MIN_INTERVAL_MS.

In `@server/src/modules/storygraph/storygraph-settings.service.ts`:
- Around line 40-55: The upsertSettings flow in StorygraphSettingsService is
allowing trimmed empty strings to overwrite existing cookies because the update
path uses sessionCookie/rememberToken after .trim() and then falls back with ??
only, which does not protect against "". Update upsertSettings to validate
payload.sessionCookie and payload.rememberToken as non-empty after trimming on
both create and update paths, and reject empty/whitespace-only values with a
BadRequestException before building the data object so existing
repo.findSettings and repo.upsertSettings values cannot be silently blanked.

In `@server/src/modules/storygraph/storygraph-sync.service.ts`:
- Around line 402-433: The status_update_failed error in updateStatus is
reporting the first POST result even when the fallback REREADING retry is the
request that actually fails. Update the error handling in
storygraph-sync.service.ts so the thrown error uses the failed request’s status
for the path that failed: keep response.status for the initial attempt, but if
the CURRENTLY_READING retry via client.post fails, throw using retry.status
instead. Use the updateStatus, fetchBookPage, and client.post symbols to locate
the retry branch and ensure the persisted syncError matches the request that
truly failed.

---

Nitpick comments:
In `@packages/types/src/storygraph.ts`:
- Around line 49-56: `StorygraphLinkedBook` currently exposes `matchMethod` and
`matchError` as broad `string | null`, which weakens type safety for consumers.
Narrow these fields in the `StorygraphLinkedBook` interface to string-literal
unions that reflect the fixed internal match strategies/error codes used by the
matching logic, and update any related types/usages so components consuming this
model get accurate autocomplete and compile-time validation.

In `@server/src/db/schema/storygraph.ts`:
- Around line 28-52: Add a standalone index on bookId in storygraphBookState to
support efficient ON DELETE CASCADE lookups from books. The existing unique
constraint storygraph_book_state_user_book_uidx only helps when userId is part
of the filter, so update the pgTable definition in storygraphBookState to
include a separate index on the bookId column alongside the current unique
constraint.

In `@server/src/modules/storygraph/dto/storygraph.dto.ts`:
- Around line 39-51: The StoryGraph DTOs currently only enforce non-empty length
on LinkStorygraphBookDto.input and SetStorygraphEditionDto.editionId, leaving
user-supplied identifiers too permissive before they reach
StorygraphBookMatchService.resolveManualInput and extractBookIdFromInput.
Tighten validation in storygraph.dto.ts by adding a pattern/regex constraint to
both fields to allow only safe StoryGraph URL/id characters, keeping the
existing length limits, so malformed or unexpected inputs are rejected at the
boundary.

In `@server/src/modules/storygraph/storygraph-book-match.service.ts`:
- Around line 38-76: The fallback flow in matchBook continues calling
searchForBook after a redirectedToSignIn failure, which wastes requests when
cookies are invalid. Update matchBook and the searchForBook result handling so a
sign-in redirect returns a distinct invalid-session signal that short-circuits
the ISBN10/title fallbacks. Ensure callers can detect this condition and abort
the sync run early instead of continuing retries.

In `@server/src/modules/storygraph/storygraph-client.service.ts`:
- Around line 79-91: The retry logic in executeWithRetry only handles HTTP
429/5xx responses, while thrown fetch/network errors are logged and rethrown
immediately. Update the catch block to treat transient network failures like
other retryable cases: log the error, then if attempt is still below
STORYGRAPH_MAX_RETRIES, apply the same exponential backoff and recurse to
executeWithRetry instead of throwing right away. Keep the existing
errorClass/error sanitization and use the same retry path as the response.status
branch.

In `@server/src/modules/storygraph/storygraph-queue.service.test.ts`:
- Around line 26-48: The current StorygraphQueueService tests only verify that
throttle resolves, so they miss same-user concurrency races and do not validate
the actual delay. Update the throttle specs in StorygraphQueueService tests to
include a concurrent same-user case using Promise.all on throttle for the same
userId, and add stronger timing assertions by spying on setTimeout (or
equivalent timer usage) to assert the expected wait duration in the throttled
path and no wait once STORYGRAPH_MIN_INTERVAL_MS has elapsed.

In `@server/src/modules/storygraph/storygraph-settings.service.test.ts`:
- Around line 126-181: The upsertSettings tests do not cover the empty-string
cookie overwrite regression in Storygraph settings updates. Add a case in
storygraph-settings.service.test.ts for makeService().upsertSettings when an
existing row is present and sessionCookie is passed as an empty string or
whitespace, and assert the behavior matches the intended fix in upsertSettings
(either reject it with BadRequestException or preserve the existing stored
cookie). Use the existing upsertSettings and
mockRepo.findSettings/mockRepo.upsertSettings setup to verify the cookie field
is not overwritten incorrectly.

In `@server/src/modules/storygraph/storygraph-sync.service.test.ts`:
- Around line 1-505: The StorygraphSyncService test suite is missing regression
coverage for the retry-fallback status handling in updateStatus and the
cancelSync/syncAll race condition. Add a test around updateStatus that forces
both the initial status update and the retry to fail, and assert the thrown
error message/reporting uses the retry response’s status from the retry path
rather than the original response. Also add a syncAll/cancelSync race test on
StorygraphSyncService that calls cancelSync() and then immediately triggers
syncAll() again, verifying only one runSyncAll execution proceeds and the
cancelled run does not continue.

In `@server/src/modules/storygraph/storygraph.controller.test.ts`:
- Around line 13-24: The controller test suite defines
mockSyncService.cancelSync but never verifies it is used, leaving the
cancel-sync path untested. Add a delegation test in
storygraph.controller.test.ts for the StorygraphController cancel-sync endpoint
that asserts the controller calls mockSyncService.cancelSync with the expected
arguments, matching the existing coverage pattern used for syncAll,
getSyncStatus, and the other service methods.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 706481c7-9514-46b2-b8db-c93b0d28784e

📥 Commits

Reviewing files that changed from the base of the PR and between 395f9dd and ab188b5.

📒 Files selected for processing (45)
  • client/src/features/admin/UserFormDrawer.vue
  • client/src/features/settings/SettingsHeader.vue
  • client/src/features/settings/__tests__/SettingsHeader.spec.ts
  • client/src/features/storygraph/api/storygraph.api.ts
  • client/src/features/storygraph/components/StorygraphConnectionCard.vue
  • client/src/features/storygraph/components/StorygraphLinkedBooks.vue
  • client/src/features/storygraph/components/StorygraphSettings.vue
  • client/src/features/storygraph/components/StorygraphSyncProgress.vue
  • client/src/features/storygraph/composables/__tests__/useStorygraphSettings.test.ts
  • client/src/features/storygraph/composables/__tests__/useStorygraphSync.test.ts
  • client/src/features/storygraph/composables/useStorygraphSettings.ts
  • client/src/features/storygraph/composables/useStorygraphSync.ts
  • client/src/router/index.ts
  • packages/types/src/index.ts
  • packages/types/src/permissions.ts
  • packages/types/src/storygraph.ts
  • server/src/app.module.ts
  • server/src/db/migrations/0037_add-storygraph-sync.sql
  • server/src/db/migrations/meta/0037_snapshot.json
  • server/src/db/migrations/meta/_journal.json
  • server/src/db/schema/index.ts
  • server/src/db/schema/storygraph.ts
  • server/src/modules/storygraph/dto/index.ts
  • server/src/modules/storygraph/dto/storygraph.dto.ts
  • server/src/modules/storygraph/storygraph-auto-sync-scheduler.service.test.ts
  • server/src/modules/storygraph/storygraph-auto-sync-scheduler.service.ts
  • server/src/modules/storygraph/storygraph-book-match.service.test.ts
  • server/src/modules/storygraph/storygraph-book-match.service.ts
  • server/src/modules/storygraph/storygraph-client.service.test.ts
  • server/src/modules/storygraph/storygraph-client.service.ts
  • server/src/modules/storygraph/storygraph-event-listener.service.test.ts
  • server/src/modules/storygraph/storygraph-event-listener.service.ts
  • server/src/modules/storygraph/storygraph-queue.service.test.ts
  • server/src/modules/storygraph/storygraph-queue.service.ts
  • server/src/modules/storygraph/storygraph-settings.service.test.ts
  • server/src/modules/storygraph/storygraph-settings.service.ts
  • server/src/modules/storygraph/storygraph-sync.service.test.ts
  • server/src/modules/storygraph/storygraph-sync.service.ts
  • server/src/modules/storygraph/storygraph.constants.ts
  • server/src/modules/storygraph/storygraph.controller.test.ts
  • server/src/modules/storygraph/storygraph.controller.ts
  • server/src/modules/storygraph/storygraph.module.ts
  • server/src/modules/storygraph/storygraph.repository.test.ts
  • server/src/modules/storygraph/storygraph.repository.ts
  • server/test/authorization-matrix.e2e-spec.ts

Comment thread client/src/features/storygraph/components/StorygraphLinkedBooks.vue Outdated
Comment on lines +6 to +26
export const storygraphUserSettings = pgTable('storygraph_user_settings', {
id: serial('id').primaryKey(),
userId: integer('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' })
.unique(),
sessionCookie: varchar('session_cookie', { length: 4096 }).notNull(),
rememberToken: varchar('remember_token', { length: 4096 }).notNull(),
enabled: boolean('enabled').notNull().default(true),
autoSyncOnStatusChange: boolean('auto_sync_on_status_change').notNull().default(true),
autoSyncOnProgressUpdate: boolean('auto_sync_on_progress_update').notNull().default(true),
lastSyncedAt: timestamp('last_synced_at', { withTimezone: true }),
// When the user first connected StoryGraph. Books already finished before this moment are
// assumed to already be logged on StoryGraph and are skipped by automatic/bulk sync.
connectedAt: timestamp('connected_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true })
.defaultNow()
.notNull()
.$onUpdateFn(() => new Date()),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Session cookie/remember token stored in plaintext.

sessionCookie and rememberToken are live StoryGraph authentication credentials (per PR description, lifted directly from a logged-in browser session) and are stored unencrypted in the database. A DB leak or backup exposure would grant full access to each connected user's actual StoryGraph account.

Consider encrypting these columns at rest (e.g., application-level envelope encryption with a rotated key) before persisting, rather than storing raw cookie values.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/db/schema/storygraph.ts` around lines 6 - 26, The StoryGraph
credentials in storygraphUserSettings are being persisted in plaintext, so
update the storage flow for sessionCookie and rememberToken to encrypt them
before saving and decrypt them when reading. Implement this in the StoryGraph
settings model/DB access path that uses storygraphUserSettings, ideally with
application-level envelope encryption and a managed key, and keep the public
field names unchanged so existing callers continue to work.

Comment thread server/src/modules/storygraph/storygraph-client.service.ts
Comment thread server/src/modules/storygraph/storygraph-queue.service.ts
Comment thread server/src/modules/storygraph/storygraph-settings.service.ts
Comment thread server/src/modules/storygraph/storygraph-sync.service.ts
@thomasplevy

Copy link
Copy Markdown

i'll probably have availability towards the end of the week / weekend to have a look. Thanks @blinkidy!

@blinkidy

blinkidy commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

@thomasplevy Awesome thank you again!

@neonsolstice I also have two more pieces built that mirror the Hardcover integration's controls. A sync-scope option (all eligible books vs. selected books only) and a per-book sync control on the book details page.

They're ready on a branch if you want to look: https://github.com/blinkidy/bookorbit/tree/BO-274-storygraph-sync-full (same base as this PR, one commit ahead)

Would you prefer them added to this PR, or kept as a follow-up feature request once this one is reviewed? Happy either way, whatever makes review easier for you and Thomas.

image image

- 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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
server/src/modules/storygraph/storygraph-queue.service.test.ts (1)

14-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing vi.restoreAllMocks() in top-level afterEach.

Two separate tests (lines 19 and 60) call vi.spyOn(global, 'setTimeout') but the shared afterEach (lines 14-16) only restores real timers, never restores mocks/spies. Since these tests run sequentially against the same global.setTimeout, the spy created in the first test is never torn down before the second one re-spies the same global, which can cause nested/stale mock wrapping and flaky call-count assertions depending on how Vitest's spyOn handles an already-spied target interacting with useFakeTimers/useRealTimers transitions.

🧪 Suggested fix
   afterEach(() => {
     vi.useRealTimers();
+    vi.restoreAllMocks();
   });

Also applies to: 18-24, 58-71

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/modules/storygraph/storygraph-queue.service.test.ts` around lines
14 - 16, The shared cleanup in storygraph-queue.service.test.ts only switches
timers back, but it leaves the global setTimeout spies active across tests.
Update the top-level afterEach to also call vi.restoreAllMocks alongside
vi.useRealTimers so the vi.spyOn(global, 'setTimeout') calls in the affected
tests are fully torn down before the next test runs. Keep the fix centralized in
the existing afterEach near the test setup so the storygraph queue tests don’t
re-spy on a mocked global.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@server/src/modules/storygraph/storygraph-queue.service.test.ts`:
- Around line 14-16: The shared cleanup in storygraph-queue.service.test.ts only
switches timers back, but it leaves the global setTimeout spies active across
tests. Update the top-level afterEach to also call vi.restoreAllMocks alongside
vi.useRealTimers so the vi.spyOn(global, 'setTimeout') calls in the affected
tests are fully torn down before the next test runs. Keep the fix centralized in
the existing afterEach near the test setup so the storygraph queue tests don’t
re-spy on a mocked global.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 74fb87a7-67c7-4765-9045-a693a2c67cc4

📥 Commits

Reviewing files that changed from the base of the PR and between ab188b5 and 8abae42.

📒 Files selected for processing (12)
  • client/src/features/storygraph/components/StorygraphConnectionCard.vue
  • client/src/features/storygraph/components/StorygraphLinkedBooks.vue
  • client/src/features/storygraph/components/__tests__/StorygraphConnectionCard.spec.ts
  • client/src/features/storygraph/components/__tests__/StorygraphLinkedBooks.spec.ts
  • server/src/modules/storygraph/storygraph-client.service.ts
  • server/src/modules/storygraph/storygraph-queue.service.test.ts
  • server/src/modules/storygraph/storygraph-queue.service.ts
  • server/src/modules/storygraph/storygraph-settings.service.test.ts
  • server/src/modules/storygraph/storygraph-settings.service.ts
  • server/src/modules/storygraph/storygraph-sync.service.test.ts
  • server/src/modules/storygraph/storygraph-sync.service.ts
  • server/src/modules/storygraph/storygraph.constants.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • server/src/modules/storygraph/storygraph-queue.service.ts
  • server/src/modules/storygraph/storygraph.constants.ts
  • server/src/modules/storygraph/storygraph-settings.service.ts
  • client/src/features/storygraph/components/StorygraphLinkedBooks.vue
  • client/src/features/storygraph/components/StorygraphConnectionCard.vue
  • server/src/modules/storygraph/storygraph-settings.service.test.ts
  • server/src/modules/storygraph/storygraph-client.service.ts
  • server/src/modules/storygraph/storygraph-sync.service.ts
  • server/src/modules/storygraph/storygraph-sync.service.test.ts

@neonsolstice

neonsolstice commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

@blinkidy

Hi, I ran your code locally and noticed a few things:

  1. The manual sync progress bar doesn’t seem to update. I had around 19 books syncing, but there was no visible progress update.
  2. During sync, if a book fails to match, there doesn’t seem to be any warning or feedback in the UI.
  3. Can we make the matching UI a bit more informative visually? Right now, entries like this are hard to distinguish:

“Hardcover · 478 pages · English”
“Hardcover · 478 pages · English”
“Hardcover · 568 pages · English”

Maybe we could show something like ISBN, StoryGraph book ID, edition ID, cover, or another unique identifier so users can tell which match they’re selecting.

Also, I think adding the sync-scope option to this PR would be good. Keeping everything together in one PR should make it easier to review and test as a complete feature.

@blinkidy

blinkidy commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Hi, I ran your code locally and noticed a few things:

  1. The manual sync progress bar doesn’t seem to update. I had around 19 books syncing, but there was no visible progress update.
  2. During sync, if a book fails to match, there doesn’t seem to be any warning or feedback in the UI.
  3. Can we make the matching UI a bit more informative visually? Right now, entries like this are hard to distinguish:

“Hardcover · 478 pages · English” “Hardcover · 478 pages · English” “Hardcover · 568 pages · English”

Maybe we could show something like ISBN, StoryGraph book ID, edition ID, cover, or another unique identifier so users can tell which match they’re selecting.

Also, I think adding the sync-scope option to this PR would be good. Keeping everything together in one PR should make it easier to review and test as a complete feature.

@neonsolstice Awesome feedback thanks for that, I'll begin working on those changes right now but in the meantime I'll commit the sync-scope option as that is already completed.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/src/modules/storygraph/storygraph-sync.service.ts (1)

301-326: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Advance sync progress for every processed book.

emitProgress receives only synced, so skipped/failed books keep emitting the same value; manual sync progress appears stuck for runs with unchanged or failing books.

🐛 Proposed fix
-        this.emitProgress(userId, synced);
+        this.emitProgress(userId, synced + failed + skipped);
         continue;
       }
@@
-        this.emitProgress(userId, synced);
+        this.emitProgress(userId, synced + failed + skipped);
         continue;
       }
@@
-        this.emitProgress(userId, synced);
+        this.emitProgress(userId, synced + failed + skipped);
         continue;
       }
@@
-      this.emitProgress(userId, synced);
+      this.emitProgress(userId, synced + failed + skipped);
@@
-  private emitProgress(userId: number, synced: number): void {
+  private emitProgress(userId: number, processedBooks: number): void {
     const activeStatus = this.activeSyncs.get(userId);
-    if (activeStatus) activeStatus.syncedBooks = synced;
+    if (activeStatus) activeStatus.syncedBooks = processedBooks;
     this.emitSyncStatus(userId, activeStatus ?? null);
   }

Also applies to: 347-350

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/modules/storygraph/storygraph-sync.service.ts` around lines 301 -
326, Advance progress for every processed book in storygraph-sync.service.ts:
the current logic in the book-processing loop only emits the synced count, so
skipped or failed items do not move progress and manual sync can appear stuck.
Update the progress tracking in the loop around hasChanges,
resolveBookSyncDecision, and syncSingleBook so emitProgress reflects total
processed items (or otherwise increments on every iteration), and apply the same
adjustment in the similar block referenced by the comment.
🧹 Nitpick comments (1)
client/src/features/storygraph/composables/useStorygraphBookSyncState.ts (1)

109-125: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Client-side duplication of the server's toggle-override logic.

The syncOverride/effectiveReason/canSyncNow derivation here mirrors resolveStorygraphBookSyncOverrideForToggle in server/src/modules/storygraph/storygraph-sync-policy.ts. Keeping this pure branching logic in one shared, importable location (e.g., @bookorbit/types or a shared utils package) would remove the risk of the two copies drifting apart as the sync policy evolves.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/features/storygraph/composables/useStorygraphBookSyncState.ts`
around lines 109 - 125, The toggle-state derivation in setSyncEnabled is
duplicating the server sync policy logic, so move the
syncOverride/effectiveReason/canSyncNow branching into a shared importable
helper instead of keeping a client-only copy. Reuse the existing
resolveStorygraphBookSyncOverrideForToggle behavior from storygraph-sync-policy
in a shared location such as `@bookorbit/types` or a shared utils package, then
have useStorygraphBookSyncState call that helper when building state.value. Keep
the client and server paths aligned by making the composable depend on the
shared symbol rather than inline branching.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@client/src/features/storygraph/composables/useStorygraphBookSyncState.ts`:
- Around line 47-54: The sync-state fetch in useStorygraphBookSyncState is
missing bookSyncMode as a watch source, so mounted items can keep stale
effectiveReason and toggle state after the global mode changes. Update the watch
setup that triggers the fetch for the book sync state to also depend on
settings.value?.bookSyncMode, alongside bookId and visible, so the state is
refreshed whenever the mode flips.

---

Outside diff comments:
In `@server/src/modules/storygraph/storygraph-sync.service.ts`:
- Around line 301-326: Advance progress for every processed book in
storygraph-sync.service.ts: the current logic in the book-processing loop only
emits the synced count, so skipped or failed items do not move progress and
manual sync can appear stuck. Update the progress tracking in the loop around
hasChanges, resolveBookSyncDecision, and syncSingleBook so emitProgress reflects
total processed items (or otherwise increments on every iteration), and apply
the same adjustment in the similar block referenced by the comment.

---

Nitpick comments:
In `@client/src/features/storygraph/composables/useStorygraphBookSyncState.ts`:
- Around line 109-125: The toggle-state derivation in setSyncEnabled is
duplicating the server sync policy logic, so move the
syncOverride/effectiveReason/canSyncNow branching into a shared importable
helper instead of keeping a client-only copy. Reuse the existing
resolveStorygraphBookSyncOverrideForToggle behavior from storygraph-sync-policy
in a shared location such as `@bookorbit/types` or a shared utils package, then
have useStorygraphBookSyncState call that helper when building state.value. Keep
the client and server paths aligned by making the composable depend on the
shared symbol rather than inline branching.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d177ce6d-1c56-4988-98c0-7bf42ad0654e

📥 Commits

Reviewing files that changed from the base of the PR and between a4d76b8 and 0739b90.

📒 Files selected for processing (29)
  • client/src/features/book/components/detail/tabs/DetailsTab.vue
  • client/src/features/book/components/detail/tabs/__tests__/DetailsTab.spec.ts
  • client/src/features/storygraph/api/storygraph.api.ts
  • client/src/features/storygraph/components/StorygraphBookSyncGridItem.vue
  • client/src/features/storygraph/components/StorygraphConnectionCard.vue
  • client/src/features/storygraph/components/StorygraphSyncProgress.vue
  • client/src/features/storygraph/components/__tests__/StorygraphBookSyncGridItem.spec.ts
  • client/src/features/storygraph/components/__tests__/StorygraphConnectionCard.spec.ts
  • client/src/features/storygraph/composables/__tests__/useStorygraphSettings.test.ts
  • client/src/features/storygraph/composables/__tests__/useStorygraphSync.test.ts
  • client/src/features/storygraph/composables/useStorygraphBookSyncState.ts
  • packages/types/src/storygraph.ts
  • server/src/db/migrations/0037_add-storygraph-sync.sql
  • server/src/db/migrations/meta/0037_snapshot.json
  • server/src/db/migrations/meta/_journal.json
  • server/src/db/schema/storygraph.ts
  • server/src/modules/storygraph/dto/storygraph.dto.ts
  • server/src/modules/storygraph/storygraph-auto-sync-scheduler.service.test.ts
  • server/src/modules/storygraph/storygraph-settings.service.test.ts
  • server/src/modules/storygraph/storygraph-settings.service.ts
  • server/src/modules/storygraph/storygraph-sync-policy.test.ts
  • server/src/modules/storygraph/storygraph-sync-policy.ts
  • server/src/modules/storygraph/storygraph-sync.service.test.ts
  • server/src/modules/storygraph/storygraph-sync.service.ts
  • server/src/modules/storygraph/storygraph.controller.test.ts
  • server/src/modules/storygraph/storygraph.controller.ts
  • server/src/modules/storygraph/storygraph.module.ts
  • server/src/modules/storygraph/storygraph.repository.test.ts
  • server/src/modules/storygraph/storygraph.repository.ts
🚧 Files skipped from review as they are similar to previous changes (13)
  • server/src/modules/storygraph/storygraph.controller.test.ts
  • server/src/db/migrations/0037_add-storygraph-sync.sql
  • server/src/modules/storygraph/storygraph.module.ts
  • client/src/features/storygraph/composables/tests/useStorygraphSettings.test.ts
  • server/src/db/schema/storygraph.ts
  • server/src/modules/storygraph/dto/storygraph.dto.ts
  • server/src/db/migrations/meta/_journal.json
  • server/src/modules/storygraph/storygraph-auto-sync-scheduler.service.test.ts
  • client/src/features/storygraph/components/StorygraphSyncProgress.vue
  • client/src/features/storygraph/components/tests/StorygraphConnectionCard.spec.ts
  • server/src/modules/storygraph/storygraph.repository.test.ts
  • client/src/features/storygraph/composables/tests/useStorygraphSync.test.ts
  • client/src/features/storygraph/components/StorygraphConnectionCard.vue

blinkidy added 6 commits July 7, 2026 20:46
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 075eeaa)
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 09ec111)
…ed 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 1d0cfa1)
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 cdf4d64)
…arify 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 771e7e4)
…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 f267088)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/src/modules/storygraph/storygraph-book-match.service.ts (1)

172-196: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Encode the manual StoryGraph book id before building the request path.
extractBookIdFromInput can return raw user input, and /books/${bookId} will treat ?, #, or / as URL syntax instead of part of the id.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/modules/storygraph/storygraph-book-match.service.ts` around lines
172 - 196, The manual StoryGraph lookup path in resolveManualInput is using the
raw value from extractBookIdFromInput, so characters like ?, #, or / can break
the /books/ request. Update resolveManualInput to encode the extracted book id
before interpolating it into the client.get path, while keeping
extractBookIdFromInput focused on parsing the id from input.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/src/modules/storygraph/storygraph-sync.service.test.ts`:
- Around line 372-373: The test block in storygraph-sync.service.test.ts has a
duplicate const runs declaration that causes a compile-time redeclaration error.
In the affected test scope, keep only one runs binding and remove the repeated
declaration so the emissions.filter assignment is defined once and referenced
consistently. Use the existing runs identifier in that test case to locate and
clean up the duplicate statement.

---

Outside diff comments:
In `@server/src/modules/storygraph/storygraph-book-match.service.ts`:
- Around line 172-196: The manual StoryGraph lookup path in resolveManualInput
is using the raw value from extractBookIdFromInput, so characters like ?, #, or
/ can break the /books/ request. Update resolveManualInput to encode the
extracted book id before interpolating it into the client.get path, while
keeping extractBookIdFromInput focused on parsing the id from input.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2bfbaedf-b619-4aee-a34d-17ef7bde23a9

📥 Commits

Reviewing files that changed from the base of the PR and between 0739b90 and 8fed21b.

📒 Files selected for processing (17)
  • client/src/features/storygraph/api/storygraph.api.ts
  • client/src/features/storygraph/components/StorygraphLinkedBooks.vue
  • client/src/features/storygraph/components/StorygraphSyncProgress.vue
  • client/src/features/storygraph/components/__tests__/StorygraphLinkedBooks.spec.ts
  • client/src/features/storygraph/components/__tests__/StorygraphSyncProgress.spec.ts
  • client/src/features/storygraph/composables/__tests__/useStorygraphSync.test.ts
  • client/src/features/storygraph/composables/useStorygraphBookSyncState.ts
  • client/src/features/storygraph/composables/useStorygraphSync.ts
  • packages/types/src/storygraph.ts
  • server/src/modules/storygraph/storygraph-book-match.service.test.ts
  • server/src/modules/storygraph/storygraph-book-match.service.ts
  • server/src/modules/storygraph/storygraph-sync.service.test.ts
  • server/src/modules/storygraph/storygraph-sync.service.ts
  • server/src/modules/storygraph/storygraph.controller.test.ts
  • server/src/modules/storygraph/storygraph.controller.ts
  • server/src/modules/storygraph/storygraph.repository.test.ts
  • server/src/modules/storygraph/storygraph.repository.ts
🚧 Files skipped from review as they are similar to previous changes (11)
  • client/src/features/storygraph/composables/useStorygraphBookSyncState.ts
  • packages/types/src/storygraph.ts
  • server/src/modules/storygraph/storygraph.controller.test.ts
  • client/src/features/storygraph/composables/tests/useStorygraphSync.test.ts
  • client/src/features/storygraph/components/StorygraphSyncProgress.vue
  • client/src/features/storygraph/composables/useStorygraphSync.ts
  • client/src/features/storygraph/components/StorygraphLinkedBooks.vue
  • server/src/modules/storygraph/storygraph-sync.service.ts
  • server/src/modules/storygraph/storygraph-book-match.service.test.ts
  • server/src/modules/storygraph/storygraph.controller.ts
  • client/src/features/storygraph/api/storygraph.api.ts

Comment thread server/src/modules/storygraph/storygraph-sync.service.test.ts
blinkidy added 2 commits July 8, 2026 01:50
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.
# Conflicts:
#	client/src/features/admin/UserFormDrawer.vue
#	client/src/features/settings/SettingsHeader.vue
#	client/src/features/settings/__tests__/SettingsHeader.spec.ts
#	client/src/router/index.ts
#	packages/types/src/index.ts
#	packages/types/src/permissions.ts
#	server/src/app.module.ts
#	server/src/db/migrations/meta/0037_snapshot.json
#	server/src/db/migrations/meta/_journal.json
#	server/src/db/schema/index.ts
#	server/test/authorization-matrix.e2e-spec.ts
@blinkidy

blinkidy commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

@neonsolstice

Thanks again for the review. I pushed a batch of changes that address the feedback and fold in some fixes I found while testing on my own server. Quick rundown of what's in here now:

Sync progress reliability

Fixed a frozen progress bar during syncs. The status object was being mutated in place, so the SSE stream's distinctUntilChanged saw the same reference and dropped updates. It now emits immutable snapshots and tracks processed count (synced + skipped + failed), so the bar actually moves.
Fixed a re-sync edge case: marking a book unread and later re-reading it was being skipped as "no change" because stale sync state hung around. Re-adding a book after it was marked unread now resets that state and syncs correctly.

Manual linking / matching

Encoded the manually-entered StoryGraph book id before putting it in the lookup URL. A pasted id with a stray ?, #, or / could otherwise change which page we requested. (This was the actionable item CodeRabbit flagged.)
Clarified the match-method labels (ISBN / Title / Manual) and the "Paste a StoryGraph URL or book ID" placeholder so it's clearer how a book got linked.

Linked-books UI polish (from testing on a real library)

The linked list now refreshes quietly after a sync run finishes, without flashing the loading spinner or collapsing a row you have expanded.
Added a small hint on books that couldn't be matched, so it's obvious why they're unlinked rather than just missing.
Replaced the long raw UUID "open on StoryGraph" link with a compact icon button, and made identifiers wrap properly on mobile.

Housekeeping

Rebased/merged onto the latest main and resolved the conflicts from the Readwise integration that landed in the meantime. The settings tabs, permissions, routes, and module wiring now carry both Readwise and StoryGraph side by side, and the StoryGraph migration was regenerated on top of the current schema so it applies cleanly.
Full test suite (server + client), lint, and typecheck are all green.

Needless to say this was done large part with Claude, so please review it carefully. It should at the least give @thomasplevy a working baseline to work with and work their magic!

Screenshots attached with the changes:

image image

@neonsolstice

Copy link
Copy Markdown
Collaborator

@blinkidy Thanks for the updates! Is the code ready for a final review now?

I’ll take a look and merge it if everything looks good, unless you’re still planning to push more changes.

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.
@blinkidy

blinkidy commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

@neonsolstice That's all I have unless you can think of anything else you'd like for me implemented/fix. So no more changes planned and its ready for a heavy review.

Thanks!

@neonsolstice
neonsolstice self-requested a review July 8, 2026 23:01
@neonsolstice
neonsolstice merged commit 2937606 into bookorbit:main Jul 8, 2026
25 checks passed
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 2.2.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@github-actions github-actions Bot added the released Issue or PR is included in a released version. label Jul 10, 2026
@thomasplevy

Copy link
Copy Markdown

by the time i got to look at this it was already released, i just installed and it looks awesome. Everything linked up perfect and adding a new book synced progress through automatically.

I'm using koreader so now I can download books via the koreader plugin and my reading progress syncs through to story graph automatically through koreader. Very cool thank you both @blinkidy and @neonsolstice !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

client Changes affecting the client application or frontend behavior. packages released Issue or PR is included in a released version. server Changes affecting server-side code, APIs, or backend behavior.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Storygraph Sync

4 participants