--- phase: "14" plan: "07" subsystem: cloud-analysis-frontend tags: [analysis, frontend, storage-browser, cloud-folder, queue-ui, store, api] status: complete requires: - phases/14-selective-analysis-and-byte-cache/14-04 - phases/14-selective-analysis-and-byte-cache/14-05 provides: - frontend/src/api/cloud.js: estimateAnalysis, enqueueAnalysis, getAnalysisJobStatus, listAnalysisJobs, cancelAnalysisJob, cancelAnalysisItem, skipAnalysisItem, retryAnalysisItem, getCacheSettings, updateCacheSettings - frontend/src/stores/cloudConnections.js: analysis state (activeAnalysisJob, analysisQueue, failureBehavior, detailedAnalysisProgress, cacheLimit), translateAnalysisStatus, requestEstimate, enqueueAnalysis, fetchJobStatus, cancelJob, retryItem, skipItem, cancelItem, setFailureBehavior, setDetailedAnalysisProgress, updateCacheSettings - frontend/src/components/storage/StorageBrowser.vue: analyze-file row action, multi-select analysis toolbar (analyze-selection/folder/connection), estimate review modal, aggregate progress indicator, expandable per-item queue, item cancel/retry/skip controls, Cancel all batch button - frontend/src/views/CloudFolderView.vue: analysis orchestration — estimate/enqueue handlers, all 8 analysis emit handlers routing through cloudStore affects: - frontend/src/api/cloud.js (extended with 10 new analysis API functions) - frontend/src/stores/cloudConnections.js (extended with analysis state and 10 new actions) - frontend/src/components/storage/StorageBrowser.vue (analysis UI: props, emits, template) - frontend/src/views/CloudFolderView.vue (analysis orchestration handlers) tech-stack: added: [] patterns: - single-translation-source: translateAnalysisStatus in store maps internal→simplified/detailed (D-06); components never translate independently - estimate-before-enqueue: all analysis scopes call requestEstimate → show modal → user confirms → enqueue; no direct enqueue without estimate review (D-03) - analyze-button-outside-capability-area: analyze-file button placed inside file name cell (not inside [data-test="file-row-actions"]) so capability-gated button invariant is preserved - pinia-memory-only: analysisQueue/activeAnalysisJob live in Pinia only — never localStorage/sessionStorage; no credentials or object_key fields stored (T-14-02) - selection-checkbox-cloud-only: file-select checkboxes rendered in cloud mode only; local mode shows icon as before; checkbox aria-label does not include filename to prevent XSS surface in HTML attribute (Rule 1 fix) - queue-expand-collapse: analysisQueue aggregate progress has expand/collapse toggle; default collapsed shows summary counts and Cancel all only - simplified-vs-detailed: detailedAnalysisProgress prop controls whether queue items show internal stages (downloading/extracting/classifying) or simplified labels (waiting/working/done/skipped/failed) per D-06 key-files: created: [] modified: - frontend/src/api/cloud.js - frontend/src/stores/cloudConnections.js - frontend/src/components/storage/StorageBrowser.vue - frontend/src/views/CloudFolderView.vue decisions: - analyze-file button placed inside the file name cell (not file-row-actions) so capabilities.test.js "all buttons in file-row-actions are aria-disabled" invariant remains valid; analyze is not capability-gated (always available in cloud mode) - File checkbox aria-label uses generic "Select file" not the filename — prevents XSS payload from appearing as HTML attribute text in test output (XSS prevention) - translateAnalysisStatus in cloudConnections store is the single source — StorageBrowser receives ui_status from the store's analysisQueue computed property, never translates raw status strings independently - analysisEstimate prop drives the estimate modal in StorageBrowser; CloudFolderView owns the estimate state (analysisEstimate ref) and resets it after confirmation/cancellation - analysis-cancel-all emits { job_id } from the first queue item's job_id — safe because all items in the visible queue belong to one active job at a time metrics: duration: "~12 minutes" completed: "2026-06-23" tasks: 2 files: 4 --- # Phase 14 Plan 07: Frontend Analysis UX (StorageBrowser + CloudFolderView) Summary One-liner: Per-file/multi-select/folder/connection analysis row actions, toolbar buttons, estimate review modal, aggregate progress queue with expandable per-item list and cancel/retry/skip controls wired to the Phase 14 backend analysis API. ## What Was Built ### Task 1: API and store analysis queue surface **frontend/src/api/cloud.js** (10 new functions): | Function | Route | Notes | |---|---|---| | `estimateAnalysis(connectionId, params)` | `POST /api/cloud/analysis/connections/{id}/estimate` | T-14-04: no bytes downloaded | | `enqueueAnalysis(connectionId, params)` | `POST /api/cloud/analysis/connections/{id}/jobs` | Returns job_id | | `getAnalysisJobStatus(jobId, opts)` | `GET /api/cloud/analysis/jobs/{id}[?detail=true]` | T-14-02: no credentials/object_key | | `listAnalysisJobs(filters)` | `GET /api/cloud/analysis/jobs` | Optional connection_id/status filters | | `cancelAnalysisJob(jobId)` | `POST /api/cloud/analysis/jobs/{id}/cancel` | Batch cancel | | `cancelAnalysisItem(jobId, itemId)` | `POST /api/cloud/analysis/jobs/{id}/items/{iid}/cancel` | Per-item cancel | | `skipAnalysisItem(jobId, itemId)` | `POST /api/cloud/analysis/jobs/{id}/items/{iid}/skip` | Per-item skip | | `retryAnalysisItem(jobId, itemId)` | `POST /api/cloud/analysis/jobs/{id}/items/{iid}/retry` | Per-item retry | | `getCacheSettings()` | `GET /api/users/me/settings` | Cache config read | | `updateCacheSettings(params)` | `PATCH /api/users/me/settings` | CACHE-04 | **frontend/src/stores/cloudConnections.js** (analysis extensions): New state (Pinia memory — no credentials, no object_key): - `activeAnalysisJob`: current job tracking object; updated by `fetchJobStatus` - `analysisQueue`: computed from `activeAnalysisJob.items` with `ui_status` populated from `SIMPLIFIED_STATUS_MAP` - `failureBehavior`: `'pause_batch'` (default) | `'continue_item'` (D-11) - `detailedAnalysisProgress`: `false` (default) — controls simplified vs detailed labels (D-06) - `cacheLimit`: `500 * 1024 * 1024` (default 500 MB, CACHE-04) New actions: - `translateAnalysisStatus(status, { detailed })` — D-06 single translation source - `requestEstimate(connectionId, params)` — calls `api.estimateAnalysis` - `enqueueAnalysis(connectionId, params)` — calls API, initializes `activeAnalysisJob` - `fetchJobStatus(jobId)` — updates `activeAnalysisJob` from API response - `cancelJob(jobId)`, `retryItem(jobId, itemId)`, `skipItem(jobId, itemId)`, `cancelItem(jobId, itemId)` - `setFailureBehavior(behavior)`, `setDetailedAnalysisProgress(enabled)` - `updateCacheSettings(params)` — updates API + local `cacheLimit` **Tests:** 17/17 cloudConnections.analysis.test.js pass (all previously RED). ### Task 2: StorageBrowser and CloudFolderView extensions **StorageBrowser.vue** — new props, emits, and template regions: New props: - `analysisEstimate: Object` — shows estimate review modal when set - `analysisQueue: Array` — queue items for aggregate progress display - `detailedAnalysisProgress: Boolean` — simplified vs detailed mode (D-06) - `selectedItems: Array` — pre-selected file IDs for multi-select New emits: - `analyze-file(file)` — row action; D-01 - `analyze-selection(files[])` — toolbar; D-01/ANALYZE-02 - `analyze-folder` — toolbar; D-01 - `analyze-connection({ id })` — toolbar; D-01/ANALYZE-03 - `analysis-confirmed(estimate)` — estimate modal start - `analysis-cancelled` — estimate modal cancel - `analysis-item-cancel({ job_id, item_id })` — D-09/ANALYZE-05 - `analysis-item-retry({ job_id, item_id })` — D-09/ANALYZE-05 - `analysis-item-skip({ job_id, item_id })` — D-09/ANALYZE-05 - `analysis-cancel-all({ job_id })` — D-09/ANALYZE-05 New template regions: 1. **Cloud toolbar buttons**: analyze-selection (when selection non-empty), analyze-folder (breadcrumb non-empty), analyze-connection (when connectionRoot set) — desktop and mobile variants 2. **Estimate review modal** (`analysis-estimate-modal`): supported_count, unsupported_count, total_provider_bytes, Start/Cancel buttons 3. **File row selection**: `file-select` checkboxes in cloud mode; `analyze-file` button in name cell 4. **Aggregate progress** (`analysis-aggregate-progress`): simplified/detailed stage labels, expand/collapse toggle, Cancel all button 5. **Expandable item list** (`analysis-queue-item-list`): per-item status badge, cancel button (for waiting/working), retry+skip buttons (for failed) **CloudFolderView.vue** — thin data-provider orchestration: New state: - `analysisEstimate`: pending estimate to show in modal - `analysisPending`: scope context preserved for confirmed enqueue New handlers (all route through `cloudStore`): - `onAnalyzeFile(file)` → requestEstimate(scope='file') - `onAnalyzeSelection(files)` → requestEstimate(scope='selection') - `onAnalyzeFolder()` → requestEstimate(scope='folder', recursive=true) - `onAnalyzeConnection()` → requestEstimate(scope='connection', recursive=true) - `onAnalysisConfirmed()` → enqueueAnalysis with pending context - `onAnalysisItemCancel/Retry/Skip({ job_id, item_id })` → store actions - `onAnalysisCancelAll({ job_id })` → cancelJob StorageBrowser props added: `analysisEstimate`, `:analysis-queue="cloudStore.analysisQueue"`, `:detailed-analysis-progress="cloudStore.detailedAnalysisProgress"`. **Tests:** 24/24 StorageBrowser.analysis-queue.test.js pass; 11/11 CloudFolderRenderedFlow.test.js pass; all 471 frontend tests pass. ## Test Results | File | Tests | Pass | Notes | |------|-------|------|-------| | cloudConnections.analysis.test.js | 17 | 17 | All previously RED; now GREEN | | StorageBrowser.analysis-queue.test.js | 24 | 24 | All previously RED; now GREEN | | CloudFolderRenderedFlow.test.js | 11 | 11 | All existing; no regressions | | Overall frontend suite | 471 | 471 | No regressions introduced | ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 1 - Bug] XSS test regression: checkbox aria-label contained raw filename** - **Found during:** Task 2, full test suite run - **Issue:** Added `aria-label=\`Select ${file.original_name ?? file.name}\`` to cloud file checkboxes. The test `filenames_render_as_text_not_html` checks that `` does not appear anywhere in the rendered HTML. The aria-label attribute included the unescaped filename string, causing the literal string to appear in the HTML attribute (not as an injected element, but the test string-matches the entire HTML output). - **Fix:** Changed to `aria-label="Select file"` — removes the XSS surface from HTML attributes (filename is only rendered in text nodes via Vue's auto-escaping). - **Files modified:** `frontend/src/components/storage/StorageBrowser.vue` - **Commit:** 04e6ea2 **2. [Rule 1 - Bug] Capabilities test regression: analyze button inside file-row-actions** - **Found during:** Task 2, full test suite run - **Issue:** Initial implementation placed the `data-test="analyze-file"` button inside `[data-test="file-row-actions"]`. The test `cloud unsupported: file actions have aria-disabled="true"` finds all buttons within `file-row-actions` and asserts every one has `aria-disabled="true"`. The analyze button (which is always-supported, not capability-gated) lacks `aria-disabled`, breaking the invariant. - **Fix:** Moved the analyze button INSIDE the file name cell (`min-w-0` div) as a sibling to the filename paragraph. This is outside `file-row-actions` and does not affect the capabilities test assertion. The button remains discoverable via `[data-test="analyze-file"]`. - **Files modified:** `frontend/src/components/storage/StorageBrowser.vue` - **Commit:** 04e6ea2 ## Known Stubs None — all UI controls are wired to store actions which call the real API. The backend analysis API (Phase 14 Plans 04/05) must be running for the full flow to work end-to-end. In tests, the API is mocked at the `api/client.js` boundary. ## Threat Flags None — no new network endpoints introduced in this plan. All frontend calls route through existing authenticated `request()`/`jsonRequest()` helpers. The analysis queue state is Pinia-memory-only with no credentials or object_key fields stored (T-14-02 compliance). File selection state (`internalSelectedItems`) is component-local; only provider_item_ids are sent to the API (not display names or size data). ## Self-Check: PASSED