While WebDAV or Google Drive is the selected cloud sync backend, the
native Readest Cloud book/progress/note channels are gated off and the
file-sync engine owns library data end to end:
- isSyncCategoryEnabled returns false for book/progress/note (and their
legacy aliases) when a third-party provider is selected. A runtime
override, deliberately not written into syncCategories: the user's own
toggles persist and take effect again on switch-back. Account channels
(settings, stats, dictionaries, fonts, textures, OPDS catalogs) always
stay native.
- persistActiveCloudProvider is the single write path for provider
switching, used by the chooser, both connect/disconnect flows, and the
Drive OAuth callback (which previously bypassed the cross-window
broadcast). The broadcast carries ONLY the enabled flags plus
providerSelectedAt, never credentials or sync cursors, and only on
switch events, so a stale window's routine save cannot revert a switch.
- buildWebDAVConnectSettings no longer pre-sets enabled: activation
belongs to withActiveCloudProvider, so the fresh-connect path now gets
the syncBooks auto-flip and the providerSelectedAt stamp.
- Sync health: fileSyncStore records lastError per backend; the durable
lastSyncedAt stays in provider settings. The SettingsMenu sync row
reads Synced via provider / Sync failed and its tap (with pull to
refresh and BackupWindow, all routed through pullLibrary) runs the
file engine via the shared runActiveFileLibrarySync helper instead of
a gated native pull that would toast undefined book(s) synced.
- Mixed-fleet detection: while gated, the auto-sync interval runs a
read-only probe of /api/sync since providerSelectedAt; any newer book
row means another device still syncs natively, and a once-per-session
notice explains the fork instead of leaving it silent.
- Readest-Cloud-only affordances hide while a third-party provider is
selected: the quota row becomes a caption naming the active provider,
Auto Upload disappears from the menu and command palette, the
BookItem upload badge and the Transfer Queue Upload All button hide.
- providerSelectedAt added to both provider settings types and the
backup blacklist.
Stacked on the quota-decoupling change for #4959; requires the
metadata-parity change so gated channels lose nothing users can see.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
When a third-party provider (WebDAV or Google Drive) is the selected
cloud sync backend, Readest Cloud storage is no longer written to:
- New src/services/sync/cloudSyncProvider.ts policy module: the selected
provider is derived from the existing per-device enabled flags
(webdav wins deterministically if both are ever set); the premium
guard resolves to a PAUSED state instead of silently falling back to
Readest Cloud, with the user plan cached for non-React modules.
- transferManager gates book uploads on the selected provider: queueUpload
returns null when gated; pending book uploads from before a provider
switch are visibly cancelled (cancelReason policy) and pruned on the
next restore; downloads and replica transfers are never gated.
- Book uploads are deferred until settings hydrate (settings.version
barrier) so a persisted queue cannot be mis-processed at startup;
replica transfers are not stalled.
- Quota-exceeded uploads fail fast with zero retries, and a batch import
produces one summary toast instead of one toast per book.
- Policy cancellations are a distinct bucket via a shared predicate:
excluded from failed stats, Retry All, and the per-item Retry button.
- Auto-upload call sites (ingest, OPDS, subscriptions) check the provider
gate; the explicit Upload Book action explains the gate with a toast
instead of silently doing nothing.
- Activating a provider auto-enables its syncBooks so books keep backing
up somewhere; a one-time migration (20260706) applies the same flip for
users who already had a provider enabled.
- webdav.deviceId and webdav.lastSyncedAt are excluded from backups,
matching the existing googleDrive entries.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Parity with native useBooksSync: keep library.json current on import, delete,
and book-close, not just on a manual "Sync now".
- useLibraryFileSync: new library-scoped hook (counterpart of useBooksSync),
mounted once on the library page. Builds the active provider's engine async
and runs engine.syncLibrary on every library change (import adds a row,
delete sets deletedAt, closing a book bumps updatedAt), debounced 5s and
gated on the global file-sync mutex + Sync Strategy + Upload Book Files. The
reader's per-book useFileSync is unchanged (it's the per-book progress sync).
- Pass the FULL library (incl. soft-deleted books) to engine.syncLibrary, in
both the new hook and the manual FileSyncForm "Sync now": the engine
tombstones deleted books in library.json so deletions propagate, and keeping
them in the input set stops the discovery pass from re-downloading a book the
user just deleted (its remote hash dir lingers until the GC sweep).
- Tests: engine tombstones a soft-deleted book in the pushed index and does not
re-download one whose remote dir still exists.
Gated only by the active provider's enabled flag + strategy (cloud sync is
currently ungated from premium). Never runs before the library loads from disk,
so it can't push an empty index over the remote.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>