feat: Docker diagnostic hints on disconnection
- Classify Docker errors into categories (socket_not_found, connection_refused, permission_denied, timeout, tls_error) with platform-specific hints - Enrich GET /api/health with structured diagnostics (category, hints, platform) - Expandable hints panel in sidebar when Docker is disconnected - "Retry now" button for immediate re-check - Collapsible raw error details for advanced users
This commit is contained in:
+3
-2
@@ -3,6 +3,7 @@ import type {
|
||||
ContainerStats,
|
||||
Deploy,
|
||||
DeployLog,
|
||||
DockerHealth,
|
||||
EventLogEntry,
|
||||
EventLogStats,
|
||||
InspectResult,
|
||||
@@ -267,8 +268,8 @@ export function listNpmCertificates(): Promise<NpmCertificate[]> {
|
||||
|
||||
// ── Health ──────────────────────────────────────────────────────────
|
||||
|
||||
export function getHealth(): Promise<{ docker: boolean }> {
|
||||
return get<{ docker: boolean }>('/api/health');
|
||||
export function getHealth(): Promise<{ docker: DockerHealth }> {
|
||||
return get<{ docker: DockerHealth }>('/api/health');
|
||||
}
|
||||
|
||||
// ── Auth ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
},
|
||||
"health": {
|
||||
"connected": "connected",
|
||||
"disconnected": "disconnected"
|
||||
"disconnected": "disconnected",
|
||||
"rawError": "Technical details",
|
||||
"retryNow": "Retry now"
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "Dashboard",
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
},
|
||||
"health": {
|
||||
"connected": "подключён",
|
||||
"disconnected": "отключён"
|
||||
"disconnected": "отключён",
|
||||
"rawError": "Технические детали",
|
||||
"retryNow": "Проверить сейчас"
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "Панель",
|
||||
|
||||
@@ -172,6 +172,16 @@ export interface Volume {
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/** Docker daemon health check result. */
|
||||
export interface DockerHealth {
|
||||
connected: boolean;
|
||||
error?: string;
|
||||
category?: string;
|
||||
hints?: string[];
|
||||
platform?: string;
|
||||
checked_at?: string;
|
||||
}
|
||||
|
||||
/** A persistent event log entry. */
|
||||
export interface EventLogEntry {
|
||||
id: number;
|
||||
|
||||
@@ -34,11 +34,16 @@
|
||||
return pathname.startsWith(href);
|
||||
}
|
||||
|
||||
import type { DockerHealth } from '$lib/types';
|
||||
|
||||
let sseConnection: SSEConnection | null = null;
|
||||
let sidebarOpen = $state(false);
|
||||
let dockerConnected = $state<boolean | null>(null);
|
||||
let dockerHealth = $state<DockerHealth | null>(null);
|
||||
let healthChecked = $state(false);
|
||||
let healthInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let hintsExpanded = $state(false);
|
||||
|
||||
const dockerConnected = $derived(dockerHealth?.connected ?? false);
|
||||
|
||||
// Hide sidebar and chrome on the login page.
|
||||
const isLoginPage = $derived($page.url.pathname === '/login');
|
||||
@@ -91,9 +96,9 @@
|
||||
async function checkHealth() {
|
||||
try {
|
||||
const h = await api.getHealth();
|
||||
dockerConnected = h.docker;
|
||||
dockerHealth = h.docker;
|
||||
} catch {
|
||||
dockerConnected = false;
|
||||
dockerHealth = { connected: false };
|
||||
}
|
||||
healthChecked = true;
|
||||
}
|
||||
@@ -181,14 +186,56 @@
|
||||
<!-- Footer controls -->
|
||||
<div class="space-y-3 border-t border-[var(--border-primary)] px-4 py-3">
|
||||
{#if healthChecked}
|
||||
<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>
|
||||
<div class="rounded-md {dockerConnected ? '' : 'bg-red-50 dark:bg-red-950/30'}">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-2 px-2 py-1.5 text-xs {dockerConnected ? 'text-emerald-600' : 'text-red-500 cursor-pointer'}"
|
||||
onclick={() => { if (!dockerConnected) hintsExpanded = !hintsExpanded; }}
|
||||
disabled={dockerConnected}
|
||||
>
|
||||
<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>
|
||||
<span class="flex-1 text-left">Docker {dockerConnected ? $t('health.connected') : $t('health.disconnected')}</span>
|
||||
{#if !dockerConnected}
|
||||
<svg class="h-3 w-3 transition-transform {hintsExpanded ? 'rotate-180' : ''}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
{/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')}
|
||||
</button>
|
||||
{#if !dockerConnected && hintsExpanded && dockerHealth?.hints?.length}
|
||||
<div class="px-2 pb-2">
|
||||
<ul class="space-y-1 text-[11px] text-red-600 dark:text-red-400">
|
||||
{#each dockerHealth.hints as hint}
|
||||
<li class="flex gap-1.5">
|
||||
<span class="mt-0.5 shrink-0">•</span>
|
||||
<span>{hint}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{#if dockerHealth.error}
|
||||
<details class="mt-1.5">
|
||||
<summary class="text-[10px] text-[var(--text-tertiary)] cursor-pointer">{$t('health.rawError')}</summary>
|
||||
<code class="mt-1 block text-[10px] text-[var(--text-tertiary)] break-all">{dockerHealth.error}</code>
|
||||
</details>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="mt-2 w-full rounded border border-red-300 dark:border-red-700 px-2 py-1 text-[11px] font-medium text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/30 transition-colors"
|
||||
onclick={async () => {
|
||||
try {
|
||||
const h = await api.getHealth();
|
||||
dockerHealth = h.docker;
|
||||
} catch {
|
||||
dockerHealth = { connected: false };
|
||||
}
|
||||
}}
|
||||
>
|
||||
{$t('health.retryNow')}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex items-center justify-between">
|
||||
|
||||
Reference in New Issue
Block a user