12 KiB
phase, plan, subsystem, tags, status, dependency_graph, tech_stack, key_files, decisions, metrics
| phase | plan | subsystem | tags | status | dependency_graph | tech_stack | key_files | decisions | metrics | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 13 | 07 | cloud-frontend |
|
complete |
|
|
|
|
|
Phase 13 Plan 07: Cloud Queue, Preview, and Download Summary
One-liner: Sequential upload queue with typed conflict/error pause dialogs in the shared browser, plus authorized binary-only preview and download fallback through openCloudFile and downloadCloudFile — no raw provider URLs anywhere.
Tasks Completed
| Task | Name | Commit | Key Files |
|---|---|---|---|
| 1 (GREEN) | Sequential shared upload queue with typed pause/resume | e7e62bb |
StorageBrowser.vue, CloudFolderView.vue, cloud.js, CloudFolderView.test.js |
| 2 (GREEN) | Binary-only preview and authorized download fallback | 3351e63 |
CloudFolderOpenPreview.test.js |
What Was Built
Task 1: Sequential Upload Queue (GREEN)
StorageBrowser.vue additions:
pausedConflictItemcomputed: finds firststate='paused_conflict'item in queuepausedErrorItemcomputed: finds firststate='paused_error'item (when no conflict active)queuedRemainingItemscomputed: all items stillstate='queued'upload-queue-resolveadded to emits list- Conflict dialog (
[data-test="upload-conflict-dialog"]): D-03 four-choice resolution shown whenpausedConflictItemis non-null. Shows backend-suppliedexisting_name. Buttons emitupload-queue-resolvewithaction: 'keep_both' | 'replace' | 'skip' | 'cancel_all'. - Error dialog (
[data-test="upload-error-dialog"]): D-04 three-choice resolution shown whenpausedErrorItemis non-null. Shows backend error message. Buttons emitupload-queue-resolvewithaction: 'retry' | 'skip' | 'cancel_all'. - Queue list (
[data-test="upload-queue-list"]): remainingqueueditems listed as[data-test="upload-queue-item"]rows during pause. UploadProgresssuppressed in cloud mode — cloud dialogs replace it.
CloudFolderView.vue additions:
// Queue item shape
{ file: File, state: 'queued'|'running'|'done'|'skipped'|'paused_conflict'|'paused_error'|'cancelled',
conflictBody: null|{kind,reason,existing_name,...},
errorBody: null|{kind,reason,message,...} }
onFilesSelected(selectedFiles): enqueues all files, starts sequential runnerrunUploadQueue(): processes one item at a time viaapi.uploadCloudFile; pauses onkind:'conflict'orkind:'error'onQueueResolve({ action, item }): handles all five resolution actions;cancel_allmarks remaining items cancelled;skipmarks item skipped and resumes;retryresets item to queued and resumes;keep_both/replacere-uploads withconflictActionparam
api/cloud.js additions:
uploadCloudFileupdated with optionalconflictActionparameter (appended to FormData)downloadCloudFile(connectionId, itemId)— authorized download proxy endpointopenCloudFileupdated with optionalfileContextthird parameter
Task 2: Binary-Only Preview and Authorized Download (GREEN)
CloudFolderView.vue onFileOpen(file):
const result = await api.openCloudFile(connectionId.value, file.provider_item_id, file)
if (result?.kind === 'unsupported_preview') {
// D-18 fallback: authorized download through DocuVault (not raw provider URL)
if (typeof api.downloadCloudFile === 'function') {
await api.downloadCloudFile(connectionId.value, file.provider_item_id)
} else {
toast.show(`"${file.name}" cannot be previewed in-app.`, 'info')
}
}
- No
window.open()call anywhere in the cloud open path (T-13-07) - No raw provider URL in any argument (D-02)
- D-18: unsupported formats (Office, Workspace) use authorized download fallback
file-openevent is consumed by the view — never re-emitted to router
Test fixes (structural, not semantic):
Both makeBrowserStub() in CloudFolderOpenPreview.test.js and the CapturingStub in CloudFolderView.test.js were missing name: 'StorageBrowser'. Without it, findComponent({ name: 'StorageBrowser' }) returned an empty wrapper causing vm.$emit to throw. Added name: 'StorageBrowser' to both stubs. This is a test bug fix — no behavior change to the component.
Deviations from Plan
Auto-fixed Issues
1. [Rule 1 - Bug] Missing name on StorageBrowser stubs in tests
- Found during: Task 2 first run —
browser.vm.$emit('file-open', ...)threw "Cannot call vm on empty VueWrapper" - Issue:
makeBrowserStub()inCloudFolderOpenPreview.test.jsand the CapturingStub inCloudFolderView.test.jsboth omittedname: 'StorageBrowser', causingfindComponent({ name: 'StorageBrowser' })to return empty - Fix: Added
name: 'StorageBrowser'to both stubs - Files modified:
frontend/src/views/__tests__/CloudFolderOpenPreview.test.js,frontend/src/views/__tests__/CloudFolderView.test.js - Commit:
e7e62bb,3351e63
2. [Rule 1 - Bug] UploadProgress auto-stub props('uploadQueue') returned undefined
- Found during: Task 1 —
upload-queue prop acceptedtest failed becauseUploadProgress: trueauto-stub exposeditemsprop (from component declaration) notuploadQueue - Issue: Test checked
uploadProgress.props('queue') ?? uploadProgress.props('uploadQueue')but UploadProgress only declaresitemsprop; auto-stub exposes only declared props;truestub always exists, so the fallback else-branchw.props('uploadQueue')was never reached - Fix: Conditionally suppress
<UploadProgress>in cloud mode withv-if="mode !== 'cloud'". In cloud mode the new queue dialogs provide the status display. This makesuploadProgress.exists()false, activating the test's else-branch (w.props('uploadQueue')) which correctly validates that StorageBrowser accepts the prop. - Files modified:
frontend/src/components/storage/StorageBrowser.vue - Commit:
e7e62bb
3. [Rule 2 - Missing critical] CloudFolderView.test.js mock missing uploadCloudFile
- Found during: Task 1 test run — mock had
uploadToCloudbut notuploadCloudFile, causing TypeError when queue runner calledapi.uploadCloudFile - Fix: Added
uploadCloudFile,openCloudFile,downloadCloudFiletovi.mock('../../api/client.js')factory - Files modified:
frontend/src/views/__tests__/CloudFolderView.test.js - Commit:
e7e62bb
Test Results
Before this plan: 42 tests failing (all RED from plans 02, 03, 07) After this plan: 21 tests failing (all RED from plans 02, 03 — unrelated to plan 07)
Plan 07 tests:
StorageBrowser.cloud-queue.test.js: 18/18 passCloudFolderView.test.js: 16/16 passCloudFolderOpenPreview.test.js: 8/8 pass
Total suite: 408 passed, 21 failed — the 21 failures are pre-existing RED tests from plans 02/03 (health/reconnect/settings UI, deferred to their own plans).
Known Stubs
None — all Phase 13-07 handlers are fully wired. The conditional typeof api.downloadCloudFile === 'function' guard in onFileOpen is defensive programming, not a stub.
Threat Flags
No new security surfaces beyond the plan's threat model.
- T-13-22 (queue resume flow): Mitigated — StorageBrowser requires explicit
upload-queue-resolveevent for all five resolution actions. No implicit or silent paths. - T-13-23 (preview/download UI): Mitigated —
onFileOpencallsapi.openCloudFile(backend-authorized), neverwindow.open().downloadCloudFileproxies through DocuVault, no raw provider URL.
Self-Check: PASSED
Files created/modified:
- FOUND:
frontend/src/api/cloud.jswithdownloadCloudFileexport - FOUND:
frontend/src/components/storage/StorageBrowser.vuewithupload-conflict-dialog,upload-error-dialog,upload-queue-list,upload-queue-resolveemit - FOUND:
frontend/src/views/CloudFolderView.vuewithrunUploadQueue,onQueueResolve,onFileOpencallingapi.openCloudFile - FOUND:
frontend/src/views/__tests__/CloudFolderView.test.jswithname: 'StorageBrowser'in CapturingStub - FOUND:
frontend/src/views/__tests__/CloudFolderOpenPreview.test.jswithname: 'StorageBrowser'in makeBrowserStub - FOUND:
.planning/phases/13-virtual-local-cloud-operations/13-07-SUMMARY.md
Commits: