feat: global Docker health indicator and graceful degradation

- GET /api/health endpoint returning Docker connectivity status
- Sidebar shows Docker connection dot (green=connected, red=disconnected)
- Stale scanner returns store-only results when Docker is unavailable
- Polls health every 30s
This commit is contained in:
2026-03-30 13:43:33 +03:00
parent b57b164be0
commit 37cfa090ac
15 changed files with 317 additions and 277 deletions
+38 -10
View File
@@ -8,6 +8,8 @@
import LocaleSwitcher from '$lib/components/LocaleSwitcher.svelte';
import { IconDashboard, IconProjects, IconDeploy, IconProxies, IconEvents, IconSettings, IconMenu, IconX, IconLogout } from '$lib/components/icons';
import { connectGlobalEvents, type SSEConnection } from '$lib/sse';
import { isAuthenticated, clearAuth } from '$lib/auth';
import * as api from '$lib/api';
import { instanceStatusStore } from '$lib/stores/instance-status';
import { resolvedTheme, applyTheme } from '$lib/stores/theme';
import { t } from '$lib/i18n';
@@ -34,6 +36,8 @@
let sseConnection: SSEConnection | null = null;
let sidebarOpen = $state(false);
let dockerConnected = $state<boolean | null>(null);
let healthInterval: ReturnType<typeof setInterval> | null = null;
// Hide sidebar and chrome on the login page.
const isLoginPage = $derived($page.url.pathname === '/login');
@@ -59,28 +63,41 @@
});
function logout() {
if (typeof localStorage !== 'undefined') {
localStorage.removeItem('auth_token');
}
clearAuth();
sseConnection?.close();
sseConnection = null;
window.location.href = '/login';
}
onMount(() => {
sseConnection = connectGlobalEvents({
onInstanceStatus(payload) {
instanceStatusStore.update(payload);
},
onDeployStatus(payload) {
instanceStatusStore.notifyDeploy(payload);
if (isAuthenticated()) {
sseConnection = connectGlobalEvents({
onInstanceStatus(payload) {
instanceStatusStore.update(payload);
},
onDeployStatus(payload) {
instanceStatusStore.notifyDeploy(payload);
}
});
// Poll Docker health every 30s.
async function checkHealth() {
try {
const h = await api.getHealth();
dockerConnected = h.docker;
} catch {
dockerConnected = null;
}
}
});
checkHealth();
healthInterval = setInterval(checkHealth, 30_000);
}
});
onDestroy(() => {
sseConnection?.close();
sseConnection = null;
if (healthInterval) clearInterval(healthInterval);
});
</script>
@@ -156,6 +173,17 @@
<!-- Footer controls -->
<div class="space-y-3 border-t border-[var(--border-primary)] px-4 py-3">
{#if dockerConnected !== null}
<div class="flex items-center gap-2 rounded-md px-2 py-1.5 text-xs {dockerConnected ? 'text-emerald-600' : 'text-red-500'}">
<span class="relative flex h-2 w-2">
{#if dockerConnected}
<span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-50"></span>
{/if}
<span class="relative inline-flex h-2 w-2 rounded-full {dockerConnected ? 'bg-emerald-500' : 'bg-red-500'}"></span>
</span>
Docker {dockerConnected ? $t('health.connected') : $t('health.disconnected')}
</div>
{/if}
<div class="flex items-center justify-between">
<ThemeToggle />
<LocaleSwitcher />