feat(observability): phases 4-7 - complete frontend UI (big bang)

Add all frontend pages for observability & proxy management:
- Proxy Viewer: /proxies with grouped view, filtering, health indicators
- Proxy Creation: form with live validation, diagnostic hints, edit/delete
- Stale Containers: /containers/stale with dashboard widget, cleanup actions
- Event Log: /events with filters, pagination, real-time SSE streaming
- Navigation: proxies and events links in sidebar
- i18n: full EN/RU translations for all new features
- Settings: stale threshold configuration
This commit is contained in:
2026-03-30 11:29:10 +03:00
parent 7a85441b81
commit 79a40f3d9c
29 changed files with 2237 additions and 12 deletions
+19 -4
View File
@@ -1,14 +1,15 @@
<script lang="ts">
import type { Project, Instance } from '$lib/types';
import type { Project, Instance, StaleContainer } from '$lib/types';
import * as api from '$lib/api';
import ProjectCard from '$lib/components/ProjectCard.svelte';
import SkeletonCard from '$lib/components/SkeletonCard.svelte';
import EmptyState from '$lib/components/EmptyState.svelte';
import { IconDeploy, IconBox, IconServer, IconAlert } from '$lib/components/icons';
import { IconDeploy, IconBox, IconServer, IconAlert, IconClock } from '$lib/components/icons';
import { t } from '$lib/i18n';
let projects = $state<Project[]>([]);
let instancesByProject = $state<Record<string, Instance[]>>({});
let staleContainers = $state<StaleContainer[]>([]);
let loading = $state(true);
let error = $state('');
@@ -31,12 +32,16 @@
}
});
const results = await Promise.all(detailPromises);
const [results, staleResult] = await Promise.all([
Promise.all(detailPromises),
api.fetchStaleContainers().catch(() => [] as StaleContainer[])
]);
const mapped: Record<string, Instance[]> = {};
for (const r of results) {
mapped[r.projectId] = r.instances;
}
instancesByProject = mapped;
staleContainers = staleResult;
} catch (e) {
error = e instanceof Error ? e.message : $t('dashboard.loadFailed');
} finally {
@@ -59,6 +64,7 @@
.flat()
.filter((i) => i.status === 'failed').length
);
const totalStale = $derived(staleContainers.length);
</script>
<svelte:head>
@@ -79,7 +85,7 @@
</div>
<!-- Stats cards -->
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div class="flex items-center gap-4 rounded-xl border border-[var(--border-primary)] bg-[var(--surface-card)] p-5 shadow-[var(--shadow-sm)]">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-[var(--color-brand-50)] text-[var(--color-brand-600)]">
<IconBox size={24} />
@@ -107,6 +113,15 @@
<p class="mt-0.5 text-2xl font-bold {totalFailed > 0 ? 'text-red-600' : 'text-[var(--text-primary)]'}">{totalFailed}</p>
</div>
</div>
<a href="/containers/stale" class="flex items-center gap-4 rounded-xl border border-[var(--border-primary)] bg-[var(--surface-card)] p-5 shadow-[var(--shadow-sm)] transition-colors hover:bg-[var(--surface-card-hover)]">
<div class="flex h-12 w-12 items-center justify-center rounded-xl {totalStale > 0 ? 'bg-amber-50 text-amber-600' : 'bg-gray-50 text-gray-400'}">
<IconClock size={24} />
</div>
<div>
<p class="text-sm text-[var(--text-secondary)]">{$t('dashboard.staleContainers')}</p>
<p class="mt-0.5 text-2xl font-bold {totalStale > 0 ? 'text-amber-600' : 'text-[var(--text-primary)]'}">{totalStale}</p>
</div>
</a>
</div>
<!-- Project cards -->