Skip to content

feat(kobo): sync smart scopes to kobo#570

Merged
neonsolstice merged 1 commit into
bookorbit:mainfrom
imanel:BO-202-smart-scopes-sync-to-kobo
Jul 7, 2026
Merged

feat(kobo): sync smart scopes to kobo#570
neonsolstice merged 1 commit into
bookorbit:mainfrom
imanel:BO-202-smart-scopes-sync-to-kobo

Conversation

@imanel

@imanel imanel commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Add a possibility to sync Smart Scopes to Kobo

Fixes #202

How did you test this?

Automatic tests + spun up dev environment, tested the UI and sync works locally.

Screenshots

Screenshot 2026-07-07 at 01 57 25 Screenshot 2026-07-07 at 01 58 07

Anything non-obvious in the diff?

Tried to keep it to the minimum, so it should be clear.

AI Disclosure

AI tools used: Claude
Extent: all code, including tests, was generated. I was only reviewer and tester.

Checklist

  • I've read through my own diff
  • This PR was discussed in an issue and has maintainer approval (for new features)
  • Lint and tests pass locally
  • No unintended files included (build artifacts, .env, personal configs)

Summary by CodeRabbit

  • New Features

    • Added a “Sync to Kobo” option when creating or editing smart scopes.
    • Smart scopes can now be marked to sync matching books to Kobo automatically.
  • Bug Fixes

    • Kobo sync now recognizes smart-scope tags alongside existing collection tags, improving sync behavior for matching books.

@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 7, 2026
@coderabbitai

coderabbitai Bot commented Jul 7, 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 a syncToKobo flag across SmartScope types, DB schema/migration, create/update DTOs, and service persistence. Extends Kobo sync service to compute cached smart-scope book matches, merge them into eligibility WHERE clauses and tag generation, generalizes controller tag routing (col-/ss-), and adds UI toggles in the create dialog and editor panel.

Changes

Sync Smart Scopes to Kobo

Layer / File(s) Summary
SmartScope data model and schema
packages/types/src/smart-scope.ts, server/src/db/schema/smart-scopes.ts, server/src/db/migrations/0039_add_sync_to_kobo_smart_scopes.sql, server/src/db/migrations/meta/_journal.json
Adds syncToKobo to SmartScope/CreateSmartScopePayload types and a sync_to_kobo boolean column (not null, default false) with migration and journal entry.
DTO validation and service persistence
server/src/modules/smart-scope/dto/*.dto.ts, server/src/modules/smart-scope/smart-scope.service.ts, server/src/modules/smart-scope/smart-scope.service.test.ts
Adds optional validated syncToKobo to create/update DTOs and persists it in create/update, with test fixtures updated.
Kobo sync tag generation and eligibility
server/src/modules/kobo/services/kobo-sync.service.ts, server/src/modules/kobo/services/kobo-sync.service.test.ts
Injects BookQueryBuilder, adds a per-request smartScopeMatchCache, threads it through getDelta, getPageFromSnapshot, buildTagItems, buildEligibleBooksWhereClause, and book-fetch helpers to include synced smart-scope matches in tags and eligibility filtering, with expanded tests.
Controller tag routing
server/src/modules/kobo/kobo-sync.controller.ts
Adds isBookOrbitTag helper accepting col- and ss- prefixes and updates tag add/delete endpoints accordingly.
Frontend Sync to Kobo UI
client/src/features/smart-scope/components/CreateSmartScopeDialog.vue, client/src/features/smart-scope/components/SmartScopeEditorPanel.vue, client/src/features/smart-scope/composables/__tests__/useSmartScopes.spec.ts
Adds a "Sync to Kobo" checkbox/switch to the create dialog and editor panel, wires the flag into create/update payloads, and updates test fixtures.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant KoboSyncController
  participant KoboSyncService
  participant BookQueryBuilder
  participant Database

  Client->>KoboSyncController: request delta / tag items
  KoboSyncController->>KoboSyncService: getDelta / buildTagItems
  KoboSyncService->>Database: load synced smart scopes
  KoboSyncService->>BookQueryBuilder: buildWhere(filter, libraryIds)
  BookQueryBuilder-->>KoboSyncService: SQL where clause
  KoboSyncService->>Database: fetch matching book ids (cached)
  Database-->>KoboSyncService: eligible book ids
  KoboSyncService-->>KoboSyncController: tags/entitlements including smart-scope matches
  KoboSyncController-->>Client: response
Loading

Poem

A checkbox hops onto the screen,
"Sync to Kobo" — a toggle so keen!
Smart scopes now dance to the shelf's own beat,
With cached lil' burrows for eligible feet. 🐇
Col- and ss- tags, side by side,
This bunny's proud — hop on for the ride! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: syncing Smart Scopes to Kobo.
Description check ✅ Passed The PR description follows the template well, includes the issue link, testing notes, screenshots, and non-obvious details.
Linked Issues check ✅ Passed The changes implement Smart Scope Kobo syncing, including UI, persistence, schema, and Kobo sync handling required by #202.
Out of Scope Changes check ✅ Passed The diff stays focused on Smart Scope Kobo syncing and its supporting tests, schema, and sync logic.
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: 3

🧹 Nitpick comments (2)
server/src/modules/kobo/services/kobo-sync.service.ts (1)

471-495: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Using the SmartScope object itself as a Map key is fragile.

Map<schema.SmartScope, number[]> relies on reference identity of the fetched row objects. It works today because the same instances are produced and consumed within this method/caller, but it's an unusual pattern that could break silently if scopes are ever re-fetched or reconstructed between set/get. Keying by scope.id (storing { scope, bookIds } as the value) would be more robust and conventional.

🤖 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/kobo/services/kobo-sync.service.ts` around lines 471 -
495, The SmartScope lookup in getSyncedSmartScopeMatches uses the fetched
SmartScope object itself as the Map key, which depends on reference identity and
is fragile. Update getSyncedSmartScopeMatches in kobo-sync.service.ts to key the
result by a stable identifier such as scope.id, or store the scope alongside the
book IDs in the Map value, and then adjust the caller that consumes this map to
use that identifier instead of the object reference.
client/src/features/smart-scope/components/SmartScopeEditorPanel.vue (1)

204-214: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add ARIA switch semantics to the toggle button.

The custom toggle only conveys state visually; screen readers will announce it as a plain button without an on/off state. Adding role="switch" with a bound aria-checked fixes this.

♿️ Proposed fix
               <button
                 type="button"
                 class="w-11 h-6 rounded-full transition-colors relative shrink-0"
                 :class="draftSyncToKobo ? 'bg-primary' : 'bg-muted'"
+                role="switch"
+                :aria-checked="draftSyncToKobo"
+                aria-label="Sync to Kobo"
                 `@click`="draftSyncToKobo = !draftSyncToKobo"
               >
🤖 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/smart-scope/components/SmartScopeEditorPanel.vue` around
lines 204 - 214, Add switch semantics to the custom toggle in
SmartScopeEditorPanel so assistive tech can announce its state: update the
existing toggle button that flips draftSyncToKobo to expose it as a switch with
a bound checked state. Keep the current click behavior, but add the appropriate
ARIA role and state binding on the button element so screen readers report
on/off instead of a generic button.
🤖 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/kobo/services/kobo-sync.service.ts`:
- Around line 438-441: The smart-scope matching logic in getDelta is being
queried repeatedly within the same sync request, causing unnecessary DB round
trips. Compute getSyncedSmartScopeMatches once per request using the existing
userId and accessibleLibraryIds, then pass or reuse that cached result in
buildEligibleBooksWhereClause, fetchEligibleSnapshotRows,
fetchEligibleBooksByIds, and buildTagItems instead of recomputing it. If
threading the value through is too invasive, add request-scoped memoization
keyed by (userId, accessibleLibraryIds) inside KoboSyncService.
- Around line 471-495: The getSyncedSmartScopeMatches method is issuing one
awaited db.select() per SmartScope inside the for...of loop, creating sequential
N+1 queries on the sync path. Refactor this so the per-scope book lookups run
independently in parallel, e.g. by building the queries from each scope’s filter
and resolving them together before populating the Map. Keep the existing
behavior for scopes without a filter and preserve the same return shape from
getSyncedSmartScopeMatches.
- Around line 471-495: The special-case in getSyncedSmartScopeMatches is wrong
for SmartScopes with no filter, because it forces an empty match list instead of
using the existing BookQueryBuilder.buildWhere behavior that returns all
accessible books. Remove the !scope.filter branch and always build the where
clause for each scope so no-filter scopes sync the full accessible set. Make
sure the change preserves the existing accessibleLibraryIds/userId inputs and
also covers the eligibility path that reuses getSyncedSmartScopeMatches.

---

Nitpick comments:
In `@client/src/features/smart-scope/components/SmartScopeEditorPanel.vue`:
- Around line 204-214: Add switch semantics to the custom toggle in
SmartScopeEditorPanel so assistive tech can announce its state: update the
existing toggle button that flips draftSyncToKobo to expose it as a switch with
a bound checked state. Keep the current click behavior, but add the appropriate
ARIA role and state binding on the button element so screen readers report
on/off instead of a generic button.

In `@server/src/modules/kobo/services/kobo-sync.service.ts`:
- Around line 471-495: The SmartScope lookup in getSyncedSmartScopeMatches uses
the fetched SmartScope object itself as the Map key, which depends on reference
identity and is fragile. Update getSyncedSmartScopeMatches in
kobo-sync.service.ts to key the result by a stable identifier such as scope.id,
or store the scope alongside the book IDs in the Map value, and then adjust the
caller that consumes this map to use that identifier instead of the object
reference.
🪄 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: 87de185d-00c2-42fc-8585-968c563b6eae

📥 Commits

Reviewing files that changed from the base of the PR and between 89773af and 09da5ae.

📒 Files selected for processing (15)
  • client/src/features/smart-scope/components/CreateSmartScopeDialog.vue
  • client/src/features/smart-scope/components/SmartScopeEditorPanel.vue
  • client/src/features/smart-scope/composables/__tests__/useSmartScopes.spec.ts
  • packages/types/src/smart-scope.ts
  • server/src/db/migrations/0037_add_sync_to_kobo_smart_scopes.sql
  • server/src/db/migrations/meta/0037_snapshot.json
  • server/src/db/migrations/meta/_journal.json
  • server/src/db/schema/smart-scopes.ts
  • server/src/modules/kobo/kobo-sync.controller.ts
  • server/src/modules/kobo/services/kobo-sync.service.test.ts
  • server/src/modules/kobo/services/kobo-sync.service.ts
  • server/src/modules/smart-scope/dto/create-smart-scope.dto.ts
  • server/src/modules/smart-scope/dto/update-smart-scope.dto.ts
  • server/src/modules/smart-scope/smart-scope.service.test.ts
  • server/src/modules/smart-scope/smart-scope.service.ts

Comment thread server/src/modules/kobo/services/kobo-sync.service.ts Outdated
Comment thread server/src/modules/kobo/services/kobo-sync.service.ts Outdated
@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 71.92982% with 16 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...s/smart-scope/components/SmartScopeEditorPanel.vue 0.00% 7 Missing ⚠️
...ver/src/modules/kobo/services/kobo-sync.service.ts 87.80% 0 Missing and 5 partials ⚠️
.../smart-scope/components/CreateSmartScopeDialog.vue 0.00% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

@imanel
imanel force-pushed the BO-202-smart-scopes-sync-to-kobo branch from a642ad0 to 1220f3a Compare July 7, 2026 13:46

@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

🤖 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/kobo/services/kobo-sync.service.ts`:
- Around line 719-722: The smart-scope membership filter in kobo-sync.service.ts
is flattening all matched book IDs into a large inArray(...) bind list, which
can explode parameter counts on the request path. Update the logic around
getSyncedSmartScopeMatchesCached and membershipFilter to avoid materializing all
smartScopeBookIds; instead build SQL/EXISTS-based OR predicates like the
collection branch, or chunk the IDs before combining them if reuse is needed.
🪄 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: 6323589d-944a-442f-85d1-37b5fcf28a47

📥 Commits

Reviewing files that changed from the base of the PR and between a642ad0 and 1220f3a.

📒 Files selected for processing (3)
  • client/src/features/smart-scope/components/SmartScopeEditorPanel.vue
  • server/src/modules/kobo/services/kobo-sync.service.test.ts
  • server/src/modules/kobo/services/kobo-sync.service.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • client/src/features/smart-scope/components/SmartScopeEditorPanel.vue
  • server/src/modules/kobo/services/kobo-sync.service.test.ts

Comment thread server/src/modules/kobo/services/kobo-sync.service.ts Outdated
@neonsolstice
neonsolstice force-pushed the BO-202-smart-scopes-sync-to-kobo branch from 821b0b4 to 1f63acc Compare July 7, 2026 21:26
Allow smart scopes to opt into Kobo sync while preserving collection sync behavior. Generate the current Drizzle migration and keep smart-scope matching aligned with the normal book query path.

Fixes bookorbit#202

Co-authored-by: Jane Doe <[email protected]>
@neonsolstice
neonsolstice force-pushed the BO-202-smart-scopes-sync-to-kobo branch from 1f63acc to 4498117 Compare July 7, 2026 21:43
@neonsolstice
neonsolstice merged commit 7e48d59 into bookorbit:main Jul 7, 2026
24 of 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
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] Sync Smart Scopes to Kobo

2 participants