From 127e94678025163475f84338147f3346e58c006f Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 28 Dec 2025 00:46:06 +0100 Subject: [PATCH] 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" + /> +
+ +
+ +