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:
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user