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 icon()} +
+
+ {title} + {#if description} + {description} + {/if} +
+
+
+ + {@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 icon()} + + + {#snippet icon()} + + + {#snippet icon()} + + + {#snippet icon()} + + + {#snippet icon()} +
- - - Активные IP (агрегат) - С GeoIP при включённой базе на шлюзе - - -
+ + {#snippet icon()} +
+ - - - По нодам - Health, версия, аптайм, трафик ноды - - + + {#snippet icon()} + - + - - - Топ пользователей по трафику - - + + {#snippet icon()} + - + {/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 icon()} + + + {#snippet icon()} +
- - + + {#snippet icon()} + - + {/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()} + - + {:else} GeoIP выключен @@ -528,8 +528,10 @@ {/if} - - + + {#snippet icon()} + - + {/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 icon()} + + + {#snippet icon()} +
- - - Последние incidents из snapshot - Всего: {snapshot?.counts?.total ?? snapshot?.incidents?.length ?? 0} - - + + {#snippet icon()} + - + 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 icon()} + + + {#snippet icon()} + + + {#snippet icon()} +
{#if sys} - - - Система - - + + {#snippet icon()} +
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()} + - + 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()} + - + {:else if dcs?.reason} DC status: {dcs.reason} {/if} - - - ME pool state - Сырой JSON - - + + {#snippet icon()} + - + {/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 @@