From 1291040f9bb6a252417af883298fbe89a0492e5e Mon Sep 17 00:00:00 2001 From: curo1305 Date: Tue, 23 Jun 2026 19:35:33 +0200 Subject: [PATCH] feat(14-07): add analysis API helpers and store state/actions (Task 1) - Add estimateAnalysis, enqueueAnalysis, getAnalysisJobStatus, listAnalysisJobs, cancelAnalysisJob, cancelAnalysisItem, skipAnalysisItem, retryAnalysisItem, getCacheSettings, updateCacheSettings to api/cloud.js - Extend cloudConnections store with analysis state: activeAnalysisJob, analysisQueue (computed), failureBehavior, detailedAnalysisProgress, cacheLimit - Add store actions: requestEstimate, enqueueAnalysis, fetchJobStatus, cancelJob, retryItem, skipItem, cancelItem, setFailureBehavior, setDetailedAnalysisProgress, updateCacheSettings - Add translateAnalysisStatus (D-06: single translation source for components) - Status labels: simplified (waiting/working/done/skipped/failed) and detailed modes - Credentials never stored in queue state (T-14-02) --- frontend/src/api/cloud.js | 115 +++++++++++ frontend/src/stores/cloudConnections.js | 257 ++++++++++++++++++++++++ 2 files changed, 372 insertions(+) diff --git a/frontend/src/api/cloud.js b/frontend/src/api/cloud.js index c097a11..ab81ad2 100644 --- a/frontend/src/api/cloud.js +++ b/frontend/src/api/cloud.js @@ -255,3 +255,118 @@ export function downloadCloudFile(connectionId, itemId) { { method: 'GET' }, ) } + +// ── Phase 14: Analysis API ──────────────────────────────────────────────────── + +/** + * Estimate the scope of an analysis job for a cloud connection. + * + * T-14-04: No provider bytes are downloaded during estimate — metadata only. + * Returns AnalysisEstimateOut: {supported_count, unsupported_count, total_provider_bytes, + * recursive, is_partial, scope_kind} + * + * @param {string} connectionId - Connection UUID + * @param {{ scope: string, provider_item_ids?: string[], recursive?: boolean }} params + */ +export function estimateAnalysis(connectionId, params) { + return jsonRequest(`/api/cloud/analysis/connections/${connectionId}/estimate`, 'POST', params) +} + +/** + * Enqueue an analysis job for a cloud connection scope. + * + * Returns AnalysisEnqueueOut: {job_id, status, total_count, queued_count, + * already_current_count, unsupported_count} + * + * @param {string} connectionId - Connection UUID + * @param {{ scope: string, provider_item_ids?: string[], recursive?: boolean, + * failure_behavior?: string }} params + */ +export function enqueueAnalysis(connectionId, params) { + return jsonRequest(`/api/cloud/analysis/connections/${connectionId}/jobs`, 'POST', params) +} + +/** + * Get the status of an analysis job. + * + * T-14-02: Response never includes credentials or object_key. + * + * @param {string} jobId - Job UUID + * @param {{ detail?: boolean }} [opts] + */ +export function getAnalysisJobStatus(jobId, opts = {}) { + const qs = opts.detail ? '?detail=true' : '' + return request(`/api/cloud/analysis/jobs/${jobId}${qs}`) +} + +/** + * List analysis jobs, optionally filtered by connection or status. + * + * @param {{ connection_id?: string, status?: string }} [filters] + */ +export function listAnalysisJobs(filters = {}) { + const params = new URLSearchParams() + if (filters.connection_id) params.set('connection_id', filters.connection_id) + if (filters.status) params.set('status', filters.status) + const qs = params.toString() ? `?${params}` : '' + return request(`/api/cloud/analysis/jobs${qs}`) +} + +/** + * Cancel an entire analysis job batch. + * + * Returns AnalysisControlOut: {kind, reason, status} + * + * @param {string} jobId - Job UUID + */ +export function cancelAnalysisJob(jobId) { + return jsonRequest(`/api/cloud/analysis/jobs/${jobId}/cancel`, 'POST', {}) +} + +/** + * Cancel a single analysis job item. + * + * @param {string} jobId - Job UUID + * @param {string} itemId - Item UUID + */ +export function cancelAnalysisItem(jobId, itemId) { + return jsonRequest(`/api/cloud/analysis/jobs/${jobId}/items/${itemId}/cancel`, 'POST', {}) +} + +/** + * Skip a queued or failed analysis job item. + * + * @param {string} jobId - Job UUID + * @param {string} itemId - Item UUID + */ +export function skipAnalysisItem(jobId, itemId) { + return jsonRequest(`/api/cloud/analysis/jobs/${jobId}/items/${itemId}/skip`, 'POST', {}) +} + +/** + * Retry a failed analysis job item. + * + * @param {string} jobId - Job UUID + * @param {string} itemId - Item UUID + */ +export function retryAnalysisItem(jobId, itemId) { + return jsonRequest(`/api/cloud/analysis/jobs/${jobId}/items/${itemId}/retry`, 'POST', {}) +} + +/** + * Get the current user cache settings. + * + * Returns { cloud_cache_limit_bytes, ... } + */ +export function getCacheSettings() { + return request('/api/users/me/settings') +} + +/** + * Update the current user cache settings. + * + * @param {{ cloud_cache_limit_bytes?: number }} params + */ +export function updateCacheSettings(params) { + return jsonRequest('/api/users/me/settings', 'PATCH', params) +} diff --git a/frontend/src/stores/cloudConnections.js b/frontend/src/stores/cloudConnections.js index 1a4ffd7..d84c18a 100644 --- a/frontend/src/stores/cloudConnections.js +++ b/frontend/src/stores/cloudConnections.js @@ -41,6 +41,38 @@ export function loadLastFolder(connectionId) { } } +// ── Phase 14: Analysis status translation ──────────────────────────────────── + +/** + * D-06: Single source for internal status → simplified UI label mapping. + * Components never translate independently — they call translateAnalysisStatus. + */ +const SIMPLIFIED_STATUS_MAP = { + queued: 'waiting', + downloading: 'working', + extracting: 'working', + classifying: 'working', + indexed: 'done', + already_current: 'done', + cancelled: 'skipped', + failed: 'failed', + unsupported: 'skipped', + stale: 'working', +} + +const DETAILED_STATUS_MAP = { + queued: 'queued', + downloading: 'downloading', + extracting: 'extracting', + classifying: 'classifying', + indexed: 'indexed', + already_current: 'indexed', + cancelled: 'cancelled', + failed: 'failed', + unsupported: 'unsupported', + stale: 'stale', +} + export const useCloudConnectionsStore = defineStore('cloudConnections', () => { const connections = ref([]) const loading = ref(false) @@ -59,6 +91,53 @@ export const useCloudConnectionsStore = defineStore('cloudConnections', () => { /** 'cached' | 'cloud_only' | null */ const byteAvailability = ref(null) + // ── Phase 14: Analysis state ───────────────────────────────────────────────── + + /** + * Active analysis job for the current connection session. + * Stored in Pinia memory only — never persisted to localStorage/sessionStorage. + * T-14-02: No credentials or object_key fields stored here. + */ + const activeAnalysisJob = ref(null) + + /** + * Analysis queue items from the active job for display in StorageBrowser. + * Derived from activeAnalysisJob.items when a job is active. + */ + const analysisQueue = computed(() => { + if (!activeAnalysisJob.value) return [] + const items = activeAnalysisJob.value.items ?? [] + return items.map(item => ({ + job_id: activeAnalysisJob.value.job_id, + item_id: item.item_id, + name: item.name, + status: item.status, + ui_status: SIMPLIFIED_STATUS_MAP[item.status] ?? item.status, + progress: item.progress ?? 0, + error: item.error ?? null, + })) + }) + + /** + * D-11: Failure behavior for analysis jobs. + * 'pause_batch' — pause the whole batch on any item failure (default) + * 'continue_item' — skip failed items and continue + */ + const failureBehavior = ref('pause_batch') + + /** + * D-06: When true, per-item status shows detailed internal stages + * (downloading/extracting/classifying). When false (default), shows simplified + * labels (waiting/working/done/skipped/failed). + */ + const detailedAnalysisProgress = ref(false) + + /** + * CACHE-04: User-configured cloud cache limit in bytes. + * Default: 500 MB (500 * 1024 * 1024). + */ + const cacheLimit = ref(500 * 1024 * 1024) + /** * D-12: Connection health state map — single source for both browser compact status * and Settings full diagnostics. @@ -302,6 +381,163 @@ export const useCloudConnectionsStore = defineStore('cloudConnections', () => { return connectionHealth.value[id] ?? { state: 'unknown', error_code: null, error_message: null, checked_at: null } } + // ── Phase 14: Analysis actions ───────────────────────────────────────────── + + /** + * D-06: Translate an internal analysis status to a UI label. + * Single translation source — components must not translate independently. + * + * @param {string} status - Internal status (queued, downloading, extracting, etc.) + * @param {{ detailed?: boolean }} [opts] + * @returns {string} - UI label + */ + function translateAnalysisStatus(status, opts = {}) { + const map = opts.detailed ? DETAILED_STATUS_MAP : SIMPLIFIED_STATUS_MAP + return map[status] ?? status + } + + /** + * ANALYZE-01 / ANALYZE-03: Request a scope estimate for an analysis job. + * No provider bytes are downloaded (T-14-04). + * + * @param {string} connectionId - Connection UUID + * @param {{ scope: string, provider_item_ids?: string[], recursive?: boolean }} params + * @returns {Promise} AnalysisEstimateOut + */ + async function requestEstimate(connectionId, params) { + return api.estimateAnalysis(connectionId, params) + } + + /** + * ANALYZE-01: Enqueue an analysis job and track it in store state. + * + * @param {string} connectionId - Connection UUID + * @param {{ scope: string, provider_item_ids?: string[], recursive?: boolean, + * failure_behavior?: string }} params + * @returns {Promise} AnalysisEnqueueOut + */ + async function enqueueAnalysis(connectionId, params) { + const result = await api.enqueueAnalysis(connectionId, { + ...params, + failure_behavior: params.failure_behavior ?? failureBehavior.value, + }) + // Initialize the active job in memory (no credentials stored) + activeAnalysisJob.value = { + job_id: result.job_id, + connection_id: connectionId, + status: result.status, + total_count: result.total_count, + queued_count: result.queued_count, + already_current_count: result.already_current_count, + unsupported_count: result.unsupported_count, + items: [], + } + return result + } + + /** + * ANALYZE-04: Fetch and update status of the active analysis job. + * T-14-02: Response must never include credentials or object_key. + * + * @param {string} jobId - Job UUID + * @returns {Promise} AnalysisJobOut + */ + async function fetchJobStatus(jobId) { + const result = await api.getAnalysisJobStatus(jobId) + // Store job state in Pinia memory — no credentials/object_key fields exposed + activeAnalysisJob.value = { + job_id: result.job_id ?? jobId, + connection_id: result.connection_id ?? activeAnalysisJob.value?.connection_id, + status: result.status, + total_count: result.total_count ?? 0, + queued_count: result.queued_count ?? 0, + already_current_count: result.already_current_count ?? 0, + unsupported_count: result.unsupported_count ?? 0, + items: (result.items ?? []).map(item => ({ + item_id: item.item_id, + cloud_item_id: item.cloud_item_id, + name: item.name, + status: item.status, + error: item.error ?? null, + })), + } + return result + } + + /** + * ANALYZE-05: Cancel the entire analysis job batch. + * + * @param {string} jobId - Job UUID + */ + async function cancelJob(jobId) { + const result = await api.cancelAnalysisJob(jobId) + if (activeAnalysisJob.value?.job_id === jobId) { + activeAnalysisJob.value = { ...activeAnalysisJob.value, status: 'cancelled' } + } + return result + } + + /** + * ANALYZE-05: Retry a failed analysis job item. + * + * @param {string} jobId - Job UUID + * @param {string} itemId - Item UUID + */ + async function retryItem(jobId, itemId) { + return api.retryAnalysisItem(jobId, itemId) + } + + /** + * ANALYZE-05: Skip a queued or failed analysis job item. + * + * @param {string} jobId - Job UUID + * @param {string} itemId - Item UUID + */ + async function skipItem(jobId, itemId) { + return api.skipAnalysisItem(jobId, itemId) + } + + /** + * ANALYZE-05: Cancel a single analysis job item. + * + * @param {string} jobId - Job UUID + * @param {string} itemId - Item UUID + */ + async function cancelItem(jobId, itemId) { + return api.cancelAnalysisItem(jobId, itemId) + } + + /** + * D-11: Set the failure behavior for analysis jobs. + * + * @param {'pause_batch'|'continue_item'} behavior + */ + function setFailureBehavior(behavior) { + failureBehavior.value = behavior + } + + /** + * D-06: Toggle detailed analysis progress mode. + * + * @param {boolean} enabled + */ + function setDetailedAnalysisProgress(enabled) { + detailedAnalysisProgress.value = !!enabled + } + + /** + * CACHE-04: Update user cache settings via the API. + * + * @param {{ cloud_cache_limit_bytes?: number }} params + */ + async function updateCacheSettings(params) { + const result = await api.updateCacheSettings(params) + if (result?.cloud_cache_limit_bytes !== undefined) { + cacheLimit.value = result.cloud_cache_limit_bytes + } + return result + } + return { connections, loading, @@ -338,5 +574,26 @@ export const useCloudConnectionsStore = defineStore('cloudConnections', () => { markReconnecting, testConnection, reconnect, + // ── Phase 14: Analysis state and actions ────────────────────────────────── + // State (Pinia memory only — no credentials stored) + activeAnalysisJob, + analysisQueue, + failureBehavior, + detailedAnalysisProgress, + cacheLimit, + // D-06: single status translation source + translateAnalysisStatus, + // Analysis job lifecycle + requestEstimate, + enqueueAnalysis, + fetchJobStatus, + cancelJob, + retryItem, + skipItem, + cancelItem, + // User preferences + setFailureBehavior, + setDetailedAnalysisProgress, + updateCacheSettings, } })