Skip to content

Phase 4: OneDrive provider - #124

Open
umerfaruk wants to merge 8 commits into
mainfrom
phase-4-onedrive-provider
Open

umerfaruk wants to merge 8 commits into
mainfrom
phase-4-onedrive-provider

Conversation

@umerfaruk

Copy link
Copy Markdown
Contributor

Summary

  • Brings OneDrive up as a third cloud storage provider alongside S3 and Google Drive (phase 5, already merged).
  • OneDriveStorageProvider (Microsoft Graph SDK, path-based item addressing) and the Electron-side OAuth2+PKCE loopback sign-in flow were already built on this branch; this PR merges in main (picking up Google Drive support and the cover-loading fixes) and adds the missing end-to-end wiring: Settings → Libraries "Connect OneDrive…" form, a Reconnect modal generalized across S3/Google Drive/OneDrive, translations (EN+UR), user docs, and a CLAUDE.md walkthrough for the one-time Azure AD app registration.
  • Implements IStorageProvider.GetRemoteDatabaseLastModifiedAsync for OneDrive (a member the interface gained after this branch was originally written), so the "don't unconditionally overwrite local edits on open/switch" logic works for OneDrive too.

Known gap

oneDriveAuth.ts/OneDriveTokenManager's CLIENT_ID is still a 00000000-... placeholder — OneDrive sign-in will fail until a real Azure AD app registration is created and its Application (client) ID is pasted into both files. Steps are in CLAUDE.md's new "Setting up the Azure AD app registration" section (personal Microsoft account, no Azure subscription/cost needed).

Test plan

  • dotnet build backend/Maktaba.sln
  • npm run build:frontend
  • npm run build:desktop
  • npm run docs:build
  • Live sign-in / connect / sync smoke test — blocked on the Azure AD app registration above

🤖 Generated with Claude Code

umerfaruk and others added 5 commits September 13, 2026 22:09
oauthLoopback.ts: a provider-agnostic PKCE (RFC 7636) authorization-code
flow for a public client - starts a temporary HTTP server on a free
loopback port, opens the identity provider's consent screen in the
system browser via shell.openExternal, and resolves once it redirects
back with an authorization code (rejecting on an error response, a
state mismatch, or a timeout). Deliberately not OneDrive-specific: Cloud:
Phase 5's Google Drive OAuth (#99) is the same shape and can reuse this
directly.

