7c57c740b4
Add container monitoring and notification system: - Docker Stats API: real-time CPU/memory for running containers - Webhook notifications for errors (deploy failures, stale, proxy unhealthy) - Event log auto-pruning (daily, 30-day retention) - ContainerStats component with auto-polling progress bars - SystemHealthCard dashboard widget with running/proxy/error counts - Full EN/RU i18n for stats and system health
171 lines
6.2 KiB
Svelte
171 lines
6.2 KiB
Svelte
<script lang="ts">
|
|
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 SystemHealthCard from '$lib/components/SystemHealthCard.svelte';
|
|
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('');
|
|
|
|
async function loadDashboard() {
|
|
loading = true;
|
|
error = '';
|
|
try {
|
|
projects = await api.listProjects();
|
|
|
|
const detailPromises = projects.map(async (p) => {
|
|
try {
|
|
const detail = await api.getProject(p.id);
|
|
const stages = detail.stages ?? [];
|
|
const stageInstances = await Promise.all(
|
|
stages.map((s) => api.listInstances(p.id, s.id))
|
|
);
|
|
return { projectId: p.id, instances: stageInstances.flat() };
|
|
} catch {
|
|
return { projectId: p.id, instances: [] };
|
|
}
|
|
});
|
|
|
|
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 {
|
|
loading = false;
|
|
}
|
|
}
|
|
|
|
$effect(() => {
|
|
loadDashboard();
|
|
});
|
|
|
|
const totalProjects = $derived(projects.length);
|
|
const totalRunning = $derived(
|
|
Object.values(instancesByProject)
|
|
.flat()
|
|
.filter((i) => i.status === 'running').length
|
|
);
|
|
const totalFailed = $derived(
|
|
Object.values(instancesByProject)
|
|
.flat()
|
|
.filter((i) => i.status === 'failed').length
|
|
);
|
|
const totalStale = $derived(staleContainers.length);
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>{$t('dashboard.title')} - {$t('app.name')}</title>
|
|
</svelte:head>
|
|
|
|
<div class="space-y-6">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between">
|
|
<h1 class="text-2xl font-bold text-[var(--text-primary)]">{$t('dashboard.title')}</h1>
|
|
<a
|
|
href="/deploy"
|
|
class="inline-flex items-center gap-2 rounded-lg bg-[var(--color-brand-600)] px-4 py-2.5 text-sm font-medium text-white shadow-sm transition-all duration-150 hover:bg-[var(--color-brand-700)] active:animate-press"
|
|
>
|
|
<IconDeploy size={16} />
|
|
{$t('dashboard.quickDeploy')}
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Stats cards -->
|
|
<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} />
|
|
</div>
|
|
<div>
|
|
<p class="text-sm text-[var(--text-secondary)]">{$t('dashboard.totalProjects')}</p>
|
|
<p class="mt-0.5 text-2xl font-bold text-[var(--text-primary)]">{totalProjects}</p>
|
|
</div>
|
|
</div>
|
|
<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-emerald-50 text-emerald-600">
|
|
<IconServer size={24} />
|
|
</div>
|
|
<div>
|
|
<p class="text-sm text-[var(--text-secondary)]">{$t('dashboard.runningInstances')}</p>
|
|
<p class="mt-0.5 text-2xl font-bold text-emerald-600">{totalRunning}</p>
|
|
</div>
|
|
</div>
|
|
<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 {totalFailed > 0 ? 'bg-red-50 text-red-600' : 'bg-gray-50 text-gray-400'}">
|
|
<IconAlert size={24} />
|
|
</div>
|
|
<div>
|
|
<p class="text-sm text-[var(--text-secondary)]">{$t('dashboard.failedInstances')}</p>
|
|
<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>
|
|
|
|
<!-- System health summary -->
|
|
<SystemHealthCard />
|
|
|
|
<!-- Project cards -->
|
|
<div>
|
|
<h2 class="text-lg font-semibold text-[var(--text-primary)]">{$t('dashboard.projects')}</h2>
|
|
|
|
{#if loading}
|
|
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
{#each Array(3) as _}
|
|
<SkeletonCard />
|
|
{/each}
|
|
</div>
|
|
{:else if error}
|
|
<div class="mt-4 rounded-xl border border-[var(--color-danger-light)] bg-[var(--color-danger-light)] p-4">
|
|
<p class="text-sm text-[var(--color-danger)]">{error}</p>
|
|
<button
|
|
type="button"
|
|
class="mt-2 text-sm font-medium text-[var(--color-danger)] underline hover:no-underline"
|
|
onclick={loadDashboard}
|
|
>
|
|
{$t('dashboard.retry')}
|
|
</button>
|
|
</div>
|
|
{:else if projects.length === 0}
|
|
<div class="mt-4">
|
|
<EmptyState
|
|
title={$t('empty.noProjects')}
|
|
description={$t('empty.noProjectsDesc')}
|
|
actionLabel={$t('empty.createProject')}
|
|
actionHref="/projects"
|
|
icon="projects"
|
|
/>
|
|
</div>
|
|
{:else}
|
|
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
{#each projects as project (project.id)}
|
|
<ProjectCard {project} instances={instancesByProject[project.id] ?? []} />
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|