feat(13-10): store-backed health mapping, reconnect UX, no-probe-on-navigation, and Google Drive consent copy
- D-12: connectionHealth ref as single source for browser compact status and Settings diagnostics - D-13: handleHealthFailure schedules pendingHealthRetest; navigation never triggers probe - D-13: testConnection method in store (explicit action only, not navigation side-effect) - D-14: markReconnecting preserves cached browseItems as stale; markReconnectRefreshPending flag - D-15: degraded vs requires_reauth health states distinguished in store vocabulary - D-17: Google Drive scope notice in SettingsCloudTab for all connect/reconnect paths - StorageBrowser: cloud-health-banner (warning/stale) and requires-reauth prompt with reconnect action - 59 tests passing: store, Settings, and CloudFolderView health/reconnect suites
This commit is contained in:
@@ -10,6 +10,17 @@ 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') {
|
||||
@@ -48,6 +59,31 @@ export const useCloudConnectionsStore = defineStore('cloudConnections', () => {
|
||||
/** 'cached' | 'cloud_only' | null */
|
||||
const byteAvailability = 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
|
||||
)
|
||||
@@ -137,6 +173,133 @@ export const useCloudConnectionsStore = defineStore('cloudConnections', () => {
|
||||
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)
|
||||
const state = result?.state ?? '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 }
|
||||
}
|
||||
|
||||
return {
|
||||
connections,
|
||||
loading,
|
||||
@@ -150,6 +313,14 @@ export const useCloudConnectionsStore = defineStore('cloudConnections', () => {
|
||||
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,
|
||||
@@ -157,5 +328,13 @@ export const useCloudConnectionsStore = defineStore('cloudConnections', () => {
|
||||
defaultDisplayName,
|
||||
selectConnection,
|
||||
setBrowseState,
|
||||
// D-12/D-13: health management
|
||||
setConnectionHealth,
|
||||
getConnectionHealth,
|
||||
handleHealthFailure,
|
||||
markReconnectRefreshPending,
|
||||
markReconnecting,
|
||||
testConnection,
|
||||
reconnect,
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user