Node live data

This commit is contained in:
2025-12-27 14:43:32 +01:00
parent 1ab87edacd
commit e8dc4a4590
6 changed files with 111 additions and 86 deletions
+12 -2
View File
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import testAuth from "../auth"; import testAuth from "../auth";
import { StatusCodes } from "http-status-codes"; import { StatusCodes } from "http-status-codes";
import si from "systeminformation"; import si from "systeminformation";
import { NodeLiveData } from "@/lib/types";
export async function GET(req: NextRequest): Promise<NextResponse> { export async function GET(req: NextRequest): Promise<NextResponse> {
const auth = await testAuth(req); const auth = await testAuth(req);
@@ -21,6 +22,10 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
si.networkStats(), si.networkStats(),
]); ]);
const disks = await si.fsSize();
const root = disks.find((d) => d.mount === "/" || d.fs.includes("disk3"));
return NextResponse.json({ return NextResponse.json({
message: "ok", message: "ok",
liveData: { liveData: {
@@ -36,7 +41,12 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
free: mem.free, free: mem.free,
usage: ((mem.total - mem.free) / mem.total) * 100, usage: ((mem.total - mem.free) / mem.total) * 100,
}, },
disk: disk.map((d) => ({ disk: {
used: (root?.size || 0) - (root?.available || 0),
size: root?.size || 0,
available: root?.available || 0,
},
disks: disk.map((d) => ({
fs: d.fs, fs: d.fs,
size: d.size, size: d.size,
used: d.used, used: d.used,
@@ -48,5 +58,5 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
tx: n.tx_bytes, tx: n.tx_bytes,
})), })),
}, },
}); } as NodeLiveData);
} }
+73 -71
View File
@@ -4,62 +4,49 @@ import { DashboardLayout } from "@/components/dashboard/DashboardLayout";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { StatusIndicator } from "@/components/dashboard/StatusIndicator";
import { Server, Plus, Settings, Trash2 } from "lucide-react"; import { Server, Plus, Settings, Trash2 } from "lucide-react";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import { Node, NodeLiveData } from "@/lib/types";
import { useEffect, useState } from "react";
import { getLiveNodes } from "@/core/client/node";
import { formatBytes } from "@/lib/format";
const nodes = [ function DiskUsage({ liveData }: NodeLiveData) {
{ const usage = (liveData.disk.used / liveData.disk.size) * 100;
id: "node-01",
name: "node-01", return (
hostname: "dockship-node-01.local", <div>
ip: "10.0.1.10", <div className="text-xs text-muted-foreground mb-2">Disk</div>
os: "Ubuntu 22.04 LTS", <div className="stat-value mb-2">{usage.toFixed(0)}%</div>
docker: "24.0.7", <Progress value={usage} className="w-full" />
cpu: { cores: 8, usage: 72 }, <div className="text-[10px] text-muted-foreground mt-1 font-mono">
memory: { total: "32 GB", used: "21.8 GB", percentage: 68 }, {formatBytes(liveData.disk.used)} / {formatBytes(liveData.disk.size)}
disk: { total: "500 GB", used: "245 GB", percentage: 49 }, </div>
containers: 8, </div>
status: "running" as const, );
labels: ["production", "primary"], }
},
{
id: "node-02",
name: "node-02",
hostname: "dockship-node-02.local",
ip: "10.0.1.11",
os: "Ubuntu 22.04 LTS",
docker: "24.0.7",
cpu: { cores: 8, usage: 45 },
memory: { total: "32 GB", used: "16.6 GB", percentage: 52 },
disk: { total: "500 GB", used: "178 GB", percentage: 36 },
containers: 6,
status: "running" as const,
labels: ["production"],
},
{
id: "node-03",
name: "node-03",
hostname: "dockship-node-03.local",
ip: "10.0.1.12",
os: "Ubuntu 22.04 LTS",
docker: "24.0.7",
cpu: { cores: 16, usage: 89 },
memory: { total: "64 GB", used: "54.4 GB", percentage: 85 },
disk: { total: "1 TB", used: "612 GB", percentage: 61 },
containers: 12,
status: "running" as const,
labels: ["production", "high-memory"],
},
];
export default function NodesPage() { export default function NodesPage() {
const [nodes, setNodes] = useState<(NodeLiveData & Node)[]>([]);
useEffect(() => {
const fetchNodes = async () => {
const nodes = await getLiveNodes();
setNodes(nodes);
};
fetchNodes();
const interval = setInterval(fetchNodes, 3000);
return () => clearInterval(interval);
}, []);
return ( return (
<DashboardLayout title="Nodes" subtitle="Cluster node management"> <DashboardLayout title="Nodes" subtitle="Cluster node management">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between mb-6 gap-2"> <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between mb-6 gap-2">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<Badge>{nodes.length} nodes</Badge> <Badge>{nodes.length} nodes</Badge>
<Badge>{nodes.filter((n) => n.status === "running").length} online</Badge> <Badge>
{/* {nodes.filter((n) => n.status === "running").length} online */}
</Badge>
</div> </div>
<Button size="sm" className="gap-2"> <Button size="sm" className="gap-2">
<Plus className="w-4 h-4" /> Add Node <Plus className="w-4 h-4" /> Add Node
@@ -68,7 +55,7 @@ export default function NodesPage() {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{nodes.map((node) => ( {nodes.map((node) => (
<Card key={node.id}> <Card key={node.node_id}>
<CardHeader className="flex flex-col sm:flex-row sm:items-start sm:justify-between pb-2 gap-2 sm:gap-0"> <CardHeader className="flex flex-col sm:flex-row sm:items-start sm:justify-between pb-2 gap-2 sm:gap-0">
<div className="flex flex-col sm:flex-row sm:items-center gap-3"> <div className="flex flex-col sm:flex-row sm:items-center gap-3">
<div className="w-10 h-10 bg-secondary flex items-center justify-center"> <div className="w-10 h-10 bg-secondary flex items-center justify-center">
@@ -76,17 +63,22 @@ export default function NodesPage() {
</div> </div>
<div> <div>
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<StatusIndicator status={node.status} /> {/* <StatusIndicator status={node.status} /> */}
<CardTitle className="font-mono">{node.name}</CardTitle> <CardTitle className="font-mono">{node.name}</CardTitle>
</div> </div>
<div className="text-xs text-muted-foreground font-mono"> <div className="text-xs text-muted-foreground font-mono">
{node.hostname} • {node.ip} {node.ip} • {node.ip}
</div> </div>
</div> </div>
</div> </div>
<div className="flex flex-wrap items-center gap-2 mt-2 sm:mt-0"> <div className="flex flex-wrap items-center gap-2 mt-2 sm:mt-0">
{node.labels.map((label) => ( {node.labels &&
<Badge key={label} variant="outline" className="text-[10px]"> node.labels.map((label) => (
<Badge
key={label}
variant="outline"
className="text-[10px]"
>
{label} {label}
</Badge> </Badge>
))} ))}
@@ -102,19 +94,21 @@ export default function NodesPage() {
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-5 gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-5 gap-6">
{/* System Info */} {/* System Info */}
<div className="border-b sm:border-b-0 sm:border-r border-border pb-2 sm:pb-0 pr-0 sm:pr-6"> <div className="border-b sm:border-b-0 sm:border-r border-border pb-2 sm:pb-0 pr-0 sm:pr-6">
<div className="text-xs text-muted-foreground mb-2">System</div> <div className="text-xs text-muted-foreground mb-2">
System
</div>
<div className="space-y-1"> <div className="space-y-1">
<div className="flex justify-between text-xs"> <div className="flex justify-between text-xs">
<span className="text-muted-foreground">OS</span> <span className="text-muted-foreground">OS</span>
<span className="font-mono">{node.os}</span> {/* <span className="font-mono">{node.os}</span> */}
</div> </div>
<div className="flex justify-between text-xs"> <div className="flex justify-between text-xs">
<span className="text-muted-foreground">Docker</span> <span className="text-muted-foreground">Docker</span>
<span className="font-mono">{node.docker}</span> {/* <span className="font-mono">{node.docker}</span> */}
</div> </div>
<div className="flex justify-between text-xs"> <div className="flex justify-between text-xs">
<span className="text-muted-foreground">Containers</span> <span className="text-muted-foreground">Containers</span>
<span className="font-mono">{node.containers}</span> {/* <span className="font-mono">{node.containers}</span> */}
</div> </div>
</div> </div>
</div> </div>
@@ -122,35 +116,43 @@ export default function NodesPage() {
{/* CPU */} {/* CPU */}
<div> <div>
<div className="text-xs text-muted-foreground mb-2"> <div className="text-xs text-muted-foreground mb-2">
CPU ({node.cpu.cores} cores) CPU ({node.liveData.cpu.cores} cores)
</div> </div>
<div className="stat-value mb-2">{node.cpu.usage}%</div> <div className="stat-value mb-2">
<Progress value={node.cpu.usage} className="w-full" /> {node.liveData.cpu.usage.toFixed(0)}%
</div>
<Progress
value={node.liveData.cpu.usage}
className="w-full"
/>
</div> </div>
{/* Memory */} {/* Memory */}
<div> <div>
<div className="text-xs text-muted-foreground mb-2">Memory</div> <div className="text-xs text-muted-foreground mb-2">
<div className="stat-value mb-2">{node.memory.percentage}%</div> Memory
<Progress value={node.memory.percentage} className="w-full" /> </div>
<div className="stat-value mb-2">
{node.liveData.memory.usage.toFixed(0)}%
</div>
<Progress
value={node.liveData.memory.usage}
className="w-full"
/>
<div className="text-[10px] text-muted-foreground mt-1 font-mono"> <div className="text-[10px] text-muted-foreground mt-1 font-mono">
{node.memory.used} / {node.memory.total} {formatBytes(node.liveData.memory.used)} /{" "}
{formatBytes(node.liveData.memory.total)}
</div> </div>
</div> </div>
{/* Disk */} {/* Disk */}
<div> <DiskUsage liveData={node.liveData} />
<div className="text-xs text-muted-foreground mb-2">Disk</div>
<div className="stat-value mb-2">{node.disk.percentage}%</div>
<Progress value={node.disk.percentage} className="w-full" />
<div className="text-[10px] text-muted-foreground mt-1 font-mono">
{node.disk.used} / {node.disk.total}
</div>
</div>
{/* Quick Actions */} {/* Quick Actions */}
<div> <div>
<div className="text-xs text-muted-foreground mb-2">Quick Actions</div> <div className="text-xs text-muted-foreground mb-2">
Quick Actions
</div>
<div className="space-y-1"> <div className="space-y-1">
<Button size="sm" className="w-full justify-start"> <Button size="sm" className="w-full justify-start">
SSH Connect SSH Connect
+2 -2
View File
@@ -16,7 +16,7 @@ import {
} from "lucide-react"; } from "lucide-react";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import Link from "next/link"; import Link from "next/link";
import { getLiveNodes, getNodes } from "@/core/client/node"; import { getLiveNodes } from "@/core/client/node";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Node, NodeLiveData } from "@/lib/types"; import { Node, NodeLiveData } from "@/lib/types";
@@ -90,7 +90,7 @@ const applications = [
].slice(-5); ].slice(-5);
export default function OverviewPage() { export default function OverviewPage() {
const [nodes, setNodes] = useState<NodeLiveData[]>([]); const [nodes, setNodes] = useState<(NodeLiveData & Node)[]>([]);
useEffect(() => { useEffect(() => {
const fetchNodes = async () => { const fetchNodes = async () => {
+2 -2
View File
@@ -17,12 +17,12 @@ export async function getNode(node_id: string): Promise<Node | undefined> {
return nodes.find((v) => v.node_id === node_id); return nodes.find((v) => v.node_id === node_id);
} }
export async function getLiveNodes(): Promise<NodeLiveData[]> { export async function getLiveNodes(): Promise<(NodeLiveData & Node)[]> {
const nodes = await read(() => [], "data", "nodes.json"); const nodes = await read(() => [], "data", "nodes.json");
return await Promise.all(nodes.map(getLiveNode)); return await Promise.all(nodes.map(getLiveNode));
} }
export async function getLiveNode(node: Node): Promise<NodeLiveData> { export async function getLiveNode(node: Node): Promise<NodeLiveData & Node> {
const data = await ( const data = await (
await axios.get(`http://${node.ip}:3000/api/status`, { await axios.get(`http://${node.ip}:3000/api/status`, {
headers: { Authorization: `ApiKey ${node.key}` }, headers: { Authorization: `ApiKey ${node.key}` },
+11
View File
@@ -0,0 +1,11 @@
export function formatBytes(bytes: number, decimals = 2) {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB", "TB", "PB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))} ${
sizes[i]
}`;
}
+8 -6
View File
@@ -1,15 +1,12 @@
export interface Node { export interface Node {
name: string;
node_id: string; node_id: string;
ip: string;
key: string; key: string;
ip: string;
name: string;
labels?: string[];
} }
export interface NodeLiveData { export interface NodeLiveData {
name: string;
node_id: string;
ip: string;
key: string;
liveData: { liveData: {
cpu: { cpu: {
manufacturer: string; manufacturer: string;
@@ -24,6 +21,11 @@ export interface NodeLiveData {
usage: number; usage: number;
}; };
disk: { disk: {
size: number;
available: number;
used: number;
};
disks: {
fs: string; fs: string;
size: number; size: number;
used: number; used: number;