--- phase: 14.1-cloud-local-file-parity-hardening plan: 03 type: execute wave: 3 depends_on: [14.1-01, 14.1-02] files_modified: - frontend/src/components/storage/DocumentDetailSurface.vue - frontend/src/views/DocumentView.vue - frontend/src/views/CloudDetailView.vue - frontend/src/router/index.js - frontend/src/api/cloud.js - frontend/src/stores/cloudConnections.js autonomous: true requirements: [CLOUD-02, ANALYZE-01, ANALYZE-05, ANALYZE-06, CACHE-03] must_haves: truths: - "A shared DocumentDetailSurface renders header, source metadata, status, topics, and extracted text for both local and cloud detail" - "Local DocumentView and cloud CloudDetailView are thin data providers that feed the shared surface and handle its events" - "A named cloud-file-detail route opens a cloud detail view from a connection id and opaque provider item id" - "The cloud detail view shows Analyze before analysis and Re-analyze after, with confirmation for forcing a current file" - "Preview unavailable keeps Download active and never auto-downloads" - "Visible copy says Re-analyze everywhere; no rendered Re-classify remains" artifacts: - path: "frontend/src/components/storage/DocumentDetailSurface.vue" provides: "Shared detail surface (header, source metadata, status/stale, topics, extracted text, action slot)" min_lines: 80 - path: "frontend/src/views/CloudDetailView.vue" provides: "Cloud detail data-provider view backed by CloudItem detail endpoint" min_lines: 60 - path: "frontend/src/router/index.js" provides: "Named cloud-file-detail route under /cloud" contains: "cloud-file-detail" - path: "frontend/src/api/cloud.js" provides: "getCloudItemDetail API client method" contains: "getCloudItemDetail" - path: "frontend/src/stores/cloudConnections.js" provides: "reanalyze/force enqueue + detail fetch wiring; single translateAnalysisStatus source" contains: "translateAnalysisStatus" key_links: - from: "frontend/src/views/CloudDetailView.vue" to: "frontend/src/api/cloud.js" via: "calls getCloudItemDetail(connectionId, itemId)" pattern: "getCloudItemDetail" - from: "frontend/src/views/DocumentView.vue" to: "frontend/src/components/storage/DocumentDetailSurface.vue" via: "renders the shared surface with local data + handlers" pattern: "DocumentDetailSurface" - from: "frontend/src/router/index.js" to: "frontend/src/views/CloudDetailView.vue" via: "cloud-file-detail route component" pattern: "CloudDetailView" --- Extract a shared document detail surface from DocumentView.vue and build a cloud detail route/view backed by the CloudItem detail endpoint, so local and cloud files render the same detail layout, status, topics, extracted text, and action slot without forking. Rename visible Re-classify to Re-analyze, add force re-analyze with confirmation, and ensure unsupported preview never auto-downloads. Purpose: Implements D-01..D-11, D-13, D-14, D-19 frontend contracts and the UI-SPEC Detail Layout / Detail State / Preview-Unsupported / Re-Analyze contracts. Satisfies CLOUD-02 (authorized open/preview/view parity), ANALYZE-01/05/06 (analyze/re-analyze/retry/force), CACHE-03 (bytes only via authorized handlers). Output: DocumentDetailSurface.vue shared component; DocumentView.vue refactored to use it; CloudDetailView.vue + cloud-file-detail route; getCloudItemDetail API method; store wiring for force re-analyze and detail fetch. @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md @.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 @frontend/src/views/DocumentView.vue @frontend/src/views/CloudFolderView.vue @frontend/src/router/index.js @frontend/src/api/cloud.js @frontend/src/stores/cloudConnections.js @frontend/src/utils/formatters.js @frontend/src/views/__tests__/CloudDetailParity.test.js Task 1: Extract DocumentDetailSurface.vue and refactor DocumentView.vue to use it (Re-analyze copy) - frontend/src/views/DocumentView.vue (full file — header block, Topics card with Re-classify button, Extracted Text card, reclassify()/suggestTopics() methods, preview modal) - frontend/src/components/topics/TopicBadge.vue (topic badge presentational component reused by the surface) - frontend/src/utils/formatters.js (formatDate, formatSize, providerColor, providerBg, providerLabel — use these, do not redefine) - .planning/phases/14.1-cloud-local-file-parity-hardening/14.1-UI-SPEC.md (Detail Layout Contract section order; Copywriting Contract; Re-Analyze And Retry Contract) - .planning/phases/14.1-cloud-local-file-parity-hardening/14.1-CONTEXT.md (D-04, D-05, D-08, D-09, D-10) - frontend/src/views/__tests__/CloudDetailParity.test.js (paired detail section-order assertions the surface must satisfy) Create frontend/src/components/storage/DocumentDetailSurface.vue as a presentational smart component owning the detail layout for BOTH local and cloud detail. Per the UI-SPEC Detail Layout Contract, the section order is: (1) Back navigation slot, (2) Header block — title (24px semibold, break-all wrap), metadata line (date/size/type, 12-14px muted), subtle source metadata line/chips for cloud (provider chip via providerColor/providerBg + location; absent or minimal for local), and a primary action cluster, (3) Status and source notice area (current/stale/working badges using green/amber/blue per UI-SPEC), (4) Topics section (TopicBadge list + empty copy "No topics assigned yet."), (5) Extracted text section (pre block), (6) secondary controls / inline error+help copy. Define props: title, metadataLine, source (provider/location/isStale/statusLabel), topics (array), analysisStatus, extractedText, previewState ({ supported, reason }), downloadState ({ supported }), analysisAction ({ kind: 'analyze'|'reanalyze'|'retry', busy }), and optional slots for delete/share/suggest controls so local DocumentView can inject its extra buttons. Define emits: preview, download, reanalyze, retry-analysis, analyze. The single analysis action slot swaps Analyze/Re-analyze/Retry by analysisAction.kind (never accumulates buttons — UI-SPEC Re-Analyze contract). Use the Copywriting Contract literals: primary CTA "Analyze file" / "Preview file" / "Open file", empty heading "No analysis yet", empty body "Analyze this file to extract text and topics.". Use ONLY formatters from utils/formatters.js. Then refactor frontend/src/views/DocumentView.vue to render DocumentDetailSurface, feeding local document data and wiring its existing methods: pass reclassify() as the reanalyze handler, change the VISIBLE button label from "Re-classify" to "Re-analyze" (keep the internal classifyDocument API call name unchanged per D-09 / Codex discretion), keep suggestTopics and delete via the surface's secondary slots. Do not duplicate the card/grid markup in DocumentView after refactor — it becomes a data provider. Per CLAUDE.md no-dead-code rule, remove any now-unused local markup blocks in the same edit. cd frontend && npx vitest run src/views/__tests__/CloudDetailParity.test.js 2>&1 | tail -20 && grep -rc 'Re-classify' frontend/src/views/DocumentView.vue frontend/src/components/storage/DocumentDetailSurface.vue - `frontend/src/components/storage/DocumentDetailSurface.vue` exists and imports TopicBadge and formatters from utils/formatters.js (no local formatDate/formatSize/providerColor definitions): `grep -c "from '../../utils/formatters" frontend/src/components/storage/DocumentDetailSurface.vue` returns >= 1. - DocumentView renders the shared surface: `grep -c 'DocumentDetailSurface' frontend/src/views/DocumentView.vue` returns >= 1. - No rendered Re-classify in either file: `grep -c 'Re-classify' frontend/src/views/DocumentView.vue frontend/src/components/storage/DocumentDetailSurface.vue` returns 0. - Visible Re-analyze present: `grep -c 'Re-analyze' frontend/src/components/storage/DocumentDetailSurface.vue` returns >= 1. - The surface's analysis action region uses a single state-swapped slot (no two simultaneous Analyze+Re-analyze buttons) — confirm by reading the template region. - The paired detail section-order assertions in CloudDetailParity.test.js that target the shared surface pass. DocumentDetailSurface owns the local+cloud detail layout; DocumentView is a thin provider rendering it; visible copy says Re-analyze; no Re-classify remains in these files. Task 2: Cloud detail route + CloudDetailView.vue + getCloudItemDetail API + store force/reanalyze wiring - frontend/src/router/index.js (existing named routes /document/:id, cloud, cloud-folder; guard behavior) - frontend/src/api/cloud.js (openCloudFile, previewCloudFile, downloadCloudFile, enqueueAnalysis, retryAnalysisItem, jsonRequest helper — match the existing function/export style) - frontend/src/stores/cloudConnections.js (translateAnalysisStatus, enqueueAnalysis, retryItem, requestEstimate; reuse — do NOT add a second status translator) - frontend/src/views/CloudFolderView.vue (how cloud views resolve connectionId/route params, call api.* barrel, and push named routes with opaque provider_item_id) - frontend/src/components/storage/DocumentDetailSurface.vue (the surface this view feeds — created in Task 1) - .planning/phases/14.1-cloud-local-file-parity-hardening/14.1-UI-SPEC.md (Route And Navigation Contract; Detail State Contract; Preview/Download/Unsupported Contract; Re-Analyze confirmation copy) - .planning/phases/14.1-cloud-local-file-parity-hardening/14.1-CONTEXT.md (D-01, D-02, D-03, D-07, D-11, D-14) Add a named route cloud-file-detail to frontend/src/router/index.js under /cloud with an opaque provider item param, e.g. path /cloud/:connectionId/item/:itemId(.*) name 'cloud-file-detail' component () => import('../views/CloudDetailView.vue') meta { requiresAuth: true } — placed so it does not collide with the existing /cloud/:connectionId/:folderId(.*) cloud-folder route (use the distinct /item/ segment). Add getCloudItemDetail(connectionId, itemId) to frontend/src/api/cloud.js calling GET /api/cloud/connections/${connectionId}/items/${encodeURIComponent(itemId)}/detail via the same jsonRequest/get helper the other cloud methods use; never construct or expose provider URLs. Create frontend/src/views/CloudDetailView.vue as a thin data-provider view: read connectionId + itemId from route params (opaque — never split/decode provider IDs), call cloudStore/api getCloudItemDetail on mount, map the response into DocumentDetailSurface props (title=name, metadataLine from formatters, source={provider, location, isStale, statusLabel via store.translateAnalysisStatus}, topics, analysisStatus, extractedText, previewState from capabilities, downloadState from capabilities, analysisAction kind derived from analysis_status: 'analyze' when pending/none, 'reanalyze' when indexed/current/stale, 'retry' when failed/partial per D-08/Detail State Contract). Wire the surface events: preview → api.previewCloudFile (on unsupported_preview show "Preview unavailable" + backend reason and keep Download active; DO NOT auto-download — replace the CloudFolderView.onFileOpen auto-download behavior here per D-14); download → api.downloadCloudFile; analyze → store.enqueueAnalysis(scope file); reanalyze → confirm with the Copywriting Contract destructive confirmation ("Re-analyze this file? Existing extracted text and topics stay visible until the new analysis finishes."), then store.enqueueAnalysis with force:true (D-11); retry-analysis → store.retryItem or single-item retry path (D-12). Per D-07, when stale keep prior extracted_text/topics visible and promote Re-analyze. In frontend/src/stores/cloudConnections.js extend enqueueAnalysis to forward a force param into api.enqueueAnalysis params (and a convenience reanalyze action if cleaner), and add a fetchCloudItemDetail action calling api.getCloudItemDetail if components need store-level caching — keep translateAnalysisStatus as the single status source (do not add a second translator). Do not create a parallel cloud detail layout — CloudDetailView only provides data + handlers to DocumentDetailSurface. cd frontend && npx vitest run src/views/__tests__/CloudDetailParity.test.js 2>&1 | tail -25 - Named route present: `grep -c 'cloud-file-detail' frontend/src/router/index.js` returns 1 and references CloudDetailView. - API method present: `grep -c 'getCloudItemDetail' frontend/src/api/cloud.js` returns >= 1 and uses encodeURIComponent on itemId. - CloudDetailView feeds the shared surface: `grep -c 'DocumentDetailSurface' frontend/src/views/CloudDetailView.vue` returns >= 1. - Force re-analyze wired: `grep -nE 'force' frontend/src/views/CloudDetailView.vue frontend/src/stores/cloudConnections.js` shows force forwarded to enqueue. - No auto-download on unsupported preview in the detail flow: CloudDetailView's preview handler shows a reason and keeps download as a separate explicit action (confirm by reading; `grep -c 'Preview unavailable' frontend/src/views/CloudDetailView.vue` returns >= 1). - Only one status translator: `grep -c 'function translateAnalysisStatus' frontend/src/stores/cloudConnections.js` returns 1 and CloudDetailView contains no local status-translation function. - CloudDetailParity.test.js passes (route resolves, analyze→reanalyze by state, Re-analyze copy, no auto-download). cloud-file-detail route renders CloudDetailView feeding the shared surface; getCloudItemDetail loads CloudItem detail; force re-analyze confirms then enqueues with force; unsupported preview keeps Download explicit and never auto-downloads; Plan 01 cloud-detail tests pass. ## Trust Boundaries | Boundary | Description | |----------|-------------| | browser route → API | connectionId + opaque provider itemId from route params; must be encoded, never parsed/decoded for provider structure | | API response → rendered UI | rendered detail must not expose provider URLs, credentials, or cache object keys | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-14.1-07 | Information Disclosure | CloudDetailView render | mitigate | View renders only allowlisted CloudItemDetailOut fields; no provider URL/credential/object_key in template or store; UI-SPEC verification anchor | | T-14.1-08 | Tampering | provider item id in route | mitigate | itemId passed opaque, encodeURIComponent in API call, Vue Router encodes route param; never split/decode | | T-14.1-09 | Spoofing/Surprise download | unsupported preview | mitigate | Detail flow shows "Preview unavailable" + reason; Download is a separate explicit action (D-14); no auto-download | | T-14.1-SC | Tampering | npm installs | accept | No package installs (RESEARCH: none) | - `cd frontend && npx vitest run src/views/__tests__/CloudDetailParity.test.js` passes. - `cd frontend && npx vitest run 2>&1 | tail -10` — full frontend suite passes (DocumentView refactor breaks nothing). - `grep -rc 'Re-classify' frontend/src/views frontend/src/components` returns 0 for rendered templates (test snapshots may retain intentional references — confirm none are user-facing). - DocumentDetailSurface is the single shared detail layout for local + cloud. - DocumentView and CloudDetailView are thin data providers. - cloud-file-detail named route + getCloudItemDetail API + force re-analyze + no-auto-download all working. - Visible copy says Re-analyze; single translateAnalysisStatus source preserved. ## Artifacts this phase produces (Plan 03) - Component: `frontend/src/components/storage/DocumentDetailSurface.vue` — shared detail surface (props: title, metadataLine, source, topics, analysisStatus, extractedText, previewState, downloadState, analysisAction; emits: preview, download, reanalyze, retry-analysis, analyze). - View: `frontend/src/views/CloudDetailView.vue` — cloud detail data provider. - Route: named `cloud-file-detail` at `/cloud/:connectionId/item/:itemId(.*)` (frontend/src/router/index.js). - API method: `getCloudItemDetail(connectionId, itemId)` (frontend/src/api/cloud.js). - Store wiring: force-aware `enqueueAnalysis` (+ optional `fetchCloudItemDetail`) in frontend/src/stores/cloudConnections.js; DocumentView visible label changed to "Re-analyze". Create `.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-03-SUMMARY.md` when done