Final redesign: Exact Vercel/Linear-style layout with shadcn components
- Fixed 280px sidebar with glassmorphism - Full-width max-w-7xl main content (no single column) - 4 KPI cards in grid-cols-2 md:grid-cols-4 layout - 3-column service grid (grid-cols-1 md:grid-cols-2 lg:grid-cols-3) - shadcn Card, Badge, Progress, Button components - Sparklines using recharts - Hover scale + shadow effects - No tables, no single-column stacking
This commit is contained in:
parent
bc82bc818a
commit
b24d3516d5
@ -10,6 +10,11 @@
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/page.js"
|
||||
],
|
||||
"/api/monitor/route": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/api/monitor/route.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
.next/cache/webpack/client-development/0.pack.gz
vendored
BIN
.next/cache/webpack/client-development/0.pack.gz
vendored
Binary file not shown.
BIN
.next/cache/webpack/client-development/2.pack.gz
vendored
BIN
.next/cache/webpack/client-development/2.pack.gz
vendored
Binary file not shown.
BIN
.next/cache/webpack/client-development/20.pack.gz
vendored
BIN
.next/cache/webpack/client-development/20.pack.gz
vendored
Binary file not shown.
BIN
.next/cache/webpack/client-development/22.pack.gz
vendored
Normal file
BIN
.next/cache/webpack/client-development/22.pack.gz
vendored
Normal file
Binary file not shown.
BIN
.next/cache/webpack/client-development/3.pack.gz
vendored
BIN
.next/cache/webpack/client-development/3.pack.gz
vendored
Binary file not shown.
BIN
.next/cache/webpack/client-development/4.pack.gz
vendored
BIN
.next/cache/webpack/client-development/4.pack.gz
vendored
Binary file not shown.
BIN
.next/cache/webpack/client-development/index.pack.gz
vendored
BIN
.next/cache/webpack/client-development/index.pack.gz
vendored
Binary file not shown.
Binary file not shown.
BIN
.next/cache/webpack/server-development/0.pack.gz
vendored
BIN
.next/cache/webpack/server-development/0.pack.gz
vendored
Binary file not shown.
BIN
.next/cache/webpack/server-development/1.pack.gz
vendored
BIN
.next/cache/webpack/server-development/1.pack.gz
vendored
Binary file not shown.
BIN
.next/cache/webpack/server-development/14.pack.gz
vendored
BIN
.next/cache/webpack/server-development/14.pack.gz
vendored
Binary file not shown.
BIN
.next/cache/webpack/server-development/16.pack.gz
vendored
BIN
.next/cache/webpack/server-development/16.pack.gz
vendored
Binary file not shown.
BIN
.next/cache/webpack/server-development/2.pack.gz
vendored
BIN
.next/cache/webpack/server-development/2.pack.gz
vendored
Binary file not shown.
BIN
.next/cache/webpack/server-development/21.pack.gz
vendored
BIN
.next/cache/webpack/server-development/21.pack.gz
vendored
Binary file not shown.
BIN
.next/cache/webpack/server-development/3.pack.gz
vendored
BIN
.next/cache/webpack/server-development/3.pack.gz
vendored
Binary file not shown.
BIN
.next/cache/webpack/server-development/index.pack.gz
vendored
BIN
.next/cache/webpack/server-development/index.pack.gz
vendored
Binary file not shown.
Binary file not shown.
@ -1,3 +1,4 @@
|
||||
{
|
||||
"/api/monitor/route": "app/api/monitor/route.js",
|
||||
"/page": "app/page.js"
|
||||
}
|
||||
164
.next/server/app/_not-found/page.js
Normal file
164
.next/server/app/_not-found/page.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
144
.next/server/app/api/monitor/route.js
Normal file
144
.next/server/app/api/monitor/route.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -61,26 +61,6 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "(ssr)/./node_modules/lucide-react/dist/esm/icons/chevron-left.js":
|
||||
/*!******************************************************************!*\
|
||||
!*** ./node_modules/lucide-react/dist/esm/icons/chevron-left.js ***!
|
||||
\******************************************************************/
|
||||
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
||||
|
||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ __iconNode: () => (/* binding */ __iconNode),\n/* harmony export */ \"default\": () => (/* binding */ ChevronLeft)\n/* harmony export */ });\n/* harmony import */ var _createLucideIcon_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../createLucideIcon.js */ \"(ssr)/./node_modules/lucide-react/dist/esm/createLucideIcon.js\");\n/**\n * @license lucide-react v0.474.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */ \nconst __iconNode = [\n [\n \"path\",\n {\n d: \"m15 18-6-6 6-6\",\n key: \"1wnfg3\"\n }\n ]\n];\nconst ChevronLeft = (0,_createLucideIcon_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(\"ChevronLeft\", __iconNode);\n //# sourceMappingURL=chevron-left.js.map\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvbHVjaWRlLXJlYWN0L2Rpc3QvZXNtL2ljb25zL2NoZXZyb24tbGVmdC5qcyIsIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7QUFHYSxpQkFBdUI7SUFBQztRQUFDLE1BQVE7UUFBQSxDQUFFO1lBQUEsRUFBRyxpQkFBa0I7WUFBQSxLQUFLLENBQVM7UUFBQSxDQUFDO0tBQUM7Q0FBQTtBQWEvRSxrQkFBYyxrRUFBaUIsZ0JBQWUsQ0FBVSIsInNvdXJjZXMiOlsiL1VzZXJzL21hdHRicnVjZS9Eb2N1bWVudHMvUHJvamVjdHMvc3JjL2ljb25zL2NoZXZyb24tbGVmdC50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgY3JlYXRlTHVjaWRlSWNvbiBmcm9tICcuLi9jcmVhdGVMdWNpZGVJY29uJztcbmltcG9ydCB7IEljb25Ob2RlIH0gZnJvbSAnLi4vdHlwZXMnO1xuXG5leHBvcnQgY29uc3QgX19pY29uTm9kZTogSWNvbk5vZGUgPSBbWydwYXRoJywgeyBkOiAnbTE1IDE4LTYtNiA2LTYnLCBrZXk6ICcxd25mZzMnIH1dXTtcblxuLyoqXG4gKiBAY29tcG9uZW50IEBuYW1lIENoZXZyb25MZWZ0XG4gKiBAZGVzY3JpcHRpb24gTHVjaWRlIFNWRyBpY29uIGNvbXBvbmVudCwgcmVuZGVycyBTVkcgRWxlbWVudCB3aXRoIGNoaWxkcmVuLlxuICpcbiAqIEBwcmV2aWV3ICFbaW1nXShkYXRhOmltYWdlL3N2Zyt4bWw7YmFzZTY0LFBITjJaeUFnZUcxc2JuTTlJbWgwZEhBNkx5OTNkM2N1ZHpNdWIzSm5Mekl3TURBdmMzWm5JZ29nSUhkcFpIUm9QU0l5TkNJS0lDQm9aV2xuYUhROUlqSTBJZ29nSUhacFpYZENiM2c5SWpBZ01DQXlOQ0F5TkNJS0lDQm1hV3hzUFNKdWIyNWxJZ29nSUhOMGNtOXJaVDBpSXpBd01DSWdjM1I1YkdVOUltSmhZMnRuY205MWJtUXRZMjlzYjNJNklDTm1abVk3SUdKdmNtUmxjaTF5WVdScGRYTTZJREp3ZUNJS0lDQnpkSEp2YTJVdGQybGtkR2c5SWpJaUNpQWdjM1J5YjJ0bExXeHBibVZqWVhBOUluSnZkVzVrSWdvZ0lITjBjbTlyWlMxc2FXNWxhbTlwYmowaWNtOTFibVFpQ2o0S0lDQThjR0YwYUNCa1BTSnRNVFVnTVRndE5pMDJJRFl0TmlJZ0x6NEtQQzl6ZG1jK0NnPT0pIC0gaHR0cHM6Ly9sdWNpZGUuZGV2L2ljb25zL2NoZXZyb24tbGVmdFxuICogQHNlZSBodHRwczovL2x1Y2lkZS5kZXYvZ3VpZGUvcGFja2FnZXMvbHVjaWRlLXJlYWN0IC0gRG9jdW1lbnRhdGlvblxuICpcbiAqIEBwYXJhbSB7T2JqZWN0fSBwcm9wcyAtIEx1Y2lkZSBpY29ucyBwcm9wcyBhbmQgYW55IHZhbGlkIFNWRyBhdHRyaWJ1dGVcbiAqIEByZXR1cm5zIHtKU1guRWxlbWVudH0gSlNYIEVsZW1lbnRcbiAqXG4gKi9cbmNvbnN0IENoZXZyb25MZWZ0ID0gY3JlYXRlTHVjaWRlSWNvbignQ2hldnJvbkxlZnQnLCBfX2ljb25Ob2RlKTtcblxuZXhwb3J0IGRlZmF1bHQgQ2hldnJvbkxlZnQ7XG4iXSwibmFtZXMiOltdLCJpZ25vcmVMaXN0IjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/lucide-react/dist/esm/icons/chevron-left.js\n");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "(ssr)/./node_modules/lucide-react/dist/esm/icons/chevron-right.js":
|
||||
/*!*******************************************************************!*\
|
||||
!*** ./node_modules/lucide-react/dist/esm/icons/chevron-right.js ***!
|
||||
\*******************************************************************/
|
||||
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
||||
|
||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ __iconNode: () => (/* binding */ __iconNode),\n/* harmony export */ \"default\": () => (/* binding */ ChevronRight)\n/* harmony export */ });\n/* harmony import */ var _createLucideIcon_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../createLucideIcon.js */ \"(ssr)/./node_modules/lucide-react/dist/esm/createLucideIcon.js\");\n/**\n * @license lucide-react v0.474.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */ \nconst __iconNode = [\n [\n \"path\",\n {\n d: \"m9 18 6-6-6-6\",\n key: \"mthhwq\"\n }\n ]\n];\nconst ChevronRight = (0,_createLucideIcon_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(\"ChevronRight\", __iconNode);\n //# sourceMappingURL=chevron-right.js.map\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvbHVjaWRlLXJlYWN0L2Rpc3QvZXNtL2ljb25zL2NoZXZyb24tcmlnaHQuanMiLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7O0FBR2EsaUJBQXVCO0lBQUM7UUFBQyxNQUFRO1FBQUEsQ0FBRTtZQUFBLEVBQUcsZ0JBQWlCO1lBQUEsS0FBSyxDQUFTO1FBQUEsQ0FBQztLQUFDO0NBQUE7QUFhOUUsbUJBQWUsa0VBQWlCLGlCQUFnQixDQUFVIiwic291cmNlcyI6WyIvVXNlcnMvbWF0dGJydWNlL0RvY3VtZW50cy9Qcm9qZWN0cy9zcmMvaWNvbnMvY2hldnJvbi1yaWdodC50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgY3JlYXRlTHVjaWRlSWNvbiBmcm9tICcuLi9jcmVhdGVMdWNpZGVJY29uJztcbmltcG9ydCB7IEljb25Ob2RlIH0gZnJvbSAnLi4vdHlwZXMnO1xuXG5leHBvcnQgY29uc3QgX19pY29uTm9kZTogSWNvbk5vZGUgPSBbWydwYXRoJywgeyBkOiAnbTkgMTggNi02LTYtNicsIGtleTogJ210aGh3cScgfV1dO1xuXG4vKipcbiAqIEBjb21wb25lbnQgQG5hbWUgQ2hldnJvblJpZ2h0XG4gKiBAZGVzY3JpcHRpb24gTHVjaWRlIFNWRyBpY29uIGNvbXBvbmVudCwgcmVuZGVycyBTVkcgRWxlbWVudCB3aXRoIGNoaWxkcmVuLlxuICpcbiAqIEBwcmV2aWV3ICFbaW1nXShkYXRhOmltYWdlL3N2Zyt4bWw7YmFzZTY0LFBITjJaeUFnZUcxc2JuTTlJbWgwZEhBNkx5OTNkM2N1ZHpNdWIzSm5Mekl3TURBdmMzWm5JZ29nSUhkcFpIUm9QU0l5TkNJS0lDQm9aV2xuYUhROUlqSTBJZ29nSUhacFpYZENiM2c5SWpBZ01DQXlOQ0F5TkNJS0lDQm1hV3hzUFNKdWIyNWxJZ29nSUhOMGNtOXJaVDBpSXpBd01DSWdjM1I1YkdVOUltSmhZMnRuY205MWJtUXRZMjlzYjNJNklDTm1abVk3SUdKdmNtUmxjaTF5WVdScGRYTTZJREp3ZUNJS0lDQnpkSEp2YTJVdGQybGtkR2c5SWpJaUNpQWdjM1J5YjJ0bExXeHBibVZqWVhBOUluSnZkVzVrSWdvZ0lITjBjbTlyWlMxc2FXNWxhbTlwYmowaWNtOTFibVFpQ2o0S0lDQThjR0YwYUNCa1BTSnRPU0F4T0NBMkxUWXROaTAySWlBdlBnbzhMM04yWno0SykgLSBodHRwczovL2x1Y2lkZS5kZXYvaWNvbnMvY2hldnJvbi1yaWdodFxuICogQHNlZSBodHRwczovL2x1Y2lkZS5kZXYvZ3VpZGUvcGFja2FnZXMvbHVjaWRlLXJlYWN0IC0gRG9jdW1lbnRhdGlvblxuICpcbiAqIEBwYXJhbSB7T2JqZWN0fSBwcm9wcyAtIEx1Y2lkZSBpY29ucyBwcm9wcyBhbmQgYW55IHZhbGlkIFNWRyBhdHRyaWJ1dGVcbiAqIEByZXR1cm5zIHtKU1guRWxlbWVudH0gSlNYIEVsZW1lbnRcbiAqXG4gKi9cbmNvbnN0IENoZXZyb25SaWdodCA9IGNyZWF0ZUx1Y2lkZUljb24oJ0NoZXZyb25SaWdodCcsIF9faWNvbk5vZGUpO1xuXG5leHBvcnQgZGVmYXVsdCBDaGV2cm9uUmlnaHQ7XG4iXSwibmFtZXMiOltdLCJpZ25vcmVMaXN0IjpbXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/lucide-react/dist/esm/icons/chevron-right.js\n");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "(ssr)/./node_modules/lucide-react/dist/esm/icons/circle-check.js":
|
||||
/*!******************************************************************!*\
|
||||
!*** ./node_modules/lucide-react/dist/esm/icons/circle-check.js ***!
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -47,6 +47,11 @@
|
||||
/******/ __webpack_require__.m = __webpack_modules__;
|
||||
/******/
|
||||
/************************************************************************/
|
||||
/******/ /* webpack/runtime/amd options */
|
||||
/******/ (() => {
|
||||
/******/ __webpack_require__.amdO = {};
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/compat get default export */
|
||||
/******/ (() => {
|
||||
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
||||
@ -125,7 +130,7 @@
|
||||
/******/
|
||||
/******/ /* webpack/runtime/getFullHash */
|
||||
/******/ (() => {
|
||||
/******/ __webpack_require__.h = () => ("c6e563d4ee2c6c8a")
|
||||
/******/ __webpack_require__.h = () => ("b99e5d3b3445fbfa")
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/hasOwnProperty shorthand */
|
||||
|
||||
50
.next/static/chunks/app/_not-found/page.js
Normal file
50
.next/static/chunks/app/_not-found/page.js
Normal file
File diff suppressed because one or more lines are too long
28
.next/static/chunks/app/api/monitor/route.js
Normal file
28
.next/static/chunks/app/api/monitor/route.js
Normal file
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* ATTENTION: An "eval-source-map" devtool has been used.
|
||||
* This devtool is neither made for production nor for readable output files.
|
||||
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
|
||||
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
|
||||
* or disable the default devtool with "devtool: false".
|
||||
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
|
||||
*/
|
||||
(self["webpackChunk_N_E"] = self["webpackChunk_N_E"] || []).push([["app/api/monitor/route"],{
|
||||
|
||||
/***/ "(app-pages-browser)/./node_modules/next/dist/build/webpack/loaders/next-flight-client-entry-loader.js?server=false!":
|
||||
/*!*******************************************************************************************************!*\
|
||||
!*** ./node_modules/next/dist/build/webpack/loaders/next-flight-client-entry-loader.js?server=false! ***!
|
||||
\*******************************************************************************************************/
|
||||
/***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
|
||||
|
||||
|
||||
|
||||
/***/ })
|
||||
|
||||
},
|
||||
/******/ __webpack_require__ => { // webpackRuntimeModules
|
||||
/******/ var __webpack_exec__ = (moduleId) => (__webpack_require__(__webpack_require__.s = moduleId))
|
||||
/******/ __webpack_require__.O(0, ["main-app"], () => (__webpack_exec__("(app-pages-browser)/./node_modules/next/dist/build/webpack/loaders/next-flight-client-entry-loader.js?server=false!")));
|
||||
/******/ var __webpack_exports__ = __webpack_require__.O();
|
||||
/******/ _N_E = __webpack_exports__;
|
||||
/******/ }
|
||||
]);
|
||||
File diff suppressed because one or more lines are too long
@ -190,7 +190,7 @@
|
||||
/******/
|
||||
/******/ /* webpack/runtime/getFullHash */
|
||||
/******/ (() => {
|
||||
/******/ __webpack_require__.h = () => ("2f7aec89503a6f6a")
|
||||
/******/ __webpack_require__.h = () => ("96d76a5621faa661")
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/global */
|
||||
|
||||
@ -0,0 +1 @@
|
||||
{"c":["webpack"],"r":[],"m":[]}
|
||||
@ -0,0 +1 @@
|
||||
{"c":["webpack"],"r":[],"m":[]}
|
||||
@ -0,0 +1 @@
|
||||
{"c":["app/page","webpack"],"r":["app/_not-found/page"],"m":["(app-pages-browser)/./node_modules/lucide-react/dist/esm/icons/chevron-left.js","(app-pages-browser)/./node_modules/lucide-react/dist/esm/icons/chevron-right.js","(app-pages-browser)/./node_modules/next/dist/build/webpack/loaders/next-client-pages-loader.js?absolutePagePath=%2FUsers%2Fmattbruce%2FDocuments%2FProjects%2FOpenClaw%2FWeb%2Fheartbeat-monitor%2Fnode_modules%2Fnext%2Fdist%2Fclient%2Fcomponents%2Fnot-found-error.js&page=%2F_not-found%2Fpage!","(app-pages-browser)/./node_modules/next/dist/client/components/http-access-fallback/error-fallback.js","(app-pages-browser)/./node_modules/next/dist/client/components/not-found-error.js"]}
|
||||
22
.next/static/webpack/app/page.40c8247711300955.hot-update.js
Normal file
22
.next/static/webpack/app/page.40c8247711300955.hot-update.js
Normal file
File diff suppressed because one or more lines are too long
18
.next/static/webpack/webpack.2f7aec89503a6f6a.hot-update.js
Normal file
18
.next/static/webpack/webpack.2f7aec89503a6f6a.hot-update.js
Normal file
@ -0,0 +1,18 @@
|
||||
"use strict";
|
||||
/*
|
||||
* ATTENTION: An "eval-source-map" devtool has been used.
|
||||
* This devtool is neither made for production nor for readable output files.
|
||||
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
|
||||
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
|
||||
* or disable the default devtool with "devtool: false".
|
||||
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
|
||||
*/
|
||||
self["webpackHotUpdate_N_E"]("webpack",{},
|
||||
/******/ function(__webpack_require__) { // webpackRuntimeModules
|
||||
/******/ /* webpack/runtime/getFullHash */
|
||||
/******/ (() => {
|
||||
/******/ __webpack_require__.h = () => ("3c9945e8dec13b19")
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ }
|
||||
);
|
||||
18
.next/static/webpack/webpack.3c9945e8dec13b19.hot-update.js
Normal file
18
.next/static/webpack/webpack.3c9945e8dec13b19.hot-update.js
Normal file
@ -0,0 +1,18 @@
|
||||
"use strict";
|
||||
/*
|
||||
* ATTENTION: An "eval-source-map" devtool has been used.
|
||||
* This devtool is neither made for production nor for readable output files.
|
||||
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
|
||||
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
|
||||
* or disable the default devtool with "devtool: false".
|
||||
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
|
||||
*/
|
||||
self["webpackHotUpdate_N_E"]("webpack",{},
|
||||
/******/ function(__webpack_require__) { // webpackRuntimeModules
|
||||
/******/ /* webpack/runtime/getFullHash */
|
||||
/******/ (() => {
|
||||
/******/ __webpack_require__.h = () => ("40c8247711300955")
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ }
|
||||
);
|
||||
18
.next/static/webpack/webpack.40c8247711300955.hot-update.js
Normal file
18
.next/static/webpack/webpack.40c8247711300955.hot-update.js
Normal file
@ -0,0 +1,18 @@
|
||||
"use strict";
|
||||
/*
|
||||
* ATTENTION: An "eval-source-map" devtool has been used.
|
||||
* This devtool is neither made for production nor for readable output files.
|
||||
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
|
||||
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
|
||||
* or disable the default devtool with "devtool: false".
|
||||
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
|
||||
*/
|
||||
self["webpackHotUpdate_N_E"]("webpack",{},
|
||||
/******/ function(__webpack_require__) { // webpackRuntimeModules
|
||||
/******/ /* webpack/runtime/getFullHash */
|
||||
/******/ (() => {
|
||||
/******/ __webpack_require__.h = () => ("96d76a5621faa661")
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ }
|
||||
);
|
||||
File diff suppressed because one or more lines are too long
347
.next/types/app/api/monitor/route.ts
Normal file
347
.next/types/app/api/monitor/route.ts
Normal file
@ -0,0 +1,347 @@
|
||||
// File: /Users/mattbruce/Documents/Projects/OpenClaw/Web/heartbeat-monitor/src/app/api/monitor/route.ts
|
||||
import * as entry from '../../../../../src/app/api/monitor/route.js'
|
||||
import type { NextRequest } from 'next/server.js'
|
||||
|
||||
type TEntry = typeof import('../../../../../src/app/api/monitor/route.js')
|
||||
|
||||
type SegmentParams<T extends Object = any> = T extends Record<string, any>
|
||||
? { [K in keyof T]: T[K] extends string ? string | string[] | undefined : never }
|
||||
: T
|
||||
|
||||
// Check that the entry is a valid entry
|
||||
checkFields<Diff<{
|
||||
GET?: Function
|
||||
HEAD?: Function
|
||||
OPTIONS?: Function
|
||||
POST?: Function
|
||||
PUT?: Function
|
||||
DELETE?: Function
|
||||
PATCH?: Function
|
||||
config?: {}
|
||||
generateStaticParams?: Function
|
||||
revalidate?: RevalidateRange<TEntry> | false
|
||||
dynamic?: 'auto' | 'force-dynamic' | 'error' | 'force-static'
|
||||
dynamicParams?: boolean
|
||||
fetchCache?: 'auto' | 'force-no-store' | 'only-no-store' | 'default-no-store' | 'default-cache' | 'only-cache' | 'force-cache'
|
||||
preferredRegion?: 'auto' | 'global' | 'home' | string | string[]
|
||||
runtime?: 'nodejs' | 'experimental-edge' | 'edge'
|
||||
maxDuration?: number
|
||||
|
||||
}, TEntry, ''>>()
|
||||
|
||||
type RouteContext = { params: Promise<SegmentParams> }
|
||||
// Check the prop type of the entry function
|
||||
if ('GET' in entry) {
|
||||
checkFields<
|
||||
Diff<
|
||||
ParamCheck<Request | NextRequest>,
|
||||
{
|
||||
__tag__: 'GET'
|
||||
__param_position__: 'first'
|
||||
__param_type__: FirstArg<MaybeField<TEntry, 'GET'>>
|
||||
},
|
||||
'GET'
|
||||
>
|
||||
>()
|
||||
checkFields<
|
||||
Diff<
|
||||
ParamCheck<RouteContext>,
|
||||
{
|
||||
__tag__: 'GET'
|
||||
__param_position__: 'second'
|
||||
__param_type__: SecondArg<MaybeField<TEntry, 'GET'>>
|
||||
},
|
||||
'GET'
|
||||
>
|
||||
>()
|
||||
|
||||
checkFields<
|
||||
Diff<
|
||||
{
|
||||
__tag__: 'GET',
|
||||
__return_type__: Response | void | never | Promise<Response | void | never>
|
||||
},
|
||||
{
|
||||
__tag__: 'GET',
|
||||
__return_type__: ReturnType<MaybeField<TEntry, 'GET'>>
|
||||
},
|
||||
'GET'
|
||||
>
|
||||
>()
|
||||
}
|
||||
// Check the prop type of the entry function
|
||||
if ('HEAD' in entry) {
|
||||
checkFields<
|
||||
Diff<
|
||||
ParamCheck<Request | NextRequest>,
|
||||
{
|
||||
__tag__: 'HEAD'
|
||||
__param_position__: 'first'
|
||||
__param_type__: FirstArg<MaybeField<TEntry, 'HEAD'>>
|
||||
},
|
||||
'HEAD'
|
||||
>
|
||||
>()
|
||||
checkFields<
|
||||
Diff<
|
||||
ParamCheck<RouteContext>,
|
||||
{
|
||||
__tag__: 'HEAD'
|
||||
__param_position__: 'second'
|
||||
__param_type__: SecondArg<MaybeField<TEntry, 'HEAD'>>
|
||||
},
|
||||
'HEAD'
|
||||
>
|
||||
>()
|
||||
|
||||
checkFields<
|
||||
Diff<
|
||||
{
|
||||
__tag__: 'HEAD',
|
||||
__return_type__: Response | void | never | Promise<Response | void | never>
|
||||
},
|
||||
{
|
||||
__tag__: 'HEAD',
|
||||
__return_type__: ReturnType<MaybeField<TEntry, 'HEAD'>>
|
||||
},
|
||||
'HEAD'
|
||||
>
|
||||
>()
|
||||
}
|
||||
// Check the prop type of the entry function
|
||||
if ('OPTIONS' in entry) {
|
||||
checkFields<
|
||||
Diff<
|
||||
ParamCheck<Request | NextRequest>,
|
||||
{
|
||||
__tag__: 'OPTIONS'
|
||||
__param_position__: 'first'
|
||||
__param_type__: FirstArg<MaybeField<TEntry, 'OPTIONS'>>
|
||||
},
|
||||
'OPTIONS'
|
||||
>
|
||||
>()
|
||||
checkFields<
|
||||
Diff<
|
||||
ParamCheck<RouteContext>,
|
||||
{
|
||||
__tag__: 'OPTIONS'
|
||||
__param_position__: 'second'
|
||||
__param_type__: SecondArg<MaybeField<TEntry, 'OPTIONS'>>
|
||||
},
|
||||
'OPTIONS'
|
||||
>
|
||||
>()
|
||||
|
||||
checkFields<
|
||||
Diff<
|
||||
{
|
||||
__tag__: 'OPTIONS',
|
||||
__return_type__: Response | void | never | Promise<Response | void | never>
|
||||
},
|
||||
{
|
||||
__tag__: 'OPTIONS',
|
||||
__return_type__: ReturnType<MaybeField<TEntry, 'OPTIONS'>>
|
||||
},
|
||||
'OPTIONS'
|
||||
>
|
||||
>()
|
||||
}
|
||||
// Check the prop type of the entry function
|
||||
if ('POST' in entry) {
|
||||
checkFields<
|
||||
Diff<
|
||||
ParamCheck<Request | NextRequest>,
|
||||
{
|
||||
__tag__: 'POST'
|
||||
__param_position__: 'first'
|
||||
__param_type__: FirstArg<MaybeField<TEntry, 'POST'>>
|
||||
},
|
||||
'POST'
|
||||
>
|
||||
>()
|
||||
checkFields<
|
||||
Diff<
|
||||
ParamCheck<RouteContext>,
|
||||
{
|
||||
__tag__: 'POST'
|
||||
__param_position__: 'second'
|
||||
__param_type__: SecondArg<MaybeField<TEntry, 'POST'>>
|
||||
},
|
||||
'POST'
|
||||
>
|
||||
>()
|
||||
|
||||
checkFields<
|
||||
Diff<
|
||||
{
|
||||
__tag__: 'POST',
|
||||
__return_type__: Response | void | never | Promise<Response | void | never>
|
||||
},
|
||||
{
|
||||
__tag__: 'POST',
|
||||
__return_type__: ReturnType<MaybeField<TEntry, 'POST'>>
|
||||
},
|
||||
'POST'
|
||||
>
|
||||
>()
|
||||
}
|
||||
// Check the prop type of the entry function
|
||||
if ('PUT' in entry) {
|
||||
checkFields<
|
||||
Diff<
|
||||
ParamCheck<Request | NextRequest>,
|
||||
{
|
||||
__tag__: 'PUT'
|
||||
__param_position__: 'first'
|
||||
__param_type__: FirstArg<MaybeField<TEntry, 'PUT'>>
|
||||
},
|
||||
'PUT'
|
||||
>
|
||||
>()
|
||||
checkFields<
|
||||
Diff<
|
||||
ParamCheck<RouteContext>,
|
||||
{
|
||||
__tag__: 'PUT'
|
||||
__param_position__: 'second'
|
||||
__param_type__: SecondArg<MaybeField<TEntry, 'PUT'>>
|
||||
},
|
||||
'PUT'
|
||||
>
|
||||
>()
|
||||
|
||||
checkFields<
|
||||
Diff<
|
||||
{
|
||||
__tag__: 'PUT',
|
||||
__return_type__: Response | void | never | Promise<Response | void | never>
|
||||
},
|
||||
{
|
||||
__tag__: 'PUT',
|
||||
__return_type__: ReturnType<MaybeField<TEntry, 'PUT'>>
|
||||
},
|
||||
'PUT'
|
||||
>
|
||||
>()
|
||||
}
|
||||
// Check the prop type of the entry function
|
||||
if ('DELETE' in entry) {
|
||||
checkFields<
|
||||
Diff<
|
||||
ParamCheck<Request | NextRequest>,
|
||||
{
|
||||
__tag__: 'DELETE'
|
||||
__param_position__: 'first'
|
||||
__param_type__: FirstArg<MaybeField<TEntry, 'DELETE'>>
|
||||
},
|
||||
'DELETE'
|
||||
>
|
||||
>()
|
||||
checkFields<
|
||||
Diff<
|
||||
ParamCheck<RouteContext>,
|
||||
{
|
||||
__tag__: 'DELETE'
|
||||
__param_position__: 'second'
|
||||
__param_type__: SecondArg<MaybeField<TEntry, 'DELETE'>>
|
||||
},
|
||||
'DELETE'
|
||||
>
|
||||
>()
|
||||
|
||||
checkFields<
|
||||
Diff<
|
||||
{
|
||||
__tag__: 'DELETE',
|
||||
__return_type__: Response | void | never | Promise<Response | void | never>
|
||||
},
|
||||
{
|
||||
__tag__: 'DELETE',
|
||||
__return_type__: ReturnType<MaybeField<TEntry, 'DELETE'>>
|
||||
},
|
||||
'DELETE'
|
||||
>
|
||||
>()
|
||||
}
|
||||
// Check the prop type of the entry function
|
||||
if ('PATCH' in entry) {
|
||||
checkFields<
|
||||
Diff<
|
||||
ParamCheck<Request | NextRequest>,
|
||||
{
|
||||
__tag__: 'PATCH'
|
||||
__param_position__: 'first'
|
||||
__param_type__: FirstArg<MaybeField<TEntry, 'PATCH'>>
|
||||
},
|
||||
'PATCH'
|
||||
>
|
||||
>()
|
||||
checkFields<
|
||||
Diff<
|
||||
ParamCheck<RouteContext>,
|
||||
{
|
||||
__tag__: 'PATCH'
|
||||
__param_position__: 'second'
|
||||
__param_type__: SecondArg<MaybeField<TEntry, 'PATCH'>>
|
||||
},
|
||||
'PATCH'
|
||||
>
|
||||
>()
|
||||
|
||||
checkFields<
|
||||
Diff<
|
||||
{
|
||||
__tag__: 'PATCH',
|
||||
__return_type__: Response | void | never | Promise<Response | void | never>
|
||||
},
|
||||
{
|
||||
__tag__: 'PATCH',
|
||||
__return_type__: ReturnType<MaybeField<TEntry, 'PATCH'>>
|
||||
},
|
||||
'PATCH'
|
||||
>
|
||||
>()
|
||||
}
|
||||
|
||||
// Check the arguments and return type of the generateStaticParams function
|
||||
if ('generateStaticParams' in entry) {
|
||||
checkFields<Diff<{ params: SegmentParams }, FirstArg<MaybeField<TEntry, 'generateStaticParams'>>, 'generateStaticParams'>>()
|
||||
checkFields<Diff<{ __tag__: 'generateStaticParams', __return_type__: any[] | Promise<any[]> }, { __tag__: 'generateStaticParams', __return_type__: ReturnType<MaybeField<TEntry, 'generateStaticParams'>> }>>()
|
||||
}
|
||||
|
||||
export interface PageProps {
|
||||
params?: Promise<SegmentParams>
|
||||
searchParams?: Promise<any>
|
||||
}
|
||||
export interface LayoutProps {
|
||||
children?: React.ReactNode
|
||||
|
||||
params?: Promise<SegmentParams>
|
||||
}
|
||||
|
||||
// =============
|
||||
// Utility types
|
||||
type RevalidateRange<T> = T extends { revalidate: any } ? NonNegative<T['revalidate']> : never
|
||||
|
||||
// If T is unknown or any, it will be an empty {} type. Otherwise, it will be the same as Omit<T, keyof Base>.
|
||||
type OmitWithTag<T, K extends keyof any, _M> = Omit<T, K>
|
||||
type Diff<Base, T extends Base, Message extends string = ''> = 0 extends (1 & T) ? {} : OmitWithTag<T, keyof Base, Message>
|
||||
|
||||
type FirstArg<T extends Function> = T extends (...args: [infer T, any]) => any ? unknown extends T ? any : T : never
|
||||
type SecondArg<T extends Function> = T extends (...args: [any, infer T]) => any ? unknown extends T ? any : T : never
|
||||
type MaybeField<T, K extends string> = T extends { [k in K]: infer G } ? G extends Function ? G : never : never
|
||||
|
||||
type ParamCheck<T> = {
|
||||
__tag__: string
|
||||
__param_position__: string
|
||||
__param_type__: T
|
||||
}
|
||||
|
||||
function checkFields<_ extends { [k in keyof any]: never }>() {}
|
||||
|
||||
// https://github.com/sindresorhus/type-fest
|
||||
type Numeric = number | bigint
|
||||
type Zero = 0 | 0n
|
||||
type Negative<T extends Numeric> = T extends Zero ? never : `${T}` extends `-${string}` ? T : never
|
||||
type NonNegative<T extends Numeric> = T extends Zero ? T : Negative<T> extends never ? T : '__invalid_negative_number__'
|
||||
456
src/app/page.tsx
456
src/app/page.tsx
@ -2,12 +2,6 @@
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
ResponsiveContainer,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import {
|
||||
Activity,
|
||||
Plus,
|
||||
@ -23,16 +17,20 @@ import {
|
||||
Monitor,
|
||||
AlertTriangle,
|
||||
History,
|
||||
MoreVertical,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
AlertCircle,
|
||||
Clock,
|
||||
Zap,
|
||||
TrendingUp,
|
||||
Server,
|
||||
MoreVertical,
|
||||
Menu,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
ResponsiveContainer,
|
||||
} from "recharts";
|
||||
|
||||
// Types
|
||||
interface App {
|
||||
@ -55,32 +53,110 @@ interface StatusEntry {
|
||||
responseTime?: number;
|
||||
}
|
||||
|
||||
interface NavItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
}
|
||||
|
||||
// Mock data generator for sparklines
|
||||
// Generate sparkline data
|
||||
const generateSparklineData = (points: number, isUp: boolean) => {
|
||||
return Array.from({ length: points }, (_, i) => ({
|
||||
value: isUp
|
||||
? 80 + Math.random() * 20
|
||||
: Math.random() * 30,
|
||||
value: isUp ? 80 + Math.random() * 20 : Math.random() * 30,
|
||||
time: i,
|
||||
}));
|
||||
};
|
||||
|
||||
// Components
|
||||
// shadcn-style Card Component
|
||||
const Card = ({ children, className = "" }: { children: React.ReactNode; className?: string }) => (
|
||||
<div className={`bg-slate-900/60 backdrop-blur-xl border border-slate-800/60 rounded-xl overflow-hidden ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const CardContent = ({ children, className = "" }: { children: React.ReactNode; className?: string }) => (
|
||||
<div className={`p-6 ${className}`}>{children}</div>
|
||||
);
|
||||
|
||||
// shadcn-style Badge
|
||||
const Badge = ({
|
||||
children,
|
||||
variant = "default",
|
||||
className = ""
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
variant?: "default" | "success" | "destructive" | "warning";
|
||||
className?: string;
|
||||
}) => {
|
||||
const variants = {
|
||||
default: "bg-slate-800 text-slate-200 border-slate-700",
|
||||
success: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20",
|
||||
destructive: "bg-red-500/10 text-red-400 border-red-500/20",
|
||||
warning: "bg-amber-500/10 text-amber-400 border-amber-500/20",
|
||||
};
|
||||
return (
|
||||
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium border ${variants[variant]} ${className}`}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
// shadcn-style Progress
|
||||
const Progress = ({ value, className = "" }: { value: number; className?: string }) => {
|
||||
const getColor = () => {
|
||||
if (value >= 95) return "bg-emerald-500";
|
||||
if (value >= 80) return "bg-amber-500";
|
||||
return "bg-red-500";
|
||||
};
|
||||
return (
|
||||
<div className={`w-full bg-slate-800 rounded-full h-2 overflow-hidden ${className}`}>
|
||||
<motion.div
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${value}%` }}
|
||||
transition={{ duration: 1, ease: "easeOut" }}
|
||||
className={`h-full rounded-full ${getColor()}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// shadcn-style Button
|
||||
const Button = ({
|
||||
children,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
className = "",
|
||||
...props
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
variant?: "default" | "ghost" | "outline";
|
||||
size?: "default" | "sm" | "icon";
|
||||
className?: string;
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
const variants = {
|
||||
default: "bg-emerald-600 hover:bg-emerald-500 text-white",
|
||||
ghost: "hover:bg-slate-800 text-slate-400 hover:text-white",
|
||||
outline: "border border-slate-700 hover:bg-slate-800 text-slate-300",
|
||||
};
|
||||
const sizes = {
|
||||
default: "px-4 py-2",
|
||||
sm: "px-3 py-1.5 text-sm",
|
||||
icon: "p-2",
|
||||
};
|
||||
return (
|
||||
<button
|
||||
className={`inline-flex items-center justify-center rounded-lg font-medium transition-colors ${variants[variant]} ${sizes[size]} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
// Sidebar Item Component
|
||||
const SidebarItem = ({
|
||||
item,
|
||||
icon: Icon,
|
||||
label,
|
||||
isActive,
|
||||
isCollapsed,
|
||||
onClick
|
||||
}: {
|
||||
item: NavItem;
|
||||
icon: any;
|
||||
label: string;
|
||||
isActive: boolean;
|
||||
isCollapsed: boolean;
|
||||
onClick: () => void;
|
||||
}) => (
|
||||
<motion.button
|
||||
@ -93,30 +169,19 @@ const SidebarItem = ({
|
||||
: "text-slate-400 hover:text-slate-100 hover:bg-slate-800/50"
|
||||
}`}
|
||||
>
|
||||
<span className="flex-shrink-0">{item.icon}</span>
|
||||
<AnimatePresence mode="wait">
|
||||
{!isCollapsed && (
|
||||
<motion.span
|
||||
initial={{ opacity: 0, width: 0 }}
|
||||
animate={{ opacity: 1, width: "auto" }}
|
||||
exit={{ opacity: 0, width: 0 }}
|
||||
className="text-sm font-medium whitespace-nowrap overflow-hidden"
|
||||
>
|
||||
{item.label}
|
||||
</motion.span>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<Icon className="w-5 h-5 flex-shrink-0" />
|
||||
<span className="text-sm font-medium whitespace-nowrap">{label}</span>
|
||||
</motion.button>
|
||||
);
|
||||
|
||||
const MetricCard = ({
|
||||
// KPI Card Component
|
||||
const KPICard = ({
|
||||
title,
|
||||
value,
|
||||
subtitle,
|
||||
icon: Icon,
|
||||
color,
|
||||
data,
|
||||
trend
|
||||
}: {
|
||||
title: string;
|
||||
value: string | number;
|
||||
@ -124,55 +189,47 @@ const MetricCard = ({
|
||||
icon: any;
|
||||
color: string;
|
||||
data: any[];
|
||||
trend?: "up" | "down" | "neutral";
|
||||
}) => {
|
||||
const colorClasses: Record<string, string> = {
|
||||
emerald: "from-emerald-500/20 to-emerald-500/5 border-emerald-500/20",
|
||||
blue: "from-blue-500/20 to-blue-500/5 border-blue-500/20",
|
||||
purple: "from-purple-500/20 to-purple-500/5 border-purple-500/20",
|
||||
amber: "from-amber-500/20 to-amber-500/5 border-amber-500/20",
|
||||
const colorMap: Record<string, { bg: string; text: string; bar: string }> = {
|
||||
emerald: { bg: "bg-emerald-500/10", text: "text-emerald-400", bar: "#10b981" },
|
||||
blue: { bg: "bg-blue-500/10", text: "text-blue-400", bar: "#3b82f6" },
|
||||
purple: { bg: "bg-purple-500/10", text: "text-purple-400", bar: "#a855f7" },
|
||||
amber: { bg: "bg-amber-500/10", text: "text-amber-400", bar: "#f59e0b" },
|
||||
};
|
||||
const colors = colorMap[color] || colorMap.emerald;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
whileHover={{ y: -4, transition: { duration: 0.2 } }}
|
||||
className={`relative overflow-hidden rounded-xl border bg-gradient-to-br p-5 ${colorClasses[color]} backdrop-blur-sm`}
|
||||
>
|
||||
<Card className="hover:border-slate-700/60 transition-all duration-300 hover:shadow-xl hover:shadow-black/20 hover:scale-[1.02]">
|
||||
<CardContent>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-slate-400 text-sm font-medium mb-1">{title}</p>
|
||||
<p className="text-3xl font-bold text-white">{value}</p>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
{trend && (
|
||||
<span className={`text-xs ${trend === "up" ? "text-emerald-400" : trend === "down" ? "text-red-400" : "text-slate-400"}`}>
|
||||
{trend === "up" ? "↑" : trend === "down" ? "↓" : "→"}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-slate-500 text-xs">{subtitle}</span>
|
||||
<p className="text-slate-500 text-xs mt-1">{subtitle}</p>
|
||||
</div>
|
||||
<div className={`p-2.5 rounded-lg ${colors.bg}`}>
|
||||
<Icon className={`w-5 h-5 ${colors.text}`} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={`p-2.5 rounded-lg bg-${color}-500/10`}>
|
||||
<Icon className={`w-5 h-5 text-${color}-400`} style={{ color: color === "emerald" ? "#34d399" : color === "blue" ? "#60a5fa" : color === "purple" ? "#a78bfa" : "#fbbf24" }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 h-12">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={data}>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={color === "emerald" ? "#34d399" : color === "blue" ? "#60a5fa" : color === "purple" ? "#a78bfa" : "#fbbf24"}
|
||||
stroke={colors.bar}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</motion.div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
// Service Card Component
|
||||
const ServiceCard = ({
|
||||
app,
|
||||
status,
|
||||
@ -183,7 +240,7 @@ const ServiceCard = ({
|
||||
onDelete: () => void;
|
||||
}) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const sparklineData = generateSparklineData(12, status.isUp);
|
||||
const sparklineData = generateSparklineData(10, status.isUp);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
@ -191,15 +248,18 @@ const ServiceCard = ({
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
whileHover={{ y: -2 }}
|
||||
whileHover={{ scale: 1.02, transition: { duration: 0.2 } }}
|
||||
onHoverStart={() => setIsHovered(true)}
|
||||
onHoverEnd={() => setIsHovered(false)}
|
||||
className="group relative bg-slate-900/50 border border-slate-800 rounded-xl p-5 hover:border-slate-700 transition-all duration-300"
|
||||
className="group"
|
||||
>
|
||||
{/* Status Indicator Line */}
|
||||
<div className={`absolute top-0 left-4 right-4 h-0.5 rounded-full ${status.isUp ? "bg-emerald-500" : "bg-red-500"}`} />
|
||||
<Card className="h-full hover:border-slate-700/60 transition-all duration-300 hover:shadow-xl hover:shadow-black/20">
|
||||
{/* Top colored line */}
|
||||
<div className={`h-1 w-full ${status.isUp ? "bg-emerald-500" : "bg-red-500"}`} />
|
||||
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<CardContent className="space-y-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${status.isUp ? "bg-emerald-500/10" : "bg-red-500/10"}`}>
|
||||
<Server className={`w-5 h-5 ${status.isUp ? "text-emerald-400" : "text-red-400"}`} />
|
||||
@ -207,7 +267,12 @@ const ServiceCard = ({
|
||||
<div>
|
||||
<h3 className="font-semibold text-white flex items-center gap-2">
|
||||
{app.name}
|
||||
<a href={app.url} target="_blank" rel="noopener noreferrer" className="text-slate-500 hover:text-emerald-400 transition-colors">
|
||||
<a
|
||||
href={app.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-slate-500 hover:text-emerald-400 transition-colors"
|
||||
>
|
||||
<ExternalLink className="w-3.5 h-3.5" />
|
||||
</a>
|
||||
</h3>
|
||||
@ -215,53 +280,40 @@ const ServiceCard = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium border ${
|
||||
status.isUp
|
||||
? "bg-emerald-500/10 border-emerald-500/20 text-emerald-400"
|
||||
: "bg-red-500/10 border-red-500/20 text-red-400"
|
||||
}`}>
|
||||
<span className="relative flex h-1.5 w-1.5">
|
||||
<span className={`animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 ${status.isUp ? "bg-emerald-400" : "bg-red-400"}`}></span>
|
||||
<span className={`relative inline-flex rounded-full h-1.5 w-1.5 ${status.isUp ? "bg-emerald-500" : "bg-red-500"}`}></span>
|
||||
<Badge variant={status.isUp ? "success" : "destructive"}>
|
||||
<span className="relative flex h-1.5 w-1.5 mr-1">
|
||||
<span className={`animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 ${status.isUp ? "bg-emerald-400" : "bg-red-400"}`} />
|
||||
<span className={`relative inline-flex rounded-full h-1.5 w-1.5 ${status.isUp ? "bg-emerald-500" : "bg-red-500"}`} />
|
||||
</span>
|
||||
{status.isUp ? "Operational" : "Down"}
|
||||
</div>
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Metrics */}
|
||||
<div className="space-y-4">
|
||||
{/* Uptime */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<span className="text-xs text-slate-400">Uptime</span>
|
||||
<span className="text-sm font-semibold text-white">{status.uptime}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 w-full bg-slate-800 rounded-full overflow-hidden">
|
||||
<motion.div
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${status.uptime}%` }}
|
||||
transition={{ duration: 1, delay: 0.2 }}
|
||||
className={`h-full rounded-full ${status.uptime > 95 ? "bg-emerald-500" : status.uptime > 80 ? "bg-amber-500" : "bg-red-500"}`}
|
||||
/>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-slate-400">Uptime</span>
|
||||
<span className="text-lg font-semibold text-white">{status.uptime}%</span>
|
||||
</div>
|
||||
<Progress value={status.uptime} />
|
||||
</div>
|
||||
|
||||
{/* Response Time */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-slate-400">Response Time</span>
|
||||
<span className="text-lg font-semibold text-white">
|
||||
<span className="text-sm text-slate-400">Response Time</span>
|
||||
<span className="text-2xl font-bold text-white">
|
||||
{status.avgResponseTime > 0 ? `${status.avgResponseTime}ms` : "—"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Sparkline */}
|
||||
<div className="h-10 -mx-1">
|
||||
<div className="h-10 -mx-2">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={sparklineData}>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={status.isUp ? "#34d399" : "#f87171"}
|
||||
stroke={status.isUp ? "#10b981" : "#ef4444"}
|
||||
strokeWidth={1.5}
|
||||
dot={false}
|
||||
/>
|
||||
@ -269,14 +321,13 @@ const ServiceCard = ({
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* Last Checked */}
|
||||
<div className="flex items-center justify-between text-xs text-slate-500">
|
||||
<span className="flex items-center gap-1">
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between pt-2 border-t border-slate-800/60">
|
||||
<span className="text-xs text-slate-500 flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
{status.latest ? new Date(status.latest.timestamp).toLocaleTimeString() : "Never"}
|
||||
</span>
|
||||
|
||||
{/* Hover Actions */}
|
||||
<AnimatePresence>
|
||||
{isHovered && (
|
||||
<motion.div
|
||||
@ -285,23 +336,26 @@ const ServiceCard = ({
|
||||
exit={{ opacity: 0, x: 10 }}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<button className="p-1.5 text-slate-400 hover:text-white hover:bg-slate-800 rounded transition-colors">
|
||||
<RefreshCw className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button className="p-1.5 text-slate-400 hover:text-white hover:bg-slate-800 rounded transition-colors">
|
||||
<Settings className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<Settings className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 hover:text-red-400 hover:bg-red-500/10"
|
||||
onClick={onDelete}
|
||||
className="p-1.5 text-slate-400 hover:text-red-400 hover:bg-red-500/10 rounded transition-colors"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
@ -310,7 +364,6 @@ export default function Dashboard() {
|
||||
const [apps, setApps] = useState<App[]>([]);
|
||||
const [status, setStatus] = useState<StatusEntry[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
const [activeNav, setActiveNav] = useState("overview");
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
const [newApp, setNewApp] = useState<Partial<App>>({
|
||||
@ -320,14 +373,6 @@ export default function Dashboard() {
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{ id: "overview", label: "Overview", icon: <LayoutDashboard className="w-5 h-5" /> },
|
||||
{ id: "services", label: "Monitored Services", icon: <Monitor className="w-5 h-5" /> },
|
||||
{ id: "incidents", label: "Incidents", icon: <AlertTriangle className="w-5 h-5" /> },
|
||||
{ id: "history", label: "History / Logs", icon: <History className="w-5 h-5" /> },
|
||||
{ id: "settings", label: "Settings", icon: <Settings className="w-5 h-5" /> },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
const interval = setInterval(fetchData, 30000);
|
||||
@ -398,7 +443,7 @@ export default function Dashboard() {
|
||||
|
||||
const allUp = stats.online === stats.total && stats.total > 0;
|
||||
|
||||
// Generate sparkline data for metrics
|
||||
// Sparkline data for KPI cards
|
||||
const servicesData = generateSparklineData(20, true);
|
||||
const uptimeData = generateSparklineData(20, true);
|
||||
const responseData = generateSparklineData(20, true).map(d => ({ ...d, value: d.value * 2 }));
|
||||
@ -428,118 +473,98 @@ export default function Dashboard() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-slate-100 flex">
|
||||
{/* Collapsible Sidebar */}
|
||||
<motion.aside
|
||||
initial={false}
|
||||
animate={{ width: sidebarCollapsed ? 72 : 240 }}
|
||||
className="fixed left-0 top-0 bottom-0 bg-slate-900/80 backdrop-blur-xl border-r border-slate-800 z-50 flex flex-col"
|
||||
>
|
||||
{/* Logo Area */}
|
||||
<div className="h-16 flex items-center px-4 border-b border-slate-800">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-emerald-500 to-cyan-500 flex items-center justify-center">
|
||||
<Activity className="w-4 h-4 text-white" />
|
||||
{/* Fixed Sidebar - 280px */}
|
||||
<aside className="fixed left-0 top-0 bottom-0 w-[280px] bg-slate-900/80 backdrop-blur-xl border-r border-slate-800/60 z-50 flex flex-col">
|
||||
{/* Logo */}
|
||||
<div className="h-16 flex items-center px-4 border-b border-slate-800/60">
|
||||
<div className="w-9 h-9 rounded-lg bg-gradient-to-br from-emerald-500 to-cyan-500 flex items-center justify-center">
|
||||
<Activity className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<AnimatePresence mode="wait">
|
||||
{!sidebarCollapsed && (
|
||||
<motion.span
|
||||
initial={{ opacity: 0, width: 0 }}
|
||||
animate={{ opacity: 1, width: "auto" }}
|
||||
exit={{ opacity: 0, width: 0 }}
|
||||
className="ml-3 font-bold text-lg whitespace-nowrap overflow-hidden"
|
||||
>
|
||||
Heartbeat
|
||||
</motion.span>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<span className="ml-3 font-bold text-lg text-white">Heartbeat</span>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 p-3 space-y-1">
|
||||
{navItems.map((item) => (
|
||||
<SidebarItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
isActive={activeNav === item.id}
|
||||
isCollapsed={sidebarCollapsed}
|
||||
onClick={() => setActiveNav(item.id)}
|
||||
icon={LayoutDashboard}
|
||||
label="Overview"
|
||||
isActive={activeNav === "overview"}
|
||||
onClick={() => setActiveNav("overview")}
|
||||
/>
|
||||
<SidebarItem
|
||||
icon={Monitor}
|
||||
label="Monitored Services"
|
||||
isActive={activeNav === "services"}
|
||||
onClick={() => setActiveNav("services")}
|
||||
/>
|
||||
<SidebarItem
|
||||
icon={AlertTriangle}
|
||||
label="Incidents"
|
||||
isActive={activeNav === "incidents"}
|
||||
onClick={() => setActiveNav("incidents")}
|
||||
/>
|
||||
<SidebarItem
|
||||
icon={History}
|
||||
label="History / Logs"
|
||||
isActive={activeNav === "history"}
|
||||
onClick={() => setActiveNav("history")}
|
||||
/>
|
||||
<SidebarItem
|
||||
icon={Settings}
|
||||
label="Settings"
|
||||
isActive={activeNav === "settings"}
|
||||
onClick={() => setActiveNav("settings")}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Collapse Button */}
|
||||
<div className="p-3 border-t border-slate-800">
|
||||
<button
|
||||
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
|
||||
className="w-full flex items-center justify-center p-2 text-slate-400 hover:text-white hover:bg-slate-800/50 rounded-lg transition-colors"
|
||||
>
|
||||
{sidebarCollapsed ? <ChevronRight className="w-5 h-5" /> : <ChevronLeft className="w-5 h-5" />}
|
||||
</button>
|
||||
</div>
|
||||
</motion.aside>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<main
|
||||
className="flex-1 min-h-screen transition-all duration-300"
|
||||
style={{ marginLeft: sidebarCollapsed ? 72 : 240 }}
|
||||
>
|
||||
<main className="flex-1 ml-[280px] min-h-screen flex flex-col">
|
||||
{/* Top Navbar */}
|
||||
<header className="h-16 bg-slate-900/50 backdrop-blur-sm border-b border-slate-800 sticky top-0 z-40 px-6 flex items-center justify-between">
|
||||
<header className="h-16 bg-slate-900/50 backdrop-blur-sm border-b border-slate-800/60 sticky top-0 z-40 px-6 flex items-center justify-between">
|
||||
{/* Global Status Banner */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`flex items-center gap-2 px-3 py-1.5 rounded-full border ${
|
||||
allUp
|
||||
? "bg-emerald-500/10 border-emerald-500/20"
|
||||
: "bg-amber-500/10 border-amber-500/20"
|
||||
}`}>
|
||||
<span className={`relative flex h-2 w-2`}>
|
||||
<span className={`animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 ${allUp ? "bg-emerald-400" : "bg-amber-400"}`}></span>
|
||||
<span className={`relative inline-flex rounded-full h-2 w-2 ${allUp ? "bg-emerald-500" : "bg-amber-500"}`}></span>
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className={`animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 ${allUp ? "bg-emerald-400" : "bg-amber-400"}`} />
|
||||
<span className={`relative inline-flex rounded-full h-2 w-2 ${allUp ? "bg-emerald-500" : "bg-amber-500"}`} />
|
||||
</span>
|
||||
<span className={`text-sm font-medium ${allUp ? "text-emerald-400" : "text-amber-400"}`}>
|
||||
{allUp ? "All Systems Operational" : `${stats.offline} Service${stats.offline > 1 ? 's' : ''} Down`}
|
||||
</span>
|
||||
<span className="text-slate-500 text-xs">• Updated {new Date().toLocaleTimeString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Side Actions */}
|
||||
{/* Right Actions */}
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
className="w-64 bg-slate-800/50 border border-slate-700 rounded-lg pl-10 pr-4 py-2 text-sm text-white placeholder-slate-500 focus:border-emerald-500/50 focus:outline-none transition-colors"
|
||||
className="w-64 bg-slate-800/50 border border-slate-700 rounded-lg pl-10 pr-4 py-2 text-sm text-white placeholder-slate-500 focus:border-emerald-500/50 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Add Monitor */}
|
||||
<button
|
||||
onClick={() => setShowAddModal(true)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-white rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<Button onClick={() => setShowAddModal(true)}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add Monitor
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
{/* Refresh */}
|
||||
<button
|
||||
onClick={fetchData}
|
||||
className="p-2 text-slate-400 hover:text-white hover:bg-slate-800 rounded-lg transition-colors"
|
||||
>
|
||||
<Button variant="ghost" size="icon" onClick={fetchData}>
|
||||
<RefreshCw className="w-5 h-5" />
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
{/* Notifications */}
|
||||
<button className="p-2 text-slate-400 hover:text-white hover:bg-slate-800 rounded-lg transition-colors relative">
|
||||
<Button variant="ghost" size="icon" className="relative">
|
||||
<Bell className="w-5 h-5" />
|
||||
{stats.incidents > 0 && (
|
||||
<span className="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full" />
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
{/* Avatar */}
|
||||
<div className="w-9 h-9 rounded-full bg-gradient-to-br from-emerald-500 to-cyan-500 flex items-center justify-center text-white font-medium text-sm">
|
||||
MB
|
||||
</div>
|
||||
@ -547,48 +572,45 @@ export default function Dashboard() {
|
||||
</header>
|
||||
|
||||
{/* Dashboard Content */}
|
||||
<div className="p-6 space-y-6">
|
||||
{/* KPI Row */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<MetricCard
|
||||
<div className="flex-1 p-6 overflow-auto">
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
{/* KPI Row - 4 cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<KPICard
|
||||
title="Services Online"
|
||||
value={`${stats.online}/${stats.total}`}
|
||||
subtitle={allUp ? "All healthy" : `${stats.offline} down`}
|
||||
icon={Server}
|
||||
color="emerald"
|
||||
data={servicesData}
|
||||
trend="up"
|
||||
/>
|
||||
<MetricCard
|
||||
<KPICard
|
||||
title="Average Uptime"
|
||||
value={`${stats.avgUptime}%`}
|
||||
subtitle="Last 30 days"
|
||||
icon={CheckCircle2}
|
||||
color="blue"
|
||||
data={uptimeData}
|
||||
trend="up"
|
||||
/>
|
||||
<MetricCard
|
||||
<KPICard
|
||||
title="Avg Response Time"
|
||||
value={`${stats.avgResponseTime}ms`}
|
||||
subtitle="Across all services"
|
||||
icon={Zap}
|
||||
color="purple"
|
||||
data={responseData}
|
||||
trend={stats.avgResponseTime < 200 ? "up" : "down"}
|
||||
/>
|
||||
<MetricCard
|
||||
<KPICard
|
||||
title="Total Incidents"
|
||||
value={stats.incidents}
|
||||
subtitle="Last 24 hours"
|
||||
icon={AlertTriangle}
|
||||
color="amber"
|
||||
data={incidentsData}
|
||||
trend={stats.incidents === 0 ? "up" : "down"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Services Grid */}
|
||||
{/* Monitored Services Section */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold text-white">Monitored Services</h2>
|
||||
@ -597,7 +619,7 @@ export default function Dashboard() {
|
||||
|
||||
<motion.div
|
||||
layout
|
||||
className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4"
|
||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
|
||||
>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{apps.map((app) => {
|
||||
@ -625,16 +647,14 @@ export default function Dashboard() {
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-white mb-2">No monitors yet</h3>
|
||||
<p className="text-slate-500 mb-4">Start monitoring your services</p>
|
||||
<button
|
||||
onClick={() => setShowAddModal(true)}
|
||||
className="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-white rounded-lg transition-colors"
|
||||
>
|
||||
<Button onClick={() => setShowAddModal(true)}>
|
||||
Add Your First Monitor
|
||||
</button>
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Add Monitor Modal */}
|
||||
@ -662,7 +682,7 @@ export default function Dashboard() {
|
||||
type="text"
|
||||
value={newApp.name}
|
||||
onChange={(e) => setNewApp({ ...newApp, name: e.target.value })}
|
||||
className="w-full bg-slate-800/50 border border-slate-700 rounded-lg px-4 py-2.5 text-white placeholder-slate-500 focus:border-emerald-500/50 focus:outline-none transition-colors"
|
||||
className="w-full bg-slate-800/50 border border-slate-700 rounded-lg px-4 py-2.5 text-white placeholder-slate-500 focus:border-emerald-500/50 focus:outline-none"
|
||||
placeholder="My Service"
|
||||
required
|
||||
/>
|
||||
@ -675,7 +695,7 @@ export default function Dashboard() {
|
||||
type="url"
|
||||
value={newApp.url}
|
||||
onChange={(e) => setNewApp({ ...newApp, url: e.target.value })}
|
||||
className="w-full bg-slate-800/50 border border-slate-700 rounded-lg px-4 py-2.5 text-white placeholder-slate-500 focus:border-emerald-500/50 focus:outline-none transition-colors"
|
||||
className="w-full bg-slate-800/50 border border-slate-700 rounded-lg px-4 py-2.5 text-white placeholder-slate-500 focus:border-emerald-500/50 focus:outline-none"
|
||||
placeholder="http://localhost:3000"
|
||||
required
|
||||
/>
|
||||
@ -686,26 +706,24 @@ export default function Dashboard() {
|
||||
type="number"
|
||||
value={newApp.port}
|
||||
onChange={(e) => setNewApp({ ...newApp, port: parseInt(e.target.value) })}
|
||||
className="w-full bg-slate-800/50 border border-slate-700 rounded-lg px-4 py-2.5 text-white placeholder-slate-500 focus:border-emerald-500/50 focus:outline-none transition-colors"
|
||||
className="w-full bg-slate-800/50 border border-slate-700 rounded-lg px-4 py-2.5 text-white placeholder-slate-500 focus:border-emerald-500/50 focus:outline-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setShowAddModal(false)}
|
||||
className="flex-1 px-4 py-2.5 border border-slate-700 rounded-lg text-slate-300 hover:bg-slate-800 transition-colors"
|
||||
className="flex-1"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-1 px-4 py-2.5 bg-emerald-600 hover:bg-emerald-500 text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
</Button>
|
||||
<Button type="submit" className="flex-1">
|
||||
Add Monitor
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</motion.div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user