Compare commits
50
Commits
37d1e758f1
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5fb73f61e | ||
|
|
665de9e0fc | ||
|
|
4a6b498879 | ||
|
|
7db6cc32b6 | ||
|
|
e9900ef20c | ||
|
|
ed9fa16b57 | ||
|
|
1640230c66 | ||
|
|
29ad253a61 | ||
|
|
5c37f3f611 | ||
|
|
fbaead91a9 | ||
|
|
c891439857 | ||
|
|
f77493a04b | ||
|
|
137eecdc1d | ||
|
|
bfbfc175ca | ||
|
|
0fd992bb21 | ||
|
|
c5412fa89b | ||
|
|
b30e6936b1 | ||
|
|
293863b5b0 | ||
|
|
9ea002a7a1 | ||
|
|
e67db071bb | ||
|
|
948bbf3de5 | ||
|
|
d6ef68f450 | ||
|
|
c0875de75b | ||
|
|
17a785c9d2 | ||
|
|
915d848294 | ||
|
|
a5bf846e52 | ||
|
|
15c82ea3b7 | ||
|
|
f8b98d9b30 | ||
|
|
68a4f1e36e | ||
|
|
78d730fad9 | ||
|
|
eb80d4a0fd | ||
|
|
f35dd25819 | ||
|
|
55310768a5 | ||
|
|
4b4c0123e3 | ||
|
|
ad6b1847c5 | ||
|
|
a90d0c4333 | ||
|
|
62966fd7cf | ||
|
|
82f8b9a457 | ||
|
|
6829c7efe2 | ||
|
|
f35edb00d7 | ||
|
|
b007ced615 | ||
|
|
5370fd4795 | ||
|
|
774be48155 | ||
|
|
dcff88ec7b | ||
|
|
407aa7b899 | ||
|
|
3337a3cc27 | ||
|
|
e5331bdc01 | ||
|
|
a803849cba | ||
|
|
e006bc9c0b | ||
|
|
21056c72f3 |
@@ -5,6 +5,16 @@ const nextConfig: NextConfig = {
|
|||||||
turbopack: {
|
turbopack: {
|
||||||
root: process.cwd(),
|
root: process.cwd(),
|
||||||
},
|
},
|
||||||
|
// Nix store is read-only at runtime; Next.js tries to mkdir
|
||||||
|
// <distDir>/cache/images for the on-disk image optimizer LRU and
|
||||||
|
// crashes with EACCES unhandledRejection on every optimized image
|
||||||
|
// request. Disable the disk cache (and disable ISR flush which gates it).
|
||||||
|
images: {
|
||||||
|
maximumDiskCacheSize: 0,
|
||||||
|
},
|
||||||
|
experimental: {
|
||||||
|
isrFlushToDisk: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
+9
-9
@@ -10,30 +10,30 @@
|
|||||||
"format": "biome format --write"
|
"format": "biome format --write"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@base-ui/react": "^1.6.0",
|
"@base-ui/react": "^1.7.0",
|
||||||
"@shadcn/react": "^0.2.1",
|
"@shadcn/react": "^0.3.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.4.0",
|
"date-fns": "^4.4.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"framer-motion": "^12.42.2",
|
"framer-motion": "^13.1.0",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^1.26.0",
|
"lucide-react": "^1.31.0",
|
||||||
"next": "16.2.12",
|
"next": "16.3.1",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-day-picker": "^10.0.1",
|
"react-day-picker": "^10.0.1",
|
||||||
"react-dom": "19.2.8",
|
"react-dom": "19.2.8",
|
||||||
"react-resizable-panels": "^4.12.2",
|
"react-resizable-panels": "^4.12.3",
|
||||||
"recharts": "3.10.1",
|
"recharts": "3.10.1",
|
||||||
"shadcn": "^4.14.1",
|
"shadcn": "^4.18.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.8",
|
||||||
"tailwind-merge": "^3.6.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
"tw-animate-css": "^1.4.0"
|
"tw-animate-css": "^1.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.5.5",
|
"@biomejs/biome": "2.5.9",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^26",
|
"@types/node": "^26",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
|
|||||||
+137
-126
@@ -1,16 +1,10 @@
|
|||||||
import http from "node:http";
|
import { execFile } from "node:child_process";
|
||||||
|
import { promisify } from "node:util";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
const JAEGER = "http://jaeger:16686";
|
const execFileAsync = promisify(execFile);
|
||||||
const PROMETHEUS = "http://prometheus:9090";
|
|
||||||
const DOCKER_SOCK = "/var/run/docker.sock";
|
|
||||||
|
|
||||||
interface Container {
|
const PROMETHEUS = "http://127.0.0.1:9090";
|
||||||
Names: string[];
|
|
||||||
State: string;
|
|
||||||
Labels: Record<string, string>;
|
|
||||||
NetworkSettings?: { Networks?: Record<string, unknown> };
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Trace {
|
interface Trace {
|
||||||
service: string;
|
service: string;
|
||||||
@@ -26,24 +20,70 @@ interface Service {
|
|||||||
hasWeb: boolean;
|
hasWeb: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function dockerFetch(path: string): Promise<unknown> {
|
// Service-owning systemd units that should show up on the dashboard.
|
||||||
return new Promise((resolve, reject) => {
|
// GMW, Booster, etc. are listed even though they are headless (no web).
|
||||||
http
|
const MONITORED_UNITS = [
|
||||||
.get({ socketPath: DOCKER_SOCK, path }, (res) => {
|
"caddy",
|
||||||
let data = "";
|
"9router",
|
||||||
res.on("data", (c: string) => {
|
"booster-role",
|
||||||
data += c;
|
"gmw-backend",
|
||||||
});
|
"gmw-discord-gateway",
|
||||||
res.on("end", () => {
|
"hub",
|
||||||
try {
|
"lidm-backend",
|
||||||
resolve(JSON.parse(data));
|
"lidm-frontend",
|
||||||
} catch {
|
"llm-api",
|
||||||
resolve(null);
|
"nats",
|
||||||
}
|
"node-exporter",
|
||||||
});
|
"otel",
|
||||||
})
|
"pr-agent-server",
|
||||||
.on("error", reject);
|
"prometheus",
|
||||||
});
|
"scraper",
|
||||||
|
"teleuploader",
|
||||||
|
"tools-frontend",
|
||||||
|
"tools-gateway",
|
||||||
|
"tools-workers",
|
||||||
|
"zeavis-api",
|
||||||
|
"zeavis-ml-service",
|
||||||
|
"zeavis-web",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Units with a public HTTPS site behind Caddy.
|
||||||
|
const WEB_UNITS = new Set([
|
||||||
|
"caddy",
|
||||||
|
"9router",
|
||||||
|
"hub",
|
||||||
|
"lidm-frontend",
|
||||||
|
"pr-agent-server",
|
||||||
|
"scraper",
|
||||||
|
"teleuploader",
|
||||||
|
"tools-frontend",
|
||||||
|
"zeavis-web",
|
||||||
|
]);
|
||||||
|
|
||||||
|
async function systemdServices(): Promise<Service[]> {
|
||||||
|
const services: Service[] = [];
|
||||||
|
for (const unit of MONITORED_UNITS) {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execFileAsync("systemctl", [
|
||||||
|
"show",
|
||||||
|
`${unit}.service`,
|
||||||
|
"-p",
|
||||||
|
"ActiveState",
|
||||||
|
"-p",
|
||||||
|
"LoadState",
|
||||||
|
"--no-pager",
|
||||||
|
]);
|
||||||
|
const state = stdout.match(/ActiveState=(\w+)/)?.[1] ?? "unknown";
|
||||||
|
services.push({
|
||||||
|
name: unit,
|
||||||
|
state: state === "active" ? "running" : state,
|
||||||
|
hasWeb: WEB_UNITS.has(unit),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// unit doesn't exist — skip silently
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchJSON(url: string): Promise<unknown> {
|
async function fetchJSON(url: string): Promise<unknown> {
|
||||||
@@ -55,73 +95,6 @@ async function fetchJSON(url: string): Promise<unknown> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseServices(containers: Container[]): Service[] {
|
|
||||||
const project = containers.find((c) => c.Labels["com.docker.compose.project"])
|
|
||||||
?.Labels["com.docker.compose.project"];
|
|
||||||
|
|
||||||
return containers
|
|
||||||
.filter((c) => {
|
|
||||||
if (!c.NetworkSettings?.Networks?.["app-shared-net"]) return false;
|
|
||||||
if (project && c.Labels["com.docker.compose.project"] !== project)
|
|
||||||
return false;
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.map((c) => ({
|
|
||||||
name: c.Names[0].replace(/^\//, ""),
|
|
||||||
state: c.State,
|
|
||||||
hasWeb: Object.keys(c.Labels).some(
|
|
||||||
(k) => k.startsWith("traefik.http.routers.") && k.endsWith(".rule"),
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchTraces(): Promise<Trace[]> {
|
|
||||||
const svcRes = await fetchJSON(`${JAEGER}/api/services`);
|
|
||||||
if (!svcRes || !Array.isArray((svcRes as { data?: string[] }).data))
|
|
||||||
return [];
|
|
||||||
|
|
||||||
const now = Date.now() * 1000;
|
|
||||||
const start = now - 5 * 60 * 1_000_000;
|
|
||||||
const all: Trace[] = [];
|
|
||||||
|
|
||||||
for (const service of (svcRes as { data: string[] }).data) {
|
|
||||||
const d = await fetchJSON(
|
|
||||||
`${JAEGER}/api/traces?service=${encodeURIComponent(service)}&start=${start}&end=${now}&limit=5&lookback=5m`,
|
|
||||||
);
|
|
||||||
if (!d || !Array.isArray((d as { data?: unknown[] }).data)) continue;
|
|
||||||
|
|
||||||
for (const t of (
|
|
||||||
d as {
|
|
||||||
data: {
|
|
||||||
duration: number;
|
|
||||||
spans: {
|
|
||||||
operationName: string;
|
|
||||||
processID: string;
|
|
||||||
tags: { key: string; value: unknown }[];
|
|
||||||
}[];
|
|
||||||
processes: Record<string, { serviceName: string }>;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
).data) {
|
|
||||||
if (!t.spans?.length) continue;
|
|
||||||
const span = t.spans[0];
|
|
||||||
const svc = t.processes[span.processID]?.serviceName || "unknown";
|
|
||||||
const hasError = t.spans.some((s) =>
|
|
||||||
s.tags?.some((tag) => tag.key === "error" && tag.value === true),
|
|
||||||
);
|
|
||||||
all.push({
|
|
||||||
service: svc,
|
|
||||||
operation: span.operationName,
|
|
||||||
duration: t.duration,
|
|
||||||
spans: t.spans.length,
|
|
||||||
hasError,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return all.sort((a, b) => b.duration - a.duration).slice(0, 20);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function promRange(query: string, steps = 20): Promise<number[]> {
|
async function promRange(query: string, steps = 20): Promise<number[]> {
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
const u = `${PROMETHEUS}/api/v1/query_range?query=${encodeURIComponent(query)}&start=${now - 300}&end=${now}&step=${(300 / steps).toFixed(0)}`;
|
const u = `${PROMETHEUS}/api/v1/query_range?query=${encodeURIComponent(query)}&start=${now - 300}&end=${now}&step=${(300 / steps).toFixed(0)}`;
|
||||||
@@ -164,18 +137,20 @@ interface DashboardData {
|
|||||||
errors: number[];
|
errors: number[];
|
||||||
traceVolume: number[];
|
traceVolume: number[];
|
||||||
links: { url: string; label: string }[];
|
links: { url: string; label: string }[];
|
||||||
|
llm: {
|
||||||
|
reqRate: number | null;
|
||||||
|
tokRate: number | null;
|
||||||
|
tokSpeed: number | null;
|
||||||
|
uptime: number | null;
|
||||||
|
reqSpark: number[];
|
||||||
|
tokSpark: number[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
const [containersRaw] = await Promise.all([
|
const services = await systemdServices();
|
||||||
dockerFetch("/containers/json?all=true") as Promise<Container[]>,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const containers = Array.isArray(containersRaw) ? containersRaw : [];
|
|
||||||
const services = parseServices(containers);
|
|
||||||
|
|
||||||
const [
|
const [
|
||||||
traces,
|
|
||||||
cpu,
|
cpu,
|
||||||
ram,
|
ram,
|
||||||
disk,
|
disk,
|
||||||
@@ -184,11 +159,11 @@ export async function GET() {
|
|||||||
load15,
|
load15,
|
||||||
netIn,
|
netIn,
|
||||||
netOut,
|
netOut,
|
||||||
rps,
|
netInSpark,
|
||||||
latency,
|
loadSpark,
|
||||||
errors,
|
netOutSpark,
|
||||||
|
upstreamCount,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
fetchTraces(),
|
|
||||||
promQuery(
|
promQuery(
|
||||||
`100 - (avg by(instance)(rate(node_cpu_seconds_total{mode="idle"}[1m])) * 100)`,
|
`100 - (avg by(instance)(rate(node_cpu_seconds_total{mode="idle"}[1m])) * 100)`,
|
||||||
),
|
),
|
||||||
@@ -203,42 +178,78 @@ export async function GET() {
|
|||||||
promQuery("node_load15"),
|
promQuery("node_load15"),
|
||||||
promQuery(`rate(node_network_receive_bytes_total{device!="lo"}[1m])`),
|
promQuery(`rate(node_network_receive_bytes_total{device!="lo"}[1m])`),
|
||||||
promQuery(`rate(node_network_transmit_bytes_total{device!="lo"}[1m])`),
|
promQuery(`rate(node_network_transmit_bytes_total{device!="lo"}[1m])`),
|
||||||
promRange("sum(rate(traefik_service_requests_total[1m]))"),
|
promRange('sum(rate(node_network_receive_bytes_total{device!="lo"}[1m]))'),
|
||||||
|
promRange("avg(node_load1)"),
|
||||||
|
promRange('sum(rate(node_network_transmit_bytes_total{device!="lo"}[1m]))'),
|
||||||
|
promQuery("count(up)"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ── Error rate (llm-api) & request rate (any /metrics service) ──
|
||||||
|
const [errRate, errSpark] = await Promise.all([
|
||||||
|
promQuery(`sum(rate(llm_api_errors_total[5m]))`),
|
||||||
|
promRange(`sum(rate(llm_api_errors_total[1m]))`),
|
||||||
|
]);
|
||||||
|
const reqRate = await promQuery(
|
||||||
|
`sum(rate(llm_api_requests_total[5m])) + sum(rate(http_server_request_count_total[5m]))`,
|
||||||
|
);
|
||||||
|
const reqSpark = await promRange(
|
||||||
|
`sum(rate(llm_api_requests_total[1m])) + sum(rate(http_server_request_count_total[1m]))`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── LLM inference metrics (llm-api /metrics) ──
|
||||||
|
const [
|
||||||
|
llmReqRate,
|
||||||
|
llmTokRate,
|
||||||
|
llmTokSpeed,
|
||||||
|
llmUptime,
|
||||||
|
llmReqSpark,
|
||||||
|
llmTokSpark,
|
||||||
|
] = await Promise.all([
|
||||||
|
promQuery(
|
||||||
|
`sum(rate(llm_api_requests_total{service="llm-api"}[5m]))`,
|
||||||
|
),
|
||||||
|
promQuery(
|
||||||
|
`sum(rate(llm_api_completion_tokens_total{service="llm-api"}[5m]))`,
|
||||||
|
),
|
||||||
|
promQuery(
|
||||||
|
`avg(llm_api_tokens_per_second{service="llm-api"})`,
|
||||||
|
),
|
||||||
|
promQuery(`llm_api_uptime_seconds{service="llm-api"}`),
|
||||||
promRange(
|
promRange(
|
||||||
"avg(traefik_service_request_duration_seconds_sum / traefik_service_request_duration_seconds_count) * 1000",
|
`sum(rate(llm_api_requests_total{service="llm-api"}[1m]))`,
|
||||||
|
),
|
||||||
|
promRange(
|
||||||
|
`sum(rate(llm_api_completion_tokens_total{service="llm-api"}[1m]))`,
|
||||||
),
|
),
|
||||||
promRange('sum(rate(traefik_service_requests_total{code=~"5.."}[1m]))'),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const links: { url: string; label: string }[] = [
|
const links: { url: string; label: string }[] = [
|
||||||
{ url: "/jaeger", label: "Jaeger UI" },
|
{ url: "https://github.com/asepharyana/asepharyana-hub", label: "GitHub" },
|
||||||
];
|
];
|
||||||
if (containers.some((c) => c.Names?.some((n) => n.includes("prometheus")))) {
|
const domains = ["asepharyana.my.id"];
|
||||||
links.push({ url: "/api/prometheus/targets", label: "Prometheus" });
|
|
||||||
}
|
|
||||||
links.push({
|
|
||||||
url: "https://github.com/asepharyana/asepharyana-hub",
|
|
||||||
label: "GitHub",
|
|
||||||
});
|
|
||||||
|
|
||||||
const domains = ["asepharyana.my.id", "asepharyana.web.id"];
|
|
||||||
for (const s of services) {
|
for (const s of services) {
|
||||||
if (s.hasWeb && s.state === "running") {
|
if (s.hasWeb && s.state === "running") {
|
||||||
links.push({ url: `https://${s.name}.${domains[0]}`, label: s.name });
|
links.push({ url: `https://${s.name}.${domains[0]}`, label: s.name });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const traceVolume = [traces.length];
|
|
||||||
|
|
||||||
const data: DashboardData = {
|
const data: DashboardData = {
|
||||||
services,
|
services,
|
||||||
traces,
|
traces: [],
|
||||||
node: { cpu, ram, disk, load1, load5, load15, netIn, netOut },
|
node: { cpu, ram, disk, load1, load5, load15, netIn, netOut },
|
||||||
rps,
|
rps: reqSpark,
|
||||||
latency,
|
latency: loadSpark,
|
||||||
errors,
|
errors: errSpark,
|
||||||
traceVolume,
|
traceVolume: netInSpark,
|
||||||
links,
|
links,
|
||||||
|
llm: {
|
||||||
|
reqRate: llmReqRate,
|
||||||
|
tokRate: llmTokRate,
|
||||||
|
tokSpeed: llmTokSpeed,
|
||||||
|
uptime: llmUptime,
|
||||||
|
reqSpark: llmReqSpark,
|
||||||
|
tokSpark: llmTokSpark,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return NextResponse.json(data);
|
return NextResponse.json(data);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { AnimatedNumber } from "@/components/ui/motion-primitives";
|
import { AnimatedNumber } from "@/components/ui/motion-primitives";
|
||||||
import {
|
import {
|
||||||
type DashboardData,
|
type DashboardData,
|
||||||
|
fmtUptime,
|
||||||
gaugeColor,
|
gaugeColor,
|
||||||
safeDur,
|
safeDur,
|
||||||
serviceIndicator,
|
serviceIndicator,
|
||||||
@@ -59,8 +60,8 @@ export default function Dashboard() {
|
|||||||
data?.services.filter((s) => s.state === "running").length ?? 0;
|
data?.services.filter((s) => s.state === "running").length ?? 0;
|
||||||
const degraded = (data?.services.length ?? 0) - running;
|
const degraded = (data?.services.length ?? 0) - running;
|
||||||
const hasNode = data?.node.cpu !== null || data?.node.ram !== null;
|
const hasNode = data?.node.cpu !== null || data?.node.ram !== null;
|
||||||
const hasTraffik =
|
const hasTraffic =
|
||||||
(data?.rps?.length ?? 0) > 0 || (data?.latency?.length ?? 0) > 0;
|
(data?.rps?.length ?? 0) > 0 || (data?.errors?.length ?? 0) > 0;
|
||||||
const node = data?.node;
|
const node = data?.node;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -146,7 +147,15 @@ export default function Dashboard() {
|
|||||||
label="Traces"
|
label="Traces"
|
||||||
color="text-blue-400"
|
color="text-blue-400"
|
||||||
/>
|
/>
|
||||||
<StatBox value={0} label="Errors" color="text-red-400" />
|
<StatBox
|
||||||
|
value={
|
||||||
|
data?.errors?.length
|
||||||
|
? data.errors[data.errors.length - 1].toFixed(2)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
label="LLM err/s"
|
||||||
|
color="text-red-400"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -230,8 +239,75 @@ export default function Dashboard() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* LLM Inference */}
|
||||||
|
<motion.div variants={cardVariants}>
|
||||||
|
<Card className="transition-shadow duration-300 hover:shadow-md hover:shadow-border/20">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
|
<CardTitle>LLM Inference</CardTitle>
|
||||||
|
<Badge variant="secondary" className="font-mono text-[10px] font-normal">
|
||||||
|
{data?.llm.uptime != null
|
||||||
|
? `${fmtUptime(data.llm.uptime)} up`
|
||||||
|
: "no data"}
|
||||||
|
</Badge>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
|
<StatBox
|
||||||
|
value={
|
||||||
|
data?.llm.reqRate != null
|
||||||
|
? data.llm.reqRate.toFixed(1)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
label="Req/s (5m)"
|
||||||
|
color="text-blue-400"
|
||||||
|
/>
|
||||||
|
<StatBox
|
||||||
|
value={
|
||||||
|
data?.llm.tokRate != null
|
||||||
|
? `${data.llm.tokRate.toFixed(0)}`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
label="Tokens/s (5m)"
|
||||||
|
color="text-green-400"
|
||||||
|
/>
|
||||||
|
<StatBox
|
||||||
|
value={
|
||||||
|
data?.llm.tokSpeed != null
|
||||||
|
? `${data.llm.tokSpeed.toFixed(0)}`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
label="Gen tok/s"
|
||||||
|
color="text-purple-400"
|
||||||
|
/>
|
||||||
|
<StatBox
|
||||||
|
value={data?.llm.reqSpark?.length ? "live" : undefined}
|
||||||
|
label="Sparkline"
|
||||||
|
color="text-yellow-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{data?.llm.reqSpark?.length ? (
|
||||||
|
<div className="mt-2">
|
||||||
|
<Sparkline data={data.llm.reqSpark} color="#58a6ff" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="mt-2 text-center text-[10px] text-muted-foreground">
|
||||||
|
No traffic yet — send a prompt on{" "}
|
||||||
|
<a
|
||||||
|
className="text-blue-400 underline"
|
||||||
|
href="https://ai.asepharyana.my.id"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
ai.asepharyana.my.id
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
{/* Request Rate */}
|
{/* Request Rate */}
|
||||||
{hasTraffik && (
|
{hasTraffic && (
|
||||||
<motion.div variants={cardVariants}>
|
<motion.div variants={cardVariants}>
|
||||||
<Card className="transition-shadow duration-300 hover:shadow-md hover:shadow-border/20">
|
<Card className="transition-shadow duration-300 hover:shadow-md hover:shadow-border/20">
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
@@ -249,15 +325,15 @@ export default function Dashboard() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Latency */}
|
{/* Load Average */}
|
||||||
{hasTraffik && (
|
{hasNode && (
|
||||||
<motion.div variants={cardVariants}>
|
<motion.div variants={cardVariants}>
|
||||||
<Card className="transition-shadow duration-300 hover:shadow-md hover:shadow-border/20">
|
<Card className="transition-shadow duration-300 hover:shadow-md hover:shadow-border/20">
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
<CardTitle>Latency</CardTitle>
|
<CardTitle>Load Average</CardTitle>
|
||||||
<Badge variant="secondary" className="text-[10px] font-normal">
|
<Badge variant="secondary" className="text-[10px] font-normal">
|
||||||
{data?.latency?.length
|
{data?.latency?.length
|
||||||
? `${data.latency[data.latency.length - 1].toFixed(0)}ms`
|
? data.latency[data.latency.length - 1].toFixed(2)
|
||||||
: "-"}
|
: "-"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -269,7 +345,7 @@ export default function Dashboard() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Error Rate */}
|
{/* Error Rate */}
|
||||||
{hasTraffik && (
|
{hasTraffic && (
|
||||||
<motion.div variants={cardVariants}>
|
<motion.div variants={cardVariants}>
|
||||||
<Card className="transition-shadow duration-300 hover:shadow-md hover:shadow-border/20">
|
<Card className="transition-shadow duration-300 hover:shadow-md hover:shadow-border/20">
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
|
|||||||
@@ -30,6 +30,14 @@ export interface DashboardData {
|
|||||||
errors: number[];
|
errors: number[];
|
||||||
traceVolume: number[];
|
traceVolume: number[];
|
||||||
links: { url: string; label: string }[];
|
links: { url: string; label: string }[];
|
||||||
|
llm: {
|
||||||
|
reqRate: number | null;
|
||||||
|
tokRate: number | null;
|
||||||
|
tokSpeed: number | null;
|
||||||
|
uptime: number | null;
|
||||||
|
reqSpark: number[];
|
||||||
|
tokSpark: number[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function safeDur(us: number): string {
|
export function safeDur(us: number): string {
|
||||||
@@ -55,3 +63,14 @@ export function serviceIndicator(state: string): string {
|
|||||||
return "bg-red-400";
|
return "bg-red-400";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fmtUptime(seconds: number): string {
|
||||||
|
const s = Math.max(0, Math.floor(seconds));
|
||||||
|
if (s < 60) return `${s}s`;
|
||||||
|
const m = Math.floor(s / 60);
|
||||||
|
if (m < 60) return `${m}m ${s % 60}s`;
|
||||||
|
const h = Math.floor(m / 60);
|
||||||
|
if (h < 24) return `${h}h ${m % 60}m`;
|
||||||
|
const d = Math.floor(h / 24);
|
||||||
|
return `${d}d ${h % 24}h`;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user