feat: registry health indicator

Show colored dot next to each registry name:
- Yellow (pulsing): checking connectivity
- Green: connected and reachable
- Red: unreachable or auth failed

Health is checked automatically when the registries page loads
and updated when "Test Connection" is clicked.
This commit is contained in:
2026-03-28 14:07:06 +03:00
parent 37e251da85
commit 5e366fb2ab
@@ -20,6 +20,7 @@
let formOwner = $state('');
let formSaving = $state(false);
let testingId = $state<string | null>(null);
let healthStatus = $state<Record<string, 'checking' | 'healthy' | 'unhealthy'>>({});
let errors = $state<Record<string, string>>({});
@@ -37,7 +38,11 @@
async function loadRegistryList() {
loading = true;
try { registries = await listRegistries(); } catch (err) { toasts.error(err instanceof Error ? err.message : $t('settingsRegistries.loadFailed')); } finally { loading = false; }
try {
registries = await listRegistries();
// Check health of all registries in the background.
checkAllHealth();
} catch (err) { toasts.error(err instanceof Error ? err.message : $t('settingsRegistries.loadFailed')); } finally { loading = false; }
}
async function handleSave() {
@@ -61,9 +66,26 @@
async function handleTestConnection(registry: Registry) {
testingId = registry.id;
try { await testRegistry(registry.id); toasts.success($t('settingsRegistries.testSuccess', { name: registry.name })); }
catch (err) { toasts.error(err instanceof Error ? err.message : $t('settingsRegistries.testFailed')); }
finally { testingId = null; }
try {
await testRegistry(registry.id);
toasts.success($t('settingsRegistries.testSuccess', { name: registry.name }));
healthStatus[registry.id] = 'healthy';
} catch (err) {
toasts.error(err instanceof Error ? err.message : $t('settingsRegistries.testFailed'));
healthStatus[registry.id] = 'unhealthy';
} finally { testingId = null; }
}
async function checkAllHealth() {
for (const reg of registries) {
healthStatus[reg.id] = 'checking';
try {
await testRegistry(reg.id);
healthStatus[reg.id] = 'healthy';
} catch {
healthStatus[reg.id] = 'unhealthy';
}
}
}
$effect(() => { loadRegistryList(); });
@@ -134,6 +156,13 @@
<div class="flex items-center justify-between rounded-xl border border-[var(--border-primary)] bg-[var(--surface-card)] p-4 shadow-[var(--shadow-sm)] transition-all duration-150 hover:shadow-[var(--shadow-md)]">
<div>
<div class="flex items-center gap-2">
{#if healthStatus[registry.id] === 'checking'}
<span class="h-2.5 w-2.5 shrink-0 rounded-full bg-yellow-400 animate-pulse" title="Checking..."></span>
{:else if healthStatus[registry.id] === 'healthy'}
<span class="h-2.5 w-2.5 shrink-0 rounded-full bg-emerald-500" title="Connected"></span>
{:else if healthStatus[registry.id] === 'unhealthy'}
<span class="h-2.5 w-2.5 shrink-0 rounded-full bg-red-500" title="Unreachable"></span>
{/if}
<h3 class="text-sm font-semibold text-[var(--text-primary)]">{registry.name}</h3>
<span class="rounded-full bg-[var(--surface-card-hover)] px-2 py-0.5 text-xs font-medium text-[var(--text-tertiary)]">{registry.type}</span>
</div>