oneDriveAuth.ts: builds on that for Microsoft's identity platform
specifically - the authorize/token endpoints, PKCE challenge generation,
and the offline_access/Files.ReadWrite/User.Read scopes
OneDriveStorageProvider (#97) and the connect UI (#98) will need.
CLIENT_ID is a placeholder pending an actual Azure AD app registration
(not a secret, so hardcoding the real value once known is fine - see
the TODO comment for the exact registration steps: public client,
"Mobile and desktop applications" platform, http://localhost redirect
URI, multi-tenant + personal accounts).

Wired up end to end: native.ts's new "maktaba:connect-onedrive" IPC
handler, exposed via preload.ts/maktaba.d.ts as
window.maktaba.connectOneDrive() - returns the token set to the
renderer the same way S3's credential fields do, so the connect UI
(#98) can hand them to the backend and to saveCloudCredential exactly
like S3ConnectModal already does, rather than introducing a second
credential-handling pattern.

Token refresh deliberately isn't handled here - OneDriveStorageProvider
(#97) will do that directly from the .NET backend with a plain HTTPS
call (its own copy of the client id/token endpoint), the same way
S3StorageProvider talks to AWS directly rather than routing every
request through Electron.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Second concrete IStorageProvider, following S3StorageProvider's exact
shape: a local cache mirror (ICloudCacheManager) every method downloads
into first, so everything above the provider layer keeps working with
plain local paths. Addresses items by path under the configured folder
(OneDriveProviderOptions.Folder) via Microsoft.Graph's ItemWithPath -
GetLocalPathAsync/NotifyWrittenAsync/CreateDirectoryAsync/DeleteAsync/
MoveAsync/ExistsAsync/ExistsRemoteAsync/EnumerateAsync/Pull+PushDatabaseAsync
all implemented, matching IStorageProvider's full surface (including
ExistsRemoteAsync, added during Phase 3's migration-resumability bugfix).

Notable differences from S3, since OneDrive isn't a flat keyspace:
- CreateDirectoryAsync/NotifyWrittenAsync ensure the parent folder chain
  actually exists (creating missing segments, tolerating 409 Conflict for
  ones that already do) before writing - S3 needs no such step.
- NotifyWrittenAsync uses Graph's simple PUT .../content for files under
  its 4 MiB ceiling and a manually-driven upload session (chunked PUT
  against the pre-authenticated session URL) above it, since ebook files
  routinely exceed that ceiling.
- MoveAsync uses Graph's PATCH-with-ParentReference rename/move, not an
  S3-style copy+delete.

OneDriveTokenManager refreshes the access token in-memory as needed
(60s-early, SemaphoreSlim-guarded against concurrent refreshes),
duplicating oneDriveAuth.ts's CLIENT_ID/token endpoint rather than
inventing a backend-calls-Electron channel - see that file's comment.
A rotated refresh token is kept for the process's lifetime but not
relayed back to Electron's encrypted storage yet - a documented v1 gap
the existing Reconnect action (Settings -> Libraries) already covers as
a recovery path, matching S3's own documented v1 caching limitations.

StorageProviderFactory: generalized the per-S3 provider cache into a
provider-type-agnostic one (keyed by libraryId+providerType+credential
hash) and added the "onedrive" branch to both Current and
CreateForProvider - no other change needed there, or in the /libraries/
cloud connect endpoint, or the migration wizard's backend, since both
were already provider-type-agnostic strings.

Verified against the actual installed Microsoft.Graph 6.6.0 SDK surface
via reflection (RootRequestBuilder has no Children of its own - listing
the drive's own root needs Items["root"].Children instead) rather than
guessing API shapes; NOT live-tested against a real OneDrive account yet
(pending an Azure AD app registration - see oneDriveAuth.ts).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nly)

Maktaba only targets OneDrive Personal, not OneDrive for Business - no
reason to register the app as multi-tenant ("any organizational
directory and personal Microsoft accounts") when "Personal Microsoft
accounts only" is simpler for both the Azure AD registration itself and
the end-user consent screen. That account type requires the /consumers/
endpoint rather than /common/ - using /common/ against a
consumers-only registration is a documented source of "application not
found in directory" errors.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…vider

# Conflicts:
#	apps/desktop/src/native.ts
#	apps/desktop/src/oauthLoopback.ts
#	apps/desktop/src/preload.ts
#	apps/frontend/src/maktaba.d.ts
#	backend/Maktaba.Data/Services/StorageProviderFactory.cs
The phase-4-onedrive-provider branch had OneDriveStorageProvider and
the Electron-side OAuth loopback flow built and build-verified, but
never got the actual Settings -> Libraries "Connect OneDrive..." UI,
translations, or docs that would make it reachable - Google Drive
(phase 5, merged after this branch was parked) got all of that instead.

Adds:
- OneDriveConnectModal in LibrariesSettings.tsx (name + optional
  folder + "Sign in with Microsoft"), mirroring GoogleDriveConnectModal
  exactly - an interactive sign-in rather than typed credentials.
- ReconnectModal generalized from an isGoogleDrive branch to
  isOAuthProvider (Google Drive or OneDrive), dispatching to the right
  window.maktaba.connect*/cancel*Connect pair per providerType.
- OneDriveCredential type in api.ts, alongside GoogleDriveCredential.
- EN/UR translations for the new form's labels/buttons.
- docs/en+ur/libraries.md: "Connecting a OneDrive library" section,
  and OneDrive mentions alongside Google Drive in the cloud-libraries
  overview and "if a cloud library won't reconnect" sections.
- CLAUDE.md: OneDrive added to the ProviderType list and provider
  summary, an "OneDrive specifics" paragraph (path-based Graph
  addressing vs. Drive's id-based addressing, PKCE-only public client
  with no client_secret), and a full Azure AD app registration
  walkthrough mirroring the existing Google Cloud project one.

Also fixes a compile gap from the merge with main (Cloud: Phase 5)
still to catch: added OneDriveStorageProvider.GetRemoteDatabaseLastModifiedAsync,
implementing IStorageProvider's newer member (Graph's DriveItem
LastModifiedDateTime) - the interface gained this after the OneDrive
branch was originally written, for ActivateAsync's "don't unconditionally
overwrite local edits" pull/push comparison.

CLIENT_ID in oneDriveAuth.ts/OneDriveTokenManager is still the
00000000-... placeholder - OneDrive sign-in will fail until a real
Azure AD app registration exists (see CLAUDE.md's walkthrough). Build-
verified (dotnet build, frontend/desktop/docs builds); not live-tested,
same as the rest of this phase's original work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The cloud storage epic always documented a known gap: "don't have the
same cloud library open on two devices at once (an accidental
double-open silently loses whichever side pushes second)" - and
Maktaba.Core/Sync/LibraryLockInfo.cs already existed as a scaffolded
data model (parse/serialize/staleness rules) for fixing this, but
nothing actually read or wrote a lock anywhere. This wires it up.

IStorageProvider gains ReadLockAsync/WriteLockAsync/DeleteLockAsync,
implemented for S3 and Google Drive (OneDrive's implementation lands
on that still-open branch separately, see phase-4-onedrive-provider) -
each reads/writes a plain ".maktaba-lock" text object directly against
the remote store, always bypassing the local cache mirror, since the
whole point is seeing what a *different* device just wrote. A no-op
for LocalFileSystemProvider.

LibraryService.ActivateAsync now checks the lock before pulling/
pushing a cloud library's database: a non-stale lock (LibraryLockInfo.
IsStale, 2 minutes) held by a different device throws a clear error
(surfaced to the frontend the same way any other failed-open error
already is) instead of silently racing another device's edits.
CloudSyncLifecycleService runs a second, 60s-interval timer loop
refreshing the lock for as long as this process holds the library
open (comfortably under the 2-minute staleness window), and releases
it - best-effort, alongside the existing DB push - both when switching/
removing the active library and on clean app shutdown, so another
device doesn't have to wait out the full staleness window after a
normal close.

Updated CLAUDE.md's "Cloud storage" section and docs/en+ur/libraries.md
to describe the new behavior in place of the old "known sharp edge"
wording. Build-verified (dotnet build, docs build); not live-tested
against a real two-device scenario.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
umerfaruk and others added 2 commits September 14, 2026 13:01
Merges in phase-cloud-library-locking (#126), which added
IStorageProvider.ReadLockAsync/WriteLockAsync/DeleteLockAsync for S3
and Google Drive but not OneDrive (OneDriveStorageProvider.cs doesn't
exist on main yet), and implements the same three methods here so this
branch stays compilable regardless of which of the two PRs merges
first.

Same approach as GetRemoteDatabaseLastModifiedAsync just above it:
talks to Graph directly against the ".maktaba-lock" path, bypassing
the local cache mirror entirely (the whole point is seeing what a
different device just wrote). WriteLockAsync reuses
EnsureParentFolderExistsAsync the same way NotifyWrittenAsync does, in
case the library's configured Folder doesn't exist yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
umerfaruk added a commit that referenced this pull request Sep 14, 2026
* Implement OAuth2 loopback flow for OneDrive sign-in (#96)

oauthLoopback.ts: a provider-agnostic PKCE (RFC 7636) authorization-code
flow for a public client - starts a temporary HTTP server on a free
loopback port, opens the identity provider's consent screen in the
system browser via shell.openExternal, and resolves once it redirects
back with an authorization code (rejecting on an error response, a
state mismatch, or a timeout). Deliberately not OneDrive-specific: Cloud:
Phase 5's Google Drive OAuth (#99) is the same shape and can reuse this
directly.

oneDriveAuth.ts: builds on that for Microsoft's identity platform
specifically - the authorize/token endpoints, PKCE challenge generation,
and the offline_access/Files.ReadWrite/User.Read scopes
OneDriveStorageProvider (#97) and the connect UI (#98) will need.
CLIENT_ID is a placeholder pending an actual Azure AD app registration
(not a secret, so hardcoding the real value once known is fine - see
the TODO comment for the exact registration steps: public client,
"Mobile and desktop applications" platform, http://localhost redirect
URI, multi-tenant + personal accounts).

Wired up end to end: native.ts's new "maktaba:connect-onedrive" IPC
handler, exposed via preload.ts/maktaba.d.ts as
window.maktaba.connectOneDrive() - returns the token set to the
renderer the same way S3's credential fields do, so the connect UI
(#98) can hand them to the backend and to saveCloudCredential exactly
like S3ConnectModal already does, rather than introducing a second
credential-handling pattern.

Token refresh deliberately isn't handled here - OneDriveStorageProvider
(#97) will do that directly from the .NET backend with a plain HTTPS
call (its own copy of the client id/token endpoint), the same way
S3StorageProvider talks to AWS directly rather than routing every
request through Electron.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Implement OneDriveStorageProvider via the Microsoft Graph SDK (#97)

Second concrete IStorageProvider, following S3StorageProvider's exact
shape: a local cache mirror (ICloudCacheManager) every method downloads
into first, so everything above the provider layer keeps working with
plain local paths. Addresses items by path under the configured folder
(OneDriveProviderOptions.Folder) via Microsoft.Graph's ItemWithPath -
GetLocalPathAsync/NotifyWrittenAsync/CreateDirectoryAsync/DeleteAsync/
MoveAsync/ExistsAsync/ExistsRemoteAsync/EnumerateAsync/Pull+PushDatabaseAsync
all implemented, matching IStorageProvider's full surface (including
ExistsRemoteAsync, added during Phase 3's migration-resumability bugfix).

Notable differences from S3, since OneDrive isn't a flat keyspace:
- CreateDirectoryAsync/NotifyWrittenAsync ensure the parent folder chain
  actually exists (creating missing segments, tolerating 409 Conflict for
  ones that already do) before writing - S3 needs no such step.
- NotifyWrittenAsync uses Graph's simple PUT .../content for files under
  its 4 MiB ceiling and a manually-driven upload session (chunked PUT
  against the pre-authenticated session URL) above it, since ebook files
  routinely exceed that ceiling.
- MoveAsync uses Graph's PATCH-with-ParentReference rename/move, not an
  S3-style copy+delete.

OneDriveTokenManager refreshes the access token in-memory as needed
(60s-early, SemaphoreSlim-guarded against concurrent refreshes),
duplicating oneDriveAuth.ts's CLIENT_ID/token endpoint rather than
inventing a backend-calls-Electron channel - see that file's comment.
A rotated refresh token is kept for the process's lifetime but not
relayed back to Electron's encrypted storage yet - a documented v1 gap
the existing Reconnect action (Settings -> Libraries) already covers as
a recovery path, matching S3's own documented v1 caching limitations.

StorageProviderFactory: generalized the per-S3 provider cache into a
provider-type-agnostic one (keyed by libraryId+providerType+credential
hash) and added the "onedrive" branch to both Current and
CreateForProvider - no other change needed there, or in the /libraries/
cloud connect endpoint, or the migration wizard's backend, since both
were already provider-type-agnostic strings.

Verified against the actual installed Microsoft.Graph 6.6.0 SDK surface
via reflection (RootRequestBuilder has no Children of its own - listing
the drive's own root needs Items["root"].Children instead) rather than
guessing API shapes; NOT live-tested against a real OneDrive account yet
(pending an Azure AD app registration - see oneDriveAuth.ts).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Switch OneDrive auth to the /consumers/ endpoint (personal accounts only)

Maktaba only targets OneDrive Personal, not OneDrive for Business - no
reason to register the app as multi-tenant ("any organizational
directory and personal Microsoft accounts") when "Personal Microsoft
accounts only" is simpler for both the Azure AD registration itself and
the end-user consent screen. That account type requires the /consumers/
endpoint rather than /common/ - using /common/ against a
consumers-only registration is a documented source of "application not
found in directory" errors.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Bring OneDrive support to parity with Google Drive's connect UI

The phase-4-onedrive-provider branch had OneDriveStorageProvider and
the Electron-side OAuth loopback flow built and build-verified, but
never got the actual Settings -> Libraries "Connect OneDrive..." UI,
translations, or docs that would make it reachable - Google Drive
(phase 5, merged after this branch was parked) got all of that instead.

Adds:
- OneDriveConnectModal in LibrariesSettings.tsx (name + optional
  folder + "Sign in with Microsoft"), mirroring GoogleDriveConnectModal
  exactly - an interactive sign-in rather than typed credentials.
- ReconnectModal generalized from an isGoogleDrive branch to
  isOAuthProvider (Google Drive or OneDrive), dispatching to the right
  window.maktaba.connect*/cancel*Connect pair per providerType.
- OneDriveCredential type in api.ts, alongside GoogleDriveCredential.
- EN/UR translations for the new form's labels/buttons.
- docs/en+ur/libraries.md: "Connecting a OneDrive library" section,
  and OneDrive mentions alongside Google Drive in the cloud-libraries
  overview and "if a cloud library won't reconnect" sections.
- CLAUDE.md: OneDrive added to the ProviderType list and provider
  summary, an "OneDrive specifics" paragraph (path-based Graph
  addressing vs. Drive's id-based addressing, PKCE-only public client
  with no client_secret), and a full Azure AD app registration
  walkthrough mirroring the existing Google Cloud project one.

Also fixes a compile gap from the merge with main (Cloud: Phase 5)
still to catch: added OneDriveStorageProvider.GetRemoteDatabaseLastModifiedAsync,
implementing IStorageProvider's newer member (Graph's DriveItem
LastModifiedDateTime) - the interface gained this after the OneDrive
branch was originally written, for ActivateAsync's "don't unconditionally
overwrite local edits" pull/push comparison.

CLIENT_ID in oneDriveAuth.ts/OneDriveTokenManager is still the
00000000-... placeholder - OneDrive sign-in will fail until a real
Azure AD app registration exists (see CLAUDE.md's walkthrough). Build-
verified (dotnet build, frontend/desktop/docs builds); not live-tested,
same as the rest of this phase's original work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Let the migration wizard target Google Drive, not just S3

The Migrate-to-cloud wizard's Target step was hardcoded to S3, even
though the backend side (StorageProviderFactory.CreateForProvider,
the /migrate/start endpoint, MigrationTarget) was already fully
provider-agnostic - migrating straight to Google Drive only needed
frontend wiring, unblocked by any external setup (unlike OneDrive,
which still needs a real Azure AD app registration - see CLAUDE.md).

Adds a SegmentedControl to MigrationWizard's Target step to pick S3 or
Google Drive, then shows that provider's own fields: S3CredentialFields
+ Test connection for S3 (unchanged), or a folder field + "Sign in
with Google" button for Google Drive (mirroring GoogleDriveConnectModal's
own flow, including cancelling a pending sign-in on step change/close).
`startMigration` is now generic over the credential shape, same as
`connectCloudLibrary`/`reopenCloudLibrary` already are. Exported
`PROVIDER_LABELS` from LibrariesSettings.tsx so the picker's labels
match the Connect buttons' wording exactly instead of duplicating it.

OneDrive isn't offered as a migration target yet, same reasoning as
its own Connect form not being reachable until a real Azure AD app
registration exists - adding it here later is the same pattern Google
Drive followed.

Updated docs/en+ur/libraries.md's migration wizard section and
CLAUDE.md's "Migration wizard" writeup to describe the provider picker.
Build-verified (dotnet build, frontend build); not live-tested - same
caveat as every other cloud provider change, see CLAUDE.md's "Build &
verification" section on what this sandbox can and can't check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Implement real cloud library locking (single-writer enforcement)

The cloud storage epic always documented a known gap: "don't have the
same cloud library open on two devices at once (an accidental
double-open silently loses whichever side pushes second)" - and
Maktaba.Core/Sync/LibraryLockInfo.cs already existed as a scaffolded
data model (parse/serialize/staleness rules) for fixing this, but
nothing actually read or wrote a lock anywhere. This wires it up.

IStorageProvider gains ReadLockAsync/WriteLockAsync/DeleteLockAsync,
implemented for S3 and Google Drive (OneDrive's implementation lands
on that still-open branch separately, see phase-4-onedrive-provider) -
each reads/writes a plain ".maktaba-lock" text object directly against
the remote store, always bypassing the local cache mirror, since the
whole point is seeing what a *different* device just wrote. A no-op
for LocalFileSystemProvider.

LibraryService.ActivateAsync now checks the lock before pulling/
pushing a cloud library's database: a non-stale lock (LibraryLockInfo.
IsStale, 2 minutes) held by a different device throws a clear error
(surfaced to the frontend the same way any other failed-open error
already is) instead of silently racing another device's edits.
CloudSyncLifecycleService runs a second, 60s-interval timer loop
refreshing the lock for as long as this process holds the library
open (comfortably under the 2-minute staleness window), and releases
it - best-effort, alongside the existing DB push - both when switching/
removing the active library and on clean app shutdown, so another
device doesn't have to wait out the full staleness window after a
normal close.

Updated CLAUDE.md's "Cloud storage" section and docs/en+ur/libraries.md
to describe the new behavior in place of the old "known sharp edge"
wording. Build-verified (dotnet build, docs build); not live-tested
against a real two-device scenario.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Implement OneDrive's cloud library lock methods

Merges in phase-cloud-library-locking (#126), which added
IStorageProvider.ReadLockAsync/WriteLockAsync/DeleteLockAsync for S3
and Google Drive but not OneDrive (OneDriveStorageProvider.cs doesn't
exist on main yet), and implements the same three methods here so this
branch stays compilable regardless of which of the two PRs merges
first.

Same approach as GetRemoteDatabaseLastModifiedAsync just above it:
talks to Graph directly against the ".maktaba-lock" path, bypassing
the local cache mirror entirely (the whole point is seeing what a
different device just wrote). WriteLockAsync reuses
EnsureParentFolderExistsAsync the same way NotifyWrittenAsync does, in
case the library's configured Folder doesn't exist yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Fix: deleting a book/periodical from a cloud library never deleted it remotely

Issue #102 ("Reveal/Open/Trash equivalents for cloud-backed libraries")
flagged this: BookRemovalService/PeriodicalService only ever removed
the DB rows and left the actual file deletion to the frontend's
window.maktaba.trashPath - a pure local-disk OS-trash call with zero
cloud awareness. For a cloud library this meant deleting a book only
ever removed its DB row; the book's files (every issue's, for a
periodical) stayed orphaned in the remote store forever, since
trashPath was either a no-op (nothing cached locally to trash) or, at
best, only removed the local cache mirror copy while leaving the real
remote object/folder untouched.

BookRemovalResult/PeriodicalDeleteResult gain RequiresLocalTrash (true
only for "local"). For a cloud library, the backend now calls
IStorageProvider.DeleteAsync(folderPath, recursive: true) itself
before returning (best-effort - a failed remote delete logs a warning
rather than blocking DB row removal, same philosophy as
PushCurrentLibraryIfCloudAsync's own swallowed exceptions elsewhere).
Every frontend call site that deletes a book/periodical now checks
this flag before calling trashPath at all: BookDetailPanel,
DeleteBooksConfirmDialog, MergeConfirmDialog's source-book cleanup,
PeriodicalDetailView (issue + periodical delete), PeriodicalsView.

Local libraries are completely unaffected - same OS-trash flow as
before, RequiresLocalTrash is just always true there.

Per-file deletion (BookEditService's file replace/remove paths) was
never affected by this bug - those already called Storage.DeleteAsync
directly instead of routing through the frontend's OS-trash flow.

Also confirmed (no code change needed): opening/revealing an individual
ebook file already ensures it's cached first, since every endpoint
that returns a file's absolutePath already calls
IStorageProvider.GetLocalPathAsync per-file server-side (which
downloads-into-cache if missing) before responding - issue #102's
"View in {Provider}" web-link action for a plain OS-reveal remains
open as future polish, not addressed here.

Build-verified (dotnet build, frontend build); not live-tested against
a real cloud provider.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Add a dedicated Cloud Storage help topic (issue #105)

The "Cloud libraries" content (connecting each provider, syncing,
migrating an existing local library, reconnecting, cloud-aware
delete) had grown into more than half of the "Choosing & Switching
Libraries" topic, with no dedicated entry point of its own in the
Help sidebar/topic list - issue #105 asked for it to be split into
its own registered docs/topics.cjs topic instead.

New docs/en/cloud-storage.md + docs/ur/cloud-storage.md carry all of
that content (S3, Google Drive, OneDrive, sync + the new cloud
library locking behavior, the migration wizard's provider picker, and
the cloud-aware delete fix), registered right after "libraries" in
topics.cjs so it shows up in both the VitePress site and the in-app
Help window automatically. docs/en+ur/libraries.md's own "Cloud
libraries" section is trimmed to a short pointer (its #cloud-libraries
anchor, referenced by the Relocate note further down the same page,
still resolves - the heading just moved to a shorter section, not
away). docs/en+ur/index.md's "Where to go next" list links the new
topic too. Urdu content is Claude-authored/unreviewed, same convention
as every other Urdu string added this project.

This branch merges in the four other in-flight cloud storage PRs
(#124 OneDrive, #125 migration wizard Google Drive target, #126
library locking, #127 cloud-aware delete) since the topic needed to
document all of their accumulated content - the diff will shrink to
just the docs/topics.cjs changes once those merge into main; this one
can be merged last, or rebased onto an updated main first.

Build-verified (docs build, frontend build).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant