Files
kite/.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-04-PLAN.md
T

15 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
14.1-cloud-local-file-parity-hardening 04 execute 4
14.1-01
14.1-02
14.1-03
backend/api/cloud/schemas.py
backend/api/cloud/browse.py
frontend/src/components/storage/StorageBrowser.vue
frontend/src/views/CloudFolderView.vue
true
CLOUD-02
ANALYZE-01
ANALYZE-02
ANALYZE-03
ANALYZE-04
ANALYZE-05
ANALYZE-06
CACHE-03
truths artifacts key_links
Cloud browse rows carry topics, analysis_status, and current/stale state so the shared browser can render them like local rows
Local and cloud file rows show topic badges and analysis status in the same relative slot
Clicking a cloud file row navigates to the cloud detail route, not a direct preview or download
StorageBrowser uses the store translateAnalysisStatus only — the local translateStatus duplicate is removed or routed to the store
Re-analyze / Retry analysis appear in the same row action slot for local and cloud rows by state
path provides contains
backend/api/cloud/browse.py Browse rows include lightweight topics + analysis_status + stale state analysis_status
path provides contains
frontend/src/components/storage/StorageBrowser.vue Row parity: topic/status slots, state-driven analyze/reanalyze/retry, single status translator translateAnalysisStatus
path provides contains
frontend/src/views/CloudFolderView.vue Cloud row open navigates to cloud-file-detail route (no auto preview/download) cloud-file-detail
from to via pattern
frontend/src/views/CloudFolderView.vue frontend/src/router/index.js row open pushes named cloud-file-detail route with connectionId + opaque provider_item_id cloud-file-detail
from to via pattern
frontend/src/components/storage/StorageBrowser.vue frontend/src/stores/cloudConnections.js row status rendered through store translateAnalysisStatus translateAnalysisStatus
from to via pattern
frontend/src/components/storage/StorageBrowser.vue backend/api/cloud/browse.py 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.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.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
T-14.1-11 Spoofing/Surprise navigation cloud row click mitigate Row click pushes internal cloud-file-detail route only; no window.open / provider URL (preserves Phase 13 D-02)
T-14.1-12 Surprise download unsupported preview on row mitigate Auto-download removed from row open path; download is explicit on detail (D-14)
T-14.1-SC Tampering npm/pip installs accept No package installs (RESEARCH: none)
</threat_model>
- `cd backend && python -m pytest tests/test_cloud.py tests/test_cloud_detail_parity.py tests/test_cloud_security.py -x` passes. - `cd frontend && npx vitest run src/components/storage/__tests__ src/views/__tests__/CloudFolderView.test.js src/views/__tests__/CloudFolderOpenPreview.test.js src/views/__tests__/CloudDetailParity.test.js` passes. - `cd frontend && npx vitest run 2>&1 | tail -10` — full frontend suite passes.

<success_criteria>

  • Browse rows carry lightweight topics + analysis_status + is_stale (no extracted_text in rows).
  • Local and cloud rows render topic badges + analysis status in the same relative slots via the store translator.
  • Cloud row click navigates to cloud-file-detail; no auto preview/download; no provider URL.
  • Existing browser/cloud-view tests pass with updated navigation behavior. </success_criteria>

<artifacts_produced>

Artifacts this phase produces (Plan 04)

  • Fields: topics: list[str], analysis_status: Optional[str], is_stale: bool added to CloudItemOut (backend/api/cloud/schemas.py).
  • Behavior: browse rows populate topics (batched) + analysis_status + is_stale (backend/api/cloud/browse.py).
  • 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