Plans 01–04, CONTEXT, PATTERNS, RESEARCH, UAT, and .gitkeep for the fix-nextcloud-root-listing-and-sync-visibility sub-phase. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
15 KiB
Phase 12.1 Pattern Map: Cloud Listing Correctness
Purpose
Map the existing shared patterns and exact change seams for making root and nested cloud listings correct across Nextcloud, generic WebDAV, Google Drive, and OneDrive. This is a static repository map only: no .env access, credentials, provider calls, or network operations were used.
The supplied Nextcloud root names are acceptance evidence, not configuration or test secrets:
- Folders:
Documents,docuvault,Photos,Templates - Files:
Nextcloud.png,Nextcloud Intro.mp4,Manual Nextcloud.png,Readme.md,Reasons to use Nextcloud.pdf,Template credits.md
Root-Cause Map
1. Nextcloud breaks the shared adapter contract
backend/storage/cloud_base.py defines the canonical interface:
await adapter.list_folder(connection_id, user_id, parent_ref=None, page_token=None)
-> CloudListing
backend/storage/webdav_backend.py implements that interface and already contains the reusable WebDAV/Nextcloud PROPFIND normalization into CloudResource and CloudListing.
backend/storage/nextcloud_backend.py, however, overrides list_folder with the legacy signature list_folder(folder_path="") -> list[dict]. The canonical API and Celery task call it with connection/user UUIDs plus parent_ref, so Nextcloud can raise a signature error before listing. It also returns the legacy shape instead of CloudListing.
Fix pattern: do not add another Nextcloud listing implementation. Remove the incompatible override and inherit WebDAVBackend.list_folder, or move the legacy behavior behind an explicitly named compatibility method used only by the legacy route. The canonical CloudResourceAdapter.list_folder must have one signature and one return type for every provider.
2. The frontend consumes the legacy item shape
backend/api/cloud/schemas.py exposes canonical items as:
id DocuVault stable UUID
provider_item_id provider folder/file reference
kind "folder" or "file"
frontend/src/views/CloudFolderView.vue still filters by is_dir and navigates with item.id. Consequently:
- canonical folders are not recognized as folders;
- nested navigation sends the DocuVault metadata UUID as
parent_refinstead of the provider reference; - this fails for every provider, not just Nextcloud.
Fix pattern: normalize once at the API/view boundary or consume the canonical schema directly. In CloudFolderView.vue, derive folders/files from item.kind, and navigate using item.provider_item_id. Do not teach StorageBrowser.vue provider-specific IDs or add provider branches.
3. The UI invents freshness instead of rendering provider state
backend/api/cloud/browse.py returns freshness from durable CloudFolderState, including refresh_state, last_refreshed_at, and controlled errors. frontend/src/views/CloudFolderView.vue ignores it and sets freshness: 'fresh' plus the browser's current time after every HTTP 200.
An empty listing caused by a provider failure can therefore appear synchronized.
Fix pattern: map data.freshness.refresh_state into the store's UI vocabulary and use data.freshness.last_refreshed_at; preserve error_code/error_message as the user-visible warning source. Never infer provider freshness from HTTP success.
4. Empty complete listings and failed listings must remain distinguishable
CloudListing.complete in backend/storage/cloud_base.py is the authoritative distinction:
complete=True: full listing; reconciliation may soft-delete unseen children.complete=False: partial/failed listing; reconciliation must retain cached children.
backend/services/cloud_items.py::reconcile_cloud_listing already implements this correctly. Keep it as the only metadata reconciliation entry point. Adapter/API fixes must not bypass it or update CloudItem rows directly.
The synchronous first-load path in backend/api/cloud/browse.py should only mark a folder fresh after an authoritative result. A CloudListing(complete=False) must produce a controlled warning/stale state and must not be reported as fresh. Apply the same decision in backend/tasks/cloud_tasks.py so synchronous and background refreshes cannot disagree.
Shared Pipeline and Exact Files
Adapter contract and normalization
backend/storage/cloud_base.py- Preserve
CloudResourceAdapter.list_folderas the sole browse contract. - Preserve normalized
CloudResourceidentity fields andCloudListing.completesemantics. - If validation is strengthened, add provider-neutral checks here rather than in four adapters (for example, reject duplicate provider IDs within one authoritative page/listing).
- Preserve
backend/storage/webdav_backend.py- Keep one DAV listing implementation for both WebDAV and Nextcloud.
- Root is
parent_ref=None, translated internally to the SDK's empty relative path. - Continue returning provider-relative paths as
provider_item_idand the requested parent reference asparent_ref. - Keep SSRF revalidation before outbound calls and the no-byte-download invariant.
backend/storage/nextcloud_backend.py- Remove/rename the legacy
list_folder(folder_path)override so the inherited canonical implementation is used. - Retain only genuinely Nextcloud-specific behavior such as root health-check path conventions.
- Do not duplicate the PROPFIND normalization loop.
- Remove/rename the legacy
backend/storage/google_drive_backend.py- Preserve root translation
None -> "root", provider IDs for navigation, full pagination, andcomplete=Falseon provider failure.
- Preserve root translation
backend/storage/onedrive_backend.py- Preserve root translation to
/me/drive/root/children, item IDs for navigation,@odata.nextLinkexhaustion, and incomplete failure semantics.
- Preserve root translation to
backend/storage/cloud_backend_factory.py- Continue constructing all providers behind
build_cloud_resource_adapter. - Add/retain a defensive contract assertion, but do not special-case list behavior by provider.
- Continue constructing all providers behind
Service and task patterns
backend/services/cloud_items.pyresolve_owned_connection: owner boundary before adapter access.upsert_cloud_item: stable metadata identity by(connection_id, provider_item_id).reconcile_cloud_listing: only reconciliation entry point; soft-delete only for complete listings.list_cloud_children: query by the exact canonicalparent_refused during reconciliation.update_folder_state: canonical controlled freshness/error state; never raw provider exception text.
backend/tasks/cloud_tasks.py- Keep credential decryption inside the worker.
- Call only the canonical adapter method.
- Treat
complete=Falseas refresh failure/warning rather than successful freshness. - Preserve bounded retries, owner revalidation, metadata-only operation, and no quota mutation/download.
API patterns
backend/api/cloud/browse.py- Keep
/api/cloud/connections/{connection_id}/itemsas the canonical owner-scoped endpoint. - Preserve whitelisted response conversion through
_item_out,_capability_out, and_freshness_out. - First visit may fetch synchronously, but must not mark an incomplete listing fresh.
- Cached visits continue stale-while-revalidate through
refresh_cloud_folder.delay. - Retire or isolate
_fetch_*_folderscompatibility helpers; do not let the legacy route define new listing semantics.
- Keep
backend/api/cloud/connections.py- The
/folders/{provider}/{folder_id}route is legacy. Migrate remaining consumers/tests to the connection-ID endpoint, then remove it when unreferenced rather than maintaining two provider listing pipelines.
- The
backend/api/cloud/schemas.py- Keep the explicit credential-free schema.
kindandprovider_item_idare canonical; avoid restoringis_dirsolely to accommodate stale frontend code unless a deliberate transitional compatibility field is documented and tested.
Frontend patterns
frontend/src/api/cloud.js- Keep
getCloudFoldersByConnectionId(connectionId, parentRef)as the single browse client. - Preserve URL encoding of opaque/path-like provider references.
- Remove deprecated
getCloudFolders(provider, folderId)once no active consumer remains.
- Keep
frontend/src/views/CloudFolderView.vue- Remain a thin data provider.
- Split items using
kindand navigate folders usingprovider_item_id. - Feed backend freshness and controlled warning data into the store; do not synthesize “fresh”.
- Keep connection UUID routing and session-only folder memory.
- Do not add provider switches or listing layout.
frontend/src/stores/cloudConnections.js- Keep shared browse state here.
- If freshness translation is non-trivial, add one shared mapper/action here and test it once; views should not each invent status semantics.
frontend/src/components/storage/StorageBrowser.vue- Remain the only local/cloud file browser.
- Receive already normalized folders/files and capability/freshness props.
- No provider-specific path parsing or Nextcloud-only rendering.
Test Design
Provider contract suite: one parametrized contract, provider fixtures only
Extend backend/tests/test_cloud_backends.py with a shared adapter contract harness covering Google Drive, OneDrive, WebDAV, and Nextcloud. Each provider supplies mocked SDK/HTTP responses; assertions stay provider-neutral:
- Root call accepts
(connection_id, user_id, parent_ref=None)and returnsCloudListing. - Every child is a
CloudResourcewith the supplied connection/user IDs. - Folder/file classification, names, provider IDs, parent refs, sizes, MIME types, timestamps, and etags normalize correctly.
- Nested listing accepts a returned folder's
provider_item_idas the nextparent_ref. - Pagination is exhausted before
complete=True(where applicable). - Provider/network failure returns or is translated to
complete=Falsewithout byte downloads or mutation calls. - No adapter invokes
get_object, upload, delete, move, rename, or quota code while browsing.
Add an explicit Nextcloud regression in backend/tests/test_cloud_backends.py:
- instantiate
NextcloudBackendwith mockedwebdav3client; - call the canonical four-argument adapter method;
- return the ten supplied root entries from mocked PROPFIND metadata;
- assert exactly four normalized folders and six normalized files by the expected names;
- assert root children have
parent_ref is None; - select
Documentsand prove itsprovider_item_idcan be passed back for a nested listing.
This test uses only expected names as fixture evidence. It must not contain a URL, username, app-password, encrypted credentials, or network marker.
Service reconciliation suite
Extend backend/tests/test_cloud_items.py:
- authoritative root listing upserts all ten unique Nextcloud fixture names;
- repeating the listing is idempotent;
- renamed/moved metadata preserves DocuVault item UUID by provider ID;
complete=Falseretains cached rows and records no deletions;- empty
complete=Trueis allowed to remove known children, proving failure and true-empty semantics remain distinct; - root (
None) and nested parent refs never collide across connection/user boundaries.
Task suite
Extend backend/tests/test_cloud_items.py or create backend/tests/test_cloud_tasks.py if task behavior becomes substantial:
- Nextcloud adapter receives canonical arguments through
_run; - complete listing reconciles and marks fresh;
- incomplete listing marks warning/stale and does not delete cached items;
- transient exception retries, terminal auth failure does not retry;
- credentials stay out of broker payload and logs;
- no byte or quota APIs are called.
API integration suite
Extend backend/tests/test_cloud.py and backend/tests/test_cloud_security.py:
- first root browse with a mocked provider contract returns all normalized items;
- response exposes
kindandprovider_item_id, never credentials; - incomplete first fetch returns controlled non-fresh status rather than a false fresh status;
- cached listing is returned while refresh is scheduled;
- nested browse forwards the exact provider reference;
- all four provider values pass through the same endpoint;
- wrong-owner/admin requests remain 404/403 and never invoke adapters;
- browse never downloads bytes or mutates quota.
Frontend suite
Extend frontend/src/views/__tests__/CloudFolderView.test.js:
- canonical
kind: 'folder'andkind: 'file'entries are passed toStorageBrowserin the right props; - clicking
Documentsroutes with itsprovider_item_id, not its DocuVaultid; - root request omits/translates the
rootsentinel consistently to canonicalparent_ref=Noneat the API boundary; - backend
freshness.refresh_state, timestamp, and warning message populate store/UI state; - HTTP 200 with warning/incomplete state never becomes “fresh” merely because the request resolved;
- expected ten-name Nextcloud fixture is rendered through the same shared browser as every provider.
Extend frontend/src/stores/__tests__/cloudConnections.test.js if freshness mapping is placed in the store. Preserve existing connection UUID and session-storage secrecy tests.
Acceptance Matrix
| Layer | Provider-neutral acceptance |
|---|---|
| Adapter | All four providers implement the exact CloudResourceAdapter signature and return CloudListing |
| Root identity | UI root sentinel becomes backend parent_ref=None; adapters translate root internally |
| Child identity | provider_item_id is used for provider navigation; id remains DocuVault metadata identity |
| Classification | kind is the sole canonical folder/file discriminator |
| Reconciliation | Only reconcile_cloud_listing writes listing metadata; incomplete results never delete cached rows |
| Freshness | Backend folder state is authoritative; incomplete/error results cannot display as fresh |
| Security | Owner scoping, credential secrecy, SSRF checks, no byte download, and no quota mutation remain intact |
| Nextcloud evidence | Root shows exactly the four supplied folders and six supplied files under mocked acceptance data |
Implementation Order
- Add the shared provider contract tests and failing Nextcloud signature/root regression.
- Remove the Nextcloud legacy override in favor of inherited DAV normalization.
- Make incomplete-listing handling consistent in synchronous API and background task paths.
- Fix
CloudFolderView.vueto usekind,provider_item_id, and backend freshness. - Add API and frontend regressions for root, nested navigation, warning truthfulness, and all-provider parity.
- Remove the legacy provider-slug browse path/helpers when repository search proves no active consumer remains.
No provider-specific file grid, reconciliation function, freshness mapper, or Nextcloud-only listing parser should be introduced.