- Fix getCacheSettings/updateCacheSettings URLs to use /api/cloud/analysis/cache and /api/cloud/analysis/cache/settings (Rule 1: wrong URLs per Plan 14-03 backend) - Add loadCacheSettings action: hydrates tierCapBytes, cacheUsage, cacheLimit, detailedAnalysisProgress, failureBehavior from server without clearing on error - Add tierCapBytes, cacheUsage, cacheSettingsLoading, cacheSettingsError state - updateCacheSettings merges full CacheStatusOut response into local state
704 lines
23 KiB
JavaScript
704 lines
23 KiB
JavaScript
import { defineStore } from 'pinia'
|
|
import { ref, computed } from 'vue'
|
|
import * as api from '../api/client.js'
|
|
|
|
/**
|
|
* Session-storage key for last visited folder per connection.
|
|
* Only folder navigation state is stored — never tokens or credentials.
|
|
*/
|
|
function folderSessionKey(connectionId) {
|
|
return `docuvault:cloud:folder:${connectionId}`
|
|
}
|
|
|
|
/**
|
|
* Valid health state values used by both browser compact status and Settings diagnostics.
|
|
* Translates server vocabulary to UI-actionable states (D-12).
|
|
*
|
|
* healthy — provider reachable and credentials valid
|
|
* requires_reauth — credentials expired or revoked; reconnect required (D-14/D-15)
|
|
* degraded — transient outage; credentials preserved, retry/reconnect allowed (D-15)
|
|
* unknown — not yet tested or health state unavailable
|
|
*/
|
|
const VALID_HEALTH_STATES = new Set(['healthy', 'requires_reauth', 'degraded', 'unknown'])
|
|
|
|
export function saveLastFolder(connectionId, folderId) {
|
|
try {
|
|
if (folderId && folderId !== 'root') {
|
|
sessionStorage.setItem(folderSessionKey(connectionId), folderId)
|
|
} else {
|
|
sessionStorage.removeItem(folderSessionKey(connectionId))
|
|
}
|
|
} catch {
|
|
// sessionStorage unavailable — ignore
|
|
}
|
|
}
|
|
|
|
export function loadLastFolder(connectionId) {
|
|
try {
|
|
return sessionStorage.getItem(folderSessionKey(connectionId)) || null
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
// ── 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)
|
|
const error = ref(null)
|
|
|
|
// Active browse state (populated by CloudFolderView)
|
|
const selectedConnectionId = ref(null)
|
|
const browseItems = ref([])
|
|
const browseLoading = ref(false)
|
|
const browseError = ref(null)
|
|
/** Capabilities for the currently selected connection */
|
|
const capabilities = ref(null)
|
|
/** 'refreshing' | 'fresh' | 'stale' | null */
|
|
const folderFreshness = ref(null)
|
|
const lastRefreshedAt = ref(null)
|
|
/** '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).
|
|
* Hydrated from server via loadCacheSettings — in-memory only, not persisted.
|
|
*/
|
|
const cacheLimit = ref(500 * 1024 * 1024)
|
|
|
|
/**
|
|
* Server-authoritative tier cap for the cache limit in bytes.
|
|
* Loaded from GET /api/cloud/analysis/cache (tier_cap_bytes field).
|
|
* null until loadCacheSettings has run.
|
|
*/
|
|
const tierCapBytes = ref(null)
|
|
|
|
/**
|
|
* Current aggregate cache usage from the server.
|
|
* Loaded from GET /api/cloud/analysis/cache.
|
|
* { entry_count: number, total_bytes: number } — no object_key or credentials.
|
|
*/
|
|
const cacheUsage = ref(null)
|
|
|
|
/**
|
|
* Whether the last loadCacheSettings call is in flight.
|
|
*/
|
|
const cacheSettingsLoading = ref(false)
|
|
|
|
/**
|
|
* Validation/network error from the last settings call, or null.
|
|
*/
|
|
const cacheSettingsError = ref(null)
|
|
|
|
/**
|
|
* D-12: Connection health state map — single source for both browser compact status
|
|
* and Settings full diagnostics.
|
|
*
|
|
* Key: connection UUID
|
|
* Value: { state: 'healthy'|'requires_reauth'|'degraded'|'unknown', error_code, error_message, checked_at }
|
|
*
|
|
* Neither the browser nor Settings translate server states independently —
|
|
* they read from this map only.
|
|
*/
|
|
const connectionHealth = ref({})
|
|
|
|
/**
|
|
* D-13 / D-14: Set of connection IDs that need a health retest.
|
|
* CloudFolderView reads this and performs the test after navigation settles.
|
|
* Cleared after the test is performed.
|
|
*/
|
|
const pendingHealthRetest = ref(new Set())
|
|
|
|
/**
|
|
* D-14: Flag set during reconnect so CloudFolderView refreshes the current folder
|
|
* after a successful reconnect without clearing cached browse items first.
|
|
*/
|
|
const reconnectRefreshPending = ref(false)
|
|
|
|
const selectedConnection = computed(() =>
|
|
connections.value.find(c => c.id === selectedConnectionId.value) ?? null
|
|
)
|
|
|
|
async function fetchConnections() {
|
|
loading.value = true
|
|
error.value = null
|
|
try {
|
|
const data = await api.listCloudConnections()
|
|
connections.value = data.items ?? []
|
|
} catch (e) {
|
|
error.value = e.message || 'Failed to load cloud connections'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function disconnect(id) {
|
|
try {
|
|
await api.disconnectCloud(id)
|
|
connections.value = connections.value.filter(c => c.id !== id)
|
|
if (selectedConnectionId.value === id) {
|
|
selectedConnectionId.value = null
|
|
}
|
|
} catch (e) {
|
|
throw e
|
|
}
|
|
}
|
|
|
|
async function disconnectAll() {
|
|
const ids = connections.value.map(c => c.id)
|
|
for (const id of ids) await disconnect(id)
|
|
connections.value = []
|
|
}
|
|
|
|
/**
|
|
* Rename a connection's display_name via the API.
|
|
* Only updates the single returned connection in state.
|
|
*/
|
|
async function rename(id, displayName) {
|
|
const trimmed = displayName?.trim()
|
|
if (!trimmed) throw new Error('Display name cannot be empty')
|
|
const updated = await api.renameCloudConnection(id, trimmed)
|
|
const idx = connections.value.findIndex(c => c.id === id)
|
|
if (idx !== -1) {
|
|
connections.value[idx] = { ...connections.value[idx], ...updated }
|
|
}
|
|
return updated
|
|
}
|
|
|
|
/**
|
|
* Default display_name for a connection.
|
|
* If another connection shares the same provider, append a 4-char UUID suffix.
|
|
*/
|
|
function defaultDisplayName(connection) {
|
|
const providerLabel = {
|
|
google_drive: 'Google Drive',
|
|
onedrive: 'OneDrive',
|
|
nextcloud: 'Nextcloud',
|
|
webdav: 'WebDAV',
|
|
}[connection.provider] ?? connection.provider
|
|
const siblings = connections.value.filter(
|
|
c => c.provider === connection.provider && c.id !== connection.id
|
|
)
|
|
if (siblings.length > 0) {
|
|
return `${providerLabel} (${connection.id.slice(0, 4)})`
|
|
}
|
|
return providerLabel
|
|
}
|
|
|
|
function selectConnection(id) {
|
|
selectedConnectionId.value = id
|
|
browseItems.value = []
|
|
browseError.value = null
|
|
capabilities.value = null
|
|
folderFreshness.value = null
|
|
lastRefreshedAt.value = null
|
|
byteAvailability.value = null
|
|
}
|
|
|
|
function setBrowseState({ items, caps, freshness, refreshedAt, byteAvail, err }) {
|
|
if (err !== undefined) browseError.value = err
|
|
if (items !== undefined) browseItems.value = items
|
|
if (caps !== undefined) capabilities.value = caps
|
|
if (freshness !== undefined) folderFreshness.value = freshness
|
|
if (refreshedAt !== undefined) lastRefreshedAt.value = refreshedAt
|
|
if (byteAvail !== undefined) byteAvailability.value = byteAvail
|
|
}
|
|
|
|
/**
|
|
* D-12: Set the health state for a specific connection.
|
|
* Translates server health payload to the UI-actionable vocabulary.
|
|
* Both browser compact status and Settings diagnostics consume this.
|
|
*
|
|
* @param {string} id — connection UUID
|
|
* @param {{ state: string, error_code?: string, error_message?: string, checked_at?: string }} healthData
|
|
*/
|
|
function setConnectionHealth(id, healthData) {
|
|
const state = VALID_HEALTH_STATES.has(healthData?.state) ? healthData.state : 'unknown'
|
|
connectionHealth.value = {
|
|
...connectionHealth.value,
|
|
[id]: {
|
|
state,
|
|
error_code: healthData?.error_code ?? null,
|
|
error_message: healthData?.error_message ?? null,
|
|
checked_at: healthData?.checked_at ?? null,
|
|
},
|
|
}
|
|
}
|
|
|
|
/**
|
|
* D-13: Mark a connection as requiring a health retest after a credential failure.
|
|
* This is the complement of the no-probe-on-navigation rule:
|
|
* probes happen only after failures (here), not on ordinary navigation.
|
|
*
|
|
* @param {string} id — connection UUID
|
|
* @param {{ state: string, error_code?: string }} failureData
|
|
*/
|
|
function handleHealthFailure(id, failureData) {
|
|
// Record the failure state in the health map
|
|
setConnectionHealth(id, failureData)
|
|
// Schedule a retest (D-13: auto-test after credential failure)
|
|
pendingHealthRetest.value = new Set([...pendingHealthRetest.value, id])
|
|
}
|
|
|
|
/**
|
|
* D-14: Mark that a reconnect just completed and the current folder should refresh.
|
|
* Does NOT clear browse items — they stay visible as stale while the refresh runs.
|
|
*
|
|
* @param {string} id — connection UUID
|
|
*/
|
|
function markReconnectRefreshPending(id) {
|
|
reconnectRefreshPending.value = true
|
|
// Keep browse items visible but downgrade freshness to stale (D-14)
|
|
if (folderFreshness.value !== null) {
|
|
folderFreshness.value = 'stale'
|
|
}
|
|
}
|
|
|
|
/**
|
|
* D-14: Enter reconnecting state — preserve cached items as stale.
|
|
* Does NOT call selectConnection (which clears items).
|
|
*
|
|
* @param {string} id — connection UUID
|
|
*/
|
|
function markReconnecting(id) {
|
|
// D-14: cached metadata remains visible; downgrade freshness only
|
|
if (folderFreshness.value !== null) {
|
|
folderFreshness.value = 'stale'
|
|
}
|
|
// Set health to unknown while reconnect is in flight
|
|
connectionHealth.value = {
|
|
...connectionHealth.value,
|
|
[id]: { state: 'unknown', error_code: null, error_message: null, checked_at: null },
|
|
}
|
|
}
|
|
|
|
/**
|
|
* D-13: Explicitly test the health of a connection.
|
|
* Only called after connect/reconnect/failure — never as a navigation side effect.
|
|
*
|
|
* @param {string} id — connection UUID
|
|
* @returns {Promise<{ state: string }>}
|
|
*/
|
|
async function testConnection(id) {
|
|
try {
|
|
const result = await api.testCloudConnection(id)
|
|
// WR-06: server returns { status: ... }, not { state: ... }. Translate at the
|
|
// store boundary — internal vocabulary uses `state` but the API field is `status`.
|
|
const state = result?.status ?? 'unknown'
|
|
setConnectionHealth(id, { state, ...result })
|
|
// Clear pending retest flag for this connection
|
|
const next = new Set(pendingHealthRetest.value)
|
|
next.delete(id)
|
|
pendingHealthRetest.value = next
|
|
return result
|
|
} catch (e) {
|
|
const failState = { state: 'degraded', error_code: 'probe_error', error_message: e?.message ?? 'Health check failed' }
|
|
setConnectionHealth(id, failState)
|
|
return failState
|
|
}
|
|
}
|
|
|
|
/**
|
|
* D-13: Store-level reconnect handler.
|
|
* Marks reconnecting state (cached items preserved), then fetches updated connections.
|
|
*
|
|
* @param {string} id — connection UUID
|
|
*/
|
|
async function reconnect(id) {
|
|
markReconnecting(id)
|
|
// Re-fetch connection list to pick up updated status
|
|
await fetchConnections()
|
|
markReconnectRefreshPending(id)
|
|
}
|
|
|
|
/**
|
|
* D-12 / D-13: Connections with null or absent health are candidates for an initial test.
|
|
* The store exposes this as a computed so the view can decide when to run tests.
|
|
*/
|
|
const connectionsNeedingHealthCheck = computed(() =>
|
|
connections.value.filter(c => {
|
|
const h = connectionHealth.value[c.id]
|
|
return !h || h.state === undefined || h.state === 'unknown'
|
|
})
|
|
)
|
|
|
|
/**
|
|
* D-12: Retrieve the health state for a specific connection.
|
|
* Returns { state: 'unknown', ... } if no health data is available.
|
|
*
|
|
* @param {string} id — connection UUID
|
|
* @returns {{ state: string, error_code: string|null, error_message: string|null, checked_at: string|null }}
|
|
*/
|
|
function getConnectionHealth(id) {
|
|
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: Load cache status and analysis settings from the server.
|
|
*
|
|
* Hydrates cacheLimit, tierCapBytes, cacheUsage, detailedAnalysisProgress,
|
|
* and failureBehavior from the authoritative backend state.
|
|
*
|
|
* Errors do not clear existing in-memory settings — cacheSettingsError is
|
|
* set so the UI can surface a controlled message.
|
|
*/
|
|
async function loadCacheSettings() {
|
|
cacheSettingsLoading.value = true
|
|
cacheSettingsError.value = null
|
|
try {
|
|
const result = await api.getCacheSettings()
|
|
// Hydrate server-authoritative values into local state
|
|
if (result?.cache_limit_bytes !== undefined) {
|
|
cacheLimit.value = result.cache_limit_bytes
|
|
}
|
|
if (result?.tier_cap_bytes !== undefined) {
|
|
tierCapBytes.value = result.tier_cap_bytes
|
|
}
|
|
if (result?.entry_count !== undefined || result?.total_bytes !== undefined) {
|
|
cacheUsage.value = {
|
|
entry_count: result.entry_count ?? 0,
|
|
total_bytes: result.total_bytes ?? 0,
|
|
}
|
|
}
|
|
if (result?.analysis_progress_detail !== undefined) {
|
|
detailedAnalysisProgress.value = !!result.analysis_progress_detail
|
|
}
|
|
if (result?.analysis_failure_behavior !== undefined) {
|
|
failureBehavior.value = result.analysis_failure_behavior
|
|
}
|
|
return result
|
|
} catch (e) {
|
|
// Do not clear existing settings on error — surface a controlled message
|
|
cacheSettingsError.value = e?.message ?? 'Failed to load cache settings'
|
|
return null
|
|
} finally {
|
|
cacheSettingsLoading.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* CACHE-04: Update user cache settings via the API.
|
|
*
|
|
* Merges server response back into local state after a successful update.
|
|
* Validation errors surface as a thrown error (caller responsible for display).
|
|
*
|
|
* @param {{ cloud_cache_limit_bytes?: number, analysis_progress_detail?: boolean,
|
|
* analysis_failure_behavior?: string }} params
|
|
*/
|
|
async function updateCacheSettings(params) {
|
|
cacheSettingsError.value = null
|
|
try {
|
|
const result = await api.updateCacheSettings(params)
|
|
// Merge server response into local state
|
|
if (result?.cache_limit_bytes !== undefined) {
|
|
cacheLimit.value = result.cache_limit_bytes
|
|
}
|
|
if (result?.cloud_cache_limit_bytes !== undefined) {
|
|
cacheLimit.value = result.cloud_cache_limit_bytes
|
|
}
|
|
if (result?.tier_cap_bytes !== undefined) {
|
|
tierCapBytes.value = result.tier_cap_bytes
|
|
}
|
|
if (result?.entry_count !== undefined || result?.total_bytes !== undefined) {
|
|
cacheUsage.value = {
|
|
entry_count: result.entry_count ?? 0,
|
|
total_bytes: result.total_bytes ?? 0,
|
|
}
|
|
}
|
|
if (result?.analysis_progress_detail !== undefined) {
|
|
detailedAnalysisProgress.value = !!result.analysis_progress_detail
|
|
}
|
|
if (result?.analysis_failure_behavior !== undefined) {
|
|
failureBehavior.value = result.analysis_failure_behavior
|
|
}
|
|
return result
|
|
} catch (e) {
|
|
// Surface validation error without clearing existing settings
|
|
cacheSettingsError.value = e?.message ?? 'Failed to update cache settings'
|
|
throw e
|
|
}
|
|
}
|
|
|
|
return {
|
|
connections,
|
|
loading,
|
|
error,
|
|
selectedConnectionId,
|
|
selectedConnection,
|
|
browseItems,
|
|
browseLoading,
|
|
browseError,
|
|
capabilities,
|
|
folderFreshness,
|
|
lastRefreshedAt,
|
|
byteAvailability,
|
|
// D-12: health state map — single source for browser and Settings
|
|
connectionHealth,
|
|
// D-13: pending health retests (after credential failures)
|
|
pendingHealthRetest,
|
|
// D-14: reconnect refresh flag
|
|
reconnectRefreshPending,
|
|
// D-12: connections needing initial health check
|
|
connectionsNeedingHealthCheck,
|
|
fetchConnections,
|
|
disconnect,
|
|
disconnectAll,
|
|
rename,
|
|
defaultDisplayName,
|
|
selectConnection,
|
|
setBrowseState,
|
|
// D-12/D-13: health management
|
|
setConnectionHealth,
|
|
getConnectionHealth,
|
|
handleHealthFailure,
|
|
markReconnectRefreshPending,
|
|
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 and cache settings
|
|
setFailureBehavior,
|
|
setDetailedAnalysisProgress,
|
|
loadCacheSettings,
|
|
updateCacheSettings,
|
|
// Server-authoritative cache state
|
|
tierCapBytes,
|
|
cacheUsage,
|
|
cacheSettingsLoading,
|
|
cacheSettingsError,
|
|
}
|
|
})
|