Files
kite/.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-03-SUMMARY.md
T

222 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 (1214px 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 ✓