feat(storygraph): add StoryGraph sync integration#568
Conversation
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
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds StoryGraph sync support across shared types, database schema, server services, controller/module wiring, and client API, state, and UI. ChangesStoryGraph Sync Feature
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)
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
Suggested labels: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (9)
packages/types/src/storygraph.ts (1)
49-56: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider narrowing
matchMethod/matchErrorto string unions.
matchMethodandmatchErrorare typed as loosestring | 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 winConsider a standalone index on
book_idfor cascade-delete performance.The only index covering
book_idis the composite(user_id, book_id)unique constraint, withuser_idas the leading column. Deleting a row frombooks(ON DELETE CASCADE) will need to locate matchingstorygraph_book_staterows bybook_idalone, 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 winConsider a concurrent-call test and stronger timing assertions.
None of these tests invoke
throttleconcurrently for the sameuserId(e.g. viaPromise.all), so the race condition in the underlying implementation (see comment onstorygraph-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. viasetTimeoutspy 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 tradeoffNetwork-level errors aren't retried, only HTTP status failures are.
A thrown
fetcherror (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 winFallback matching attempts continue after a likely session failure.
If
searchForBookfails due toredirectedToSignIn(invalid/expired cookies) on the ISBN13 attempt,matchBookstill 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 winNo format constraint on
input/editionIdbeyond length.
LinkStorygraphBookDto.inputaccepts any non-empty string up to 2048 chars, which is then interpolated directly into a StoryGraph request path inStorygraphBookMatchService.resolveManualInput/extractBookIdFromInputwithout further validation or encoding (seeserver/src/modules/storygraph/storygraph-book-match.service.tslines 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 winMissing 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 whereupsertSettingsis called on an existing row withsessionCookie: ''(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 winSolid coverage overall; consider adding regression tests for the two issues raised on the service file.
Neither the
response.statusvsretry.statusmismatch inupdateStatus's retry-fallback path nor thecancelSync-then-immediate-syncAllrace is currently exercised. A test asserting the thrown error message uses the retry's status when the retry also fails, and a test simulatingcancelSync()followed immediately by a secondsyncAll()call (verifying only onerunSyncAllproceeds), 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 winMissing test coverage for
cancelSync.
mockSyncService.cancelSyncis 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
📒 Files selected for processing (45)
client/src/features/admin/UserFormDrawer.vueclient/src/features/settings/SettingsHeader.vueclient/src/features/settings/__tests__/SettingsHeader.spec.tsclient/src/features/storygraph/api/storygraph.api.tsclient/src/features/storygraph/components/StorygraphConnectionCard.vueclient/src/features/storygraph/components/StorygraphLinkedBooks.vueclient/src/features/storygraph/components/StorygraphSettings.vueclient/src/features/storygraph/components/StorygraphSyncProgress.vueclient/src/features/storygraph/composables/__tests__/useStorygraphSettings.test.tsclient/src/features/storygraph/composables/__tests__/useStorygraphSync.test.tsclient/src/features/storygraph/composables/useStorygraphSettings.tsclient/src/features/storygraph/composables/useStorygraphSync.tsclient/src/router/index.tspackages/types/src/index.tspackages/types/src/permissions.tspackages/types/src/storygraph.tsserver/src/app.module.tsserver/src/db/migrations/0037_add-storygraph-sync.sqlserver/src/db/migrations/meta/0037_snapshot.jsonserver/src/db/migrations/meta/_journal.jsonserver/src/db/schema/index.tsserver/src/db/schema/storygraph.tsserver/src/modules/storygraph/dto/index.tsserver/src/modules/storygraph/dto/storygraph.dto.tsserver/src/modules/storygraph/storygraph-auto-sync-scheduler.service.test.tsserver/src/modules/storygraph/storygraph-auto-sync-scheduler.service.tsserver/src/modules/storygraph/storygraph-book-match.service.test.tsserver/src/modules/storygraph/storygraph-book-match.service.tsserver/src/modules/storygraph/storygraph-client.service.test.tsserver/src/modules/storygraph/storygraph-client.service.tsserver/src/modules/storygraph/storygraph-event-listener.service.test.tsserver/src/modules/storygraph/storygraph-event-listener.service.tsserver/src/modules/storygraph/storygraph-queue.service.test.tsserver/src/modules/storygraph/storygraph-queue.service.tsserver/src/modules/storygraph/storygraph-settings.service.test.tsserver/src/modules/storygraph/storygraph-settings.service.tsserver/src/modules/storygraph/storygraph-sync.service.test.tsserver/src/modules/storygraph/storygraph-sync.service.tsserver/src/modules/storygraph/storygraph.constants.tsserver/src/modules/storygraph/storygraph.controller.test.tsserver/src/modules/storygraph/storygraph.controller.tsserver/src/modules/storygraph/storygraph.module.tsserver/src/modules/storygraph/storygraph.repository.test.tsserver/src/modules/storygraph/storygraph.repository.tsserver/test/authorization-matrix.e2e-spec.ts
| 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()), | ||
| }); |
There was a problem hiding this comment.
🔒 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.
|
i'll probably have availability towards the end of the week / weekend to have a look. Thanks @blinkidy! |
|
@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.
|
- 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
There was a problem hiding this comment.
🧹 Nitpick comments (1)
server/src/modules/storygraph/storygraph-queue.service.test.ts (1)
14-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMissing
vi.restoreAllMocks()in top-levelafterEach.Two separate tests (lines 19 and 60) call
vi.spyOn(global, 'setTimeout')but the sharedafterEach(lines 14-16) only restores real timers, never restores mocks/spies. Since these tests run sequentially against the sameglobal.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'sspyOnhandles an already-spied target interacting withuseFakeTimers/useRealTimerstransitions.🧪 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
📒 Files selected for processing (12)
client/src/features/storygraph/components/StorygraphConnectionCard.vueclient/src/features/storygraph/components/StorygraphLinkedBooks.vueclient/src/features/storygraph/components/__tests__/StorygraphConnectionCard.spec.tsclient/src/features/storygraph/components/__tests__/StorygraphLinkedBooks.spec.tsserver/src/modules/storygraph/storygraph-client.service.tsserver/src/modules/storygraph/storygraph-queue.service.test.tsserver/src/modules/storygraph/storygraph-queue.service.tsserver/src/modules/storygraph/storygraph-settings.service.test.tsserver/src/modules/storygraph/storygraph-settings.service.tsserver/src/modules/storygraph/storygraph-sync.service.test.tsserver/src/modules/storygraph/storygraph-sync.service.tsserver/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
|
Hi, I ran your code locally and noticed a few things:
“Hardcover · 478 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.
There was a problem hiding this comment.
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 winAdvance sync progress for every processed book.
emitProgressreceives onlysynced, 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 winClient-side duplication of the server's toggle-override logic.
The
syncOverride/effectiveReason/canSyncNowderivation here mirrorsresolveStorygraphBookSyncOverrideForToggleinserver/src/modules/storygraph/storygraph-sync-policy.ts. Keeping this pure branching logic in one shared, importable location (e.g.,@bookorbit/typesor 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
📒 Files selected for processing (29)
client/src/features/book/components/detail/tabs/DetailsTab.vueclient/src/features/book/components/detail/tabs/__tests__/DetailsTab.spec.tsclient/src/features/storygraph/api/storygraph.api.tsclient/src/features/storygraph/components/StorygraphBookSyncGridItem.vueclient/src/features/storygraph/components/StorygraphConnectionCard.vueclient/src/features/storygraph/components/StorygraphSyncProgress.vueclient/src/features/storygraph/components/__tests__/StorygraphBookSyncGridItem.spec.tsclient/src/features/storygraph/components/__tests__/StorygraphConnectionCard.spec.tsclient/src/features/storygraph/composables/__tests__/useStorygraphSettings.test.tsclient/src/features/storygraph/composables/__tests__/useStorygraphSync.test.tsclient/src/features/storygraph/composables/useStorygraphBookSyncState.tspackages/types/src/storygraph.tsserver/src/db/migrations/0037_add-storygraph-sync.sqlserver/src/db/migrations/meta/0037_snapshot.jsonserver/src/db/migrations/meta/_journal.jsonserver/src/db/schema/storygraph.tsserver/src/modules/storygraph/dto/storygraph.dto.tsserver/src/modules/storygraph/storygraph-auto-sync-scheduler.service.test.tsserver/src/modules/storygraph/storygraph-settings.service.test.tsserver/src/modules/storygraph/storygraph-settings.service.tsserver/src/modules/storygraph/storygraph-sync-policy.test.tsserver/src/modules/storygraph/storygraph-sync-policy.tsserver/src/modules/storygraph/storygraph-sync.service.test.tsserver/src/modules/storygraph/storygraph-sync.service.tsserver/src/modules/storygraph/storygraph.controller.test.tsserver/src/modules/storygraph/storygraph.controller.tsserver/src/modules/storygraph/storygraph.module.tsserver/src/modules/storygraph/storygraph.repository.test.tsserver/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
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)
There was a problem hiding this comment.
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 winEncode the manual StoryGraph book id before building the request path.
extractBookIdFromInputcan 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
📒 Files selected for processing (17)
client/src/features/storygraph/api/storygraph.api.tsclient/src/features/storygraph/components/StorygraphLinkedBooks.vueclient/src/features/storygraph/components/StorygraphSyncProgress.vueclient/src/features/storygraph/components/__tests__/StorygraphLinkedBooks.spec.tsclient/src/features/storygraph/components/__tests__/StorygraphSyncProgress.spec.tsclient/src/features/storygraph/composables/__tests__/useStorygraphSync.test.tsclient/src/features/storygraph/composables/useStorygraphBookSyncState.tsclient/src/features/storygraph/composables/useStorygraphSync.tspackages/types/src/storygraph.tsserver/src/modules/storygraph/storygraph-book-match.service.test.tsserver/src/modules/storygraph/storygraph-book-match.service.tsserver/src/modules/storygraph/storygraph-sync.service.test.tsserver/src/modules/storygraph/storygraph-sync.service.tsserver/src/modules/storygraph/storygraph.controller.test.tsserver/src/modules/storygraph/storygraph.controller.tsserver/src/modules/storygraph/storygraph.repository.test.tsserver/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
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
|
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. 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.) 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. 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. 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:
|
|
@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.
|
@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! |
|
🎉 This PR is included in version 2.2.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
|
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 ! |




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.
storygraph_syncpermission wired into the existing permission groups, admin user form, settings navigation, and authorization matrix.0037_add-storygraph-sync) creatingstorygraph_user_settingsandstorygraph_book_state.No new dependencies (HTML parsing uses
cheerio, already a server dependency).Closes #274
How did you test this?
pnpm verifypasses (lint, typecheck, full server and client unit suites)./storygraph/settingsadded to the authorization-matrix e2e spec for the new permission.Screenshots
Anything non-obvious in the diff?
storygraph_user_settings; disconnecting deletes them.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
status:approved.env, personal configs)Summary by CodeRabbit
Summary by CodeRabbit
New Features
Bug Fixes
Tests