From fe718b7e21ca13ea7b6f45ae90fcaf025be1ac50 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Mon, 30 Mar 2026 12:09:34 +0700 Subject: [PATCH] Enhance dashboard KPI components with dynamic styling and icons - Introduced a new `kpiAccent` configuration to manage dynamic styling for various KPI components based on their status (success, warning, danger, etc.). - Added multiple icons to represent different metrics visually, improving user experience and accessibility. - Updated the dashboard layout to utilize the new styling and icons, ensuring a consistent and informative presentation of server health, connections, and traffic metrics. - Implemented derived states for health and connection metrics to reflect real-time data changes effectively. --- web/src/routes/+page.svelte | 198 ++++++++++++++++++++++++++++++++---- 1 file changed, 177 insertions(+), 21 deletions(-) diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index c2247cc..8c3b4ba 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -15,8 +15,56 @@ import { Alert, AlertDescription, AlertTitle } from '$lib/components/ui/alert/index.js'; import * as Table from '$lib/components/ui/table/index.js'; import { Button } from '$lib/components/ui/button/index.js'; + import { cn } from '$lib/utils.js'; import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw'; import AlertTriangleIcon from '@lucide/svelte/icons/triangle-alert'; + import ServerIcon from '@lucide/svelte/icons/server'; + import UsersIcon from '@lucide/svelte/icons/users'; + import UnplugIcon from '@lucide/svelte/icons/unplug'; + import GlobeIcon from '@lucide/svelte/icons/globe'; + import ActivityIcon from '@lucide/svelte/icons/activity'; + import LockKeyholeIcon from '@lucide/svelte/icons/lock-keyhole'; + + /** Семантика дашборда: граница + иконка + при необходимости цвет числа (WCAG: акцент не только цветом — иконка + подпись). */ + const kpiAccent = { + success: { + border: 'border-l-emerald-500', + iconWrap: 'bg-emerald-500/15 text-emerald-400', + value: '' + }, + warning: { + border: 'border-l-amber-500', + iconWrap: 'bg-amber-500/15 text-amber-400', + value: 'text-amber-400' + }, + danger: { + border: 'border-l-red-500', + iconWrap: 'bg-red-500/15 text-red-400', + value: 'text-red-400' + }, + info: { + border: 'border-l-sky-500', + iconWrap: 'bg-sky-500/15 text-sky-400', + value: '' + }, + violet: { + border: 'border-l-violet-500', + iconWrap: 'bg-violet-500/15 text-violet-400', + value: '' + }, + teal: { + border: 'border-l-teal-500', + iconWrap: 'bg-teal-500/15 text-teal-400', + value: '' + }, + neutral: { + border: 'border-l-muted-foreground/35', + iconWrap: 'bg-muted text-muted-foreground', + value: '' + } + } as const; + + type KpiAccentKey = keyof typeof kpiAccent; let loading = $state(true); let err = $state(null); @@ -120,6 +168,26 @@ return n; }); + let nodesHealthAccent = $derived.by((): KpiAccentKey => { + const t = (fleet?.servers_total ?? summary?.servers_total) ?? 0; + const ok = (fleet?.servers_all_ok ?? summary?.servers_ok) ?? 0; + if (t === 0) return 'neutral'; + if (ok === t) return 'success'; + if (ok === 0) return 'danger'; + return 'warning'; + }); + + /** Пороги условные: 0 — норма; дальше — внимание и «сильное» внимание к метрике bad. */ + let badConnectionsAccent = $derived.by((): KpiAccentKey => { + if (totalBad === 0) return 'success'; + if (totalBad < 10_000) return 'warning'; + return 'danger'; + }); + + let readOnlyAccent = $derived.by((): KpiAccentKey => + readOnlyNodes === 0 ? 'success' : 'warning' + ); + function chipLabel(ip: components['schemas']['IPAssignments']): string { const cc = ip.country_code?.trim(); const flag = flagEmoji(cc ?? undefined); @@ -165,47 +233,135 @@

Загрузка…

{:else if summary}
- + - Ноды - - {fleet?.servers_all_ok ?? summary.servers_ok}/{fleet?.servers_total ?? summary.servers_total} - +
+
+
+
+ Ноды + + {fleet?.servers_all_ok ?? summary.servers_ok}/{fleet?.servers_total ?? + summary.servers_total} + +
+
успешный опрос
- + - Соединения (флот) - {summary.fleet_total_connections} +
+
+
+
+ Соединения (флот) + {summary.fleet_total_connections} +
+
текущие по stats/users
- + - Bad connections - {totalBad} +
+
+
+
+ Bad connections + + {totalBad} + +
+
сумма по нодам (stats/summary)
- + - Уникальные IP - {uniqueIpCount} +
+
+
+
+ Уникальные IP + {uniqueIpCount} +
+
по снимку unique-ips
- + - Трафик флота - {formatMiB(summary.fleet_total_megabytes)} +
+
+
+
+ Трафик флота + {formatMiB(summary.fleet_total_megabytes)} +
+
сумма MiB
- + - Read-only API - {readOnlyNodes} +
+
+
+
+ Read-only API + + {readOnlyNodes} + +
+
нод с read_only
@@ -280,7 +436,7 @@ {@const st = nodeStats[srv.alias]} {#if !st} - {:else if 'ok' in st && st.ok === false} + {:else if st && 'ok' in st && !st.ok && 'error' in st} {:else if st && 'bad' in st && st.ok} {st.bad ?? 0}