177 lines
15 KiB
Markdown
177 lines
15 KiB
Markdown
---
|
|
phase: 14.1-cloud-local-file-parity-hardening
|
|
plan: 04
|
|
type: execute
|
|
wave: 4
|
|
depends_on: [14.1-01, 14.1-02, 14.1-03]
|
|
files_modified:
|
|
- backend/api/cloud/schemas.py
|
|
- backend/api/cloud/browse.py
|
|
- frontend/src/components/storage/StorageBrowser.vue
|
|
- frontend/src/views/CloudFolderView.vue
|
|
autonomous: true
|
|
requirements: [CLOUD-02, ANALYZE-01, ANALYZE-02, ANALYZE-03, ANALYZE-04, ANALYZE-05, ANALYZE-06, CACHE-03]
|
|
must_haves:
|
|
truths:
|
|
- "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"
|
|
artifacts:
|
|
- path: "backend/api/cloud/browse.py"
|
|
provides: "Browse rows include lightweight topics + analysis_status + stale state"
|
|
contains: "analysis_status"
|
|
- path: "frontend/src/components/storage/StorageBrowser.vue"
|
|
provides: "Row parity: topic/status slots, state-driven analyze/reanalyze/retry, single status translator"
|
|
contains: "translateAnalysisStatus"
|
|
- path: "frontend/src/views/CloudFolderView.vue"
|
|
provides: "Cloud row open navigates to cloud-file-detail route (no auto preview/download)"
|
|
contains: "cloud-file-detail"
|
|
key_links:
|
|
- from: "frontend/src/views/CloudFolderView.vue"
|
|
to: "frontend/src/router/index.js"
|
|
via: "row open pushes named cloud-file-detail route with connectionId + opaque provider_item_id"
|
|
pattern: "cloud-file-detail"
|
|
- from: "frontend/src/components/storage/StorageBrowser.vue"
|
|
to: "frontend/src/stores/cloudConnections.js"
|
|
via: "row status rendered through store translateAnalysisStatus"
|
|
pattern: "translateAnalysisStatus"
|
|
- from: "frontend/src/components/storage/StorageBrowser.vue"
|
|
to: "backend/api/cloud/browse.py"
|
|
via: "row renders topics + analysis_status fields supplied by browse response"
|
|
pattern: "analysis_status"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
|
@$HOME/.claude/gsd-core/templates/summary.md
|
|
</execution_context>
|
|
|
|
<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
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: Browse rows carry lightweight topics + analysis_status + stale; StorageBrowser renders parity slots via store translator</name>
|
|
<read_first>
|
|
- 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)
|
|
</read_first>
|
|
<action>
|
|
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.
|
|
</action>
|
|
<verify>
|
|
<automated>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</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- 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).
|
|
</acceptance_criteria>
|
|
<done>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.</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 2: Cloud row open navigates to cloud-file-detail route (no auto preview/download)</name>
|
|
<read_first>
|
|
- 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)
|
|
</read_first>
|
|
<action>
|
|
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.
|
|
</action>
|
|
<verify>
|
|
<automated>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</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- 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.
|
|
</acceptance_criteria>
|
|
<done>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.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<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>
|
|
|
|
<verification>
|
|
- `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.
|
|
</verification>
|
|
|
|
<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>
|
|
|
|
<output>
|
|
Create `.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-04-SUMMARY.md` when done
|
|
</output>
|