diff --git a/src/components/ServiceStatusPanel.ts b/src/components/ServiceStatusPanel.ts index c989caf03d..92c1f365a9 100644 --- a/src/components/ServiceStatusPanel.ts +++ b/src/components/ServiceStatusPanel.ts @@ -5,6 +5,14 @@ import { fetchServiceStatuses, type ServiceStatusResult as ServiceStatus, } from '@/services/infrastructure'; +import { + getDesktopReadinessChecks, + getKeyBackedAvailabilitySummary, + getNonParityFeatures, + type DesktopReadinessCheck, +} from '@/services/desktop-readiness'; +import { hasTauriInvokeBridge } from '@/services/tauri-bridge'; +import { isDesktopRuntime } from '@/services/runtime'; import { h, replaceChildren } from '@/utils/dom-utils'; type CategoryFilter = 'all' | 'cloud' | 'dev' | 'comm' | 'ai' | 'saas'; @@ -89,6 +97,7 @@ export class ServiceStatusPanel extends Panel { const issues = filtered.filter(s => s.status !== 'operational'); replaceChildren(this.content, + this.buildDesktopReadiness(), this.buildSummary(filtered), this.buildFilters(), h('div', { className: 'service-status-list' }, @@ -98,6 +107,80 @@ export class ServiceStatusPanel extends Panel { ); } + private buildDesktopReadiness(): HTMLElement | false { + if (!isDesktopRuntime()) return false; + + const localBackendEnabled = hasTauriInvokeBridge(); + const checks = getDesktopReadinessChecks(localBackendEnabled); + const readyCount = checks.filter(check => check.ready).length; + const keyBacked = getKeyBackedAvailabilitySummary(); + const fallbacks = [...getNonParityFeatures()].sort((a, b) => a.priority - b.priority); + + return h('section', { className: 'service-status-readiness' }, + h('div', { className: 'service-status-readiness-title' }, t('components.serviceStatus.desktopReadiness')), + h('div', { className: 'service-status-readiness-meta' }, + t('components.serviceStatus.acceptanceChecks', { + ready: String(readyCount), + total: String(checks.length), + available: String(keyBacked.available), + featureTotal: String(keyBacked.total), + }), + ), + !localBackendEnabled + ? h('div', { className: 'service-status-readiness-warning' }, t('components.serviceStatus.backendUnavailable')) + : false, + h('div', { className: 'service-status-readiness-list' }, + ...checks.map((check) => this.buildReadinessItem(check)), + ), + fallbacks.length > 0 + ? h('div', { className: 'service-status-fallbacks' }, + h('div', { className: 'service-status-fallbacks-title' }, + t('components.serviceStatus.nonParityFallbacks', { count: String(fallbacks.length) }), + ), + h('div', { className: 'service-status-fallbacks-list' }, + ...fallbacks.map((feature) => + h('div', { className: 'service-status-fallback-item' }, + h('div', { className: 'service-status-fallback-panel' }, feature.panel), + h('div', { className: 'service-status-fallback-copy' }, feature.fallback), + ), + ), + ), + ) + : false, + ); + } + + private buildReadinessItem(check: DesktopReadinessCheck): HTMLElement { + const status = check.ready ? 'operational' : 'outage'; + const hint = this.getReadinessHint(check); + + return h('div', { className: `service-status-item ${status} service-status-readiness-item` }, + h('span', { className: 'status-icon' }, this.getStatusIcon(status)), + h('div', { className: 'service-status-readiness-copy' }, + h('div', { className: 'status-name' }, check.label), + hint ? h('div', { className: 'service-status-readiness-hint' }, hint) : false, + ), + h('span', { className: `status-badge ${status}` }, + check.ready ? t('components.serviceStatus.ok').toUpperCase() : t('components.serviceStatus.outage').toUpperCase(), + ), + ); + } + + private getReadinessHint(check: DesktopReadinessCheck): string | null { + if (check.ready) return null; + + switch (check.id) { + case 'startup': + return t('components.serviceStatus.backendUnavailable'); + case 'summaries': + return 'Configure Ollama, Groq, or OpenRouter in Settings to restore provider-backed summaries.'; + case 'live-tracking': + return 'Configure AISStream or OpenSky credentials in Settings to restore live vessel and flight tracking.'; + default: + return null; + } + } + private buildSummary(services: ServiceStatus[]): HTMLElement { const operational = services.filter(s => s.status === 'operational').length; const degraded = services.filter(s => s.status === 'degraded').length; @@ -119,7 +202,6 @@ export class ServiceStatusPanel extends Panel { ); } - private buildFilters(): HTMLElement { const categories: CategoryFilter[] = ['all', 'cloud', 'dev', 'comm', 'ai', 'saas']; return h('div', { className: 'service-status-filters' }, diff --git a/src/styles/main.css b/src/styles/main.css index ea036ff6f6..3f05b8539f 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -14710,6 +14710,97 @@ a.prediction-link:hover { margin-top: 8px; } +.service-status-readiness { + display: flex; + flex-direction: column; + gap: 8px; + padding: 8px; + margin-bottom: 8px; + background: var(--darken-medium); + border: 1px solid var(--border); + border-radius: 4px; +} + +.service-status-readiness-title { + font-size: 11px; + font-weight: 700; + color: var(--text); +} + +.service-status-readiness-meta { + font-size: 9px; + color: var(--text-dim); + line-height: 1.4; +} + +.service-status-readiness-warning { + font-size: 9px; + color: var(--yellow); + line-height: 1.4; +} + +.service-status-readiness-list { + display: flex; + flex-direction: column; + gap: 4px; +} + +.service-status-readiness-item { + align-items: flex-start; +} + +.service-status-readiness-copy { + flex: 1; + min-width: 0; +} + +.service-status-readiness-hint { + margin-top: 3px; + font-size: 9px; + line-height: 1.35; + color: var(--text-dim); +} + +.service-status-fallbacks { + display: flex; + flex-direction: column; + gap: 6px; + padding-top: 8px; + border-top: 1px solid var(--border); +} + +.service-status-fallbacks-title { + font-size: 9px; + text-transform: uppercase; + letter-spacing: 0.4px; + color: var(--text-dim); +} + +.service-status-fallbacks-list { + display: flex; + flex-direction: column; + gap: 6px; +} + +.service-status-fallback-item { + padding: 6px 8px; + background: var(--overlay-subtle); + border-radius: 4px; +} + +.service-status-fallback-panel { + font-size: 10px; + font-weight: 600; + color: var(--text); + margin-bottom: 2px; +} + +.service-status-fallback-copy { + font-size: 9px; + line-height: 1.35; + color: var(--text-dim); +} + /* ===== deck.gl Map Styles ===== */ .map-container.deckgl-mode { position: relative;