row renders topics + analysis_status fields supplied by browse response
analysis_status
Make cloud browser rows/cards match local rows: surface lightweight topics + analysis status + current/stale state from the browse endpoint, render them in the same relative slots in StorageBrowser, route cloud row clicks to the cloud detail route (not auto preview/download), and collapse the duplicate status translator so the store is the single source.
Purpose: Implements D-06, D-09, D-10, D-13, D-16 row/card parity and the UI-SPEC Browser Parity Contract. Satisfies CLOUD-02 (row-level open parity), ANALYZE-01/02/03/04/05/06 (analyze/select/folder/connection/progress/retry/idempotent affordances rendered consistently), CACHE-03 (open routes through detail/authorized handlers, no row-level byte download).
Output: Browse response carries topics + analysis_status + stale; StorageBrowser renders parity slots and uses store translateAnalysisStatus; CloudFolderView routes row open to cloud-file-detail.
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-CONTEXT.md
@.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-RESEARCH.md
@.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-UI-SPEC.md
@CLAUDE.md
@backend/api/cloud/schemas.py
@backend/api/cloud/browse.py
@frontend/src/components/storage/StorageBrowser.vue
@frontend/src/views/CloudFolderView.vue
@frontend/src/views/FileManagerView.vue
@frontend/src/stores/cloudConnections.js
@frontend/src/components/storage/__tests__/StorageBrowser.parity.test.js
Task 1: Browse rows carry lightweight topics + analysis_status + stale; StorageBrowser renders parity slots via store translator
- backend/api/cloud/browse.py (_item_out builder ~line 76, browse_connection_items ~line 185 — where CloudItemOut rows are assembled)
- backend/api/cloud/schemas.py (CloudItemOut — add lightweight row fields; keep allowlist, no extracted_text/object_key/credentials_enc)
- backend/db/models.py (CloudItem.analysis_status, CloudItemTopic ↔ Topic for lightweight topic-name load)
- frontend/src/components/storage/StorageBrowser.vue (file row name cell ~line 575-660, topics in name cell ~line 49, analyze-file slot ~line 614, translateStatus def ~line 1022, analysisQueue usage ~line 304)
- frontend/src/stores/cloudConnections.js (translateAnalysisStatus — the single translator)
- frontend/src/views/FileManagerView.vue (local row: how file.topics and status render — the parity reference)
- .planning/phases/14.1-cloud-local-file-parity-hardening/14.1-UI-SPEC.md (Browser Parity Contract: title area, analysis affordance slot, status badges zone, disabled actions)
- frontend/src/components/storage/__tests__/StorageBrowser.parity.test.js (exact row parity assertions)
Backend: extend CloudItemOut in backend/api/cloud/schemas.py with lightweight, allowlisted row fields only — topics as a list of topic-name strings defaulting to empty, analysis_status as an optional string, and is_stale as a bool defaulting to false. DO NOT add extracted_text to row output (avoid list-payload inflation per RESEARCH common pitfall; full text stays on the detail endpoint). Keep CloudItemOut free of the credential field, the MinIO object-key field, the version-key field, and any provider URL. In backend/api/cloud/browse.py update _item_out (and the row assembly in browse_connection_items) to populate analysis_status from CloudItem.analysis_status, is_stale from a comparison of analysis_status to the stale literal, and topics from a lightweight CloudItemTopic→Topic name load. Batch the topic-name lookup for the page's file ids in one query to avoid N+1. Folder rows leave topics empty and analysis_status null. Frontend: in frontend/src/components/storage/StorageBrowser.vue render the cloud row's analysis status in the SAME relative slot as the local row status (name-cell zone), reuse the existing topic-badge block so cloud rows now display file.topics received from browse, and ensure the analyze/re-analyze/retry affordance occupies one shared name-cell slot that swaps by state (analyze when not analyzed, re-analyze when indexed/stale, retry when failed) per the UI-SPEC Browser Parity + Re-Analyze contracts. Replace the local translateStatus helper usage so status text comes from the store's translateAnalysisStatus (use the cloudConnections store translator) — remove the duplicate local function or make it delegate to the store, satisfying CLAUDE.md's single-translation-source rule (D-06). Use status colors per UI-SPEC: green current, amber stale/skipped, blue or violet working, red failed. Do not introduce a second cloud-only toolbar for row affordances — they stay inline in the name cell.
cd backend && python -m pytest tests/test_cloud.py tests/test_cloud_detail_parity.py -x 2>&1 | tail -12 && cd ../frontend && npx vitest run src/components/storage/__tests__/StorageBrowser.parity.test.js src/components/storage/__tests__/StorageBrowser.cloud-queue.test.js src/components/storage/__tests__/StorageBrowser.capabilities.test.js 2>&1 | tail -20
- CloudItemOut gains lightweight fields: `grep -nE 'analysis_status|is_stale|topics' backend/api/cloud/schemas.py` shows them inside the CloudItemOut class; `grep -c 'extracted_text' backend/api/cloud/schemas.py` does NOT include CloudItemOut (extracted_text only on detail schema).
- browse.py populates them: `grep -c 'analysis_status' backend/api/cloud/browse.py` returns >= 1.
- No N+1: the topic-name load for browse rows is a single batched query (confirm by reading; one select over the page's cloud_item ids).
- StorageBrowser uses store translator: `grep -c 'translateAnalysisStatus' frontend/src/components/storage/StorageBrowser.vue` returns >= 1 and the local duplicate either delegates or is removed (`grep -c 'function translateStatus' frontend/src/components/storage/StorageBrowser.vue` returns 0, or the remaining function body calls the store translator).
- StorageBrowser.parity.test.js paired row assertions pass (topic badges + status slot present in both local and cloud rows).
- Existing StorageBrowser.cloud-queue.test.js and StorageBrowser.capabilities.test.js still pass (no regression).
Cloud rows render topics + analysis status in the same slots as local rows using the store translator; browse response carries lightweight topics/analysis_status/is_stale without extracted_text; existing browser tests pass.
Task 2: Cloud row open navigates to cloud-file-detail route (no auto preview/download)
- frontend/src/views/CloudFolderView.vue (onFileOpen ~line 420 — currently calls openCloudFile and auto-falls-back to downloadCloudFile on unsupported_preview; router.push named-route patterns ~line 140-160)
- frontend/src/router/index.js (cloud-file-detail named route added in Plan 03)
- frontend/src/views/FileManagerView.vue (local file-open → /document/:id — the parity reference)
- .planning/phases/14.1-cloud-local-file-parity-hardening/14.1-UI-SPEC.md (Route And Navigation Contract: cloud row opens detail first, no auto-preview/auto-download)
- .planning/phases/14.1-cloud-local-file-parity-hardening/14.1-CONTEXT.md (D-01, D-14)
- frontend/src/views/__tests__/CloudFolderView.test.js and CloudFolderOpenPreview.test.js (existing open behavior to update — note Phase 13 P04 D-02 forbids window.open to provider URL; this stays a DocuVault route push)
Change onFileOpen in frontend/src/views/CloudFolderView.vue so a cloud file row click navigates to the cloud detail route instead of calling openCloudFile + auto-download. Replace the body with router.push({ name: 'cloud-file-detail', params: { connectionId: connectionId.value, itemId: file.provider_item_id } }) — passing the opaque provider_item_id as the route param (Vue Router encodes it; never split or decode it). Preview and download become explicit actions on the detail surface (implemented in Plan 03), NOT side effects of a row click; remove the auto-download-on-unsupported_preview branch from the row open path (D-14). Keep folder navigation (folder-navigate) unchanged. If any existing CloudFolderView open/preview test asserts the old auto-open/auto-download behavior, update that test to assert the new navigation-to-detail behavior (the row click pushes cloud-file-detail; it does NOT call downloadCloudFile). Do not introduce window.open or any provider URL navigation — the row click only pushes the DocuVault-internal named route (preserves Phase 13 D-02). Confirm local FileManagerView row open behavior to /document/:id is untouched so the parity test holds.
cd frontend && npx vitest run src/views/__tests__/CloudFolderView.test.js src/views/__tests__/CloudFolderOpenPreview.test.js src/views/__tests__/CloudDetailParity.test.js 2>&1 | tail -25
- Row open pushes the named detail route: `grep -c 'cloud-file-detail' frontend/src/views/CloudFolderView.vue` returns >= 1.
- The auto-download branch is gone from the row open path: `grep -n 'downloadCloudFile' frontend/src/views/CloudFolderView.vue` shows no call inside onFileOpen (download may still exist for explicit handlers elsewhere, but not on row click).
- No provider-URL navigation: `grep -c 'window.open' frontend/src/views/CloudFolderView.vue` returns 0.
- The row-click navigation test in CloudDetailParity.test.js passes (cloud row → cloud-file-detail; local row → /document/:id).
- Updated CloudFolderView.test.js / CloudFolderOpenPreview.test.js pass with the new navigate-to-detail behavior.
Cloud file row clicks navigate to the cloud-file-detail route with the opaque provider_item_id; no auto preview/download on row click; no provider-URL navigation; local row behavior unchanged; affected tests pass.
<threat_model>
Trust Boundaries
Boundary
Description
browse response → row render
rows carry only allowlisted lightweight fields (topics names, analysis_status) — no extracted_text/object_key/credentials/provider URL
row click → navigation
navigates to a DocuVault-internal named route with an opaque provider id; never to a provider URL
STRIDE Threat Register
Threat ID
Category
Component
Disposition
Mitigation Plan
T-14.1-10
Information Disclosure
CloudItemOut row fields
mitigate
Row schema stays an allowlist; extracted_text excluded from rows; no object_key/credentials/provider URL; Plan 01/04 tests enforce
Behavior: StorageBrowser renders cloud row topic/status parity slots and uses store translateAnalysisStatus (local translateStatus duplicate removed/delegated) (frontend/src/components/storage/StorageBrowser.vue).
Behavior: onFileOpen in CloudFolderView pushes cloud-file-detail named route with opaque provider_item_id; auto preview/download removed from row open (frontend/src/views/CloudFolderView.vue).
</artifacts_produced>
Create `.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-04-SUMMARY.md` when done