diff --git a/next.config.ts b/next.config.ts index e9ffa30..57d0761 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + serverExternalPackages: ["ssh2", "docker-modem", "dockerode"], }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 061bbc3..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,9 +24,11 @@ "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", + "systeminformation": "^5.28.2", "tailwind-merge": "^3.4.0" }, "devDependencies": { @@ -1246,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", @@ -2483,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", @@ -2804,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", @@ -2919,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", @@ -2983,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", @@ -3027,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", @@ -3352,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", @@ -3371,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", @@ -3392,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", @@ -3510,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", @@ -3528,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", @@ -3892,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", @@ -3959,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", @@ -3982,6 +4226,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", @@ -4173,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 2d826e4..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,9 +24,11 @@ "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", + "systeminformation": "^5.28.2", "tailwind-merge": "^3.4.0" }, "devDependencies": { 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/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/get-docker/route.ts b/src/app/api/get-docker/route.ts index a6866c3..c70f62c 100644 --- a/src/app/api/get-docker/route.ts +++ b/src/app/api/get-docker/route.ts @@ -1,6 +1,8 @@ import { NextRequest, NextResponse } from "next/server"; import testAuth from "../auth"; import { StatusCodes } from "http-status-codes"; +import { getDocker } from "@/core/docker"; +import { Docker } from "@/lib/types"; export async function GET(req: NextRequest): Promise { const auth = await testAuth(req); @@ -12,8 +14,11 @@ export async function GET(req: NextRequest): Promise { ); } + const docker = await getDocker(); + return NextResponse.json({ message: "ok", - containers: [], - }); + version: docker.version.Version, + containers: docker.containers, + } as Docker); } diff --git a/src/app/api/metrics/route.ts b/src/app/api/metrics/route.ts new file mode 100644 index 0000000..a727fcd --- /dev/null +++ b/src/app/api/metrics/route.ts @@ -0,0 +1,104 @@ +import { NextRequest, NextResponse } from "next/server"; +import testAuth from "../auth"; +import { StatusCodes } from "http-status-codes"; +import si from "systeminformation"; +import { average } from "@/lib/format"; + +type Usage = { + cpu: number; + memory: number; + in: number; + out: number; +}; + +const usageSecs: Usage[] = []; + +const usageMins: (Usage & { time: string })[] = []; + +const usageHours: (Usage & { time: string })[] = new Array(12).fill({ + time: "00:00", + cpu: 0, + memory: 0, + in: 0, + out: 0, +}); + +var hr = 0; + +function getTime() { + const now = new Date(); + const hours = String(now.getHours()).padStart(2, "0"); + const minutes = String(now.getMinutes()).padStart(2, "0"); + return `${hours}:${minutes}`; +} + +setInterval(async () => { + const [mem, load, network] = await Promise.all([ + si.mem(), + si.currentLoad(), + si.networkStats(), + ]); + + const sec = { + cpu: load.currentLoad, + memory: ((mem.total - mem.available) / mem.total) * 100, + in: average(network, (n) => n.rx_bytes), + out: average(network, (n) => n.tx_bytes), + }; + usageSecs.push(sec); + + if (usageSecs.length === 10) { + usageHours[hr] = { time: getTime(), ...sec }; + } else if (usageSecs.length >= 60) { + usageMins.push({ + time: getTime(), + cpu: average(usageSecs, (s) => s.cpu), + memory: average(usageSecs, (s) => s.memory), + in: average(usageSecs, (s) => s.in), + out: average(usageSecs, (s) => s.out), + }); + + if (usageMins.length >= 60) { + const time = getTime(); + if (hr >= 12) { + usageHours.shift(); + usageHours.push({ + time, + cpu: average(usageMins, (s) => s.cpu), + memory: average(usageMins, (s) => s.memory), + in: average(usageMins, (s) => s.in), + out: average(usageMins, (s) => s.out), + }); + } else { + usageHours[hr] = { + time, + cpu: average(usageMins, (s) => s.cpu), + memory: average(usageMins, (s) => s.memory), + in: average(usageMins, (s) => s.in), + out: average(usageMins, (s) => s.out), + }; + hr++; + } + + usageMins.splice(0, usageSecs.length); + } + + usageSecs.splice(0, usageSecs.length); + } +}, 1000); + +export async function GET(req: NextRequest): Promise { + const auth = await testAuth(req); + + if (auth) { + return NextResponse.json( + { message: auth }, + { status: StatusCodes.UNAUTHORIZED } + ); + } + + return NextResponse.json({ + message: "ok", + metrics: usageHours, + }); +} 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 new file mode 100644 index 0000000..a0f1186 --- /dev/null +++ b/src/app/api/status/route.ts @@ -0,0 +1,62 @@ +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); + + 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(), + ]); + + const disks = await si.fsSize(); + + const root = disks.find((d) => d.mount === "/" || d.fs.includes("disk3")); + + 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.available) / mem.total) * 100, + }, + 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, + usage: d.use, + })), + network: network.map((n) => ({ + iface: n.iface, + rx: n.rx_bytes, + tx: n.tx_bytes, + })), + }, + } as NodeLiveData); +} 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..cb0945e 100644 --- a/src/app/monitoring/page.tsx +++ b/src/app/monitoring/page.tsx @@ -1,69 +1,17 @@ "use client"; import { DashboardLayout } from "@/components/dashboard/DashboardLayout"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { - LineChart, - Line, - XAxis, - YAxis, - Tooltip, - ResponsiveContainer, - AreaChart, - Area, -} from "recharts"; import { Progress } from "@/components/ui/progress"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { type ChartConfig } from "@/components/ui/chart"; +import GradientAreaChart from "@/components/GradientAreaChart"; +import { getMetrics } from "@/core/client/metrics"; +import { useEffect, useState } from "react"; +import { useIsMobile } from "@/hooks/use-mobile"; import RadialChart from "@/components/RadialChart"; -import { ArrowDownLeft, ArrowUpRight, Cpu, MemoryStick } from "lucide-react"; - -const cpuData = [ - { time: "00:00", value: 45 }, - { time: "01:00", value: 52 }, - { time: "02:00", value: 38 }, - { time: "03:00", value: 42 }, - { time: "04:00", value: 35 }, - { time: "05:00", value: 48 }, - { time: "06:00", value: 55 }, - { time: "07:00", value: 62 }, - { time: "08:00", value: 78 }, - { time: "09:00", value: 85 }, - { time: "10:00", value: 72 }, - { time: "11:00", value: 68 }, - { time: "12:00", value: 75 }, -]; - -const memoryData = [ - { time: "00:00", value: 62 }, - { time: "01:00", value: 64 }, - { time: "02:00", value: 58 }, - { time: "03:00", value: 61 }, - { time: "04:00", value: 59 }, - { time: "05:00", value: 63 }, - { time: "06:00", value: 67 }, - { time: "07:00", value: 72 }, - { time: "08:00", value: 78 }, - { time: "09:00", value: 82 }, - { time: "10:00", value: 76 }, - { time: "11:00", value: 74 }, - { time: "12:00", value: 71 }, -]; - -const networkData = [ - { time: "00:00", in: 12, out: 8 }, - { time: "01:00", in: 15, out: 10 }, - { time: "02:00", in: 8, out: 5 }, - { time: "03:00", in: 10, out: 7 }, - { time: "04:00", in: 6, out: 4 }, - { time: "05:00", in: 14, out: 9 }, - { time: "06:00", in: 22, out: 15 }, - { time: "07:00", in: 35, out: 28 }, - { time: "08:00", in: 48, out: 38 }, - { time: "09:00", in: 52, out: 42 }, - { time: "10:00", in: 45, out: 35 }, - { time: "11:00", in: 42, out: 32 }, - { time: "12:00", in: 38, out: 28 }, -]; +import { Node, NodeLiveData } from "@/lib/types"; +import { getLiveNodes } from "@/core/client/node"; +import { average } from "@/lib/format"; const containerMetrics = [ { name: "api-gateway-1", cpu: 45, memory: 62, network: "12.4 MB/s" }, @@ -76,244 +24,146 @@ const containerMetrics = [ { name: "worker-queue-1", cpu: 55, memory: 48, network: "5.6 MB/s" }, ]; +const cpuChartConfig = { + cpu: { label: "CPU Usage", color: "var(--chart-1)" }, +} satisfies ChartConfig; + +const memoryChartConfig = { + memory: { label: "RAM Usage", color: "var(--chart-3)" }, +} satisfies ChartConfig; + +const networkChartConfig = { + in: { label: "Inbound", color: "var(--chart-2)" }, + out: { label: "Outbound", color: "var(--chart-4)" }, +} satisfies ChartConfig; + export default function MonitoringPage() { + const [data, setData] = useState< + { + time: string; + cpu: number; + memory: number; + in: number; + out: number; + }[] + >([]); + const isMobile = useIsMobile(); + const [liveData, setLiveData] = useState<{ + cpuUsage: number; + ramUsage: number; + }>(); + + useEffect(() => { + const fetchNodes = async () => { + const nodes = await getLiveNodes(); + setLiveData({ + cpuUsage: average(nodes, (n) => n.liveData.cpu.usage), + ramUsage: average(nodes, (n) => n.liveData.memory.usage), + }); + }; + fetchNodes(); + const interval = setInterval(fetchNodes, 3000); + return () => clearInterval(interval); + }, []); + + useEffect(() => { + async function fetchMetrics() { + setData(await getMetrics()); + } + + fetchMetrics(); + + const i = setInterval(fetchMetrics, 30000); + + return () => clearInterval(i); + }, []); + return ( {/* Summary Stats */} -
- - -
- Cluster CPU -
-
68%
- -
-
- - -
- Cluster Memory -
-
71%
- -
-
- - -
- Network In -
-
38 MB/s
-
↑ 12% from avg
-
-
- - -
- Network Out -
-
28 MB/s
-
- ↓ 5% from avg -
-
-
-
+ {isMobile ? ( +
+ + + CPU + + + + + RAM + + +
+ ) : ( +
+ + +
+ Cluster CPU +
+
68%
+ +
+
+ + +
+ Cluster Memory +
+
71%
+ +
+
+ + +
+ Network In +
+
38 MB/s
+
↑ 12% from avg
+
+
+ + +
+ Network Out +
+
28 MB/s
+
+ ↓ 5% from avg +
+
+
+
+ )} - {/* Charts */} + {/* CPU & Memory Charts */}
- - - - CPU Usage Last 12 hours - - - -
- - - - - - - - - - - - - - -
-
-
- - - - - Memory Usage Last 12 hours - - - -
- - - - - - - - - - - - - - -
-
-
+ +
- {/* Network Chart */} - - - - Network I/O Last 12 hours - - - -
- - - - - - - - - -
-
-
-
- Inbound -
-
-
- Outbound -
-
- - + {/* Network Metrics */} +
+ +
{/* Container Metrics */} diff --git a/src/app/nodes/page.tsx b/src/app/nodes/page.tsx index 973de4b..25564c6 100644 --- a/src/app/nodes/page.tsx +++ b/src/app/nodes/page.tsx @@ -4,71 +4,70 @@ 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 { 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"; -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)[]>([]); + const [dockerNodes, setDockerNodes] = useState([]); + + useEffect(() => { + const fetchNodes = async () => { + const nodes = await getLiveNodes(); + setNodes(nodes); + }; + fetchNodes(); + const interval = setInterval(fetchNodes, 3000); + return () => clearInterval(interval); + }, []); + + useEffect(() => { + const fetchDocker = async () => { + setDockerNodes(await Promise.all(nodes.map(getDockerNode))); + }; + fetchDocker(); + }, [nodes]); + return (
{nodes.length} nodes - {nodes.filter((n) => n.status === "running").length} online + + {/* {nodes.filter((n) => n.status === "running").length} online */} +
- + + +
- {nodes.map((node) => ( - + {nodes.map((node, i) => ( +
@@ -76,11 +75,11 @@ export default function NodesPage() {
- + {/* */} {node.name}
- {node.hostname} • {node.ip} + {node.ip} • {node.ip}
@@ -93,7 +92,15 @@ export default function NodesPage() { -
@@ -102,7 +109,9 @@ export default function NodesPage() {
{/* System Info */}
-
System
+
+ System +
OS @@ -110,11 +119,15 @@ export default function NodesPage() {
Docker - {node.docker} + + {dockerNodes[i]?.version} +
Containers - {node.containers} + + {dockerNodes[i]?.containers.length} +
@@ -122,35 +135,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 +
-
- ); + return "ok"; } diff --git a/src/components/GradientAreaChart.tsx b/src/components/GradientAreaChart.tsx new file mode 100644 index 0000000..c07125e --- /dev/null +++ b/src/components/GradientAreaChart.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { + Card, + CardContent, + CardHeader, + CardTitle, + CardDescription, +} from "@/components/ui/card"; +import { AreaChart, Area, XAxis, CartesianGrid, YAxis } from "recharts"; +import { + ChartContainer, + ChartTooltip, + ChartTooltipContent, + type ChartConfig, +} from "@/components/ui/chart"; + +export default function GradientAreaChart({ + title, + data, + config, + max, +}: { + title: string; + data: any[]; + config: ChartConfig; + max?: number; +}) { + return ( + + + {title} + Last 12 hours + + + + + + + {max && ( + + )} + } /> + + {Object.entries(config).map(([key, config]) => ( + + + + + ))} + + {Object.entries(config).map(([key, config]) => ( + + ))} + + + + + ); +} diff --git a/src/components/RadialChart.tsx b/src/components/RadialChart.tsx index 95d8361..d8afd6d 100644 --- a/src/components/RadialChart.tsx +++ b/src/components/RadialChart.tsx @@ -25,19 +25,19 @@ export default function RadialChart({ const chartData = [{ value, fill: "var(--color-chart-1)" }]; return ( - + @@ -57,7 +57,7 @@ export default function RadialChart({ y={viewBox.cy} className="fill-foreground text-xl font-bold" > - {chartData[0].value.toLocaleString()} + {chartData[0].value.toFixed(1)}% (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" + /> +
+ +
+ +