A successful Nextcloud connection opens through its UUID and never calls the UUID browse endpoint with a provider slug
A user can create, rename, edit, browse, and remove two independent connections for the same provider
Cloud folder breadcrumbs read Cloud > connection display name > folder hierarchy and every non-current segment navigates correctly even when synchronization fails
Only the deepest applicable cloud sidebar node is highlighted; the generic Cloud Storage link is active only on the provider overview
Validation failures show a useful field/provider message rather than bare HTTP 422
path
provides
contains
backend/api/cloud/connections.py
Independent connection creation and connection-ID credential update semantics
POST creates; connection-ID update edits exactly one account
connectionsFor|connectionId
from
to
via
pattern
frontend/src/views/CloudFolderView.vue
frontend/src/components/ui/BreadcrumbBar.vue
Cloud root plus connection and folder segment IDs
Cloud.*connectionRoot
Close the second Phase 12 UAT gap set: remove the remaining provider-keyed cloud navigation, implement genuinely independent same-provider connections, and make breadcrumb/sidebar state accurately represent the active cloud location.
The observed Nextcloud POST succeeded with HTTP 201. The subsequent sidebar navigation used /cloud/nextcloud/root, causing CloudFolderView to send nextcloud to the UUID endpoint and receive HTTP 422. Same-provider creation is separately blocked by backend provider upsert semantics and Settings' connectionFor(provider) first-match rendering.
@.planning/phases/12-cloud-resource-foundation/12-UAT.md
@.planning/phases/12-cloud-resource-foundation/12-UI-SPEC.md
@.planning/phases/12-cloud-resource-foundation/12-VALIDATION.md
@backend/api/cloud/connections.py
@backend/api/cloud/browse.py
@frontend/src/views/CloudFolderView.vue
@frontend/src/components/layout/AppSidebar.vue
@frontend/src/components/settings/SettingsCloudTab.vue
Task 1: Make cloud connection lifecycle connection-ID native
backend/api/cloud/connections.py, backend/tests/test_cloud.py, backend/tests/test_cloud_security.py, frontend/src/api/cloud.js, frontend/src/api/utils.js, frontend/src/components/cloud/CloudCredentialModal.vue, frontend/src/components/settings/SettingsCloudTab.vue, frontend/src/components/settings/__tests__/SettingsCloudTab.test.js, frontend/src/components/cloud/__tests__/CloudCredentialModal.test.js
- backend/api/cloud/connections.py — provider upsert, OAuth state, WebDAV create/config/delete paths
- backend/api/cloud/schemas.py — whitelisted credential-free output schemas
- backend/tests/test_cloud.py — existing Nextcloud create and connection-ID regressions
- backend/tests/test_cloud_security.py — owner scoping and credential-exclusion assertions
- frontend/src/components/settings/SettingsCloudTab.vue — first-match connectionFor rendering
- frontend/src/components/cloud/CloudCredentialModal.vue — create/edit currently share provider POST
- frontend/src/api/utils.js — structured 422 detail is currently discarded
Replace `_upsert_cloud_connection(user_id, provider)` semantics with explicit operations: creating a connection always inserts a new UUID row; editing/reconnecting credentials targets an owner-scoped connection ID and never changes another same-provider row. Add a credential-update endpoint for WebDAV/Nextcloud keyed by connection UUID; preserve the existing password when an edit omits a replacement, and validate/health-check the resulting credentials before commit. Keep OAuth creation independent as well: initiation state must distinguish a new account from a connection-ID reconnect/edit, and callback updates only the explicitly owner-scoped connection when reconnecting. Never put credentials in OAuth state, broker payloads, responses, logs, or audit metadata.
Refactor SettingsCloudTab from `connectionFor(provider)` to `connectionsFor(provider)`: render every connection as its own named row with Rename/Edit/Remove actions and retain an always-visible Add account control for each provider. CloudCredentialModal receives an existing connection ID and calls the ID update endpoint for edits; new connections call POST. Use effective custom/default names consistently and disambiguate duplicate defaults without exposing UUIDs beyond a short visual suffix where required by the UI contract.
Improve `request()` error normalization so FastAPI validation arrays become concise field messages and controlled API detail strings remain intact. This is diagnostic hardening; do not leak response internals or credentials.
- two POSTs for Nextcloud by one user create two distinct connection UUIDs and retain independent encrypted credentials
- editing/reconnecting one connection changes only that connection; wrong-owner IDs return 404 and disclose nothing
- Settings shows both same-provider accounts plus Add Nextcloud, and actions address the selected UUID
- bare HTTP 422 is replaced by a controlled actionable message
- create/update/list responses exclude `credentials_enc`, passwords, tokens, and raw provider objects
docker compose exec -T backend pytest -q tests/test_cloud.py tests/test_cloud_security.py && cd frontend && npm test -- --run src/components/settings/__tests__/SettingsCloudTab.test.js src/components/cloud/__tests__/CloudCredentialModal.test.js
Cloud accounts are independent UUID resources across create/edit/reconnect/settings, with owner-scoped security and useful validation errors.
Task 2: Remove provider-keyed navigation and repair cloud location state
frontend/src/components/cloud/CloudProviderTreeItem.vue, frontend/src/components/cloud/CloudFolderTreeItem.vue, frontend/src/components/layout/AppSidebar.vue, frontend/src/components/storage/StorageBrowser.vue, frontend/src/views/CloudFolderView.vue, frontend/src/views/CloudStorageView.vue, frontend/src/components/cloud/__tests__/CloudProviderTreeItem.test.js, frontend/src/components/cloud/__tests__/CloudFolderTreeItem.test.js, frontend/src/components/layout/__tests__/AppSidebar.test.js, frontend/src/views/__tests__/CloudFolderView.test.js, frontend/src/views/__tests__/CloudStorageView.test.js
- frontend/src/components/ui/TreeItem.vue — shared expansion and isActive contract
- frontend/src/components/ui/BreadcrumbBar.vue — root/segment emission behavior
- frontend/src/components/storage/StorageBrowser.vue — single shared browser and current root-label substitution
- frontend/src/components/cloud/CloudProviderTreeItem.vue — stale provider API/route
- frontend/src/components/cloud/CloudFolderTreeItem.vue — stale provider propagation
- frontend/src/views/CloudFolderView.vue — current connection lookup and folder-only breadcrumb
- frontend/src/components/layout/AppSidebar.vue — broad startsWith('/cloud') active state
Make every sidebar cloud request and route connection-ID based. CloudProviderTreeItem passes `connection.id` to `getCloudFoldersByConnectionId`, routes to `/cloud/{connection.id}/root`, and passes the same ID through nested CloudFolderTreeItem instances. Nested folders browse and route by connection ID and provider item/path reference; remove all active frontend use of the deprecated provider-keyed folder API.
Build the cloud breadcrumb as `Cloud > {effective connection name} > {provider folder segments}`. The Cloud root emits navigation to `/cloud`; the connection segment emits navigation to `/cloud/{id}/root`; folder segments navigate within that connection. Preserve the breadcrumb when browse refresh fails so users can always escape a stale/error view. Do this through StorageBrowser/BreadcrumbBar props and emitted events without creating a parallel breadcrumb component or mutating child props.
Make selection route-aware using TreeItem's existing `isActive` prop: generic Cloud Storage is active only when route path is exactly `/cloud`; the connection node is active at its root and while no deeper loaded node is selected; the deepest matching folder node is active for its provider reference. Use route params, not display labels or provider slugs. Keep connection nodes distinct for duplicate providers.
- no frontend cloud component calls `getCloudFolders(provider, ...)` or constructs `/cloud/{provider}`
- the exact sequence POST Nextcloud 201 → click sidebar connection calls the UUID endpoint and never returns UUID-validation 422
- failed refresh still shows a clickable Cloud crumb leading to `/cloud`
- root and nested views show Cloud > custom/default connection name > folders
- generic, connection, and folder sidebar highlighting are mutually consistent with the current route
cd frontend && npm test -- --run src/components/cloud/__tests__/CloudProviderTreeItem.test.js src/components/cloud/__tests__/CloudFolderTreeItem.test.js src/components/layout/__tests__/AppSidebar.test.js src/views/__tests__/CloudFolderView.test.js src/views/__tests__/CloudStorageView.test.js && ! rg "getCloudFolders\(|/cloud/\$\{[^}]*provider" src/components/cloud src/views src/components/layout
Cloud navigation uses connection UUIDs end-to-end, breadcrumbs expose the full hierarchy, and the sidebar highlights the actual active resource.
Task 3: Run cross-layer regressions, security gate, documentation, and release protocol
backend/main.py, frontend/package.json, frontend/package-lock.json, AGENTS.md, CLAUDE.md, README.md, SECURITY.md
- .planning/phases/12-cloud-resource-foundation/12-UAT.md — exact five user-reported symptoms and diagnosed artifacts
- AGENTS.md — tests, documentation, version, security, commit, and push requirements
- CLAUDE.md — mirrored development-agent contract
- SECURITY.md — Phase 12 threat register and accepted risks
Add/complete regression coverage for all UAT failures: successful Nextcloud creation followed by UUID browse; two same-provider connections; owner-scoped credential update; credential exclusion; Cloud overview/connection/folder breadcrumb navigation during provider failure; and exact active sidebar node. Run focused backend/frontend tests, full suites, production build, Bandit, npm audit, available pip-audit, and secret checks. Resolve introduced failures and high-severity findings without skips or suppressions. Bump patch version from 0.2.1 to 0.2.2 in backend/main.py and frontend package files. Update AGENTS.md and CLAUDE.md current state and shared architecture rules, README user-facing multi-account/navigation behavior, and SECURITY.md evidence. Commit and push atomically under `fix(12-uat): repair cloud connection navigation and multi-account support`.
- all focused regressions and full backend/frontend suites pass with zero new failures
- frontend production build and mandatory security gates pass
- no provider-keyed frontend browse path remains active
- versions are 0.2.2 and documentation describes true multi-account behavior
- Phase 12 UAT is ready to resume at Test 2, with Tests 4–6 still awaiting user verification
docker compose exec -T backend pytest -q && cd frontend && npm test -- --run && npm run build && npm audit --audit-level=high
The five reported cloud defects have regression coverage, security and full-suite gates pass, release/docs are current, and UAT can resume.
<threat_model>
Threat ID
Threat
Mitigation and evidence
T-12-06-01
IDOR while editing/browsing one of several connections
Resolve every connection ID with owner scope before decrypting, updating, browsing, or deleting; negative tests use another user's UUID
T-12-06-02
Credential disclosure through multi-account list/edit/errors
Whitelisted responses only; password omission means preserve existing secret; logs/audit/errors contain no credentials
T-12-06-03
OAuth state confusion updates another connection
Bind reconnect state to authenticated user plus explicit connection ID in expiring Redis state and re-check ownership at callback
T-12-06-04
SSRF bypass during credential edit
Re-run canonical validate_cloud_url and provider health check before storing updated WebDAV/Nextcloud credentials
T-12-06-05
Cross-connection metadata collision
Continue scoping durable cloud items and folder states by connection UUID; same-provider regression asserts isolation
T-12-06-06
Route/provider identifier confusion
UUID route parsing plus frontend tests prove provider slugs never enter connection-ID endpoints
T-12-06-SC
Dependency/supply-chain risk
No new runtime dependencies; rerun Bandit, pip/npm audits, and secret scan
</threat_model>
1. Plan structure contains three executable tasks, explicit files, acceptance criteria, and automated checks.
2. Backend tests prove independent same-provider lifecycle and owner isolation.
3. Frontend tests prove UUID-only sidebar navigation, full breadcrumb hierarchy, and deepest-node selection.
4. A cross-layer regression captures the observed POST 201 followed by sidebar browse without 422.
5. Full tests, build, security gates, version/docs, commit, and push complete before UAT resumes.
<success_criteria>
The connected Nextcloud account opens from every UI entry point without a provider-slug/UUID mismatch.
Users can manage multiple accounts from any supported provider as independent connection resources.
Cloud breadcrumbs and sidebar selection truthfully represent overview, connection, and nested folder state.
Failures remain navigable and actionable instead of trapping the user or displaying bare HTTP status text.
</success_criteria>
Create .planning/phases/12-cloud-resource-foundation/12-06-SUMMARY.md when complete.