From bbd96192903f50ba83b3aa401a9a734596c25466 Mon Sep 17 00:00:00 2001
From: Denozordec
Date: Tue, 31 Mar 2026 00:12:28 +0700
Subject: [PATCH] Refactor Svelte components to utilize KpiStatCard and
KpiPanelCard for improved UI consistency
- Replaced Card components with KpiStatCard and KpiPanelCard across various routes to enhance the visual presentation of key performance indicators and data summaries.
- Updated the structure of the dashboard and incident pages to streamline the display of critical information, improving user experience and readability.
- Enhanced loading states and skeleton displays for better feedback during data fetching processes.
---
web/src/lib/components/kpi-panel-card.svelte | 51 ++++
web/src/lib/components/kpi-stat-card.svelte | 50 ++++
web/src/lib/kpi-accents.ts | 40 +++
web/src/routes/+page.svelte | 274 +++++-------------
web/src/routes/incidents/+page.svelte | 51 ++--
web/src/routes/ips/+page.svelte | 25 +-
web/src/routes/live/+page.svelte | 74 ++---
web/src/routes/servers/[alias]/+page.svelte | 89 +++---
.../servers/[alias]/config/+page.svelte | 12 +-
.../servers/[alias]/runtime/+page.svelte | 29 +-
.../servers/[alias]/security/+page.svelte | 12 +-
.../servers/[alias]/update/+page.svelte | 15 +-
.../servers/[alias]/upstreams/+page.svelte | 12 +-
.../routes/servers/[alias]/users/+page.svelte | 12 +-
web/src/routes/users/+page.svelte | 16 +-
web/src/routes/users/[username]/+page.svelte | 63 ++--
16 files changed, 441 insertions(+), 384 deletions(-)
create mode 100644 web/src/lib/components/kpi-panel-card.svelte
create mode 100644 web/src/lib/components/kpi-stat-card.svelte
create mode 100644 web/src/lib/kpi-accents.ts
diff --git a/web/src/lib/components/kpi-panel-card.svelte b/web/src/lib/components/kpi-panel-card.svelte
new file mode 100644
index 0000000..17f760a
--- /dev/null
+++ b/web/src/lib/components/kpi-panel-card.svelte
@@ -0,0 +1,51 @@
+
+
+
+
+
+ {@render children()}
+
+
diff --git a/web/src/lib/components/kpi-stat-card.svelte b/web/src/lib/components/kpi-stat-card.svelte
new file mode 100644
index 0000000..5372636
--- /dev/null
+++ b/web/src/lib/components/kpi-stat-card.svelte
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+ {@render icon()}
+
+
+ {label}
+
+ {@render children()}
+
+
+
+
+ {#if footer}
+ {footer}
+ {/if}
+
diff --git a/web/src/lib/kpi-accents.ts b/web/src/lib/kpi-accents.ts
new file mode 100644
index 0000000..61ca43a
--- /dev/null
+++ b/web/src/lib/kpi-accents.ts
@@ -0,0 +1,40 @@
+/** Семантика KPI: граница + иконка + при необходимости цвет числа (WCAG: акцент не только цветом — иконка + подпись). */
+export 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;
+
+export type KpiAccentKey = keyof typeof kpiAccent;
diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte
index f8db772..00e9ec9 100644
--- a/web/src/routes/+page.svelte
+++ b/web/src/routes/+page.svelte
@@ -34,47 +34,10 @@
import ActivityIcon from '@lucide/svelte/icons/activity';
import LockKeyholeIcon from '@lucide/svelte/icons/lock-keyhole';
import InfoIcon from '@lucide/svelte/icons/info';
-
- /** Семантика дашборда: граница + иконка + при необходимости цвет числа (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;
+ import type { KpiAccentKey } from '$lib/kpi-accents.js';
+ import KpiStatCard from '$lib/components/kpi-stat-card.svelte';
+ import KpiPanelCard from '$lib/components/kpi-panel-card.svelte';
+ import ChartNoAxesCombinedIcon from '@lucide/svelte/icons/chart-no-axes-combined';
let loading = $state(true);
let err = $state(null);
@@ -457,11 +420,19 @@
{#snippet skeleton()}
{#each Array.from({ length: 6 }) as _, i (i)}
-
+
-
-
+
+
+
+
{/each}
@@ -470,147 +441,54 @@
{#snippet children()}
{#if summary}
-
-
-
-
-
-
-
- Ноды
-
- {fleet?.servers_all_ok ?? summary.servers_ok}/{fleet?.servers_total ??
- summary.servers_total}
-
-
-
-
- успешный опрос
-
-
-
-
-
-
-
-
- Соединения (флот)
- {summary.fleet_total_connections}
-
-
-
- текущие по stats/users
-
-
-
-
-
-
-
-
- Bad connections
-
- {totalBad}
-
-
-
-
- сумма по нодам (stats/summary)
-
-
-
-
-
-
-
-
- Уникальные IP
- {uniqueIpCount}
-
-
-
- по снимку unique-ips
-
-
-
-
-
-
- Трафик флота
- {formatMiB(summary.fleet_total_megabytes)}
-
-
-
- сумма MiB
-
-
-
-
-
-
-
-
- Read-only API
-
- {readOnlyNodes}
-
-
-
-
- нод с read_only
-
+
+ {#snippet icon()}
+
+ {/snippet}
+ {fleet?.servers_all_ok ?? summary.servers_ok}/{fleet?.servers_total ?? summary.servers_total}
+
+
+ {#snippet icon()}
+
+ {/snippet}
+ {summary.fleet_total_connections}
+
+
+ {#snippet icon()}
+
+ {/snippet}
+ {totalBad}
+
+
+ {#snippet icon()}
+
+ {/snippet}
+ {uniqueIpCount}
+
+
+ {#snippet icon()}
+
+ {/snippet}
+ {formatMiB(summary.fleet_total_megabytes)}
+
+
+ {#snippet icon()}
+
+ {/snippet}
+ {readOnlyNodes}
+
-
-
- Активные IP (агрегат)
- С GeoIP при включённой базе на шлюзе
-
-
-
+
+ {#snippet icon()}
+
+ {/snippet}
+
@@ -670,16 +548,13 @@
{/if}
-
-
-
+
+
-
-
- По нодам
- Health, версия, аптайм, трафик ноды
-
-
+
+ {#snippet icon()}
+
+ {/snippet}
@@ -737,14 +612,12 @@
{/each}
-
-
+
-
-
- Топ пользователей по трафику
-
-
+
+ {#snippet icon()}
+
+ {/snippet}
@@ -769,8 +642,7 @@
{/each}
-
-
+
{/if}
{/snippet}
diff --git a/web/src/routes/incidents/+page.svelte b/web/src/routes/incidents/+page.svelte
index 38c9c7e..f26dbb5 100644
--- a/web/src/routes/incidents/+page.svelte
+++ b/web/src/routes/incidents/+page.svelte
@@ -10,6 +10,12 @@
type IncidentsData
} from '$lib/api/client.js';
import * as Card from '$lib/components/ui/card/index.js';
+ import KpiStatCard from '$lib/components/kpi-stat-card.svelte';
+ import KpiPanelCard from '$lib/components/kpi-panel-card.svelte';
+ import CircleAlertIcon from '@lucide/svelte/icons/circle-alert';
+ import TriangleAlertIcon from '@lucide/svelte/icons/triangle-alert';
+ import InfoSmallIcon from '@lucide/svelte/icons/info';
+ import ClipboardListIcon from '@lucide/svelte/icons/clipboard-list';
import * as Table from '$lib/components/ui/table/index.js';
import { Badge } from '$lib/components/ui/badge/index.js';
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
@@ -271,28 +277,30 @@
{/snippet}
{#snippet children()}
-
-
- Critical
- {data?.critical_total ?? 0}
-
-
-
-
- Warning
- {data?.warning_total ?? 0}
-
-
-
-
- Info
- {data?.info_total ?? 0}
-
-
+
+ {#snippet icon()}
+
+ {/snippet}
+ {data?.critical_total ?? 0}
+
+
+ {#snippet icon()}
+
+ {/snippet}
+ {data?.warning_total ?? 0}
+
+
+ {#snippet icon()}
+
+ {/snippet}
+ {data?.info_total ?? 0}
+
-
-
+
+ {#snippet icon()}
+
+ {/snippet}
@@ -400,7 +408,6 @@
{/if}
-
-
+
{/snippet}
diff --git a/web/src/routes/ips/+page.svelte b/web/src/routes/ips/+page.svelte
index 62c8d3d..19c7078 100644
--- a/web/src/routes/ips/+page.svelte
+++ b/web/src/routes/ips/+page.svelte
@@ -8,6 +8,9 @@
import 'leaflet/dist/leaflet.css';
import { flagEmoji } from '$lib/format.js';
import * as Card from '$lib/components/ui/card/index.js';
+ import KpiPanelCard from '$lib/components/kpi-panel-card.svelte';
+ import MapIcon from '@lucide/svelte/icons/map';
+ import GlobeIcon from '@lucide/svelte/icons/globe';
import { Alert, AlertDescription, AlertTitle } from '$lib/components/ui/alert/index.js';
import { Button } from '$lib/components/ui/button/index.js';
import { Badge } from '$lib/components/ui/badge/index.js';
@@ -502,12 +505,10 @@
{/snippet}
{#snippet children()}
{#if geo}
-
-
- Карта подключений
- GeoIP координаты из снимка unique-ips
-
-
+
+ {#snippet icon()}
+
+ {/snippet}
{#if mapPointsCount > 0}
@@ -519,8 +520,7 @@
{mapStatus}
{/if}
-
-
+
{:else}
GeoIP выключен
@@ -528,8 +528,10 @@
{/if}
-
-
+
+ {#snippet icon()}
+
+ {/snippet}
@@ -588,8 +590,7 @@
{/each}
-
-
+
{/snippet}
diff --git a/web/src/routes/live/+page.svelte b/web/src/routes/live/+page.svelte
index 4e14eec..e05514f 100644
--- a/web/src/routes/live/+page.svelte
+++ b/web/src/routes/live/+page.svelte
@@ -9,7 +9,12 @@
type IncidentSeverity,
type IncidentStatus
} from '$lib/api/client.js';
- import * as Card from '$lib/components/ui/card/index.js';
+ import KpiStatCard from '$lib/components/kpi-stat-card.svelte';
+ import KpiPanelCard from '$lib/components/kpi-panel-card.svelte';
+ import ActivityIcon from '@lucide/svelte/icons/activity';
+ import LayersIcon from '@lucide/svelte/icons/layers';
+ import ClockIcon from '@lucide/svelte/icons/clock';
+ import BellIcon from '@lucide/svelte/icons/bell';
import { Alert, AlertDescription, AlertTitle } from '$lib/components/ui/alert/index.js';
import { Badge } from '$lib/components/ui/badge/index.js';
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
@@ -302,38 +307,42 @@
{/if}
-
-
- Общий статус
-
-
- {snapshot?.status ?? 'healthy'}
-
-
-
-
-
-
- Partial
- {snapshot?.partial ? 'yes' : 'no'}
-
-
-
-
- Timestamp
-
- {snapshot?.timestamp ? new Date(snapshot.timestamp).toLocaleString() : '—'}
-
-
-
+
+ {#snippet icon()}
+
+ {/snippet}
+
+ {snapshot?.status ?? 'healthy'}
+
+
+
+ {#snippet icon()}
+
+ {/snippet}
+ {snapshot?.partial ? 'yes' : 'no'}
+
+
+ {#snippet icon()}
+
+ {/snippet}
+ {snapshot?.timestamp ? new Date(snapshot.timestamp).toLocaleString() : '—'}
+
-
-
- Последние incidents из snapshot
- Всего: {snapshot?.counts?.total ?? snapshot?.incidents?.length ?? 0}
-
-
+
+ {#snippet icon()}
+
+ {/snippet}
@@ -381,5 +390,4 @@
{/if}
-
-
+
diff --git a/web/src/routes/servers/[alias]/+page.svelte b/web/src/routes/servers/[alias]/+page.svelte
index d158369..6328830 100644
--- a/web/src/routes/servers/[alias]/+page.svelte
+++ b/web/src/routes/servers/[alias]/+page.svelte
@@ -8,6 +8,13 @@
import type { HealthData, StatsSummaryData, SystemInfoData } from '$lib/api/telemt-v1.js';
import { formatDurationSeconds, formatMiB } from '$lib/format.js';
import * as Card from '$lib/components/ui/card/index.js';
+ import KpiStatCard from '$lib/components/kpi-stat-card.svelte';
+ import KpiPanelCard from '$lib/components/kpi-panel-card.svelte';
+ import ClockIcon from '@lucide/svelte/icons/clock';
+ import PlugIcon from '@lucide/svelte/icons/plug';
+ import UnplugIcon from '@lucide/svelte/icons/unplug';
+ import UsersIcon from '@lucide/svelte/icons/users';
+ import CpuIcon from '@lucide/svelte/icons/cpu';
import { Alert, AlertDescription, AlertTitle } from '$lib/components/ui/alert/index.js';
import { Badge } from '$lib/components/ui/badge/index.js';
import { Button } from '$lib/components/ui/button/index.js';
@@ -56,6 +63,12 @@
let hasStaleData = $derived(err != null && (health != null || summary != null));
let showSkeleton = $derived(loading && !err && health == null && summary == null);
+
+ let badConnAccent = $derived.by((): 'danger' | 'success' => {
+ const n = summary?.connections_bad_total;
+ if (typeof n !== 'number' || !Number.isFinite(n) || n === 0) return 'success';
+ return 'danger';
+ });
@@ -79,11 +92,19 @@
{#snippet skeleton()}
{#each Array.from({ length: 4 }) as _, i (i)}
-
+
-
-
+
+
+
+
{/each}
@@ -100,40 +121,37 @@
{/if}
-
-
- Аптайм
-
- {sys ? formatDurationSeconds(sys.uptime_seconds) : '—'}
-
-
-
-
-
- Соединения (всего)
- {summary?.connections_total ?? '—'}
-
-
-
-
- Bad connections
- {summary?.connections_bad_total ?? '—'}
-
-
-
-
- Пользователей в конфиге
- {summary?.configured_users ?? '—'}
-
-
+
+ {#snippet icon()}
+
+ {/snippet}
+ {sys ? formatDurationSeconds(sys.uptime_seconds) : '—'}
+
+
+ {#snippet icon()}
+
+ {/snippet}
+ {summary?.connections_total ?? '—'}
+
+
+ {#snippet icon()}
+
+ {/snippet}
+ {summary?.connections_bad_total ?? '—'}
+
+
+ {#snippet icon()}
+
+ {/snippet}
+ {summary?.configured_users ?? '—'}
+
{#if sys}
-
-
- Система
-
-
+
+ {#snippet icon()}
+
+ {/snippet}
version {sys.version}
OS / arch {sys.target_os} / {sys.target_arch}
build {sys.build_profile}
@@ -144,8 +162,7 @@
hash {sys.config_hash}
reload count {sys.config_reload_count}
-
-
+
{/if}
{/snippet}
diff --git a/web/src/routes/servers/[alias]/config/+page.svelte b/web/src/routes/servers/[alias]/config/+page.svelte
index edfde15..b13f54b 100644
--- a/web/src/routes/servers/[alias]/config/+page.svelte
+++ b/web/src/routes/servers/[alias]/config/+page.svelte
@@ -1,6 +1,7 @@
@@ -12,8 +13,10 @@
— правьте config.toml на сервере.
-
-
+
+ {#snippet icon()}
+
+ {/snippet}
См. docs/API.md
(Telemt Control API).
-
-
+
diff --git a/web/src/routes/servers/[alias]/runtime/+page.svelte b/web/src/routes/servers/[alias]/runtime/+page.svelte
index b709b26..eeef097 100644
--- a/web/src/routes/servers/[alias]/runtime/+page.svelte
+++ b/web/src/routes/servers/[alias]/runtime/+page.svelte
@@ -2,7 +2,9 @@
import { page } from '$app/state';
import { ApiError, fetchTelemt } from '$lib/api/client.js';
import type { DcStatusData, RuntimeGatesData } from '$lib/api/telemt-v1.js';
- import * as Card from '$lib/components/ui/card/index.js';
+ import KpiPanelCard from '$lib/components/kpi-panel-card.svelte';
+ import BuildingIcon from '@lucide/svelte/icons/building-2';
+ import BracesIcon from '@lucide/svelte/icons/braces';
import { Alert, AlertDescription, AlertTitle } from '$lib/components/ui/alert/index.js';
import { Badge } from '$lib/components/ui/badge/index.js';
import { Button } from '$lib/components/ui/button/index.js';
@@ -81,11 +83,10 @@
{#if dcs && dcs.middle_proxy_enabled && dcs.dcs}
-
-
- Datacenter status
-
-
+
+ {#snippet icon()}
+
+ {/snippet}
@@ -110,25 +111,21 @@
{/each}
-
-
+
{:else if dcs?.reason}
DC status: {dcs.reason}
{/if}
-
-
- ME pool state
- Сырой JSON
-
-
+
+ {#snippet icon()}
+
+ {/snippet}
{JSON.stringify(
pool,
null,
2
)}
-
-
+
{/if}
diff --git a/web/src/routes/servers/[alias]/security/+page.svelte b/web/src/routes/servers/[alias]/security/+page.svelte
index bd7bc72..1f4d0e0 100644
--- a/web/src/routes/servers/[alias]/security/+page.svelte
+++ b/web/src/routes/servers/[alias]/security/+page.svelte
@@ -1,7 +1,8 @@