--- phase: 14.1-cloud-local-file-parity-hardening plan: "03" subsystem: cloud-detail-frontend status: complete tags: [frontend, shared-surface, cloud-detail, force-reanalyze, parity, route] requirements: [CLOUD-02, ANALYZE-01, ANALYZE-05, ANALYZE-06, CACHE-03] dependency_graph: requires: - 14.1-01 (RED tests — CloudDetailParity.test.js must pass) - 14.1-02 (backend cloud detail endpoint + force field + retry route) provides: - frontend/src/components/storage/DocumentDetailSurface.vue - frontend/src/views/CloudDetailView.vue - frontend/src/router/index.js (cloud-file-detail route added) - frontend/src/api/cloud.js (getCloudItemDetail added) - frontend/src/stores/cloudConnections.js (force param + fetchCloudItemDetail) affects: - frontend/src/components/storage/StorageBrowser.vue (Plan 04 target — Re-analyze in rows) - frontend/src/views/CloudFolderView.vue (Plan 04 target — row click navigates to cloud-file-detail) tech_stack: added: [] patterns: - Shared detail surface pattern (DocumentDetailSurface as layout owner; DocumentView + CloudDetailView as thin data providers) - Single analysis action slot (swaps Analyze/Re-analyze/Retry by kind — never accumulates) - Opaque provider itemId in route params — encodeURIComponent in API, never decoded/split in frontend - Force re-analyze with confirmation dialog (Copywriting Contract destructive copy) - Preview unavailable keeps Download active, no auto-download (D-14) key_files: created: - frontend/src/components/storage/DocumentDetailSurface.vue - frontend/src/views/CloudDetailView.vue modified: - frontend/src/views/DocumentView.vue - frontend/src/router/index.js - frontend/src/api/cloud.js - frontend/src/stores/cloudConnections.js decisions: - DocumentDetailSurface is a presentational smart component with named slots for secondary-actions, topics-actions, topics-error, suggestions, and secondary-controls - Topics prop accepts both string arrays (cloud) and object arrays with {name, color} (local) — normalized in computed - DocumentView analysis action is always 'reanalyze' kind (local files are always analyzed); busy flag controlled by classifying ref - Cloud route path uses /item/ segment before itemId to disambiguate from /cloud-folder /:folderId(.*) wildcard - cloudConnections.js imports cloudApi (cloud.js) separately from api (client.js) for getCloudItemDetail — no local redefinition - Re-analyze confirmation modal shown before any force=true enqueue; cancel keeps existing analysis data visible metrics: duration: "5m" completed_date: "2026-06-26" tasks_completed: 2 files_created: 2 files_modified: 4 frontend_tests_passing: 488 test_suite_delta: +0 new failures (1 pre-existing Plan 01 RED test for Plan 04 target) --- # Phase 14.1 Plan 03: Shared Document Detail Surface + Cloud Detail Route Summary Extract a shared `DocumentDetailSurface.vue` from `DocumentView.vue`, refactor local document detail to a thin data provider using it, build `CloudDetailView.vue` backed by the Plan 02 `getCloudItemDetail` endpoint, add the named `cloud-file-detail` route, wire force re-analyze with confirmation, and ensure unsupported preview keeps Download active without auto-downloading. ## Tasks Completed ### Task 1: Extract DocumentDetailSurface.vue + refactor DocumentView.vue (Re-analyze copy) **`frontend/src/components/storage/DocumentDetailSurface.vue`** (new, 190 lines): Created as a presentational smart component owning the shared detail layout for both local and cloud files. Implements the UI-SPEC Detail Layout Contract section order: 1. Back navigation (slot — overridable) 2. Header block: title (24px semibold, break-all), metadata line (12–14px muted), cloud provider chip + location (subtle, providerColor/providerBg from formatters), primary action cluster 3. Status and source notice area (stale amber badge, preview unavailable notice, working status, cloud source footnote) 4. Topics section (TopicBadge + "No topics assigned yet." empty copy, topics-actions slot) 5. Extracted text (pre block with "No analysis yet / Analyze this file…" Copywriting Contract empty state) 6. Secondary controls slot Props: `title`, `metadataLine`, `source` (`{provider, location, isStale, statusLabel}`), `topics` (string[] or `{name, color}[]`), `analysisStatus`, `extractedText`, `previewState` (`{supported, reason, openLabel}`), `downloadState` (`{supported}`), `analysisAction` (`{kind: 'analyze'|'reanalyze'|'retry', busy}`). Emits: `back`, `preview`, `download`, `analyze`, `reanalyze`, `retry-analysis`. Single analysis action slot: exactly one of Analyze/Re-analyze/Retry buttons renders at a time — swaps by `analysisAction.kind` (UI-SPEC Re-Analyze contract). Imports exclusively from `utils/formatters.js` (no local redefinitions). **`frontend/src/views/DocumentView.vue`** (refactored): Reduced to thin data provider. Renders `DocumentDetailSurface` with local document data. Injects secondary-actions (Delete), topics-actions (Suggest Topics), topics-error (classify error), and suggestions panel via named slots. Visible button label changed from "Re-classify" to "Re-analyze" (internal `classifyDocument` API call preserved per D-09). **Verification:** - CloudDetailParity Re-classify regression test passes ✓ - DocumentView section rendering test passes ✓ - No rendered "Re-classify" in either file ✓ - `grep -c "from '../../utils/formatters"` → 1 ✓ **Commit:** 825a7b5 ### Task 2: Cloud detail route + CloudDetailView.vue + getCloudItemDetail API + store force/reanalyze wiring **`frontend/src/router/index.js`:** Added named route `cloud-file-detail` at `/cloud/:connectionId/item/:itemId(.*)` with `requiresAuth: true`. Declared before the `cloud-folder` route (which has `/:folderId(.*)`) to prevent wildcard capture. The distinct `/item/` segment disambiguates. `itemId` is opaque — Vue Router handles URI encoding (T-14.1-08). **`frontend/src/api/cloud.js`:** Added `getCloudItemDetail(connectionId, itemId)` calling `GET /api/cloud/connections/${connectionId}/items/${encodeURIComponent(itemId)}/detail` via the `request` helper (T-14.1-03/T-14.1-06: credential-free, zero bytes). **`frontend/src/views/CloudDetailView.vue`** (new, 316 lines): Thin data-provider view. Reads `connectionId` + `itemId` from route params (never split/decoded). Calls `cloudApi.getCloudItemDetail` on mount. Maps response to `DocumentDetailSurface` props: - `source.isStale` derived from `is_stale` or `analysis_status === 'stale'` (D-07) - `source.statusLabel` via `cloudStore.translateAnalysisStatus` — single translator, no local translation (D-06) - `analysisAction.kind` derived from `analysis_status`: `analyze` (none/pending), `reanalyze` (indexed/stale/current), `retry` (failed/partial), `null` (unsupported) - `previewState.supported` inferred from `content_type` (PDF/image → true; others → false with reason) - `downloadState.supported: true` (authorized download always available) Event handlers: - `preview` → `cloudApi.previewCloudFile`; on `unsupported_preview`, surfaces reason inline — never auto-downloads (D-14) - `download` → `cloudApi.downloadCloudFile` - `analyze` → `cloudStore.enqueueAnalysis({ scope: 'file', provider_item_ids: [itemId] })` - `reanalyze` → shows confirmation modal (Copywriting Contract: "Re-analyze this file? Existing extracted text and topics stay visible until the new analysis finishes.") → on confirm, `enqueueAnalysis({ force: true })` - `retry-analysis` → `enqueueAnalysis({ force: true })` (D-12 single-item retry path) **`frontend/src/stores/cloudConnections.js`:** Extended `enqueueAnalysis` to accept and forward `force` param (D-11). Added `fetchCloudItemDetail` action calling `cloudApi.getCloudItemDetail`. Imports `cloudApi` from `../api/cloud.js` separately from `api` (client.js barrel). **Verification:** - All 8 `CloudDetailParity.test.js` tests pass ✓ - Full suite: 488 passed, 1 pre-existing RED failure (Plan 04 target) ✓ **Commit:** 48afb8b ## Verification Results ### Plan verification (npx vitest run src/views/__tests__/CloudDetailParity.test.js) ``` Test Files 1 passed (1) Tests 8 passed (8) ``` All 8 RED tests from Plan 01 now green: - Route includes cloud-file-detail ✓ - Route resolves with connectionId + itemId params ✓ - Local /document/:id route preserved ✓ - Paired route parity (both routes coexist) ✓ - Cloud row navigation prerequisite met ✓ - DocumentView does not contain Re-classify ✓ - DocumentView renders extracted-text and topics ✓ - Re-analyze copy prerequisite met ✓ ### Full suite (npx vitest run) ``` Test Files 1 failed | 51 passed (52) Tests 1 failed | 488 passed (489) ``` 1 pre-existing RED failure: `StorageBrowser.parity.test.js > Cloud indexed file shows Re-analyze (not Re-classify)` — targeted by Plan 04 which adds Re-analyze copy to cloud rows in StorageBrowser. This failure existed before Plan 03 and is unaffected. ### Acceptance criteria - `grep -c "from '../../utils/formatters" frontend/src/components/storage/DocumentDetailSurface.vue` → 1 ✓ - `grep -c 'DocumentDetailSurface' frontend/src/views/DocumentView.vue` → 3 ✓ - `grep -c 'Re-classify' frontend/src/views/DocumentView.vue frontend/src/components/storage/DocumentDetailSurface.vue` → 0 in templates (1 in DocumentView comment only — confirmed not rendered) ✓ - `grep -c 'Re-analyze' frontend/src/components/storage/DocumentDetailSurface.vue` → 3 ✓ - Single analysis action slot confirmed (v-if/v-else-if chain, no simultaneous buttons) ✓ - `grep -c 'cloud-file-detail' frontend/src/router/index.js` → 2 (1 in comment, 1 in route name); CloudDetailView referenced ✓ - `grep -c 'getCloudItemDetail' frontend/src/api/cloud.js` → 1; uses encodeURIComponent ✓ - `grep -c 'DocumentDetailSurface' frontend/src/views/CloudDetailView.vue` → 4 ✓ - Force wired: `grep -nE 'force' frontend/src/views/CloudDetailView.vue` → force: true in confirmReanalyze + handleRetry; `grep -nE 'force' frontend/src/stores/cloudConnections.js` → forwarded via spread ✓ - `grep -c 'Preview unavailable' frontend/src/views/CloudDetailView.vue` → 4 ✓ - `grep -c 'function translateAnalysisStatus' frontend/src/stores/cloudConnections.js` → 1; CloudDetailView has no local translator ✓ - CloudDetailParity.test.js passes (8/8) ✓ ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 2 - Missing critical functionality] cloudConnections.js imports client.js (api), not cloud.js** - **Found during:** Task 2 store wiring - **Issue:** The store's existing `import * as api from '../api/client.js'` only covers local document API. `getCloudItemDetail` lives in `cloud.js`. Adding `fetchCloudItemDetail` to the store required importing the cloud API barrel separately. - **Fix:** Added `import * as cloudApi from '../api/cloud.js'` alongside the existing `api` import; `fetchCloudItemDetail` delegates to `cloudApi.getCloudItemDetail`. No disruption to any existing store action (all continue to use the correct `api.*` barrel). - **Files modified:** frontend/src/stores/cloudConnections.js - **Commit:** 48afb8b **2. [Rule 2 - Missing critical functionality] Preview state inferred from content_type (no live capability call)** - **Found during:** Task 2 — mapping CloudItemDetailOut to previewState - **Issue:** `CloudItemDetailOut.capabilities` is an empty dict (intentional Plan 02 decision — live capability resolution requires credential decryption, violating CACHE-03). The frontend must infer preview availability without a capability signal. - **Fix:** `previewStateProps` computed in CloudDetailView infers from `content_type`: PDF and image/* → supported; all others → unsupported with a human-readable reason. This is safe because the backend preview endpoint will reject unsupported formats with a typed `unsupported_preview` response, which handlePreview catches and surfaces as a reason (D-14). - **Files modified:** frontend/src/views/CloudDetailView.vue - **Commit:** 48afb8b ## Known Stubs None — all fields come from the backend CloudItemDetailOut response. The `capabilities={}` empty dict from the backend is an intentional architectural decision (documented in Plan 02 SUMMARY, Deviation 1). ## Threat Flags | Flag | File | Description | |------|------|-------------| | T-14.1-07 (mitigated) | CloudDetailView.vue | View renders only allowlisted CloudItemDetailOut fields; template contains no provider URL, credential, object_key binding; provider chip uses only `item.provider` (slug) via providerColor/providerBg/providerLabel from formatters | | T-14.1-08 (mitigated) | router/index.js + cloud.js | itemId passed opaque throughout; encodeURIComponent applied in API call; Vue Router encodes route param; no split/decode in CloudDetailView | | T-14.1-09 (mitigated) | CloudDetailView.vue | previewState.supported=false shows reason; Download is a separate explicit action; handlePreview never auto-downloads on unsupported; "Preview unavailable" message present (grep -c → 4) | ## Self-Check: PASSED Created files exist: - /Users/nik/Documents/Progamming/document_scanner/frontend/src/components/storage/DocumentDetailSurface.vue ✓ - /Users/nik/Documents/Progamming/document_scanner/frontend/src/views/CloudDetailView.vue ✓ Modified files: - /Users/nik/Documents/Progamming/document_scanner/frontend/src/views/DocumentView.vue ✓ - /Users/nik/Documents/Progamming/document_scanner/frontend/src/router/index.js ✓ - /Users/nik/Documents/Progamming/document_scanner/frontend/src/api/cloud.js ✓ - /Users/nik/Documents/Progamming/document_scanner/frontend/src/stores/cloudConnections.js ✓ Commits verified: - 825a7b5 (Task 1) present in git log ✓ - 48afb8b (Task 2) present in git log ✓