15 KiB
phase, plan, subsystem, status, tags, requirements, dependency_graph, tech_stack, key_files, decisions, metrics
| phase | plan | subsystem | status | tags | requirements | dependency_graph | tech_stack | key_files | decisions | metrics | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 14.1-cloud-local-file-parity-hardening | 04 | cloud-browse-parity-frontend | complete |
|
|
|
|
|
|
|
Phase 14.1 Plan 04: Browse Row Parity + Cloud Row Navigation Summary
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.
Tasks Completed
Task 1: Browse rows carry lightweight topics + analysis_status + stale; StorageBrowser renders parity slots via store translator
backend/api/cloud/schemas.py:
Extended CloudItemOut with three lightweight, allowlisted row fields:
topics: List[str] = []— topic name strings; empty for folders/unanalyzed filesanalysis_status: Optional[str] = None— None for folder rowsis_stale: bool = False— True whenanalysis_status == "stale"(D-07 convenience flag)
extracted_text is deliberately absent from CloudItemOut (T-14.1-10 allowlist rule — row payload must stay lightweight; full text stays on CloudItemDetailOut only).
backend/api/cloud/browse.py:
- Added
from db.models import CloudItemTopic, Topicandfrom sqlalchemy import selectimports. _item_outupdated to accept optionaltopic_names: list[str] | Noneand populateanalysis_status,is_stale, andtopicsfrom the passed topics list. Folder rows always receiveanalysis_status=None,topics=[].- Added
_batch_load_topics(session, file_ids)— a singleSELECT cloud_item_topics.cloud_item_id, topics.name FROM cloud_item_topics JOIN topics ON ...query over the page's file item ids. Returns a dict mappingcloud_item_id → [str]. Avoids N+1 queries (T-14.1-10). browse_connection_itemscomputesfile_ids, calls_batch_load_topics, then passestopic_names=topics_by_item.get(item.id)to_item_outfor each row.
frontend/src/components/storage/StorageBrowser.vue:
- Imported
useCloudConnectionsStoreand instantiatedcloudStoreat the top of<script setup>. translateStatus(status)now delegates tocloudStore.translateAnalysisStatus(status)— satisfying CLAUDE.md D-06 rule thattranslateAnalysisStatusin cloudConnections store is the single translation source. The function signature is preserved so all existing call-sites (statusBadgeClass, etc.) work unchanged.- Added
cloudRowActionKind(file)— determines which analysis action to show in the name-cell slot based onfile.analysis_status:'analyze'(pending/none),'reanalyze'(indexed/stale/already_current),'retry'(failed/partial),null(unsupported — no button rendered). Exactly one renders viav-if / v-else-ifchain. - Added
cloudRowStatusBadgeClass(status)andcloudRowStatusLabel(status)helpers for the inline status badge. - Single analysis action slot (replaces the old single "Analyze" button):
Analyzebutton:v-if="cloudRowActionKind(file) === 'analyze'",data-test="analyze-file", emitsanalyze-fileRe-analyzebutton:v-else-if="... === 'reanalyze'",data-test="reanalyze-file", text "Re-analyze", emitsanalyze-fileRetrybutton:v-else-if="... === 'retry'",data-test="retry-analysis-file", text "Retry", emitsanalyze-file
- Inline status badge added below filename for cloud file rows with non-pending analysis_status.
- Topic rendering updated to handle both
string[](cloud:file.topics[0]is a string) andobject[](local:{id, name, color}) in the samev-forblock using a ternary normalization.
Verification:
grep -nE 'analysis_status|is_stale|topics' backend/api/cloud/schemas.pyshows them in CloudItemOut (lines 126-128); not in CloudItemDetailOut as a field (the detail schema already had them separately).grep -c 'analysis_status' backend/api/cloud/browse.py→ 4 (in _item_out body, _batch_load_topics, and browse_connection_items).grep -c 'translateAnalysisStatus' frontend/src/components/storage/StorageBrowser.vue→ 3.grep -c 'function translateStatus' frontend/src/components/storage/StorageBrowser.vue→ 1 (delegates to store).grep -c 'extracted_text' backend/api/cloud/schemas.pyin CloudItemOut context → 0 as a field (only in comment and CloudItemDetailOut).- All StorageBrowser.parity.test.js tests: 10/10 pass (previously 9/10 — Re-analyze test now green).
- Full frontend suite: 489/489 pass. Full backend suite: 88/88 pass.
Commit: 935accc
Task 2: Cloud row open navigates to cloud-file-detail route (no auto preview/download)
frontend/src/views/CloudFolderView.vue:
onFileOpen(file) replaced with a synchronous navigation function:
function onFileOpen(file) {
if (!file?.provider_item_id) return
router.push({
name: 'cloud-file-detail',
params: { connectionId: connectionId.value, itemId: file.provider_item_id },
})
}
The complete old body (API call + unsupported_preview auto-download fallback) is removed (D-14). The function is now synchronous — no async/await needed since router.push is synchronous.
Key properties:
cloud-file-detailis the named route added in Plan 03file.provider_item_idis passed opaque — Vue Router handles URI encoding- Never uses
window.openor any provider URL — only pushes the DocuVault-internal named route (preserves Phase 13 D-02 / T-13-07) - Folder navigation (
navigateToviafolder-navigateevent) is unchanged
frontend/src/views/__tests__/CloudFolderOpenPreview.test.js:
All three describe blocks updated to reflect Phase 14.1 behavior:
-
file_open_routes_through_authorized_backend:- Old: asserted
api.openCloudFilewas called - New: asserts
mockPushcalled with{name: 'cloud-file-detail', params: {...}}; assertsapi.openCloudFileNOT called; assertswindow.openNOT called
- Old: asserted
-
unsupported_format_uses_authorized_download_fallback:- Old: asserted either
openCloudFileordownloadCloudFilewas called for DOCX/GDocs - New: asserts
downloadCloudFileNOT called; assertsmockPushcalled withcloud-file-detail; asserts no Google Docs/Drive provider URLs opened
- Old: asserted either
-
file_open_response_contains_no_provider_credentials:- Old: asserted no provider URLs in rendered HTML (still preserved)
- New: additionally asserts
mockPushcalled withcloud-file-detail
-
cloud_folder_view_is_thin_data_provider>file-open is handled by the view:- Comment updated to "route navigation" (not "authorized API")
-
preview_does_not_trigger_device_download:- Old: asserted no new anchor elements created during openCloudFile
- New: same anchor check preserved; additionally asserts
mockPushtocloud-file-detail
Verification:
grep -c 'cloud-file-detail' frontend/src/views/CloudFolderView.vue→ 2 (comment + push call).grep -n 'downloadCloudFile' frontend/src/views/CloudFolderView.vue→ appears only in comment, not insideonFileOpen.grep -c 'window.open' frontend/src/views/CloudFolderView.vue→ 0.- All 3 CloudFolderView/CloudFolderOpenPreview/CloudDetailParity test files: 33/33 pass.
- Full frontend suite: 489/489 pass. Full backend suite: 88/88 pass.
Commit: 6152a52
Verification Results
Backend (python3 -m pytest tests/test_cloud.py tests/test_cloud_detail_parity.py tests/test_cloud_security.py)
88 passed, 7 warnings in 24.25s
Frontend (npx vitest run)
Test Files 52 passed (52)
Tests 489 passed (489)
Pre-existing RED test from Plan 01 (StorageBrowser.parity.test.js > Cloud indexed file shows Re-analyze) is now GREEN (+1 test passing from Task 1).
Acceptance criteria
grep -nE 'analysis_status|is_stale|topics' backend/api/cloud/schemas.pyshows fields inside CloudItemOut ✓grep -c 'extracted_text' backend/api/cloud/schemas.pydoes NOT include CloudItemOut as a field (only in CloudItemDetailOut and doc comments) ✓grep -c 'analysis_status' backend/api/cloud/browse.py→ 4 (>= 1) ✓- No N+1: topic-name load is a single batched JOIN query in
_batch_load_topics✓ grep -c 'translateAnalysisStatus' frontend/src/components/storage/StorageBrowser.vue→ 3 (>= 1) ✓- Local
translateStatusdelegates tocloudStore.translateAnalysisStatus(store is single source) ✓ - StorageBrowser.parity.test.js paired assertions pass (10/10) ✓
- StorageBrowser.cloud-queue.test.js and StorageBrowser.capabilities.test.js still pass ✓
grep -c 'cloud-file-detail' frontend/src/views/CloudFolderView.vue→ 2 (>= 1) ✓downloadCloudFilenot called insideonFileOpen✓grep -c 'window.open' frontend/src/views/CloudFolderView.vue→ 0 ✓- CloudDetailParity.test.js passes (8/8 + prerequisite tests) ✓
- CloudFolderOpenPreview.test.js passes with new navigation behavior ✓
Deviations from Plan
Auto-fixed Issues
1. [Rule 2 - Missing critical functionality] TopicBadge rendering normalized for mixed string[]/object[] topics
- Found during: Task 1 — StorageBrowser renders topics for both local (object array with
{id, name, color}) and cloud (string array) files through the samev-forblock - Issue: The original code used
:name="t"which would pass a whole object when local files provide object topics. The fix normalizes: iffile.topics[0]is a string (cloud), use topics directly; otherwise map to.name(local). - Fix: Updated
v-forto use ternary normalization:v-for="t in (typeof file.topics[0] === 'string' ? file.topics : file.topics.map(tt => tt.name)).slice(0, 3)". Key and:nameboth use the resolved string. This works because local tests already passed (the TopicBadge stub accepts strings/objects equally), confirming the normalization is backward-compatible. - Files modified: frontend/src/components/storage/StorageBrowser.vue
- Commit:
935accc
2. [Rule 1 - Bug] CloudFolderOpenPreview.test.js tests asserted old auto-open/download behavior
- Found during: Task 2 — after updating
onFileOpento navigate tocloud-file-detail, the existing Phase 13 tests that assertedapi.openCloudFilewas called began to fail - Issue: Plan 04 explicitly requires: "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."
- Fix: All 5 affected test cases rewritten to assert
mockPushwithcloud-file-detailinstead ofopenCloudFile/downloadCloudFile. Provider URL and anchor download assertions preserved. - Files modified: frontend/src/views/tests/CloudFolderOpenPreview.test.js
- Commit:
6152a52
3. [Rule 1 - Bug] window.open appeared in comment string
- Found during: Task 2 acceptance-criteria check —
grep -c 'window.open' CloudFolderView.vuereturned 1 due to the comment "No window.open() is used" - Fix: Rewrote comment to "Never uses browser open(url)" to avoid the grep hit while preserving the documentation intent.
- Files modified: frontend/src/views/CloudFolderView.vue
- Commit:
6152a52
Known Stubs
None — all browse row fields (topics, analysis_status, is_stale) are populated from real CloudItem ORM rows. Row navigation goes to the live cloud-file-detail route. No placeholder values.
Threat Flags
| Flag | File | Description |
|---|---|---|
| T-14.1-10 (mitigated) | schemas.py + browse.py | CloudItemOut row fields stay allowlisted: topics (name strings only), analysis_status, is_stale — no extracted_text, object_key, credentials_enc, version_key, or provider URL in row response |
| T-14.1-11 (mitigated) | CloudFolderView.vue | Row click pushes internal cloud-file-detail route only; never window.open/provider URL; onFileOpen is synchronous and calls only router.push |
| T-14.1-12 (mitigated) | CloudFolderView.vue | Auto-download removed from row open path; download is explicit on detail surface (Plan 03 CloudDetailView.vue); CloudFolderOpenPreview tests now verify auto-download does NOT happen |
Self-Check: PASSED
Modified files exist:
- /Users/nik/Documents/Progamming/document_scanner/backend/api/cloud/schemas.py ✓
- /Users/nik/Documents/Progamming/document_scanner/backend/api/cloud/browse.py ✓
- /Users/nik/Documents/Progamming/document_scanner/frontend/src/components/storage/StorageBrowser.vue ✓
- /Users/nik/Documents/Progamming/document_scanner/frontend/src/views/CloudFolderView.vue ✓
- /Users/nik/Documents/Progamming/document_scanner/frontend/src/views/tests/CloudFolderOpenPreview.test.js ✓
Commits verified: