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)
This commit is contained in:
curo1305
2026-06-23 19:35:33 +02:00
parent a2b7928ba4
commit 1291040f9b
2 changed files with 372 additions and 0 deletions
+115
View File
@@ -255,3 +255,118 @@ export function downloadCloudFile(connectionId, itemId) {
{ method: 'GET' }, { 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)
}
+257
View File
@@ -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', () => { export const useCloudConnectionsStore = defineStore('cloudConnections', () => {
const connections = ref([]) const connections = ref([])
const loading = ref(false) const loading = ref(false)
@@ -59,6 +91,53 @@ export const useCloudConnectionsStore = defineStore('cloudConnections', () => {
/** 'cached' | 'cloud_only' | null */ /** 'cached' | 'cloud_only' | null */
const byteAvailability = ref(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 * D-12: Connection health state map — single source for both browser compact status
* and Settings full diagnostics. * 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 } 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<object>} 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<object>} 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<object>} 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 { return {
connections, connections,
loading, loading,
@@ -338,5 +574,26 @@ export const useCloudConnectionsStore = defineStore('cloudConnections', () => {
markReconnecting, markReconnecting,
testConnection, testConnection,
reconnect, 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,
} }
}) })