Compare commits
2
Commits
c3a0a771a3
...
deb12e72c0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
deb12e72c0 | ||
|
|
8da50bd254 |
No files matched your search
@@ -1,8 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import Chart from 'chart.js/auto';
|
import Chart from 'chart.js/auto';
|
||||||
import * as Card from '$lib/components/ui/card/index.js';
|
import KpiPanelCard from '$lib/components/kpi-panel-card.svelte';
|
||||||
import ActivityIcon from '@lucide/svelte/icons/activity';
|
import ActivityIcon from '@lucide/svelte/icons/activity';
|
||||||
|
import ChartPieIcon from '@lucide/svelte/icons/chart-pie';
|
||||||
|
import MemoryStickIcon from '@lucide/svelte/icons/memory-stick';
|
||||||
|
import PlugIcon from '@lucide/svelte/icons/plug';
|
||||||
import NetworkIcon from '@lucide/svelte/icons/network';
|
import NetworkIcon from '@lucide/svelte/icons/network';
|
||||||
import GaugeIcon from '@lucide/svelte/icons/gauge';
|
import GaugeIcon from '@lucide/svelte/icons/gauge';
|
||||||
|
|
||||||
@@ -83,7 +86,7 @@
|
|||||||
type: 'line',
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
labels: histMem.map((_, i) => String(i)),
|
labels: histMem.map((_, i) => String(i)),
|
||||||
datasets: [{ label: 'KB', data: [...histMem], borderColor: '#a78bfa', tension: 0.2, fill: true }]
|
datasets: [{ label: 'KiB', data: [...histMem], borderColor: '#a78bfa', tension: 0.2, fill: true }]
|
||||||
},
|
},
|
||||||
options: lineOpts()
|
options: lineOpts()
|
||||||
});
|
});
|
||||||
@@ -167,58 +170,76 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="grid gap-4 lg:grid-cols-2">
|
<div class="grid gap-4 lg:grid-cols-2">
|
||||||
<Card.Root>
|
<KpiPanelCard
|
||||||
<Card.Header>
|
accent="teal"
|
||||||
<Card.Title class="flex items-center gap-2 text-base">
|
title="Трафик"
|
||||||
<ActivityIcon class="size-4" /> Трафик
|
description="Скорость загрузки и скачивания (история из /traffic)"
|
||||||
</Card.Title>
|
headerClass="pb-2"
|
||||||
</Card.Header>
|
contentClass="h-56 px-4 pb-4 pt-0"
|
||||||
<Card.Content class="h-56">
|
>
|
||||||
<canvas bind:this={elTraffic} class="max-h-full w-full"></canvas>
|
{#snippet icon()}
|
||||||
</Card.Content>
|
<ActivityIcon class="size-5" aria-hidden="true" />
|
||||||
</Card.Root>
|
{/snippet}
|
||||||
<Card.Root>
|
<canvas bind:this={elTraffic} class="max-h-full w-full"></canvas>
|
||||||
<Card.Header>
|
</KpiPanelCard>
|
||||||
<Card.Title class="text-base">Поток (накоплено)</Card.Title>
|
<KpiPanelCard
|
||||||
</Card.Header>
|
accent="info"
|
||||||
<Card.Content class="h-56">
|
title="Поток (накоплено)"
|
||||||
<canvas bind:this={elFlow} class="max-h-full w-full"></canvas>
|
description="Соотношение накопленного down / up"
|
||||||
</Card.Content>
|
headerClass="pb-2"
|
||||||
</Card.Root>
|
contentClass="h-56 px-4 pb-4 pt-0"
|
||||||
<Card.Root>
|
>
|
||||||
<Card.Header>
|
{#snippet icon()}
|
||||||
<Card.Title class="text-base">Память (KB)</Card.Title>
|
<ChartPieIcon class="size-5" aria-hidden="true" />
|
||||||
</Card.Header>
|
{/snippet}
|
||||||
<Card.Content class="h-56">
|
<canvas bind:this={elFlow} class="max-h-full w-full"></canvas>
|
||||||
<canvas bind:this={elMem} class="max-h-full w-full"></canvas>
|
</KpiPanelCard>
|
||||||
</Card.Content>
|
<KpiPanelCard
|
||||||
</Card.Root>
|
accent="violet"
|
||||||
<Card.Root>
|
title="Память (KiB)"
|
||||||
<Card.Header>
|
description="Использование по снимкам (ось — KiB)"
|
||||||
<Card.Title class="text-base">Подключения</Card.Title>
|
headerClass="pb-2"
|
||||||
</Card.Header>
|
contentClass="h-56 px-4 pb-4 pt-0"
|
||||||
<Card.Content class="h-56">
|
>
|
||||||
<canvas bind:this={elConn} class="max-h-full w-full"></canvas>
|
{#snippet icon()}
|
||||||
</Card.Content>
|
<MemoryStickIcon class="size-5" aria-hidden="true" />
|
||||||
</Card.Root>
|
{/snippet}
|
||||||
<Card.Root>
|
<canvas bind:this={elMem} class="max-h-full w-full"></canvas>
|
||||||
<Card.Header>
|
</KpiPanelCard>
|
||||||
<Card.Title class="flex items-center gap-2 text-base">
|
<KpiPanelCard
|
||||||
<NetworkIcon class="size-4" /> Типы сети
|
accent="success"
|
||||||
</Card.Title>
|
title="Подключения"
|
||||||
</Card.Header>
|
description="Число активных соединений во времени"
|
||||||
<Card.Content class="h-56">
|
headerClass="pb-2"
|
||||||
<canvas bind:this={elNet} class="max-h-full w-full"></canvas>
|
contentClass="h-56 px-4 pb-4 pt-0"
|
||||||
</Card.Content>
|
>
|
||||||
</Card.Root>
|
{#snippet icon()}
|
||||||
<Card.Root>
|
<PlugIcon class="size-5" aria-hidden="true" />
|
||||||
<Card.Header>
|
{/snippet}
|
||||||
<Card.Title class="flex items-center gap-2 text-base">
|
<canvas bind:this={elConn} class="max-h-full w-full"></canvas>
|
||||||
<GaugeIcon class="size-4" /> Топ прокси (по сессиям)
|
</KpiPanelCard>
|
||||||
</Card.Title>
|
<KpiPanelCard
|
||||||
</Card.Header>
|
accent="warning"
|
||||||
<Card.Content class="h-56">
|
title="Типы сети"
|
||||||
<canvas bind:this={elTop} class="max-h-full w-full"></canvas>
|
description="TCP и UDP по текущему снимку"
|
||||||
</Card.Content>
|
headerClass="pb-2"
|
||||||
</Card.Root>
|
contentClass="h-56 px-4 pb-4 pt-0"
|
||||||
|
>
|
||||||
|
{#snippet icon()}
|
||||||
|
<NetworkIcon class="size-5" aria-hidden="true" />
|
||||||
|
{/snippet}
|
||||||
|
<canvas bind:this={elNet} class="max-h-full w-full"></canvas>
|
||||||
|
</KpiPanelCard>
|
||||||
|
<KpiPanelCard
|
||||||
|
accent="neutral"
|
||||||
|
title="Топ прокси (по сессиям)"
|
||||||
|
description="Распределение по имени прокси"
|
||||||
|
headerClass="pb-2"
|
||||||
|
contentClass="h-56 px-4 pb-4 pt-0"
|
||||||
|
>
|
||||||
|
{#snippet icon()}
|
||||||
|
<GaugeIcon class="size-5" aria-hidden="true" />
|
||||||
|
{/snippet}
|
||||||
|
<canvas bind:this={elTop} class="max-h-full w-full"></canvas>
|
||||||
|
</KpiPanelCard>
|
||||||
</div>
|
</div>
|
||||||
@@ -18,7 +18,14 @@
|
|||||||
} from '$lib/api/mihomo-types.js';
|
} from '$lib/api/mihomo-types.js';
|
||||||
import { formatBytes, formatRatePerSec } from '$lib/format.js';
|
import { formatBytes, formatRatePerSec } from '$lib/format.js';
|
||||||
import * as Card from '$lib/components/ui/card/index.js';
|
import * as Card from '$lib/components/ui/card/index.js';
|
||||||
|
import KpiStatCard from '$lib/components/kpi-stat-card.svelte';
|
||||||
import { Button } from '$lib/components/ui/button/index.js';
|
import { Button } from '$lib/components/ui/button/index.js';
|
||||||
|
import ArrowBigUpIcon from '@lucide/svelte/icons/arrow-big-up';
|
||||||
|
import ArrowBigDownIcon from '@lucide/svelte/icons/arrow-big-down';
|
||||||
|
import CloudUploadIcon from '@lucide/svelte/icons/cloud-upload';
|
||||||
|
import CloudDownloadIcon from '@lucide/svelte/icons/cloud-download';
|
||||||
|
import PlugIcon from '@lucide/svelte/icons/plug';
|
||||||
|
import MemoryStickIcon from '@lucide/svelte/icons/memory-stick';
|
||||||
import { Badge } from '$lib/components/ui/badge/index.js';
|
import { Badge } from '$lib/components/ui/badge/index.js';
|
||||||
import * as Tabs from '$lib/components/ui/tabs/index.js';
|
import * as Tabs from '$lib/components/ui/tabs/index.js';
|
||||||
import DataQueryState from '$lib/components/fleet/data-query-state.svelte';
|
import DataQueryState from '$lib/components/fleet/data-query-state.svelte';
|
||||||
@@ -36,7 +43,8 @@
|
|||||||
let downBps = $state(0);
|
let downBps = $state(0);
|
||||||
let totalUp = $state(0);
|
let totalUp = $state(0);
|
||||||
let totalDown = $state(0);
|
let totalDown = $state(0);
|
||||||
let memKB = $state(0);
|
/** Память в KiB (кибибайты): из WS Mihomo приходит в байтах — нормализуем в одном месте. */
|
||||||
|
let memKiB = $state(0);
|
||||||
let connTotal = $state(0);
|
let connTotal = $state(0);
|
||||||
let tcpN = $state(0);
|
let tcpN = $state(0);
|
||||||
let udpN = $state(0);
|
let udpN = $state(0);
|
||||||
@@ -48,6 +56,12 @@
|
|||||||
let topList = $state<{ name: string; n: number }[]>([]);
|
let topList = $state<{ name: string; n: number }[]>([]);
|
||||||
|
|
||||||
const MAX_POINTS = 72;
|
const MAX_POINTS = 72;
|
||||||
|
|
||||||
|
/** Mihomo WS: `inuse` (/memory) и `memory` (/connections) — в байтах → KiB для карточки и графика. */
|
||||||
|
function mihomoWsMemoryToKiB(bytes: number): number {
|
||||||
|
if (!Number.isFinite(bytes) || bytes <= 0) return 0;
|
||||||
|
return bytes / 1024;
|
||||||
|
}
|
||||||
/** Mihomo hub/route/proxies.go: без query `url` URLTest часто даёт delay=0 → HTTP 503. */
|
/** Mihomo hub/route/proxies.go: без query `url` URLTest часто даёт delay=0 → HTTP 503. */
|
||||||
const MIHOMO_DELAY_DEFAULT_URL = 'https://www.gstatic.com/generate_204';
|
const MIHOMO_DELAY_DEFAULT_URL = 'https://www.gstatic.com/generate_204';
|
||||||
|
|
||||||
@@ -203,8 +217,8 @@
|
|||||||
if (line) {
|
if (line) {
|
||||||
try {
|
try {
|
||||||
const o = JSON.parse(line) as { inuse?: number };
|
const o = JSON.parse(line) as { inuse?: number };
|
||||||
memKB = Number(o.inuse) || 0;
|
memKiB = mihomoWsMemoryToKiB(Number(o.inuse) || 0);
|
||||||
histMem = [...histMem, memKB];
|
histMem = [...histMem, memKiB];
|
||||||
while (histMem.length > MAX_POINTS) histMem.shift();
|
while (histMem.length > MAX_POINTS) histMem.shift();
|
||||||
seen++;
|
seen++;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -343,8 +357,8 @@
|
|||||||
ws.onmessage = (ev) => {
|
ws.onmessage = (ev) => {
|
||||||
try {
|
try {
|
||||||
const o = JSON.parse(String(ev.data)) as { inuse?: number };
|
const o = JSON.parse(String(ev.data)) as { inuse?: number };
|
||||||
memKB = Number(o.inuse) || 0;
|
memKiB = mihomoWsMemoryToKiB(Number(o.inuse) || 0);
|
||||||
histMem = [...histMem, memKB];
|
histMem = [...histMem, memKiB];
|
||||||
while (histMem.length > MAX_POINTS) histMem.shift();
|
while (histMem.length > MAX_POINTS) histMem.shift();
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
@@ -397,8 +411,8 @@
|
|||||||
totalDown = j.downloadTotal;
|
totalDown = j.downloadTotal;
|
||||||
}
|
}
|
||||||
if (typeof j.memory === 'number') {
|
if (typeof j.memory === 'number') {
|
||||||
memKB = j.memory;
|
memKiB = mihomoWsMemoryToKiB(j.memory);
|
||||||
histMem = [...histMem, memKB];
|
histMem = [...histMem, memKiB];
|
||||||
while (histMem.length > MAX_POINTS) histMem.shift();
|
while (histMem.length > MAX_POINTS) histMem.shift();
|
||||||
}
|
}
|
||||||
let t = 0,
|
let t = 0,
|
||||||
@@ -486,7 +500,7 @@
|
|||||||
totalDown = 0;
|
totalDown = 0;
|
||||||
upBps = 0;
|
upBps = 0;
|
||||||
downBps = 0;
|
downBps = 0;
|
||||||
memKB = 0;
|
memKiB = 0;
|
||||||
histUp = [];
|
histUp = [];
|
||||||
histDown = [];
|
histDown = [];
|
||||||
histMem = [];
|
histMem = [];
|
||||||
@@ -622,14 +636,40 @@
|
|||||||
<DataQueryState err={metaErr} showSkeleton={loadingMeta} isEmpty={false}>
|
<DataQueryState err={metaErr} showSkeleton={loadingMeta} isEmpty={false}>
|
||||||
{#snippet skeleton()}
|
{#snippet skeleton()}
|
||||||
<Skeleton class="mb-4 h-6 w-96 max-w-full" />
|
<Skeleton class="mb-4 h-6 w-96 max-w-full" />
|
||||||
<div class="mb-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
|
<div class="mb-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
|
||||||
{#each Array.from({ length: 6 }) as _, i (i)}
|
{#each Array.from({ length: 6 }) as _, i (i)}
|
||||||
<Skeleton class="h-24 rounded-lg" />
|
<Card.Root class="border-l-4 border-l-muted-foreground/30 shadow-sm">
|
||||||
|
<Card.Header class="pb-2">
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<Skeleton class="size-10 shrink-0 rounded-lg" />
|
||||||
|
<div class="min-w-0 flex-1 space-y-2">
|
||||||
|
<Skeleton class="h-3 w-24" />
|
||||||
|
<Skeleton class="h-8 w-20" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content class="pt-0">
|
||||||
|
<Skeleton class="h-3 w-32" />
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-4 lg:grid-cols-2">
|
<div class="grid gap-4 lg:grid-cols-2">
|
||||||
{#each Array.from({ length: 6 }) as _, i (i)}
|
{#each Array.from({ length: 6 }) as _, i (i)}
|
||||||
<Skeleton class="h-56 rounded-lg" />
|
<Card.Root class="border-l-4 border-l-muted-foreground/30 shadow-sm">
|
||||||
|
<Card.Header class="pb-2">
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<Skeleton class="size-10 shrink-0 rounded-lg" />
|
||||||
|
<div class="min-w-0 flex-1 space-y-2">
|
||||||
|
<Skeleton class="h-4 w-40" />
|
||||||
|
<Skeleton class="h-3 w-full max-w-xs" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content class="h-56 px-4 pb-4 pt-0">
|
||||||
|
<Skeleton class="h-full w-full rounded-md" />
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
@@ -640,45 +680,43 @@
|
|||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if tab === 'overview' && !metaErr}
|
{#if tab === 'overview' && !metaErr}
|
||||||
<div class="mb-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
|
<div class="mb-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
|
||||||
<Card.Root class="border-l-primary border-l-4 py-3">
|
<KpiStatCard accent="violet" label="Загрузка" footer="мгновенная скорость, /traffic" valueClass="text-xl">
|
||||||
<Card.Header class="pb-1">
|
{#snippet icon()}
|
||||||
<Card.Description>Загрузка</Card.Description>
|
<ArrowBigUpIcon class="size-5" aria-hidden="true" />
|
||||||
</Card.Header>
|
{/snippet}
|
||||||
<Card.Content class="text-lg font-semibold">{formatRatePerSec(upBps)}</Card.Content>
|
{formatRatePerSec(upBps)}
|
||||||
</Card.Root>
|
</KpiStatCard>
|
||||||
<Card.Root class="border-l-sky-500 border-l-4 py-3">
|
<KpiStatCard accent="info" label="Скачивание" footer="мгновенная скорость, /traffic" valueClass="text-xl">
|
||||||
<Card.Header class="pb-1">
|
{#snippet icon()}
|
||||||
<Card.Description>Скачивание</Card.Description>
|
<ArrowBigDownIcon class="size-5" aria-hidden="true" />
|
||||||
</Card.Header>
|
{/snippet}
|
||||||
<Card.Content class="text-lg font-semibold">{formatRatePerSec(downBps)}</Card.Content>
|
{formatRatePerSec(downBps)}
|
||||||
</Card.Root>
|
</KpiStatCard>
|
||||||
<Card.Root class="border-l-amber-500 border-l-4 py-3">
|
<KpiStatCard accent="warning" label="Всего загружено" footer="uploadTotal (снимок)" valueClass="text-xl">
|
||||||
<Card.Header class="pb-1">
|
{#snippet icon()}
|
||||||
<Card.Description>Всего загружено</Card.Description>
|
<CloudUploadIcon class="size-5" aria-hidden="true" />
|
||||||
</Card.Header>
|
{/snippet}
|
||||||
<Card.Content class="text-lg font-semibold">{formatBytes(totalUp)}</Card.Content>
|
{formatBytes(totalUp)}
|
||||||
</Card.Root>
|
</KpiStatCard>
|
||||||
<Card.Root class="border-l-cyan-500 border-l-4 py-3">
|
<KpiStatCard accent="teal" label="Всего скачано" footer="downloadTotal (снимок)" valueClass="text-xl">
|
||||||
<Card.Header class="pb-1">
|
{#snippet icon()}
|
||||||
<Card.Description>Всего скачано</Card.Description>
|
<CloudDownloadIcon class="size-5" aria-hidden="true" />
|
||||||
</Card.Header>
|
{/snippet}
|
||||||
<Card.Content class="text-lg font-semibold">{formatBytes(totalDown)}</Card.Content>
|
{formatBytes(totalDown)}
|
||||||
</Card.Root>
|
</KpiStatCard>
|
||||||
<Card.Root class="border-l-emerald-500 border-l-4 py-3">
|
<KpiStatCard accent="info" label="Активные соединения" footer="список /connections" valueClass="text-xl">
|
||||||
<Card.Header class="pb-1">
|
{#snippet icon()}
|
||||||
<Card.Description>Активные соединения</Card.Description>
|
<PlugIcon class="size-5" aria-hidden="true" />
|
||||||
</Card.Header>
|
{/snippet}
|
||||||
<Card.Content class="text-lg font-semibold">{connTotal}</Card.Content>
|
{connTotal}
|
||||||
</Card.Root>
|
</KpiStatCard>
|
||||||
<Card.Root class="border-l-violet-500 border-l-4 py-3">
|
<KpiStatCard accent="neutral" label="Память" footer="/memory или поле memory в WS" valueClass="text-xl">
|
||||||
<Card.Header class="pb-1">
|
{#snippet icon()}
|
||||||
<Card.Description>Память</Card.Description>
|
<MemoryStickIcon class="size-5" aria-hidden="true" />
|
||||||
</Card.Header>
|
{/snippet}
|
||||||
<Card.Content class="text-lg font-semibold">
|
{memKiB > 0 ? `${(memKiB / 1024).toFixed(1)} MiB` : '—'}
|
||||||
{memKB > 0 ? `${(memKB / 1024).toFixed(1)} MiB` : '—'}
|
</KpiStatCard>
|
||||||
</Card.Content>
|
|
||||||
</Card.Root>
|
|
||||||
</div>
|
</div>
|
||||||
<MihomoOverviewCharts
|
<MihomoOverviewCharts
|
||||||
histUp={histUp}
|
histUp={histUp}
|
||||||
|
|||||||
Reference in new issue
Block a user