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) {