feat(storygraph): add StoryGraph sync integration (#568)

* feat(storygraph): add StoryGraph sync integration

Sync reading status and progress from BookOrbit to StoryGraph using
session-cookie authentication. Includes book matching with manual
rematch and edition selection, per-user sync toggles, a throttled
request queue, a storygraph_sync permission, and a StoryGraph
settings page.

Closes #274

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vi6e8ZJRAGMmRSmSj9sWXR

* fix(storygraph): address automated review findings

- clear stale cookie validation badge when inputs change
- reject saves with only one cookie value instead of silently dropping it
- handle linked-books load failures with a retryable error state so post-action
  reloads cannot misattribute failures to the action
- add a 30s request timeout so a hung StoryGraph response cannot stall a user's
  sync queue indefinitely
- reserve throttle slots synchronously so concurrent requests serialize
- reject empty cookie strings that would overwrite stored credentials
- report the retry response status when the rereading fallback fails
- add component specs for the connection card and linked books, plus regression
  tests for the throttle, settings, and retry-status fixes

* test(storygraph): fully restore setTimeout spies between queue tests

* feat(storygraph): add sync-scope option

Adds the book sync scope selector (all eligible books vs selected books
only) and the per-book StoryGraph sync control on the book details page,
mirroring the Hardcover integration's controls. All books with a syncable
status are now eligible — the previous connected-at cutoff that skipped
books finished before connecting is removed in favor of explicit scope
and per-book selection.

* fix(storygraph): stream live sync progress and surface failed books

The manual sync progress stream emitted the same mutated status object,
so distinctUntilChanged dropped every update after the first and the bar
never moved. Progress snapshots are now immutable and track processed
books (synced + skipped + failed), so the bar advances for every book.

Adds live synced/skipped/failed counters, a persistent last-run summary
from a terminal completed/cancelled event, and a failed-books list under
Manual sync backed by GET /storygraph/sync/failures. Details-page sync
state now refetches when the global sync scope changes.

(cherry picked from commit 075eeaa0c8678d4ebbba5dcce7aa15df42d64f51)

* feat(storygraph): show distinguishing identifiers in the editions list

Edition rows previously showed only format, pages, and language, making
identical-looking editions impossible to tell apart. The editions parser
now also scrapes ISBN, publisher, publication date, and cover image when
present, and each row shows the edition title, those identifiers, a cover
thumbnail, and the StoryGraph edition id linking to the edition page for
verification before switching.

(cherry picked from commit 09ec111613e9be149b556f64eb242cd3d82b296c)

* fix(storygraph): auto-refresh linked books after sync and add unmatched hint

The Linked books list only loaded on mount, so a book linked during a manual
sync run stayed on 'Not linked yet' until a manual page refresh. It now
silently reloads when a run completes (no spinner flash, keeps expanded rows),
guarded by a request id so overlapping reloads can't resolve out of order.
Books StoryGraph couldn't match automatically now show a short hint pointing
at 'Try auto-match' or pasting the StoryGraph URL.

(cherry picked from commit 1d0cfa1e06f3e211258620f7ad5e61373df7a310)

* feat(storygraph): compact edition link and wrap identifiers on mobile

The editions list showed the raw StoryGraph edition UUID as a long
monospace hyperlink, which was hard to read and cluttered the row. Replace
it with a small icon-only 'Open on StoryGraph' button (aria-labelled) next
to 'Use this', and let the publisher/date/ISBN line wrap instead of
truncating so the ISBN is no longer cut off on narrow/mobile screens.

(cherry picked from commit cdf4d64063534adeea9daafad11cd58a0cbc9a00)

* fix(storygraph): re-sync books re-added after being marked unread, clarify labels

A book synced while 'read' then marked 'unread' left its synced markers in
place, so re-marking it read compared against a stale lastSyncedStatus and was
silently skipped (shown as Linked but never re-pushed) until 'Try auto-match'.
On the unread transition the synced markers are now cleared while keeping the
match, so re-adding the book re-syncs.

Also present match methods with real casing (ISBN/Title/Cached/Manual) and
clarify that the manual-link field takes a StoryGraph URL or book ID (the code
after /books/), not an ISBN.

(cherry picked from commit 771e7e4fe793afbc30800717ae1c017ca307e5e1)

* refactor(storygraph): compare skippedBooks explicitly in sync-status dedup

The active-sync stream's equality check omitted skippedBooks, relying on
processedBooks (synced+skipped+failed) to capture skip-only changes. That is
correct but non-obvious; comparing every counter explicitly makes the intent
clear. No behavior change.

(cherry picked from commit f267088939f0d2493caadab8eb90f24ab4b28c0b)

* fix(storygraph): encode manually-entered book id in the lookup path

The manual-link id can be pasted directly, so a stray ?, #, or / would change
which /books/ URL is requested. Encode it before interpolating; a bad id now
404s cleanly instead of being stored.

* style(storygraph): fix prettier formatting and remove em dashes

The CI Lint (Client) job runs prettier --check, which flagged
StorygraphLinkedBooks.vue and StorygraphSyncProgress.vue. Reflow both
to prettier style and replace em dashes in UI strings and comments with
regular punctuation per project convention.

* fix(storygraph): harden sync access and batching

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
This commit is contained in:
blinkidy
2026-07-08 16:15:17 -07:00
committed by GitHub
parent 36bfc6e83d
commit ec4e5e2fc4
57 changed files with 34185 additions and 6 deletions
@@ -431,6 +431,11 @@ describe('Authorization matrix (e2e)', () => {
path: '/readwise/settings',
token: 'allPerms',
},
[Permission.StorygraphSync]: {
method: 'GET',
path: '/storygraph/settings',
token: 'allPerms',
},
[Permission.DemoRestricted]: {
method: 'GET',
path: '/notifications',