feat(14-08): add Analysis Settings section to SettingsCloudTab

- Add cache limit number input with tier cap validation and usage display
- Add segmented control for simplified vs detailed analysis progress (D-06)
- Add segmented control for pause-batch vs continue-item failure behavior (D-11)
- Load cache settings on mount via loadCacheSettings; surface API errors
- Keep existing connection health/reconnect/disconnect UX intact
- All 471 frontend tests pass; no regressions
This commit is contained in:
curo1305
2026-06-23 19:52:23 +02:00
parent 6fc30ac755
commit ba15811f91
@@ -321,6 +321,145 @@
@close="closeModal"
@connected="handleConnected"
/>
<!-- Phase 14: Analysis and cache settings -->
<section
data-test="cache-settings-section"
class="bg-white border border-gray-200 rounded-xl p-6 mt-4"
>
<h3 class="text-lg font-semibold text-gray-800 mb-1">Analysis Settings</h3>
<!-- Settings error banner -->
<div
v-if="store.cacheSettingsError"
data-test="cache-settings-error"
class="mb-4 p-3 rounded-lg bg-red-50 border border-red-200 text-sm text-red-700"
>
{{ store.cacheSettingsError }}
</div>
<div class="space-y-5">
<!-- Cache limit -->
<div>
<label
for="cache-limit-input"
class="block text-sm font-medium text-gray-700 mb-1"
>
Cache limit (MB)
</label>
<div class="flex items-center gap-3">
<input
id="cache-limit-input"
data-test="cache-limit-input"
type="number"
:min="1"
:max="cacheLimitMaxMB"
:value="cacheLimitMB"
@change="handleCacheLimitChange"
class="w-28 border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
aria-label="Cache limit in megabytes"
/>
<span class="text-xs text-gray-500">
Max: {{ cacheLimitMaxMB }} MB
</span>
<span
v-if="cacheLimitError"
data-test="cache-limit-error"
class="text-xs text-red-600"
>
{{ cacheLimitError }}
</span>
</div>
<p
v-if="store.cacheUsage"
data-test="cache-usage"
class="text-xs text-gray-500 mt-1"
>
Currently using {{ cacheUsageMB }} MB of {{ cacheLimitMB }} MB
</p>
</div>
<!-- Analysis progress detail -->
<div>
<span class="block text-sm font-medium text-gray-700 mb-1" id="progress-detail-label">
Analysis progress detail
</span>
<div
class="flex rounded-lg border border-gray-200 overflow-hidden w-fit"
role="group"
aria-labelledby="progress-detail-label"
>
<button
data-test="progress-simplified"
:class="[
'px-4 py-1.5 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-indigo-500',
!store.detailedAnalysisProgress
? 'bg-indigo-600 text-white font-medium'
: 'bg-white text-gray-700 hover:bg-gray-50',
]"
:aria-pressed="!store.detailedAnalysisProgress"
@click="handleProgressDetail(false)"
>
Simplified
</button>
<button
data-test="progress-detailed"
:class="[
'px-4 py-1.5 text-sm border-l border-gray-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-indigo-500',
store.detailedAnalysisProgress
? 'bg-indigo-600 text-white font-medium'
: 'bg-white text-gray-700 hover:bg-gray-50',
]"
:aria-pressed="store.detailedAnalysisProgress"
@click="handleProgressDetail(true)"
>
Detailed
</button>
</div>
</div>
<!-- Failure behavior -->
<div>
<span class="block text-sm font-medium text-gray-700 mb-1" id="failure-behavior-label">
On analysis failure
</span>
<div
class="flex rounded-lg border border-gray-200 overflow-hidden w-fit"
role="group"
aria-labelledby="failure-behavior-label"
>
<button
data-test="failure-pause-batch"
:class="[
'px-4 py-1.5 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-indigo-500',
store.failureBehavior === 'pause_batch'
? 'bg-indigo-600 text-white font-medium'
: 'bg-white text-gray-700 hover:bg-gray-50',
]"
:aria-pressed="store.failureBehavior === 'pause_batch'"
@click="handleFailureBehavior('pause_batch')"
>
Pause batch
</button>
<button
data-test="failure-continue-item"
:class="[
'px-4 py-1.5 text-sm border-l border-gray-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-indigo-500',
store.failureBehavior === 'continue_item'
? 'bg-indigo-600 text-white font-medium'
: 'bg-white text-gray-700 hover:bg-gray-50',
]"
:aria-pressed="store.failureBehavior === 'continue_item'"
@click="handleFailureBehavior('continue_item')"
>
Skip and continue
</button>
</div>
</div>
</div>
</section>
</div>
</template>
@@ -334,6 +473,31 @@ import CloudCredentialModal from '../cloud/CloudCredentialModal.vue'
const store = useCloudConnectionsStore()
// ── Phase 14: Cache settings computed helpers ─────────────────────────────────
/** Default tier cap: 2 GB in MB if server hasn't responded yet. */
const DEFAULT_TIER_CAP_BYTES = 2 * 1024 * 1024 * 1024
/** Tier cap in MB (from server or fallback). */
const cacheLimitMaxMB = computed(() => {
const cap = store.tierCapBytes ?? DEFAULT_TIER_CAP_BYTES
return Math.floor(cap / (1024 * 1024))
})
/** Current cache limit in MB. */
const cacheLimitMB = computed(() => {
return Math.round(store.cacheLimit / (1024 * 1024))
})
/** Current cache usage in MB (from server). */
const cacheUsageMB = computed(() => {
if (!store.cacheUsage) return 0
return Math.round(store.cacheUsage.total_bytes / (1024 * 1024))
})
/** Local validation error for cache limit input. */
const cacheLimitError = ref('')
const PROVIDERS = [
{ key: 'google_drive', label: 'Google Drive', iconColor: 'text-blue-500' },
{ key: 'onedrive', label: 'OneDrive', iconColor: 'text-sky-500' },
@@ -358,6 +522,10 @@ const testingId = ref(null)
onMounted(() => {
store.fetchConnections()
// Phase 14: hydrate server-authoritative cache settings on mount
if (typeof store.loadCacheSettings === 'function') {
store.loadCacheSettings()
}
})
/** All connections for a given provider key (may be multiple). */
@@ -530,4 +698,69 @@ async function submitRename(id) {
renameError.value = e.message || 'Failed to rename connection'
}
}
// ── Phase 14: Cache settings handlers ────────────────────────────────────────
/**
* Handle cache limit input change — validate against tier cap, then persist.
*/
async function handleCacheLimitChange(event) {
cacheLimitError.value = ''
const rawValue = parseInt(event.target.value, 10)
if (isNaN(rawValue) || rawValue < 1) {
cacheLimitError.value = 'Enter a value of at least 1 MB'
return
}
if (rawValue > cacheLimitMaxMB.value) {
cacheLimitError.value = `Maximum cache limit is ${cacheLimitMaxMB.value} MB`
return
}
const bytes = rawValue * 1024 * 1024
try {
if (typeof store.updateCacheSettings === 'function') {
await store.updateCacheSettings({ cloud_cache_limit_bytes: bytes })
}
} catch {
// store.cacheSettingsError set by the action; input reverts on next render
}
}
/**
* Toggle simplified vs detailed analysis progress mode.
*/
async function handleProgressDetail(enabled) {
if (typeof store.setDetailedAnalysisProgress === 'function') {
store.setDetailedAnalysisProgress(enabled)
}
try {
if (typeof store.updateCacheSettings === 'function') {
await store.updateCacheSettings({ analysis_progress_detail: enabled })
}
} catch {
// Revert local state on API failure
if (typeof store.setDetailedAnalysisProgress === 'function') {
store.setDetailedAnalysisProgress(!enabled)
}
}
}
/**
* Change failure behavior for analysis jobs.
*/
async function handleFailureBehavior(behavior) {
const previous = store.failureBehavior
if (typeof store.setFailureBehavior === 'function') {
store.setFailureBehavior(behavior)
}
try {
if (typeof store.updateCacheSettings === 'function') {
await store.updateCacheSettings({ analysis_failure_behavior: behavior })
}
} catch {
// Revert on failure
if (typeof store.setFailureBehavior === 'function') {
store.setFailureBehavior(previous)
}
}
}
</script>