From eed4a0e438a1d39fd3a045f1787ea97fcf19328f Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sat, 27 Dec 2025 13:42:43 +0100 Subject: [PATCH 01/15] Nodes --- src/app/api/file.ts | 14 ++++++- src/app/api/status/route.ts | 0 src/app/overview/page.tsx | 59 ++++++++++++------------------ src/core/client/docker.ts | 12 ++++++ src/core/client/docker.tsx | 14 ------- src/core/client/node.ts | 32 ++++++++++++++++ src/core/{docker.tsx => docker.ts} | 0 src/lib/types.ts | 20 ++++++++++ 8 files changed, 100 insertions(+), 51 deletions(-) create mode 100644 src/app/api/status/route.ts create mode 100644 src/core/client/docker.ts delete mode 100644 src/core/client/docker.tsx create mode 100644 src/core/client/node.ts rename src/core/{docker.tsx => docker.ts} (100%) create mode 100644 src/lib/types.ts diff --git a/src/app/api/file.ts b/src/app/api/file.ts index de06894..caceba5 100644 --- a/src/app/api/file.ts +++ b/src/app/api/file.ts @@ -13,9 +13,21 @@ export async function read( await fs.access(filePath); } catch { await fs.mkdir(dirPath, { recursive: true }); - await fs.writeFile(filePath, JSON.stringify(template, null, 2), "utf-8"); + await fs.writeFile(filePath, JSON.stringify(template(), null, 2), "utf-8"); } const content = await fs.readFile(filePath, "utf-8"); return JSON.parse(content); } + +export async function write( + val: T, + fp: string, + ...filePaths: string[] +) { + const filePath = path.join(process.cwd(), fp, ...filePaths); + const dirPath = path.dirname(filePath); + + await fs.mkdir(dirPath, { recursive: true }); + await fs.writeFile(filePath, JSON.stringify(val, null, 2), "utf-8"); +} diff --git a/src/app/api/status/route.ts b/src/app/api/status/route.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/app/overview/page.tsx b/src/app/overview/page.tsx index 325a6b6..719dec7 100644 --- a/src/app/overview/page.tsx +++ b/src/app/overview/page.tsx @@ -16,6 +16,9 @@ import { } from "lucide-react"; import { Progress } from "@/components/ui/progress"; import Link from "next/link"; +import { getLiveNodes, getNodes } from "@/core/client/node"; +import { useEffect, useState } from "react"; +import { Node, NodeLiveData } from "@/lib/types"; const recentDeployments = [ { @@ -86,34 +89,13 @@ const applications = [ }, ]; -const nodes = [ - { - name: "node-01", - ip: "10.0.1.10", - cpu: 72, - memory: 68, - containers: 8, - status: "running" as const, - }, - { - name: "node-02", - ip: "10.0.1.11", - cpu: 45, - memory: 52, - containers: 6, - status: "running" as const, - }, - { - name: "node-03", - ip: "10.0.1.12", - cpu: 89, - memory: 85, - containers: 12, - status: "running" as const, - }, -]; - export default function OverviewPage() { + const [nodes, setNodes] = useState([]); + + useEffect(() => { + getLiveNodes().then(setNodes); + }, []); + return ( {/* Stats Grid */} @@ -246,7 +228,7 @@ export default function OverviewPage() { >
- + {node.name}
@@ -256,20 +238,25 @@ export default function OverviewPage() {
CPU - {node.cpu}% -
- -
- Memory - {node.memory}% + {node.liveData.cpu}%
+
+ Memory + {node.liveData.memory}% +
+
Containers - {node.containers} + + {node.liveData.containers} +
diff --git a/src/core/client/docker.ts b/src/core/client/docker.ts new file mode 100644 index 0000000..60103c8 --- /dev/null +++ b/src/core/client/docker.ts @@ -0,0 +1,12 @@ +"use server"; + +import axios from "axios"; +import { Node } from "@/lib/types"; + +export async function getDocker(node: Node): Promise<{ version: string }> { + return await ( + await axios.get(`http://${node.ip}:3000/api/get-docker`, { + headers: { Authorization: `ApiKey ${node.key}` }, + }) + ).data; +} diff --git a/src/core/client/docker.tsx b/src/core/client/docker.tsx deleted file mode 100644 index dcffbf8..0000000 --- a/src/core/client/docker.tsx +++ /dev/null @@ -1,14 +0,0 @@ -"use server"; - -import axios from "axios"; - -export async function getDocker( - node_ip: string, - key: string -): Promise<{ version: string }> { - return await ( - await axios.get(`http://${node_ip}:3000/api/get-docker`, { - headers: { Authorization: `ApiKey ${key}` }, - }) - ).data; -} diff --git a/src/core/client/node.ts b/src/core/client/node.ts new file mode 100644 index 0000000..aada4c6 --- /dev/null +++ b/src/core/client/node.ts @@ -0,0 +1,32 @@ +"use server"; + +import { read, write } from "@/app/api/file"; +import { Node, NodeLiveData } from "@/lib/types"; +import axios from "axios"; + +export async function getNodes(): Promise { + return await read(() => [], "data", "nodes.json"); +} + +export async function setNodes(nodes: Node[]) { + await write(nodes, "data", "nodes.json"); +} + +export async function getNode(node_id: string): Promise { + const nodes = await read(() => [], "data", "nodes.json"); + return nodes.find((v) => v.node_id === node_id); +} + +export async function getLiveNodes(): Promise { + const nodes = await read(() => [], "data", "nodes.json"); + return await Promise.all(nodes.map(getLiveNode)); +} + +export async function getLiveNode(node: Node): Promise { + const data = await ( + await axios.get(`http://${node.ip}:3000/api/status`, { + headers: { Authorization: `ApiKey ${node.key}` }, + }) + ).data; + return { ...node, liveData: data.liveData }; +} diff --git a/src/core/docker.tsx b/src/core/docker.ts similarity index 100% rename from src/core/docker.tsx rename to src/core/docker.ts diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..abb3b06 --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,20 @@ +export interface Node { + name: string; + node_id: string; + ip: string; + key: string; +} + +export interface NodeLiveData { + name: string; + node_id: string; + ip: string; + key: string; + liveData: { + status: "running" | "pending" | "error"; + cpu: number; + memory: number; + disk: number; + containers: number; + }; +} From 1ab87edacdd2cf35f1076b0233a7d3d67adc1ae3 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sat, 27 Dec 2025 14:03:51 +0100 Subject: [PATCH 02/15] Node status --- package-lock.json | 27 +++++++++++++++++++ package.json | 1 + src/app/api/status/route.ts | 52 +++++++++++++++++++++++++++++++++++++ src/app/overview/page.tsx | 28 +++++++++++++------- src/lib/types.ts | 28 ++++++++++++++++---- 5 files changed, 121 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 061bbc3..1f7a453 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "react": "19.2.3", "react-dom": "19.2.3", "recharts": "^2.15.4", + "systeminformation": "^5.28.2", "tailwind-merge": "^3.4.0" }, "devDependencies": { @@ -3982,6 +3983,32 @@ } } }, + "node_modules/systeminformation": { + "version": "5.28.2", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.28.2.tgz", + "integrity": "sha512-AWrPnmbIcfjLX7wiQMd7r6bl3PDaGoqvrfn8pCLJbk9CiB+vQXTk8RVxNs2vQRTezYlhb9skxEIyAPTm1If5bQ==", + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos", + "android" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, "node_modules/tailwind-merge": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", diff --git a/package.json b/package.json index 2d826e4..1c255a6 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "react": "19.2.3", "react-dom": "19.2.3", "recharts": "^2.15.4", + "systeminformation": "^5.28.2", "tailwind-merge": "^3.4.0" }, "devDependencies": { diff --git a/src/app/api/status/route.ts b/src/app/api/status/route.ts index e69de29..da426a0 100644 --- a/src/app/api/status/route.ts +++ b/src/app/api/status/route.ts @@ -0,0 +1,52 @@ +import { NextRequest, NextResponse } from "next/server"; +import testAuth from "../auth"; +import { StatusCodes } from "http-status-codes"; +import si from "systeminformation"; + +export async function GET(req: NextRequest): Promise { + const auth = await testAuth(req); + + if (auth) { + return NextResponse.json( + { message: auth }, + { status: StatusCodes.UNAUTHORIZED } + ); + } + + const [cpu, mem, load, disk, network] = await Promise.all([ + si.cpu(), + si.mem(), + si.currentLoad(), + si.fsSize(), + si.networkStats(), + ]); + + return NextResponse.json({ + message: "ok", + liveData: { + cpu: { + manufacturer: cpu.manufacturer, + brand: cpu.brand, + cores: cpu.cores, + usage: load.currentLoad, + }, + memory: { + total: mem.total, + used: mem.used, + free: mem.free, + usage: ((mem.total - mem.free) / mem.total) * 100, + }, + disk: disk.map((d) => ({ + fs: d.fs, + size: d.size, + used: d.used, + usage: d.use, + })), + network: network.map((n) => ({ + iface: n.iface, + rx: n.rx_bytes, + tx: n.tx_bytes, + })), + }, + }); +} diff --git a/src/app/overview/page.tsx b/src/app/overview/page.tsx index 719dec7..5672b1f 100644 --- a/src/app/overview/page.tsx +++ b/src/app/overview/page.tsx @@ -87,13 +87,19 @@ const applications = [ memory: 48, status: "pending" as const, }, -]; +].slice(-5); export default function OverviewPage() { const [nodes, setNodes] = useState([]); useEffect(() => { - getLiveNodes().then(setNodes); + const fetchNodes = async () => { + const nodes = await getLiveNodes(); + setNodes(nodes); + }; + fetchNodes(); + const interval = setInterval(fetchNodes, 3000); + return () => clearInterval(interval); }, []); return ( @@ -228,7 +234,7 @@ export default function OverviewPage() { >
- + {node.name}
@@ -238,25 +244,27 @@ export default function OverviewPage() {
CPU - {node.liveData.cpu}% + + {node.liveData.cpu.usage.toFixed(0)}% +
Memory - {node.liveData.memory}% + + {node.liveData.memory.usage.toFixed(0)}% +
Containers - - {node.liveData.containers} - + {6}
diff --git a/src/lib/types.ts b/src/lib/types.ts index abb3b06..e8f981b 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -11,10 +11,28 @@ export interface NodeLiveData { ip: string; key: string; liveData: { - status: "running" | "pending" | "error"; - cpu: number; - memory: number; - disk: number; - containers: number; + cpu: { + manufacturer: string; + brand: string; + cores: number; + usage: number; + }; + memory: { + total: number; + used: number; + free: number; + usage: number; + }; + disk: { + fs: string; + size: number; + used: number; + usage: number; + }[]; + network: { + iface: string; + rx: number; + tx: number; + }[]; }; } From e8dc4a4590cef73113e908315f1e61f0671890ca Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sat, 27 Dec 2025 14:43:32 +0100 Subject: [PATCH 03/15] Node live data --- src/app/api/status/route.ts | 14 +++- src/app/nodes/page.tsx | 150 ++++++++++++++++++------------------ src/app/overview/page.tsx | 4 +- src/core/client/node.ts | 4 +- src/lib/format.ts | 11 +++ src/lib/types.ts | 14 ++-- 6 files changed, 111 insertions(+), 86 deletions(-) create mode 100644 src/lib/format.ts diff --git a/src/app/api/status/route.ts b/src/app/api/status/route.ts index da426a0..cc10a22 100644 --- a/src/app/api/status/route.ts +++ b/src/app/api/status/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import testAuth from "../auth"; import { StatusCodes } from "http-status-codes"; import si from "systeminformation"; +import { NodeLiveData } from "@/lib/types"; export async function GET(req: NextRequest): Promise { const auth = await testAuth(req); @@ -21,6 +22,10 @@ export async function GET(req: NextRequest): Promise { si.networkStats(), ]); + const disks = await si.fsSize(); + + const root = disks.find((d) => d.mount === "/" || d.fs.includes("disk3")); + return NextResponse.json({ message: "ok", liveData: { @@ -36,7 +41,12 @@ export async function GET(req: NextRequest): Promise { free: mem.free, 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, size: d.size, used: d.used, @@ -48,5 +58,5 @@ export async function GET(req: NextRequest): Promise { tx: n.tx_bytes, })), }, - }); + } as NodeLiveData); } diff --git a/src/app/nodes/page.tsx b/src/app/nodes/page.tsx index 973de4b..2cc30ff 100644 --- a/src/app/nodes/page.tsx +++ b/src/app/nodes/page.tsx @@ -4,62 +4,49 @@ import { DashboardLayout } from "@/components/dashboard/DashboardLayout"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { StatusIndicator } from "@/components/dashboard/StatusIndicator"; import { Server, Plus, Settings, Trash2 } from "lucide-react"; 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 = [ - { - id: "node-01", - name: "node-01", - hostname: "dockship-node-01.local", - ip: "10.0.1.10", - os: "Ubuntu 22.04 LTS", - docker: "24.0.7", - cpu: { cores: 8, usage: 72 }, - memory: { total: "32 GB", used: "21.8 GB", percentage: 68 }, - disk: { total: "500 GB", used: "245 GB", percentage: 49 }, - containers: 8, - 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"], - }, -]; +function DiskUsage({ liveData }: NodeLiveData) { + const usage = (liveData.disk.used / liveData.disk.size) * 100; + + return ( +
+
Disk
+
{usage.toFixed(0)}%
+ +
+ {formatBytes(liveData.disk.used)} / {formatBytes(liveData.disk.size)} +
+
+ ); +} 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 (
{nodes.length} nodes - {nodes.filter((n) => n.status === "running").length} online + + {/* {nodes.filter((n) => n.status === "running").length} online */} +
@@ -102,19 +94,21 @@ export default function NodesPage() {
{/* System Info */}
-
System
+
+ System +
OS - {node.os} + {/* {node.os} */}
Docker - {node.docker} + {/* {node.docker} */}
Containers - {node.containers} + {/* {node.containers} */}
@@ -122,35 +116,43 @@ export default function NodesPage() { {/* CPU */}
- CPU ({node.cpu.cores} cores) + CPU ({node.liveData.cpu.cores} cores)
-
{node.cpu.usage}%
- +
+ {node.liveData.cpu.usage.toFixed(0)}% +
+
{/* Memory */}
-
Memory
-
{node.memory.percentage}%
- +
+ Memory +
+
+ {node.liveData.memory.usage.toFixed(0)}% +
+
- {node.memory.used} / {node.memory.total} + {formatBytes(node.liveData.memory.used)} /{" "} + {formatBytes(node.liveData.memory.total)}
{/* Disk */} -
-
Disk
-
{node.disk.percentage}%
- -
- {node.disk.used} / {node.disk.total} -
-
+ {/* Quick Actions */}
-
Quick Actions
+
+ Quick Actions +
@@ -100,15 +103,19 @@ export default function NodesPage() {
OS - {/* {node.os} */} + {node.os}
Docker - {/* {node.docker} */} + + {dockerNodes[i]?.version} +
Containers - {/* {node.containers} */} + + {dockerNodes[i]?.containers.length} +
diff --git a/src/core/client/node.ts b/src/core/client/node.ts index 1add48b..5358031 100644 --- a/src/core/client/node.ts +++ b/src/core/client/node.ts @@ -1,7 +1,7 @@ "use server"; import { read, write } from "@/app/api/file"; -import { Node, NodeLiveData } from "@/lib/types"; +import { Docker, Node, NodeLiveData } from "@/lib/types"; import axios from "axios"; export async function getNodes(): Promise { @@ -17,11 +17,6 @@ export async function getNode(node_id: string): Promise { return nodes.find((v) => v.node_id === node_id); } -export async function getLiveNodes(): Promise<(NodeLiveData & Node)[]> { - const nodes = await read(() => [], "data", "nodes.json"); - return await Promise.all(nodes.map(getLiveNode)); -} - export async function getLiveNode(node: Node): Promise { const data = await ( await axios.get(`http://${node.ip}:3000/api/status`, { @@ -30,3 +25,17 @@ export async function getLiveNode(node: Node): Promise { ).data; return { ...node, liveData: data.liveData }; } + +export async function getLiveNodes(): Promise<(NodeLiveData & Node)[]> { + const nodes = await read(() => [], "data", "nodes.json"); + return await Promise.all(nodes.map(getLiveNode)); +} + +export async function getDockerNode(node: Node): Promise { + const data = await ( + await axios.get(`http://${node.ip}:3000/api/get-docker`, { + headers: { Authorization: `ApiKey ${node.key}` }, + }) + ).data; + return { ...node, containers: data.containers, version: data.version }; +} diff --git a/src/core/docker.ts b/src/core/docker.ts index 2fbb53b..fdb79a2 100644 --- a/src/core/docker.ts +++ b/src/core/docker.ts @@ -6,6 +6,6 @@ const docker = new Docker(); export async function getDocker() { const version = await docker.version(); - console.log("Docker version:", version); - return version; + const containers = await docker.listContainers(); + return { version, containers }; } diff --git a/src/lib/types.ts b/src/lib/types.ts index d136f5f..5a69e60 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,9 +1,12 @@ +import { ContainerInfo } from "dockerode"; + export interface Node { node_id: string; key: string; ip: string; + os: string; name: string; - labels?: string[]; + labels: string[]; } export interface NodeLiveData { @@ -38,3 +41,8 @@ export interface NodeLiveData { }[]; }; } + +export interface Docker { + version: string; + containers: ContainerInfo[]; +} From 97d374920b71bdb3636dfd035e88c0586dee3fdd Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sat, 27 Dec 2025 22:20:51 +0100 Subject: [PATCH 05/15] Fixed memory, and updated color scheme --- src/app/api/status/route.ts | 4 +- src/app/globals.css | 242 +++++++++++++++++++++++------------- src/app/monitoring/page.tsx | 16 +-- 3 files changed, 166 insertions(+), 96 deletions(-) diff --git a/src/app/api/status/route.ts b/src/app/api/status/route.ts index cc10a22..4a0ace8 100644 --- a/src/app/api/status/route.ts +++ b/src/app/api/status/route.ts @@ -26,6 +26,8 @@ export async function GET(req: NextRequest): Promise { const root = disks.find((d) => d.mount === "/" || d.fs.includes("disk3")); + console.log(mem.available, mem.total); + return NextResponse.json({ message: "ok", liveData: { @@ -39,7 +41,7 @@ export async function GET(req: NextRequest): Promise { total: mem.total, used: mem.used, free: mem.free, - usage: ((mem.total - mem.free) / mem.total) * 100, + usage: ((mem.total - mem.available) / mem.total) * 100, }, disk: { used: (root?.size || 0) - (root?.available || 0), diff --git a/src/app/globals.css b/src/app/globals.css index 0402e06..35672be 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -45,96 +45,164 @@ } :root { - --background: oklch(0.97 0.02 225.66); - --foreground: oklch(0.15 0.02 269.18); - --card: oklch(0.98 0.01 228.79); - --card-foreground: oklch(0.15 0.02 269.18); - --popover: oklch(0.98 0.01 238.45); - --popover-foreground: oklch(0.15 0.02 269.18); - --primary: oklch(0.71 0.15 239.15); - --primary-foreground: oklch(0.94 0.03 232.39); - --secondary: oklch(0.91 0.03 229.20); - --secondary-foreground: oklch(0.15 0.02 269.18); - --muted: oklch(0.89 0.02 225.69); - --muted-foreground: oklch(0.36 0.03 231.55); - --accent: oklch(0.88 0.02 235.72); - --accent-foreground: oklch(0.34 0.05 229.20); - --destructive: oklch(0.61 0.24 20.96); - --border: oklch(0.82 0.02 240.77); - --input: oklch(0.82 0.02 240.77); - --ring: oklch(0.55 0.10 235.72); - --chart-1: oklch(0.57 0.11 228.92); - --chart-2: oklch(0.45 0.10 270.08); - --chart-3: oklch(0.65 0.15 159.03); - --chart-4: oklch(0.75 0.10 100.01); - --chart-5: oklch(0.55 0.15 299.88); - --sidebar: oklch(0.93 0.01 238.46); - --sidebar-foreground: oklch(0.15 0.02 269.18); - --sidebar-primary: oklch(0.57 0.11 228.92); - --sidebar-primary-foreground: oklch(0.99 0.01 203.97); - --sidebar-accent: oklch(0.88 0.02 235.72); - --sidebar-accent-foreground: oklch(0.15 0.02 269.18); - --sidebar-border: oklch(0.82 0.02 240.77); - --sidebar-ring: oklch(0.57 0.11 228.92); - - --font-sans: 'Source Code Pro', 'Geist', 'Geist Fallback', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; - --font-serif: 'Source Serif 4', 'Geist', 'Geist Fallback', ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif; - --font-mono: 'Source Code Pro', 'Geist Mono', 'Geist Mono Fallback', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; - - --radius: 0rem; - - --shadow-2xs: 0px 1px 2.5px 0px oklch(0.49 0.09 235.00 / 0.03); - --shadow-xs: 0px 1px 2.5px 0px oklch(0.49 0.09 235.00 / 0.03); - --shadow-sm: 0px 1px 2.5px 0px oklch(0.49 0.09 235.00 / 0.06), 0px 1px 2px -1px oklch(0.49 0.09 235.00 / 0.06); - --shadow: 0px 1px 2.5px 0px oklch(0.49 0.09 235.00 / 0.06), 0px 1px 2px -1px oklch(0.49 0.09 235.00 / 0.06); - --shadow-md: 0px 1px 2.5px 0px oklch(0.49 0.09 235.00 / 0.06), 0px 2px 4px -1px oklch(0.49 0.09 235.00 / 0.06); - --shadow-lg: 0px 1px 2.5px 0px oklch(0.49 0.09 235.00 / 0.06), 0px 4px 6px -1px oklch(0.49 0.09 235.00 / 0.06); - --shadow-xl: 0px 1px 2.5px 0px oklch(0.49 0.09 235.00 / 0.06), 0px 8px 10px -1px oklch(0.49 0.09 235.00 / 0.06); - --shadow-2xl: 0px 1px 2.5px 0px oklch(0.49 0.09 235.00 / 0.15); - + --background: oklch(0.9842 0.0010 264.5478); + --foreground: oklch(0.2115 0.0072 264.4099); + --card: oklch(1.0000 0 0); + --card-foreground: oklch(0.2115 0.0072 264.4099); + --popover: oklch(1.0000 0 0); + --popover-foreground: oklch(0.2115 0.0072 264.4099); + --primary: oklch(0.5096 0.1483 253.4619); + --primary-foreground: oklch(1.0000 0 0); + --secondary: oklch(0.9210 0.0088 247.9219); + --secondary-foreground: oklch(0.4114 0.0323 248.6180); + --muted: oklch(0.9607 0.0044 247.8691); + --muted-foreground: oklch(0.5921 0.0247 248.2437); + --accent: oklch(0.9607 0.0044 247.8691); + --accent-foreground: oklch(0.4144 0.0162 248.2179); + --destructive: oklch(0.5875 0.2303 28.2622); + --destructive-foreground: oklch(1.0000 0 0); + --border: oklch(0.9050 0.0107 247.9443); + --input: oklch(0.9210 0.0088 247.9219); + --ring: oklch(0.5096 0.1483 253.4619); + --chart-1: oklch(0.5096 0.1483 253.4619); + --chart-2: oklch(0.6610 0.1560 154.8128); + --chart-3: oklch(0.7139 0.1737 55.1486); + --chart-4: oklch(0.5103 0.2201 301.6038); + --chart-5: oklch(0.5875 0.2303 28.2622); + --sidebar: oklch(0.9605 0.0024 264.5432); + --sidebar-foreground: oklch(0.2639 0.0102 264.3844); + --sidebar-primary: oklch(0.5546 0.1629 253.5932); + --sidebar-primary-foreground: oklch(1.0000 0 0); + --sidebar-accent: oklch(0.9210 0.0088 247.9219); + --sidebar-accent-foreground: oklch(0.4144 0.0162 248.2179); + --sidebar-border: oklch(0.8809 0.0134 247.9797); + --sidebar-ring: oklch(0.5096 0.1483 253.4619); + --font-sans: 'Inter', sans-serif; + --font-serif: 'Lora', serif; + --font-mono: 'Fira Code', monospace; + --radius: 0.5rem; + --shadow-x: 0rem; + --shadow-y: 0.25rem; + --shadow-blur: 0.5rem; + --shadow-spread: 0rem; + --shadow-opacity: 0.1; + --shadow-color: 0 0 0; + --shadow-2xs: 0rem 0.25rem 0.5rem 0rem hsl(0 0 0 / 0.05); + --shadow-xs: 0rem 0.25rem 0.5rem 0rem hsl(0 0 0 / 0.05); + --shadow-sm: 0rem 0.25rem 0.5rem 0rem hsl(0 0 0 / 0.10), 0rem 1px 2px -1px hsl(0 0 0 / 0.10); + --shadow: 0rem 0.25rem 0.5rem 0rem hsl(0 0 0 / 0.10), 0rem 1px 2px -1px hsl(0 0 0 / 0.10); + --shadow-md: 0rem 0.25rem 0.5rem 0rem hsl(0 0 0 / 0.10), 0rem 2px 4px -1px hsl(0 0 0 / 0.10); + --shadow-lg: 0rem 0.25rem 0.5rem 0rem hsl(0 0 0 / 0.10), 0rem 4px 6px -1px hsl(0 0 0 / 0.10); + --shadow-xl: 0rem 0.25rem 0.5rem 0rem hsl(0 0 0 / 0.10), 0rem 8px 10px -1px hsl(0 0 0 / 0.10); + --shadow-2xl: 0rem 0.25rem 0.5rem 0rem hsl(0 0 0 / 0.25); + --tracking-normal: 0em; + --spacing: 0.25rem; } .dark { - --background: oklch(0.18 0.02 271.27); - --foreground: oklch(0.90 0.01 238.47); - --card: oklch(0.22 0.02 271.67); - --card-foreground: oklch(0.90 0.01 238.47); - --popover: oklch(0.22 0.02 271.67); - --popover-foreground: oklch(0.90 0.01 238.47); - --primary: oklch(52.5% 0.105 236); - --primary-foreground: oklch(95.1% 0.00932 232.789); - --secondary: oklch(0.28 0.03 270.91); - --secondary-foreground: oklch(0.90 0.01 238.47); - --muted: oklch(0.28 0.03 270.91); - --muted-foreground: oklch(0.60 0.03 269.46); - --accent: oklch(0.28 0.03 270.91); - --accent-foreground: oklch(0.90 0.01 238.47); - --destructive: oklch(0.64 0.25 19.69); - --border: oklch(0.90 0.01 238.47 / 15%); - --input: oklch(0.90 0.01 238.47 / 20%); - --ring: oklch(0.66 0.13 227.70); - --chart-1: oklch(0.66 0.13 227.70); - --chart-2: oklch(0.60 0.10 269.83); - --chart-3: oklch(0.70 0.15 159.83); - --chart-4: oklch(0.80 0.10 100.65); - --chart-5: oklch(0.60 0.15 300.14); - --sidebar: oklch(0.22 0.02 271.67); - --sidebar-foreground: oklch(0.90 0.01 238.47); - --sidebar-primary: oklch(0.66 0.13 227.70); - --sidebar-primary-foreground: oklch(0.18 0.02 271.27); - --sidebar-accent: oklch(0.28 0.03 270.91); - --sidebar-accent-foreground: oklch(0.90 0.01 238.47); - --sidebar-border: oklch(0.90 0.01 238.47 / 15%); - --sidebar-ring: oklch(0.66 0.13 227.70); - - --shadow-2xs: 0px 1px 2px 0px oklch(0 0 0 / 0.01); - --shadow-xs: 0px 1px 2px 0px oklch(0 0 0 / 0.01); - --shadow-sm: 0px 1px 2px 0px oklch(0 0 0 / 0.01), 0px 1px 2px -1px oklch(0 0 0 / 0.01); - --shadow: 0px 1px 2px 0px oklch(0 0 0 / 0.01), 0px 1px 2px -1px oklch(0 0 0 / 0.01); - --shadow-md: 0px 1px 2px 0px oklch(0 0 0 / 0.01), 0px 2px 4px -1px oklch(0 0 0 / 0.01); - --shadow-lg: 0px 1px 2px 0px oklch(0 0 0 / 0.01), 0px 4px 6px -1px oklch(0 0 0 / 0.01); - --shadow-xl: 0px 1px 2px 0px oklch(0 0 0 / 0.01), 0px 8px 10px -1px oklch(0 0 0 / 0.01); - --shadow-2xl: 0px 1px 2px 0px oklch(0 0 0 / 0.03); + --background: oklch(0.1984 0.0154 256.8228); + --foreground: oklch(0.8809 0.0134 247.9797); + --card: oklch(0.2292 0.0222 256.8398); + --card-foreground: oklch(0.8809 0.0134 247.9797); + --popover: oklch(0.2292 0.0222 256.8398); + --popover-foreground: oklch(0.8809 0.0134 247.9797); + --primary: oklch(0.5096 0.1483 253.4619); + --primary-foreground: oklch(0.9843 0.0017 247.8393); + --secondary: oklch(0.3095 0.0306 256.8416); + --secondary-foreground: oklch(0.8809 0.0134 247.9797); + --muted: oklch(0.2605 0.0240 256.8358); + --muted-foreground: oklch(0.6780 0.0191 248.1098); + --accent: oklch(0.2605 0.0240 256.8358); + --accent-foreground: oklch(0.8809 0.0134 247.9797); + --destructive: oklch(0.5875 0.2303 28.2622); + --destructive-foreground: oklch(0.9843 0.0017 247.8393); + --border: oklch(0.3567 0.0369 256.8455); + --input: oklch(0.3095 0.0306 256.8416); + --ring: oklch(0.5096 0.1483 253.4619); + --chart-1: oklch(0.5096 0.1483 253.4619); + --chart-2: oklch(0.6610 0.1560 154.8128); + --chart-3: oklch(0.7139 0.1737 55.1486); + --chart-4: oklch(0.5103 0.2201 301.6038); + --chart-5: oklch(0.5875 0.2303 28.2622); + --sidebar: oklch(0.2292 0.0222 256.8398); + --sidebar-foreground: oklch(0.9210 0.0088 247.9219); + --sidebar-primary: oklch(0.5096 0.1483 253.4619); + --sidebar-primary-foreground: oklch(0.9843 0.0017 247.8393); + --sidebar-accent: oklch(0.2605 0.0240 256.8358); + --sidebar-accent-foreground: oklch(0.8809 0.0134 247.9797); + --sidebar-border: oklch(0.3567 0.0369 256.8455); + --sidebar-ring: oklch(0.5096 0.1483 253.4619); + --font-sans: 'Roboto Mono', monospace; + --font-serif: 'Roboto Slab', serif; + --font-mono: 'Fira Code', monospace; + --radius: 0.25rem; + --shadow-x: 0rem; + --shadow-y: 0.5rem; + --shadow-blur: 0.75rem; + --shadow-spread: 0rem; + --shadow-opacity: 0.3; + --shadow-color: 0 0 0; + --shadow-2xs: 0rem 0.5rem 0.75rem 0rem hsl(0 0 0 / 0.15); + --shadow-xs: 0rem 0.5rem 0.75rem 0rem hsl(0 0 0 / 0.15); + --shadow-sm: 0rem 0.5rem 0.75rem 0rem hsl(0 0 0 / 0.30), 0rem 1px 2px -1px hsl(0 0 0 / 0.30); + --shadow: 0rem 0.5rem 0.75rem 0rem hsl(0 0 0 / 0.30), 0rem 1px 2px -1px hsl(0 0 0 / 0.30); + --shadow-md: 0rem 0.5rem 0.75rem 0rem hsl(0 0 0 / 0.30), 0rem 2px 4px -1px hsl(0 0 0 / 0.30); + --shadow-lg: 0rem 0.5rem 0.75rem 0rem hsl(0 0 0 / 0.30), 0rem 4px 6px -1px hsl(0 0 0 / 0.30); + --shadow-xl: 0rem 0.5rem 0.75rem 0rem hsl(0 0 0 / 0.30), 0rem 8px 10px -1px hsl(0 0 0 / 0.30); + --shadow-2xl: 0rem 0.5rem 0.75rem 0rem hsl(0 0 0 / 0.75); +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + + --font-sans: var(--font-sans); + --font-mono: var(--font-mono); + --font-serif: var(--font-serif); + + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + + --shadow-2xs: var(--shadow-2xs); + --shadow-xs: var(--shadow-xs); + --shadow-sm: var(--shadow-sm); + --shadow: var(--shadow); + --shadow-md: var(--shadow-md); + --shadow-lg: var(--shadow-lg); + --shadow-xl: var(--shadow-xl); + --shadow-2xl: var(--shadow-2xl); } @theme inline { diff --git a/src/app/monitoring/page.tsx b/src/app/monitoring/page.tsx index bb18e13..ff5903c 100644 --- a/src/app/monitoring/page.tsx +++ b/src/app/monitoring/page.tsx @@ -146,12 +146,12 @@ export default function MonitoringPage() { > @@ -179,7 +179,7 @@ export default function MonitoringPage() { @@ -209,12 +209,12 @@ export default function MonitoringPage() { > @@ -242,7 +242,7 @@ export default function MonitoringPage() { @@ -286,7 +286,7 @@ export default function MonitoringPage() { Date: Sun, 28 Dec 2025 00:46:06 +0100 Subject: [PATCH 06/15] Node management --- package-lock.json | 273 +++++++++++++++++++++++++++++ package.json | 2 + src/app/api/auth/route.ts | 9 +- src/app/api/route.ts | 6 +- src/app/api/status/route.ts | 2 - src/app/nodes/page.tsx | 22 ++- src/app/overview/page.tsx | 7 +- src/app/page.tsx | 14 +- src/components/dialogs/AddNode.tsx | 166 ++++++++++++++++++ src/components/ui/dialog.tsx | 143 +++++++++++++++ src/components/ui/label.tsx | 24 +++ src/components/ui/textarea.tsx | 18 ++ src/core/client/node.ts | 10 ++ src/lib/types.ts | 1 + 14 files changed, 666 insertions(+), 31 deletions(-) create mode 100644 src/components/dialogs/AddNode.tsx create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/components/ui/label.tsx create mode 100644 src/components/ui/textarea.tsx diff --git a/package-lock.json b/package-lock.json index 1f7a453..a3f93d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", @@ -23,6 +24,7 @@ "js-cookie": "^3.0.5", "lucide-react": "^0.562.0", "next": "16.1.1", + "os-name": "^6.1.0", "react": "19.2.3", "react-dom": "19.2.3", "recharts": "^2.15.4", @@ -1247,6 +1249,29 @@ } } }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", @@ -2484,6 +2509,20 @@ "node": ">=10.0.0" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -2805,6 +2844,29 @@ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "license": "MIT" }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, "node_modules/fast-equals": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", @@ -2920,6 +2982,18 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2984,6 +3058,15 @@ "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", "license": "MIT" }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -3028,6 +3111,24 @@ "node": ">=8" } }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -3353,6 +3454,18 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/macos-release": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-3.4.0.tgz", + "integrity": "sha512-wpGPwyg/xrSp4H4Db4xYSeAr6+cFQGHfspHzDUdYxswDnUW0L5Ov63UuJiSr8NMSpyaChO4u1n0MXUvVPtrN6A==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -3372,6 +3485,12 @@ "node": ">= 0.4" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -3393,6 +3512,18 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -3511,6 +3642,33 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3529,6 +3687,46 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-name": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/os-name/-/os-name-6.1.0.tgz", + "integrity": "sha512-zBd1G8HkewNd2A8oQ8c6BN/f/c9EId7rSUueOLGu28govmUctXmM+3765GwsByv9nYUdrLqHphXlYIc86saYsg==", + "license": "MIT", + "dependencies": { + "macos-release": "^3.3.0", + "windows-release": "^6.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3893,6 +4091,39 @@ "@img/sharp-win32-x64": "0.34.5" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3960,6 +4191,18 @@ "node": ">=8" } }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -4200,6 +4443,36 @@ "d3-timer": "^3.0.1" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/windows-release": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-6.1.0.tgz", + "integrity": "sha512-1lOb3qdzw6OFmOzoY0nauhLG72TpWtb5qgYPiSh/62rjc1XidBSDio2qw0pwHh17VINF217ebIkZJdFLZFn9SA==", + "license": "MIT", + "dependencies": { + "execa": "^8.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index 1c255a6..59f0325 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ }, "dependencies": { "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", @@ -23,6 +24,7 @@ "js-cookie": "^3.0.5", "lucide-react": "^0.562.0", "next": "16.1.1", + "os-name": "^6.1.0", "react": "19.2.3", "react-dom": "19.2.3", "recharts": "^2.15.4", diff --git a/src/app/api/auth/route.ts b/src/app/api/auth/route.ts index b2a97fe..37b97e3 100644 --- a/src/app/api/auth/route.ts +++ b/src/app/api/auth/route.ts @@ -2,9 +2,12 @@ import { NextRequest, NextResponse } from "next/server"; import { version } from "@/../package.json"; import testAuth from "../auth"; import { StatusCodes } from "http-status-codes"; +import { read } from "@/app/api/file"; +import { NodeJsonTemplate } from "../types"; +import osName from "os-name"; export async function GET(req: NextRequest): Promise { - const auth = testAuth(req); + const auth = await testAuth(req); if (auth) { return NextResponse.json( @@ -13,9 +16,13 @@ export async function GET(req: NextRequest): Promise { ); } + const { node_id } = await read(NodeJsonTemplate, "data", "node.json"); + return NextResponse.json({ message: "ok", app: "dockship", version, + node_id, + os: osName(), }); } diff --git a/src/app/api/route.ts b/src/app/api/route.ts index bd46b5c..bf529b3 100644 --- a/src/app/api/route.ts +++ b/src/app/api/route.ts @@ -1,10 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { version } from "@/../package.json"; -import { read } from "@/app/api/file"; -import { NodeJsonTemplate } from "@/app/api/types"; export async function GET(req: NextRequest): Promise { - const { node_id } = await read(NodeJsonTemplate, "data", "node.json"); - - return NextResponse.json({ app: "dockship", version, node_id }); + return NextResponse.json({ app: "dockship", version }); } diff --git a/src/app/api/status/route.ts b/src/app/api/status/route.ts index 4a0ace8..a0f1186 100644 --- a/src/app/api/status/route.ts +++ b/src/app/api/status/route.ts @@ -26,8 +26,6 @@ export async function GET(req: NextRequest): Promise { const root = disks.find((d) => d.mount === "/" || d.fs.includes("disk3")); - console.log(mem.available, mem.total); - return NextResponse.json({ message: "ok", liveData: { diff --git a/src/app/nodes/page.tsx b/src/app/nodes/page.tsx index 7235425..25564c6 100644 --- a/src/app/nodes/page.tsx +++ b/src/app/nodes/page.tsx @@ -10,6 +10,8 @@ import { Docker, Node, NodeLiveData } from "@/lib/types"; import { useEffect, useState } from "react"; import { getDockerNode, getLiveNodes } from "@/core/client/node"; import { formatBytes } from "@/lib/format"; +import { setNodes as updateNodes } from "@/core/client/node"; +import AddNode from "@/components/dialogs/AddNode"; function DiskUsage({ liveData }: NodeLiveData) { const usage = (liveData.disk.used / liveData.disk.size) * 100; @@ -56,14 +58,16 @@ export default function NodesPage() { {/* {nodes.filter((n) => n.status === "running").length} online */}
- + + +
{nodes.map((node, i) => ( - +
@@ -88,7 +92,15 @@ export default function NodesPage() { -
diff --git a/src/app/overview/page.tsx b/src/app/overview/page.tsx index 3f2089e..98f59bf 100644 --- a/src/app/overview/page.tsx +++ b/src/app/overview/page.tsx @@ -227,11 +227,8 @@ export default function OverviewPage() {
- {nodes.map((node) => ( -
+ {nodes.map((node, i) => ( +
diff --git a/src/app/page.tsx b/src/app/page.tsx index fa0bcfb..49cf960 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,17 +1,5 @@ "use client"; -import { getDocker } from "@/core/client/docker"; - export default function Home() { - return ( -
- -
- ); + return "ok"; } diff --git a/src/components/dialogs/AddNode.tsx b/src/components/dialogs/AddNode.tsx new file mode 100644 index 0000000..29b80d5 --- /dev/null +++ b/src/components/dialogs/AddNode.tsx @@ -0,0 +1,166 @@ +"use client"; + +import { useState, ReactNode } from "react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "../ui/textarea"; +import { + authNode, + getNodes, + setNodes as updateNodes, +} from "@/core/client/node"; + +export default function AddNode({ children }: { children: ReactNode }) { + const [step, setStep] = useState<1 | 2>(1); + const [loading, setLoading] = useState(false); + const [ip, setIp] = useState(""); + const [key, setKey] = useState(""); + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [auth, setAuth] = useState<{ node_id: string; os: string }>(); + + const authenticateNode = async () => { + setLoading(true); + + try { + setAuth(await authNode(ip.trim(), key.trim())); + setStep(2); + } catch (e) { + console.log(e); + } finally { + setLoading(false); + } + }; + + const addNode = async () => { + if (!auth) return; + const nodes = await getNodes(); + + await updateNodes([ + ...nodes, + { + node_id: auth.node_id, + key, + name, + description, + ip, + os: auth.os, + labels: [], + }, + ]); + setStep(1); + setAuth(undefined); + setIp(""); + setKey(""); + setName(""); + setDescription(""); + }; + + return ( + + {children} + + + + + {step === 1 && "Add Node"} + {step === 2 && "Node Details"} + + + + {/* STEP 1: IP & Node Key */} + {step === 1 && ( +
+
+ + setIp(e.target.value)} + placeholder="10.7.1.21" + /> +
+ +
+ + setKey(e.target.value)} + placeholder="xxxxxxxxxxxxxxxx" + /> +
+
+ )} + + {/* STEP 3: Node Name & Description */} + {step === 2 && ( +
+
+ + setName(e.target.value)} + placeholder="Captain's node" + /> +
+ +
+ +