From 04e6ea2ba225f2b804387a844784cbce431a1278 Mon Sep 17 00:00:00 2001 From: curo1305 Date: Tue, 23 Jun 2026 19:42:53 +0200 Subject: [PATCH] feat(14-07): extend StorageBrowser and CloudFolderView with analysis UX (Task 2) StorageBrowser: - Add analyze-file button per cloud file row (D-01); placed inside name cell to avoid interfering with capability-gated file-row-actions (Rule 1 compliance) - Add file-select checkboxes for multi-select analysis (cloud mode only) - Add analyze-selection toolbar button (D-01, ANALYZE-02) - Add analyze-folder toolbar button when breadcrumb is non-empty (D-01) - Add analyze-connection toolbar button when connectionRoot is set (D-01, ANALYZE-03) - Add analysisEstimate prop + analysis-estimate-modal (D-03) with estimate-start button - Add analysisQueue prop + analysis-aggregate-progress with simplified/detailed labels (D-05/D-06) - Add expandable per-item list via analysis-queue-expand toggle (D-05) - Add per-item cancel/retry/skip controls in expanded list (D-09, ANALYZE-05) - Add Cancel all batch button (D-09, ANALYZE-05) - Emit: analyze-file, analyze-selection, analyze-folder, analyze-connection, analysis-confirmed, analysis-cancelled, analysis-item-cancel/retry/skip, analysis-cancel-all CloudFolderView: - Pass analysis props to StorageBrowser from cloudStore - Add analysis estimate, pending request state - Handle all analysis emits: onAnalyzeFile, onAnalyzeSelection, onAnalyzeFolder, onAnalyzeConnection, onAnalysisConfirmed, onAnalysisItemCancel/Retry/Skip, onAnalysisCancelAll - Routes all API calls through cloudStore (requestEstimate, enqueueAnalysis, etc.) --- .../src/components/storage/StorageBrowser.vue | 375 +++++++++++++++++- frontend/src/views/CloudFolderView.vue | 196 +++++++++ 2 files changed, 569 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/storage/StorageBrowser.vue b/frontend/src/components/storage/StorageBrowser.vue index debd361..3a17ff0 100644 --- a/frontend/src/components/storage/StorageBrowser.vue +++ b/frontend/src/components/storage/StorageBrowser.vue @@ -22,6 +22,48 @@ :order="sortOrder" @change="handleSortChange" /> + + + + + + + + + + + + + + + @@ -40,6 +53,23 @@ const loading = ref(true) const error = ref('') const uploadQueue = ref([]) +// ── Phase 14: Analysis state ────────────────────────────────────────────────── + +/** + * Pending analysis estimate to show in the review modal. + * When set, StorageBrowser renders the estimate modal for confirmation. + * After confirmation/cancellation, set back to null. + * + * T-14-02: Never contains credentials or object_key. + */ +const analysisEstimate = ref(null) + +/** + * Pending analysis request context — stored so onAnalysisConfirmed + * can enqueue the job with the right scope/items after estimate review. + */ +const analysisPending = ref(null) + /** Connection UUID from route — never uses provider slug */ const connectionId = computed(() => route.params.connectionId) const folderId = computed(() => route.params.folderId) @@ -408,6 +438,172 @@ async function onFileOpen(file) { } } +// ── Phase 14: Analysis handlers ─────────────────────────────────────────────── + +/** + * ANALYZE-01 / D-01: Handle analyze-file emission from StorageBrowser. + * Calls estimate for a single file; shows estimate modal when result is non-trivial. + * + * @param {object} file - CloudItemOut with provider_item_id + */ +async function onAnalyzeFile(file) { + if (!file?.provider_item_id) return + try { + const estimate = await cloudStore.requestEstimate(connectionId.value, { + scope: 'file', + provider_item_ids: [file.provider_item_id], + recursive: false, + }) + analysisPending.value = { + scope: 'file', + provider_item_ids: [file.provider_item_id], + recursive: false, + } + // Show estimate modal — confirmed via onAnalysisConfirmed + analysisEstimate.value = estimate + } catch (e) { + toast.show(`Analysis estimate failed: ${e.message || 'Unknown error'}`, 'error') + } +} + +/** + * ANALYZE-02 / D-01: Handle analyze-selection emission from StorageBrowser. + * Calls estimate for the selected files. + * + * @param {object[]} selectedFiles - Array of CloudItemOut items + */ +async function onAnalyzeSelection(selectedFiles) { + if (!selectedFiles?.length) return + try { + const providerItemIds = selectedFiles.map(f => f.provider_item_id).filter(Boolean) + const estimate = await cloudStore.requestEstimate(connectionId.value, { + scope: 'selection', + provider_item_ids: providerItemIds, + recursive: false, + }) + analysisPending.value = { + scope: 'selection', + provider_item_ids: providerItemIds, + recursive: false, + } + analysisEstimate.value = estimate + } catch (e) { + toast.show(`Analysis estimate failed: ${e.message || 'Unknown error'}`, 'error') + } +} + +/** + * ANALYZE-02 / D-01: Handle analyze-folder emission from StorageBrowser. + * Calls estimate for the current folder. + */ +async function onAnalyzeFolder() { + const folderRef = folderId.value && folderId.value !== 'root' ? folderId.value : null + if (!folderRef) return + try { + const estimate = await cloudStore.requestEstimate(connectionId.value, { + scope: 'folder', + provider_item_ids: [folderRef], + recursive: true, + }) + analysisPending.value = { + scope: 'folder', + provider_item_ids: [folderRef], + recursive: true, + } + analysisEstimate.value = estimate + } catch (e) { + toast.show(`Analysis estimate failed: ${e.message || 'Unknown error'}`, 'error') + } +} + +/** + * ANALYZE-03 / D-01: Handle analyze-connection emission from StorageBrowser. + * Calls estimate for the entire connection. + */ +async function onAnalyzeConnection() { + try { + const estimate = await cloudStore.requestEstimate(connectionId.value, { + scope: 'connection', + recursive: true, + }) + analysisPending.value = { + scope: 'connection', + recursive: true, + } + analysisEstimate.value = estimate + } catch (e) { + toast.show(`Analysis estimate failed: ${e.message || 'Unknown error'}`, 'error') + } +} + +/** + * D-03: Handle analysis-confirmed emission from StorageBrowser estimate modal. + * Enqueues the analysis job with the pending scope context. + */ +async function onAnalysisConfirmed() { + if (!analysisPending.value) return + const pending = analysisPending.value + analysisPending.value = null + analysisEstimate.value = null + try { + await cloudStore.enqueueAnalysis(connectionId.value, pending) + } catch (e) { + toast.show(`Failed to start analysis: ${e.message || 'Unknown error'}`, 'error') + } +} + +/** + * ANALYZE-05 / D-09: Cancel a single analysis job item. + * + * @param {{ job_id: string, item_id: string }} payload + */ +async function onAnalysisItemCancel({ job_id, item_id }) { + try { + await cloudStore.cancelItem(job_id, item_id) + } catch (e) { + toast.show(`Failed to cancel item: ${e.message || 'Unknown error'}`, 'error') + } +} + +/** + * ANALYZE-05 / D-09: Retry a failed analysis job item. + * + * @param {{ job_id: string, item_id: string }} payload + */ +async function onAnalysisItemRetry({ job_id, item_id }) { + try { + await cloudStore.retryItem(job_id, item_id) + } catch (e) { + toast.show(`Failed to retry item: ${e.message || 'Unknown error'}`, 'error') + } +} + +/** + * ANALYZE-05 / D-09: Skip a failed analysis job item. + * + * @param {{ job_id: string, item_id: string }} payload + */ +async function onAnalysisItemSkip({ job_id, item_id }) { + try { + await cloudStore.skipItem(job_id, item_id) + } catch (e) { + toast.show(`Failed to skip item: ${e.message || 'Unknown error'}`, 'error') + } +} + +/** + * ANALYZE-05 / D-09: Cancel the entire analysis job. + * + * @param {{ job_id: string }} payload + */ +async function onAnalysisCancelAll({ job_id }) { + try { + await cloudStore.cancelJob(job_id) + } catch (e) { + toast.show(`Failed to cancel analysis: ${e.message || 'Unknown error'}`, 'error') + } +} + onMounted(async () => { // Ensure connections are loaded so connectionRoot resolves if (cloudStore.connections.length === 0) {