188 lines
17 KiB
Markdown
188 lines
17 KiB
Markdown
---
|
|
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"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</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
|
|
@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
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: Extract DocumentDetailSurface.vue and refactor DocumentView.vue to use it (Re-analyze copy)</name>
|
|
<read_first>
|
|
- 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)
|
|
</read_first>
|
|
<action>
|
|
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.
|
|
</action>
|
|
<verify>
|
|
<automated>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</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `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.
|
|
</acceptance_criteria>
|
|
<done>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.</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 2: Cloud detail route + CloudDetailView.vue + getCloudItemDetail API + store force/reanalyze wiring</name>
|
|
<read_first>
|
|
- 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)
|
|
</read_first>
|
|
<action>
|
|
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.
|
|
</action>
|
|
<verify>
|
|
<automated>cd frontend && npx vitest run src/views/__tests__/CloudDetailParity.test.js 2>&1 | tail -25</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- 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).
|
|
</acceptance_criteria>
|
|
<done>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.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## 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) |
|
|
</threat_model>
|
|
|
|
<verification>
|
|
- `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).
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- 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.
|
|
</success_criteria>
|
|
|
|
<artifacts_produced>
|
|
## 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".
|
|
</artifacts_produced>
|
|
|
|
<output>
|
|
Create `.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-03-SUMMARY.md` when done
|
|
</output>
|