+1
-1
@@ -1,7 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
serverExternalPackages: ["ssh2", "docker-modem", "dockerode"],
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
Generated
+300
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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<NextResponse> {
|
||||
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<NextResponse> {
|
||||
);
|
||||
}
|
||||
|
||||
const { node_id } = await read(NodeJsonTemplate, "data", "node.json");
|
||||
|
||||
return NextResponse.json({
|
||||
message: "ok",
|
||||
app: "dockship",
|
||||
version,
|
||||
node_id,
|
||||
os: osName(),
|
||||
});
|
||||
}
|
||||
|
||||
+13
-1
@@ -13,9 +13,21 @@ export async function read<T extends object>(
|
||||
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<T extends object>(
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -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<NextResponse> {
|
||||
const auth = await testAuth(req);
|
||||
@@ -12,8 +14,11 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
|
||||
);
|
||||
}
|
||||
|
||||
const docker = await getDocker();
|
||||
|
||||
return NextResponse.json({
|
||||
message: "ok",
|
||||
containers: [],
|
||||
});
|
||||
version: docker.version.Version,
|
||||
containers: docker.containers,
|
||||
} as Docker);
|
||||
}
|
||||
|
||||
@@ -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<NextResponse> {
|
||||
const auth = await testAuth(req);
|
||||
|
||||
if (auth) {
|
||||
return NextResponse.json(
|
||||
{ message: auth },
|
||||
{ status: StatusCodes.UNAUTHORIZED }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
message: "ok",
|
||||
metrics: usageHours,
|
||||
});
|
||||
}
|
||||
@@ -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<NextResponse> {
|
||||
const { node_id } = await read(NodeJsonTemplate, "data", "node.json");
|
||||
|
||||
return NextResponse.json({ app: "dockship", version, node_id });
|
||||
return NextResponse.json({ app: "dockship", version });
|
||||
}
|
||||
|
||||
@@ -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<NextResponse> {
|
||||
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);
|
||||
}
|
||||
+154
-86
@@ -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);
|
||||
--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);
|
||||
}
|
||||
|
||||
--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);
|
||||
@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 {
|
||||
|
||||
+94
-244
@@ -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,13 +24,80 @@ 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 (
|
||||
<DashboardLayout
|
||||
title="Monitoring"
|
||||
subtitle="Resource utilization and metrics"
|
||||
>
|
||||
{/* Summary Stats */}
|
||||
{isMobile ? (
|
||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||
<Card>
|
||||
<CardContent className="px-4">
|
||||
<RadialChart value={liveData?.cpuUsage || 0}>CPU</RadialChart>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="px-4">
|
||||
<RadialChart value={liveData?.ramUsage || 0}>RAM</RadialChart>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
@@ -123,197 +138,32 @@ export default function MonitoringPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Charts */}
|
||||
{/* CPU & Memory Charts */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center justify-between flex-wrap gap-2">
|
||||
CPU Usage <Badge variant="outline">Last 12 hours</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-48 w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={cpuData}>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="cpuGradient"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="5%"
|
||||
stopColor="oklch(0.66 0.13 227.70)"
|
||||
stopOpacity={0.3}
|
||||
<GradientAreaChart
|
||||
title="CPU Usage"
|
||||
data={data}
|
||||
config={cpuChartConfig}
|
||||
max={100}
|
||||
/>
|
||||
<stop
|
||||
offset="95%"
|
||||
stopColor="oklch(0.66 0.13 227.70)"
|
||||
stopOpacity={0}
|
||||
<GradientAreaChart
|
||||
title="Memory Usage"
|
||||
data={data}
|
||||
config={memoryChartConfig}
|
||||
max={100}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: "hsl(0, 0%, 55%)", fontSize: 10 }}
|
||||
/>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: "hsl(0, 0%, 55%)", fontSize: 10 }}
|
||||
domain={[0, 100]}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: "hsl(0,0%,7%)",
|
||||
border: "1px solid hsl(0,0%,18%)",
|
||||
borderRadius: "4px",
|
||||
fontSize: "12px",
|
||||
}}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke="oklch(0.66 0.13 227.70)"
|
||||
strokeWidth={2}
|
||||
fill="url(#cpuGradient)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center justify-between flex-wrap gap-2">
|
||||
Memory Usage <Badge variant="outline">Last 12 hours</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-48 w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={memoryData}>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="memoryGradient"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="5%"
|
||||
stopColor="oklch(0.70 0.15 159.83)"
|
||||
stopOpacity={0.3}
|
||||
/>
|
||||
<stop
|
||||
offset="95%"
|
||||
stopColor="oklch(0.70 0.15 159.83)"
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: "hsl(0, 0%, 55%)", fontSize: 10 }}
|
||||
/>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: "hsl(0, 0%, 55%)", fontSize: 10 }}
|
||||
domain={[0, 100]}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: "hsl(0,0%,7%)",
|
||||
border: "1px solid hsl(0,0%,18%)",
|
||||
borderRadius: "4px",
|
||||
fontSize: "12px",
|
||||
}}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke="oklch(0.70 0.15 159.83)"
|
||||
strokeWidth={2}
|
||||
fill="url(#memoryGradient)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Network Chart */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center justify-between flex-wrap gap-2">
|
||||
Network I/O <Badge variant="outline">Last 12 hours</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-48 w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={networkData}>
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: "hsl(0, 0%, 55%)", fontSize: 10 }}
|
||||
{/* Network Metrics */}
|
||||
<div className="mb-6">
|
||||
<GradientAreaChart
|
||||
title="Network I/O"
|
||||
data={data}
|
||||
config={networkChartConfig}
|
||||
/>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: "hsl(0, 0%, 55%)", fontSize: 10 }}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: "hsl(0,0%,7%)",
|
||||
border: "1px solid hsl(0,0%,18%)",
|
||||
borderRadius: "4px",
|
||||
fontSize: "12px",
|
||||
}}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="in"
|
||||
stroke="oklch(0.80 0.10 100.65)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
name="Inbound"
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="out"
|
||||
stroke="oklch(0.60 0.15 300.14)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
name="Outbound"
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-6 mt-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-0.5 bg-primary" />
|
||||
<span className="text-xs text-muted-foreground">Inbound</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-0.5 bg-warning" />
|
||||
<span className="text-xs text-muted-foreground">Outbound</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Container Metrics */}
|
||||
<Card>
|
||||
|
||||
+91
-70
@@ -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 (
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-2">Disk</div>
|
||||
<div className="stat-value mb-2">{usage.toFixed(0)}%</div>
|
||||
<Progress value={usage} className="w-full" />
|
||||
<div className="text-[10px] text-muted-foreground mt-1 font-mono">
|
||||
{formatBytes(liveData.disk.used)} / {formatBytes(liveData.disk.size)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function NodesPage() {
|
||||
const [nodes, setNodes] = useState<(NodeLiveData & Node)[]>([]);
|
||||
const [dockerNodes, setDockerNodes] = useState<Docker[]>([]);
|
||||
|
||||
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 (
|
||||
<DashboardLayout title="Nodes" subtitle="Cluster node management">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between mb-6 gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge>{nodes.length} nodes</Badge>
|
||||
<Badge>{nodes.filter((n) => n.status === "running").length} online</Badge>
|
||||
<Badge>
|
||||
{/* {nodes.filter((n) => n.status === "running").length} online */}
|
||||
</Badge>
|
||||
</div>
|
||||
<AddNode>
|
||||
<Button size="sm" className="gap-2">
|
||||
<Plus className="w-4 h-4" /> Add Node
|
||||
</Button>
|
||||
</AddNode>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
{nodes.map((node) => (
|
||||
<Card key={node.id}>
|
||||
{nodes.map((node, i) => (
|
||||
<Card key={i}>
|
||||
<CardHeader className="flex flex-col sm:flex-row sm:items-start sm:justify-between pb-2 gap-2 sm:gap-0">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
|
||||
<div className="w-10 h-10 bg-secondary flex items-center justify-center">
|
||||
@@ -76,11 +75,11 @@ export default function NodesPage() {
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<StatusIndicator status={node.status} />
|
||||
{/* <StatusIndicator status={node.status} /> */}
|
||||
<CardTitle className="font-mono">{node.name}</CardTitle>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground font-mono">
|
||||
{node.hostname} • {node.ip}
|
||||
{node.ip} • {node.ip}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,7 +92,15 @@ export default function NodesPage() {
|
||||
<Button variant="ghost" size="icon-sm">
|
||||
<Settings className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon-sm">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={async () => {
|
||||
const newNodes = nodes.filter((_, index) => index !== i);
|
||||
setNodes(newNodes);
|
||||
await updateNodes(newNodes);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -102,7 +109,9 @@ export default function NodesPage() {
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-5 gap-6">
|
||||
{/* System Info */}
|
||||
<div className="border-b sm:border-b-0 sm:border-r border-border pb-2 sm:pb-0 pr-0 sm:pr-6">
|
||||
<div className="text-xs text-muted-foreground mb-2">System</div>
|
||||
<div className="text-xs text-muted-foreground mb-2">
|
||||
System
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-muted-foreground">OS</span>
|
||||
@@ -110,11 +119,15 @@ export default function NodesPage() {
|
||||
</div>
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-muted-foreground">Docker</span>
|
||||
<span className="font-mono">{node.docker}</span>
|
||||
<span className="font-mono">
|
||||
{dockerNodes[i]?.version}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-muted-foreground">Containers</span>
|
||||
<span className="font-mono">{node.containers}</span>
|
||||
<span className="font-mono">
|
||||
{dockerNodes[i]?.containers.length}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -122,35 +135,43 @@ export default function NodesPage() {
|
||||
{/* CPU */}
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-2">
|
||||
CPU ({node.cpu.cores} cores)
|
||||
CPU ({node.liveData.cpu.cores} cores)
|
||||
</div>
|
||||
<div className="stat-value mb-2">{node.cpu.usage}%</div>
|
||||
<Progress value={node.cpu.usage} className="w-full" />
|
||||
<div className="stat-value mb-2">
|
||||
{node.liveData.cpu.usage.toFixed(0)}%
|
||||
</div>
|
||||
<Progress
|
||||
value={node.liveData.cpu.usage}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Memory */}
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-2">Memory</div>
|
||||
<div className="stat-value mb-2">{node.memory.percentage}%</div>
|
||||
<Progress value={node.memory.percentage} className="w-full" />
|
||||
<div className="text-xs text-muted-foreground mb-2">
|
||||
Memory
|
||||
</div>
|
||||
<div className="stat-value mb-2">
|
||||
{node.liveData.memory.usage.toFixed(0)}%
|
||||
</div>
|
||||
<Progress
|
||||
value={node.liveData.memory.usage}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="text-[10px] text-muted-foreground mt-1 font-mono">
|
||||
{node.memory.used} / {node.memory.total}
|
||||
{formatBytes(node.liveData.memory.used)} /{" "}
|
||||
{formatBytes(node.liveData.memory.total)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Disk */}
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-2">Disk</div>
|
||||
<div className="stat-value mb-2">{node.disk.percentage}%</div>
|
||||
<Progress value={node.disk.percentage} className="w-full" />
|
||||
<div className="text-[10px] text-muted-foreground mt-1 font-mono">
|
||||
{node.disk.used} / {node.disk.total}
|
||||
</div>
|
||||
</div>
|
||||
<DiskUsage liveData={node.liveData} />
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-2">Quick Actions</div>
|
||||
<div className="text-xs text-muted-foreground mb-2">
|
||||
Quick Actions
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Button size="sm" className="w-full justify-start">
|
||||
SSH Connect
|
||||
|
||||
+34
-42
@@ -16,6 +16,9 @@ import {
|
||||
} from "lucide-react";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import Link from "next/link";
|
||||
import { getLiveNodes } from "@/core/client/node";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Node, NodeLiveData } from "@/lib/types";
|
||||
|
||||
const recentDeployments = [
|
||||
{
|
||||
@@ -84,36 +87,21 @@ const applications = [
|
||||
memory: 48,
|
||||
status: "pending" as const,
|
||||
},
|
||||
];
|
||||
|
||||
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,
|
||||
},
|
||||
];
|
||||
].slice(-5);
|
||||
|
||||
export default function OverviewPage() {
|
||||
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 (
|
||||
<DashboardLayout title="Overview" subtitle="System status and metrics">
|
||||
{/* Stats Grid */}
|
||||
@@ -239,14 +227,11 @@ export default function OverviewPage() {
|
||||
</CardHeader>
|
||||
<CardContent className="overflow-x-auto">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 min-w-75">
|
||||
{nodes.map((node) => (
|
||||
<div
|
||||
key={node.name}
|
||||
className="border border-border p-4 bg-secondary/30"
|
||||
>
|
||||
{nodes.map((node, i) => (
|
||||
<div key={i} className="border border-border p-4 bg-secondary/30">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-3 gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusIndicator status={node.status} />
|
||||
<StatusIndicator status={"running"} />
|
||||
<span className="font-mono text-sm">{node.name}</span>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
@@ -256,20 +241,27 @@ export default function OverviewPage() {
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-muted-foreground">CPU</span>
|
||||
<span className="font-mono">{node.cpu}%</span>
|
||||
</div>
|
||||
<Progress value={node.cpu} className="[&>div]:bg-chart-1" />
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-muted-foreground">Memory</span>
|
||||
<span className="font-mono">{node.memory}%</span>
|
||||
<span className="font-mono">
|
||||
{node.liveData.cpu.usage.toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={node.memory}
|
||||
value={node.liveData.cpu.usage}
|
||||
className="[&>div]:bg-chart-1"
|
||||
/>
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-muted-foreground">Memory</span>
|
||||
<span className="font-mono">
|
||||
{node.liveData.memory.usage.toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={node.liveData.memory.usage}
|
||||
className="w-full [&>div]:bg-chart-3"
|
||||
/>
|
||||
<div className="flex items-center justify-between text-xs pt-2 border-t border-border mt-2">
|
||||
<span className="text-muted-foreground">Containers</span>
|
||||
<span className="font-mono">{node.containers}</span>
|
||||
<span className="font-mono">{6}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+1
-13
@@ -1,17 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { getDocker } from "@/core/client/docker";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={async () => {
|
||||
console.log(await getDocker("localhost", "oD7tMrnkxIvuFrR+b6QZh66E4PU="));
|
||||
}}
|
||||
>
|
||||
Get docker
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
return "ok";
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
<CardDescription>Last 12 hours</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={config} className="h-60 w-full">
|
||||
<AreaChart data={data} margin={{ left: 12, right: 12 }}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tickMargin={8}
|
||||
/>
|
||||
{max && (
|
||||
<YAxis
|
||||
domain={[0, max]}
|
||||
width={16}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
/>
|
||||
)}
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
|
||||
<defs>
|
||||
{Object.entries(config).map(([key, config]) => (
|
||||
<linearGradient
|
||||
key={key}
|
||||
id={`fill-${key}`}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="5%"
|
||||
stopColor={config.color}
|
||||
stopOpacity={0.7}
|
||||
/>
|
||||
<stop
|
||||
offset="95%"
|
||||
stopColor={config.color}
|
||||
stopOpacity={0.1}
|
||||
/>
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
{Object.entries(config).map(([key, config]) => (
|
||||
<Area
|
||||
key={key}
|
||||
type="natural"
|
||||
dataKey={key}
|
||||
stroke={config.color}
|
||||
fill={`url(#fill-${key})`}
|
||||
fillOpacity={0.4}
|
||||
stackId="a"
|
||||
/>
|
||||
))}
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -25,19 +25,19 @@ export default function RadialChart({
|
||||
const chartData = [{ value, fill: "var(--color-chart-1)" }];
|
||||
|
||||
return (
|
||||
<ChartContainer config={chartConfig} className="size-30">
|
||||
<ChartContainer config={chartConfig} className="w-full aspect-square">
|
||||
<RadialBarChart
|
||||
data={chartData}
|
||||
startAngle={-50}
|
||||
endAngle={(value / 100) * 230}
|
||||
innerRadius="65%"
|
||||
outerRadius="100%"
|
||||
startAngle={230 - 360 * (value / 100)}
|
||||
endAngle={230}
|
||||
innerRadius={80}
|
||||
outerRadius={110}
|
||||
>
|
||||
<PolarGrid
|
||||
gridType="circle"
|
||||
radialLines={false}
|
||||
stroke="none"
|
||||
className=""
|
||||
className="first:fill-muted last:fill-background"
|
||||
polarRadius={[86, 74]}
|
||||
/>
|
||||
<RadialBar dataKey="value" background cornerRadius={10} />
|
||||
@@ -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)}%
|
||||
</tspan>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
|
||||
@@ -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 (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
|
||||
<DialogContent className="sm:max-w-106.25">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{step === 1 && "Add Node"}
|
||||
{step === 2 && "Node Details"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{/* STEP 1: IP & Node Key */}
|
||||
{step === 1 && (
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="ip">
|
||||
IP address <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="ip"
|
||||
value={ip}
|
||||
onChange={(e) => setIp(e.target.value)}
|
||||
placeholder="10.7.1.21"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="key">
|
||||
Node Key <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="key"
|
||||
type="password"
|
||||
value={key}
|
||||
onChange={(e) => setKey(e.target.value)}
|
||||
placeholder="xxxxxxxxxxxxxxxx"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* STEP 3: Node Name & Description */}
|
||||
{step === 2 && (
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="name">
|
||||
Node Name <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Captain's node"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="description">
|
||||
Node Description{" "}
|
||||
<span className="text-muted-foreground/60">(optional)</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="A node for the Captain's main operations"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
{step === 1 && (
|
||||
<>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button onClick={authenticateNode} disabled={loading}>
|
||||
Next
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{step === 2 && (
|
||||
<>
|
||||
<Button variant="outline" onClick={() => setStep(1)}>
|
||||
Back
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button onClick={addNode}>Add Node</Button>
|
||||
</DialogClose>
|
||||
</>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Textarea }
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
"use server";
|
||||
|
||||
import { Node } from "@/lib/types";
|
||||
import axios from "axios";
|
||||
import { getNodes } from "./node";
|
||||
|
||||
export async function getMetrics(): Promise<
|
||||
{
|
||||
time: string;
|
||||
cpu: number;
|
||||
memory: number;
|
||||
in: number;
|
||||
out: number;
|
||||
}[]
|
||||
> {
|
||||
const nodes = await getNodes();
|
||||
const nodeMetrics = await Promise.all(nodes.map(getNodeMetrics));
|
||||
|
||||
if (nodeMetrics.length === 0) return [];
|
||||
|
||||
const numNodes = nodeMetrics.length;
|
||||
const numPoints = nodeMetrics[0].length;
|
||||
|
||||
const averaged: (typeof nodeMetrics)[0] = [];
|
||||
|
||||
for (let i = 0; i < numPoints; i++) {
|
||||
const sum = nodeMetrics.reduce(
|
||||
(acc, metrics) => {
|
||||
const m = metrics[i];
|
||||
acc.cpu += m.cpu;
|
||||
acc.memory += m.memory;
|
||||
acc.in += m.in;
|
||||
acc.out += m.out;
|
||||
return acc;
|
||||
},
|
||||
{ cpu: 0, memory: 0, in: 0, out: 0 }
|
||||
);
|
||||
|
||||
averaged.push({
|
||||
time: nodeMetrics[0][i].time,
|
||||
cpu: sum.cpu / numNodes,
|
||||
memory: sum.memory / numNodes,
|
||||
in: sum.in / numNodes,
|
||||
out: sum.out / numNodes,
|
||||
});
|
||||
}
|
||||
|
||||
return averaged;
|
||||
}
|
||||
|
||||
export async function getNodeMetrics(node: Node): Promise<
|
||||
{
|
||||
time: string;
|
||||
cpu: number;
|
||||
memory: number;
|
||||
in: number;
|
||||
out: number;
|
||||
}[]
|
||||
> {
|
||||
const data = await (
|
||||
await axios.get(`http://${node.ip}:3000/api/metrics`, {
|
||||
headers: { Authorization: `ApiKey ${node.key}` },
|
||||
})
|
||||
).data;
|
||||
|
||||
return data.metrics;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
"use server";
|
||||
|
||||
import { read, write } from "@/app/api/file";
|
||||
import { Docker, Node, NodeLiveData } from "@/lib/types";
|
||||
import axios from "axios";
|
||||
|
||||
export async function getNodes(): Promise<Node[]> {
|
||||
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<Node | undefined> {
|
||||
const nodes = await read<Node[]>(() => [], "data", "nodes.json");
|
||||
return nodes.find((v) => v.node_id === node_id);
|
||||
}
|
||||
|
||||
export async function getLiveNode(node: Node): Promise<NodeLiveData & Node> {
|
||||
const data = await (
|
||||
await axios.get(`http://${node.ip}:3000/api/status`, {
|
||||
headers: { Authorization: `ApiKey ${node.key}` },
|
||||
})
|
||||
).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<Docker> {
|
||||
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 };
|
||||
}
|
||||
|
||||
export async function authNode(ip: string, key: string) {
|
||||
const data = await (
|
||||
await axios.get(`http://${ip}:3000/api/auth`, {
|
||||
headers: { Authorization: `ApiKey ${key}` },
|
||||
})
|
||||
).data;
|
||||
|
||||
return { version: data.version, node_id: data.node_id, os: data.os };
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export function formatBytes(bytes: number, decimals = 2) {
|
||||
if (bytes === 0) return "0 B";
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB", "PB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))} ${
|
||||
sizes[i]
|
||||
}`;
|
||||
}
|
||||
|
||||
export function average<T>(values: Array<T>, p: (v: T) => number) {
|
||||
return values.length > 0
|
||||
? values.reduce((sum, value) => sum + p(value), 0) / values.length
|
||||
: 0;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { ContainerInfo } from "dockerode";
|
||||
|
||||
export interface Node {
|
||||
node_id: string;
|
||||
key: string;
|
||||
ip: string;
|
||||
os: string;
|
||||
name: string;
|
||||
description: string;
|
||||
labels: string[];
|
||||
}
|
||||
|
||||
export interface NodeLiveData {
|
||||
liveData: {
|
||||
cpu: {
|
||||
manufacturer: string;
|
||||
brand: string;
|
||||
cores: number;
|
||||
usage: number;
|
||||
};
|
||||
memory: {
|
||||
total: number;
|
||||
used: number;
|
||||
free: number;
|
||||
usage: number;
|
||||
};
|
||||
disk: {
|
||||
size: number;
|
||||
available: number;
|
||||
used: number;
|
||||
};
|
||||
disks: {
|
||||
fs: string;
|
||||
size: number;
|
||||
used: number;
|
||||
usage: number;
|
||||
}[];
|
||||
network: {
|
||||
iface: string;
|
||||
rx: number;
|
||||
tx: number;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface Docker {
|
||||
version: string;
|
||||
containers: ContainerInfo[];
|
||||
}
|
||||
Reference in New Issue
Block a user