Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 83 additions & 1 deletion src/components/ServiceStatusPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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' },
Expand All @@ -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;
Expand All @@ -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' },
Expand Down
91 changes: 91 additions & 0 deletions src/styles/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading