Complete UI/UX redesign with modern dashboard aesthetics

- Applied dark OLED theme with slate color palette
- Added Fira Code/Inter typography for technical look
- Implemented glassmorphism cards with gradient accents
- Added smooth area charts with gradient fills
- Improved stats cards with glow effects
- Added tabbed navigation (Dashboard/Settings)
- Enhanced modal designs with better spacing
- Added custom scrollbar styling
- Implemented responsive grid layout
- Added hover animations and micro-interactions
- Improved accessibility with focus states
This commit is contained in:
OpenClaw Bot 2026-02-18 12:06:39 -06:00
parent bed1169443
commit 9568bd81d1
64 changed files with 13600 additions and 285 deletions

View File

@ -4,6 +4,17 @@
"static/chunks/webpack.js", "static/chunks/webpack.js",
"static/chunks/main-app.js", "static/chunks/main-app.js",
"static/chunks/app/api/monitor/route.js" "static/chunks/app/api/monitor/route.js"
],
"/layout": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/css/app/layout.css",
"static/chunks/app/layout.js"
],
"/page": [
"static/chunks/webpack.js",
"static/chunks/main-app.js",
"static/chunks/app/page.js"
] ]
} }
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,3 +1,4 @@
{ {
"/api/monitor/route": "app/api/monitor/route.js" "/api/monitor/route": "app/api/monitor/route.js",
"/page": "app/page.js"
} }

File diff suppressed because one or more lines are too long

218
.next/server/app/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

View File

@ -1 +1 @@
self.__NEXT_FONT_MANIFEST="{\"pages\":{},\"app\":{},\"appUsingSizeAdjust\":false,\"pagesUsingSizeAdjust\":false}" self.__NEXT_FONT_MANIFEST="{\"pages\":{},\"app\":{\"/Users/mattbruce/Documents/Projects/OpenClaw/Web/heartbeat-monitor/src/app/layout\":[\"static/media/4cf2300e9c8272f7-s.p.woff2\",\"static/media/93f479601ee12b01-s.p.woff2\"]},\"appUsingSizeAdjust\":true,\"pagesUsingSizeAdjust\":false}"

View File

@ -1 +1 @@
{"pages":{},"app":{},"appUsingSizeAdjust":false,"pagesUsingSizeAdjust":false} {"pages":{},"app":{"/Users/mattbruce/Documents/Projects/OpenClaw/Web/heartbeat-monitor/src/app/layout":["static/media/4cf2300e9c8272f7-s.p.woff2","static/media/93f479601ee12b01-s.p.woff2"]},"appUsingSizeAdjust":true,"pagesUsingSizeAdjust":false}

View File

@ -0,0 +1,65 @@
"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/).
*/
exports.id = "vendor-chunks/@babel";
exports.ids = ["vendor-chunks/@babel"];
exports.modules = {
/***/ "(ssr)/./node_modules/@babel/runtime/helpers/esm/assertThisInitialized.js":
/*!**************************************************************************!*\
!*** ./node_modules/@babel/runtime/helpers/esm/assertThisInitialized.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 */ \"default\": () => (/* binding */ _assertThisInitialized)\n/* harmony export */ });\nfunction _assertThisInitialized(e) {\n if (void 0 === e) throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");\n return e;\n}\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQGJhYmVsL3J1bnRpbWUvaGVscGVycy9lc20vYXNzZXJ0VGhpc0luaXRpYWxpemVkLmpzIiwibWFwcGluZ3MiOiI7Ozs7QUFBQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsiL1VzZXJzL21hdHRicnVjZS9Eb2N1bWVudHMvUHJvamVjdHMvT3BlbkNsYXcvV2ViL2hlYXJ0YmVhdC1tb25pdG9yL25vZGVfbW9kdWxlcy9AYmFiZWwvcnVudGltZS9oZWxwZXJzL2VzbS9hc3NlcnRUaGlzSW5pdGlhbGl6ZWQuanMiXSwic291cmNlc0NvbnRlbnQiOlsiZnVuY3Rpb24gX2Fzc2VydFRoaXNJbml0aWFsaXplZChlKSB7XG4gIGlmICh2b2lkIDAgPT09IGUpIHRocm93IG5ldyBSZWZlcmVuY2VFcnJvcihcInRoaXMgaGFzbid0IGJlZW4gaW5pdGlhbGlzZWQgLSBzdXBlcigpIGhhc24ndCBiZWVuIGNhbGxlZFwiKTtcbiAgcmV0dXJuIGU7XG59XG5leHBvcnQgeyBfYXNzZXJ0VGhpc0luaXRpYWxpemVkIGFzIGRlZmF1bHQgfTsiXSwibmFtZXMiOltdLCJpZ25vcmVMaXN0IjpbMF0sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@babel/runtime/helpers/esm/assertThisInitialized.js\n");
/***/ }),
/***/ "(ssr)/./node_modules/@babel/runtime/helpers/esm/extends.js":
/*!************************************************************!*\
!*** ./node_modules/@babel/runtime/helpers/esm/extends.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 */ \"default\": () => (/* binding */ _extends)\n/* harmony export */ });\nfunction _extends() {\n return _extends = Object.assign ? Object.assign.bind() : function (n) {\n for (var e = 1; e < arguments.length; e++) {\n var t = arguments[e];\n for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]);\n }\n return n;\n }, _extends.apply(null, arguments);\n}\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQGJhYmVsL3J1bnRpbWUvaGVscGVycy9lc20vZXh0ZW5kcy5qcyIsIm1hcHBpbmdzIjoiOzs7O0FBQUE7QUFDQTtBQUNBLG9CQUFvQixzQkFBc0I7QUFDMUM7QUFDQSwwQkFBMEI7QUFDMUI7QUFDQTtBQUNBLEdBQUc7QUFDSCIsInNvdXJjZXMiOlsiL1VzZXJzL21hdHRicnVjZS9Eb2N1bWVudHMvUHJvamVjdHMvT3BlbkNsYXcvV2ViL2hlYXJ0YmVhdC1tb25pdG9yL25vZGVfbW9kdWxlcy9AYmFiZWwvcnVudGltZS9oZWxwZXJzL2VzbS9leHRlbmRzLmpzIl0sInNvdXJjZXNDb250ZW50IjpbImZ1bmN0aW9uIF9leHRlbmRzKCkge1xuICByZXR1cm4gX2V4dGVuZHMgPSBPYmplY3QuYXNzaWduID8gT2JqZWN0LmFzc2lnbi5iaW5kKCkgOiBmdW5jdGlvbiAobikge1xuICAgIGZvciAodmFyIGUgPSAxOyBlIDwgYXJndW1lbnRzLmxlbmd0aDsgZSsrKSB7XG4gICAgICB2YXIgdCA9IGFyZ3VtZW50c1tlXTtcbiAgICAgIGZvciAodmFyIHIgaW4gdCkgKHt9KS5oYXNPd25Qcm9wZXJ0eS5jYWxsKHQsIHIpICYmIChuW3JdID0gdFtyXSk7XG4gICAgfVxuICAgIHJldHVybiBuO1xuICB9LCBfZXh0ZW5kcy5hcHBseShudWxsLCBhcmd1bWVudHMpO1xufVxuZXhwb3J0IHsgX2V4dGVuZHMgYXMgZGVmYXVsdCB9OyJdLCJuYW1lcyI6W10sImlnbm9yZUxpc3QiOlswXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@babel/runtime/helpers/esm/extends.js\n");
/***/ }),
/***/ "(ssr)/./node_modules/@babel/runtime/helpers/esm/inheritsLoose.js":
/*!******************************************************************!*\
!*** ./node_modules/@babel/runtime/helpers/esm/inheritsLoose.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 */ \"default\": () => (/* binding */ _inheritsLoose)\n/* harmony export */ });\n/* harmony import */ var _setPrototypeOf_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./setPrototypeOf.js */ \"(ssr)/./node_modules/@babel/runtime/helpers/esm/setPrototypeOf.js\");\n\nfunction _inheritsLoose(t, o) {\n t.prototype = Object.create(o.prototype), t.prototype.constructor = t, (0,_setPrototypeOf_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(t, o);\n}\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQGJhYmVsL3J1bnRpbWUvaGVscGVycy9lc20vaW5oZXJpdHNMb29zZS5qcyIsIm1hcHBpbmdzIjoiOzs7OztBQUFpRDtBQUNqRDtBQUNBLHlFQUF5RSw4REFBYztBQUN2RiIsInNvdXJjZXMiOlsiL1VzZXJzL21hdHRicnVjZS9Eb2N1bWVudHMvUHJvamVjdHMvT3BlbkNsYXcvV2ViL2hlYXJ0YmVhdC1tb25pdG9yL25vZGVfbW9kdWxlcy9AYmFiZWwvcnVudGltZS9oZWxwZXJzL2VzbS9pbmhlcml0c0xvb3NlLmpzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBzZXRQcm90b3R5cGVPZiBmcm9tIFwiLi9zZXRQcm90b3R5cGVPZi5qc1wiO1xuZnVuY3Rpb24gX2luaGVyaXRzTG9vc2UodCwgbykge1xuICB0LnByb3RvdHlwZSA9IE9iamVjdC5jcmVhdGUoby5wcm90b3R5cGUpLCB0LnByb3RvdHlwZS5jb25zdHJ1Y3RvciA9IHQsIHNldFByb3RvdHlwZU9mKHQsIG8pO1xufVxuZXhwb3J0IHsgX2luaGVyaXRzTG9vc2UgYXMgZGVmYXVsdCB9OyJdLCJuYW1lcyI6W10sImlnbm9yZUxpc3QiOlswXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@babel/runtime/helpers/esm/inheritsLoose.js\n");
/***/ }),
/***/ "(ssr)/./node_modules/@babel/runtime/helpers/esm/objectWithoutPropertiesLoose.js":
/*!*********************************************************************************!*\
!*** ./node_modules/@babel/runtime/helpers/esm/objectWithoutPropertiesLoose.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 */ \"default\": () => (/* binding */ _objectWithoutPropertiesLoose)\n/* harmony export */ });\nfunction _objectWithoutPropertiesLoose(r, e) {\n if (null == r) return {};\n var t = {};\n for (var n in r) if ({}.hasOwnProperty.call(r, n)) {\n if (-1 !== e.indexOf(n)) continue;\n t[n] = r[n];\n }\n return t;\n}\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQGJhYmVsL3J1bnRpbWUvaGVscGVycy9lc20vb2JqZWN0V2l0aG91dFByb3BlcnRpZXNMb29zZS5qcyIsIm1hcHBpbmdzIjoiOzs7O0FBQUE7QUFDQTtBQUNBO0FBQ0EseUJBQXlCO0FBQ3pCO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VzIjpbIi9Vc2Vycy9tYXR0YnJ1Y2UvRG9jdW1lbnRzL1Byb2plY3RzL09wZW5DbGF3L1dlYi9oZWFydGJlYXQtbW9uaXRvci9ub2RlX21vZHVsZXMvQGJhYmVsL3J1bnRpbWUvaGVscGVycy9lc20vb2JqZWN0V2l0aG91dFByb3BlcnRpZXNMb29zZS5qcyJdLCJzb3VyY2VzQ29udGVudCI6WyJmdW5jdGlvbiBfb2JqZWN0V2l0aG91dFByb3BlcnRpZXNMb29zZShyLCBlKSB7XG4gIGlmIChudWxsID09IHIpIHJldHVybiB7fTtcbiAgdmFyIHQgPSB7fTtcbiAgZm9yICh2YXIgbiBpbiByKSBpZiAoe30uaGFzT3duUHJvcGVydHkuY2FsbChyLCBuKSkge1xuICAgIGlmICgtMSAhPT0gZS5pbmRleE9mKG4pKSBjb250aW51ZTtcbiAgICB0W25dID0gcltuXTtcbiAgfVxuICByZXR1cm4gdDtcbn1cbmV4cG9ydCB7IF9vYmplY3RXaXRob3V0UHJvcGVydGllc0xvb3NlIGFzIGRlZmF1bHQgfTsiXSwibmFtZXMiOltdLCJpZ25vcmVMaXN0IjpbMF0sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@babel/runtime/helpers/esm/objectWithoutPropertiesLoose.js\n");
/***/ }),
/***/ "(ssr)/./node_modules/@babel/runtime/helpers/esm/setPrototypeOf.js":
/*!*******************************************************************!*\
!*** ./node_modules/@babel/runtime/helpers/esm/setPrototypeOf.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 */ \"default\": () => (/* binding */ _setPrototypeOf)\n/* harmony export */ });\nfunction _setPrototypeOf(t, e) {\n return _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function (t, e) {\n return t.__proto__ = e, t;\n }, _setPrototypeOf(t, e);\n}\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQGJhYmVsL3J1bnRpbWUvaGVscGVycy9lc20vc2V0UHJvdG90eXBlT2YuanMiLCJtYXBwaW5ncyI6Ijs7OztBQUFBO0FBQ0E7QUFDQTtBQUNBLEdBQUc7QUFDSCIsInNvdXJjZXMiOlsiL1VzZXJzL21hdHRicnVjZS9Eb2N1bWVudHMvUHJvamVjdHMvT3BlbkNsYXcvV2ViL2hlYXJ0YmVhdC1tb25pdG9yL25vZGVfbW9kdWxlcy9AYmFiZWwvcnVudGltZS9oZWxwZXJzL2VzbS9zZXRQcm90b3R5cGVPZi5qcyJdLCJzb3VyY2VzQ29udGVudCI6WyJmdW5jdGlvbiBfc2V0UHJvdG90eXBlT2YodCwgZSkge1xuICByZXR1cm4gX3NldFByb3RvdHlwZU9mID0gT2JqZWN0LnNldFByb3RvdHlwZU9mID8gT2JqZWN0LnNldFByb3RvdHlwZU9mLmJpbmQoKSA6IGZ1bmN0aW9uICh0LCBlKSB7XG4gICAgcmV0dXJuIHQuX19wcm90b19fID0gZSwgdDtcbiAgfSwgX3NldFByb3RvdHlwZU9mKHQsIGUpO1xufVxuZXhwb3J0IHsgX3NldFByb3RvdHlwZU9mIGFzIGRlZmF1bHQgfTsiXSwibmFtZXMiOltdLCJpZ25vcmVMaXN0IjpbMF0sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@babel/runtime/helpers/esm/setPrototypeOf.js\n");
/***/ })
};
;

View File

@ -0,0 +1,55 @@
"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/).
*/
exports.id = "vendor-chunks/@swc";
exports.ids = ["vendor-chunks/@swc"];
exports.modules = {
/***/ "(ssr)/./node_modules/@swc/helpers/esm/_interop_require_default.js":
/*!*******************************************************************!*\
!*** ./node_modules/@swc/helpers/esm/_interop_require_default.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 */ _: () => (/* binding */ _interop_require_default)\n/* harmony export */ });\nfunction _interop_require_default(obj) {\n return obj && obj.__esModule ? obj : { default: obj };\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9faW50ZXJvcF9yZXF1aXJlX2RlZmF1bHQuanMiLCJtYXBwaW5ncyI6Ijs7OztBQUFBO0FBQ0EsMkNBQTJDO0FBQzNDO0FBQ3lDIiwic291cmNlcyI6WyIvVXNlcnMvbWF0dGJydWNlL0RvY3VtZW50cy9Qcm9qZWN0cy9PcGVuQ2xhdy9XZWIvaGVhcnRiZWF0LW1vbml0b3Ivbm9kZV9tb2R1bGVzL0Bzd2MvaGVscGVycy9lc20vX2ludGVyb3BfcmVxdWlyZV9kZWZhdWx0LmpzIl0sInNvdXJjZXNDb250ZW50IjpbImZ1bmN0aW9uIF9pbnRlcm9wX3JlcXVpcmVfZGVmYXVsdChvYmopIHtcbiAgICByZXR1cm4gb2JqICYmIG9iai5fX2VzTW9kdWxlID8gb2JqIDogeyBkZWZhdWx0OiBvYmogfTtcbn1cbmV4cG9ydCB7IF9pbnRlcm9wX3JlcXVpcmVfZGVmYXVsdCBhcyBfIH07XG4iXSwibmFtZXMiOltdLCJpZ25vcmVMaXN0IjpbMF0sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@swc/helpers/esm/_interop_require_default.js\n");
/***/ }),
/***/ "(ssr)/./node_modules/@swc/helpers/esm/_interop_require_wildcard.js":
/*!********************************************************************!*\
!*** ./node_modules/@swc/helpers/esm/_interop_require_wildcard.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 */ _: () => (/* binding */ _interop_require_wildcard)\n/* harmony export */ });\nfunction _getRequireWildcardCache(nodeInterop) {\n if (typeof WeakMap !== \"function\") return null;\n\n var cacheBabelInterop = new WeakMap();\n var cacheNodeInterop = new WeakMap();\n\n return (_getRequireWildcardCache = function(nodeInterop) {\n return nodeInterop ? cacheNodeInterop : cacheBabelInterop;\n })(nodeInterop);\n}\nfunction _interop_require_wildcard(obj, nodeInterop) {\n if (!nodeInterop && obj && obj.__esModule) return obj;\n if (obj === null || typeof obj !== \"object\" && typeof obj !== \"function\") return { default: obj };\n\n var cache = _getRequireWildcardCache(nodeInterop);\n\n if (cache && cache.has(obj)) return cache.get(obj);\n\n var newObj = { __proto__: null };\n var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;\n\n for (var key in obj) {\n if (key !== \"default\" && Object.prototype.hasOwnProperty.call(obj, key)) {\n var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;\n if (desc && (desc.get || desc.set)) Object.defineProperty(newObj, key, desc);\n else newObj[key] = obj[key];\n }\n }\n\n newObj.default = obj;\n\n if (cache) cache.set(obj, newObj);\n\n return newObj;\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9faW50ZXJvcF9yZXF1aXJlX3dpbGRjYXJkLmpzIiwibWFwcGluZ3MiOiI7Ozs7QUFBQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBLEtBQUs7QUFDTDtBQUNBO0FBQ0E7QUFDQSx1RkFBdUY7O0FBRXZGOztBQUVBOztBQUVBLG1CQUFtQjtBQUNuQjs7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUFFQTs7QUFFQTs7QUFFQTtBQUNBO0FBQzBDIiwic291cmNlcyI6WyIvVXNlcnMvbWF0dGJydWNlL0RvY3VtZW50cy9Qcm9qZWN0cy9PcGVuQ2xhdy9XZWIvaGVhcnRiZWF0LW1vbml0b3Ivbm9kZV9tb2R1bGVzL0Bzd2MvaGVscGVycy9lc20vX2ludGVyb3BfcmVxdWlyZV93aWxkY2FyZC5qcyJdLCJzb3VyY2VzQ29udGVudCI6WyJmdW5jdGlvbiBfZ2V0UmVxdWlyZVdpbGRjYXJkQ2FjaGUobm9kZUludGVyb3ApIHtcbiAgICBpZiAodHlwZW9mIFdlYWtNYXAgIT09IFwiZnVuY3Rpb25cIikgcmV0dXJuIG51bGw7XG5cbiAgICB2YXIgY2FjaGVCYWJlbEludGVyb3AgPSBuZXcgV2Vha01hcCgpO1xuICAgIHZhciBjYWNoZU5vZGVJbnRlcm9wID0gbmV3IFdlYWtNYXAoKTtcblxuICAgIHJldHVybiAoX2dldFJlcXVpcmVXaWxkY2FyZENhY2hlID0gZnVuY3Rpb24obm9kZUludGVyb3ApIHtcbiAgICAgICAgcmV0dXJuIG5vZGVJbnRlcm9wID8gY2FjaGVOb2RlSW50ZXJvcCA6IGNhY2hlQmFiZWxJbnRlcm9wO1xuICAgIH0pKG5vZGVJbnRlcm9wKTtcbn1cbmZ1bmN0aW9uIF9pbnRlcm9wX3JlcXVpcmVfd2lsZGNhcmQob2JqLCBub2RlSW50ZXJvcCkge1xuICAgIGlmICghbm9kZUludGVyb3AgJiYgb2JqICYmIG9iai5fX2VzTW9kdWxlKSByZXR1cm4gb2JqO1xuICAgIGlmIChvYmogPT09IG51bGwgfHwgdHlwZW9mIG9iaiAhPT0gXCJvYmplY3RcIiAmJiB0eXBlb2Ygb2JqICE9PSBcImZ1bmN0aW9uXCIpIHJldHVybiB7IGRlZmF1bHQ6IG9iaiB9O1xuXG4gICAgdmFyIGNhY2hlID0gX2dldFJlcXVpcmVXaWxkY2FyZENhY2hlKG5vZGVJbnRlcm9wKTtcblxuICAgIGlmIChjYWNoZSAmJiBjYWNoZS5oYXMob2JqKSkgcmV0dXJuIGNhY2hlLmdldChvYmopO1xuXG4gICAgdmFyIG5ld09iaiA9IHsgX19wcm90b19fOiBudWxsIH07XG4gICAgdmFyIGhhc1Byb3BlcnR5RGVzY3JpcHRvciA9IE9iamVjdC5kZWZpbmVQcm9wZXJ0eSAmJiBPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yO1xuXG4gICAgZm9yICh2YXIga2V5IGluIG9iaikge1xuICAgICAgICBpZiAoa2V5ICE9PSBcImRlZmF1bHRcIiAmJiBPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwob2JqLCBrZXkpKSB7XG4gICAgICAgICAgICB2YXIgZGVzYyA9IGhhc1Byb3BlcnR5RGVzY3JpcHRvciA/IE9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3Iob2JqLCBrZXkpIDogbnVsbDtcbiAgICAgICAgICAgIGlmIChkZXNjICYmIChkZXNjLmdldCB8fCBkZXNjLnNldCkpIE9iamVjdC5kZWZpbmVQcm9wZXJ0eShuZXdPYmosIGtleSwgZGVzYyk7XG4gICAgICAgICAgICBlbHNlIG5ld09ialtrZXldID0gb2JqW2tleV07XG4gICAgICAgIH1cbiAgICB9XG5cbiAgICBuZXdPYmouZGVmYXVsdCA9IG9iajtcblxuICAgIGlmIChjYWNoZSkgY2FjaGUuc2V0KG9iaiwgbmV3T2JqKTtcblxuICAgIHJldHVybiBuZXdPYmo7XG59XG5leHBvcnQgeyBfaW50ZXJvcF9yZXF1aXJlX3dpbGRjYXJkIGFzIF8gfTtcbiJdLCJuYW1lcyI6W10sImlnbm9yZUxpc3QiOlswXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@swc/helpers/esm/_interop_require_wildcard.js\n");
/***/ }),
/***/ "(ssr)/./node_modules/@swc/helpers/esm/_tagged_template_literal_loose.js":
/*!*************************************************************************!*\
!*** ./node_modules/@swc/helpers/esm/_tagged_template_literal_loose.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 */ _: () => (/* binding */ _tagged_template_literal_loose)\n/* harmony export */ });\nfunction _tagged_template_literal_loose(strings, raw) {\n if (!raw) raw = strings.slice(0);\n\n strings.raw = raw;\n\n return strings;\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9fdGFnZ2VkX3RlbXBsYXRlX2xpdGVyYWxfbG9vc2UuanMiLCJtYXBwaW5ncyI6Ijs7OztBQUFBO0FBQ0E7O0FBRUE7O0FBRUE7QUFDQTtBQUMrQyIsInNvdXJjZXMiOlsiL1VzZXJzL21hdHRicnVjZS9Eb2N1bWVudHMvUHJvamVjdHMvT3BlbkNsYXcvV2ViL2hlYXJ0YmVhdC1tb25pdG9yL25vZGVfbW9kdWxlcy9Ac3djL2hlbHBlcnMvZXNtL190YWdnZWRfdGVtcGxhdGVfbGl0ZXJhbF9sb29zZS5qcyJdLCJzb3VyY2VzQ29udGVudCI6WyJmdW5jdGlvbiBfdGFnZ2VkX3RlbXBsYXRlX2xpdGVyYWxfbG9vc2Uoc3RyaW5ncywgcmF3KSB7XG4gICAgaWYgKCFyYXcpIHJhdyA9IHN0cmluZ3Muc2xpY2UoMCk7XG5cbiAgICBzdHJpbmdzLnJhdyA9IHJhdztcblxuICAgIHJldHVybiBzdHJpbmdzO1xufVxuZXhwb3J0IHsgX3RhZ2dlZF90ZW1wbGF0ZV9saXRlcmFsX2xvb3NlIGFzIF8gfTtcbiJdLCJuYW1lcyI6W10sImlnbm9yZUxpc3QiOlswXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/@swc/helpers/esm/_tagged_template_literal_loose.js\n");
/***/ }),
/***/ "(rsc)/./node_modules/@swc/helpers/esm/_interop_require_default.js":
/*!*******************************************************************!*\
!*** ./node_modules/@swc/helpers/esm/_interop_require_default.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 */ _: () => (/* binding */ _interop_require_default)\n/* harmony export */ });\nfunction _interop_require_default(obj) {\n return obj && obj.__esModule ? obj : { default: obj };\n}\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHJzYykvLi9ub2RlX21vZHVsZXMvQHN3Yy9oZWxwZXJzL2VzbS9faW50ZXJvcF9yZXF1aXJlX2RlZmF1bHQuanMiLCJtYXBwaW5ncyI6Ijs7OztBQUFBO0FBQ0EsMkNBQTJDO0FBQzNDO0FBQ3lDIiwic291cmNlcyI6WyIvVXNlcnMvbWF0dGJydWNlL0RvY3VtZW50cy9Qcm9qZWN0cy9PcGVuQ2xhdy9XZWIvaGVhcnRiZWF0LW1vbml0b3Ivbm9kZV9tb2R1bGVzL0Bzd2MvaGVscGVycy9lc20vX2ludGVyb3BfcmVxdWlyZV9kZWZhdWx0LmpzIl0sInNvdXJjZXNDb250ZW50IjpbImZ1bmN0aW9uIF9pbnRlcm9wX3JlcXVpcmVfZGVmYXVsdChvYmopIHtcbiAgICByZXR1cm4gb2JqICYmIG9iai5fX2VzTW9kdWxlID8gb2JqIDogeyBkZWZhdWx0OiBvYmogfTtcbn1cbmV4cG9ydCB7IF9pbnRlcm9wX3JlcXVpcmVfZGVmYXVsdCBhcyBfIH07XG4iXSwibmFtZXMiOltdLCJpZ25vcmVMaXN0IjpbMF0sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///(rsc)/./node_modules/@swc/helpers/esm/_interop_require_default.js\n");
/***/ })
};
;

View File

@ -0,0 +1,25 @@
"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/).
*/
exports.id = "vendor-chunks/clsx";
exports.ids = ["vendor-chunks/clsx"];
exports.modules = {
/***/ "(ssr)/./node_modules/clsx/dist/clsx.mjs":
/*!*****************************************!*\
!*** ./node_modules/clsx/dist/clsx.mjs ***!
\*****************************************/
/***/ ((__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 */ clsx: () => (/* binding */ clsx),\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\nfunction r(e){var t,f,n=\"\";if(\"string\"==typeof e||\"number\"==typeof e)n+=e;else if(\"object\"==typeof e)if(Array.isArray(e)){var o=e.length;for(t=0;t<o;t++)e[t]&&(f=r(e[t]))&&(n&&(n+=\" \"),n+=f)}else for(f in e)e[f]&&(n&&(n+=\" \"),n+=f);return n}function clsx(){for(var e,t,f=0,n=\"\",o=arguments.length;f<o;f++)(e=arguments[f])&&(t=r(e))&&(n&&(n+=\" \"),n+=t);return n}/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (clsx);//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvY2xzeC9kaXN0L2Nsc3gubWpzIiwibWFwcGluZ3MiOiI7Ozs7O0FBQUEsY0FBYyxhQUFhLCtDQUErQyxnREFBZ0QsZUFBZSxRQUFRLElBQUksMENBQTBDLHlDQUF5QyxTQUFnQixnQkFBZ0Isd0NBQXdDLElBQUksbURBQW1ELFNBQVMsaUVBQWUsSUFBSSIsInNvdXJjZXMiOlsiL1VzZXJzL21hdHRicnVjZS9Eb2N1bWVudHMvUHJvamVjdHMvT3BlbkNsYXcvV2ViL2hlYXJ0YmVhdC1tb25pdG9yL25vZGVfbW9kdWxlcy9jbHN4L2Rpc3QvY2xzeC5tanMiXSwic291cmNlc0NvbnRlbnQiOlsiZnVuY3Rpb24gcihlKXt2YXIgdCxmLG49XCJcIjtpZihcInN0cmluZ1wiPT10eXBlb2YgZXx8XCJudW1iZXJcIj09dHlwZW9mIGUpbis9ZTtlbHNlIGlmKFwib2JqZWN0XCI9PXR5cGVvZiBlKWlmKEFycmF5LmlzQXJyYXkoZSkpe3ZhciBvPWUubGVuZ3RoO2Zvcih0PTA7dDxvO3QrKyllW3RdJiYoZj1yKGVbdF0pKSYmKG4mJihuKz1cIiBcIiksbis9Zil9ZWxzZSBmb3IoZiBpbiBlKWVbZl0mJihuJiYobis9XCIgXCIpLG4rPWYpO3JldHVybiBufWV4cG9ydCBmdW5jdGlvbiBjbHN4KCl7Zm9yKHZhciBlLHQsZj0wLG49XCJcIixvPWFyZ3VtZW50cy5sZW5ndGg7ZjxvO2YrKykoZT1hcmd1bWVudHNbZl0pJiYodD1yKGUpKSYmKG4mJihuKz1cIiBcIiksbis9dCk7cmV0dXJuIG59ZXhwb3J0IGRlZmF1bHQgY2xzeDsiXSwibmFtZXMiOltdLCJpZ25vcmVMaXN0IjpbMF0sInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/clsx/dist/clsx.mjs\n");
/***/ })
};
;

175
.next/server/vendor-chunks/d3-array.js vendored Normal file

File diff suppressed because one or more lines are too long

35
.next/server/vendor-chunks/d3-color.js vendored Normal file

File diff suppressed because one or more lines are too long

165
.next/server/vendor-chunks/d3-format.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

25
.next/server/vendor-chunks/d3-path.js vendored Normal file

File diff suppressed because one or more lines are too long

245
.next/server/vendor-chunks/d3-scale.js vendored Normal file

File diff suppressed because one or more lines are too long

615
.next/server/vendor-chunks/d3-shape.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

125
.next/server/vendor-chunks/d3-time.js vendored 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

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

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

File diff suppressed because one or more lines are too long

35
.next/server/vendor-chunks/react-is.js vendored 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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,25 @@
"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/).
*/
exports.id = "vendor-chunks/tiny-invariant";
exports.ids = ["vendor-chunks/tiny-invariant"];
exports.modules = {
/***/ "(ssr)/./node_modules/tiny-invariant/dist/esm/tiny-invariant.js":
/*!****************************************************************!*\
!*** ./node_modules/tiny-invariant/dist/esm/tiny-invariant.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 */ \"default\": () => (/* binding */ invariant)\n/* harmony export */ });\nvar isProduction = \"development\" === 'production';\nvar prefix = 'Invariant failed';\nfunction invariant(condition, message) {\n if (condition) {\n return;\n }\n if (isProduction) {\n throw new Error(prefix);\n }\n var provided = typeof message === 'function' ? message() : message;\n var value = provided ? \"\".concat(prefix, \": \").concat(provided) : prefix;\n throw new Error(value);\n}\n\n\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHNzcikvLi9ub2RlX21vZHVsZXMvdGlueS1pbnZhcmlhbnQvZGlzdC9lc20vdGlueS1pbnZhcmlhbnQuanMiLCJtYXBwaW5ncyI6Ijs7OztBQUFBLG1CQUFtQixhQUFvQjtBQUN2QztBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRWdDIiwic291cmNlcyI6WyIvVXNlcnMvbWF0dGJydWNlL0RvY3VtZW50cy9Qcm9qZWN0cy9PcGVuQ2xhdy9XZWIvaGVhcnRiZWF0LW1vbml0b3Ivbm9kZV9tb2R1bGVzL3RpbnktaW52YXJpYW50L2Rpc3QvZXNtL3RpbnktaW52YXJpYW50LmpzIl0sInNvdXJjZXNDb250ZW50IjpbInZhciBpc1Byb2R1Y3Rpb24gPSBwcm9jZXNzLmVudi5OT0RFX0VOViA9PT0gJ3Byb2R1Y3Rpb24nO1xudmFyIHByZWZpeCA9ICdJbnZhcmlhbnQgZmFpbGVkJztcbmZ1bmN0aW9uIGludmFyaWFudChjb25kaXRpb24sIG1lc3NhZ2UpIHtcbiAgICBpZiAoY29uZGl0aW9uKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgaWYgKGlzUHJvZHVjdGlvbikge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IocHJlZml4KTtcbiAgICB9XG4gICAgdmFyIHByb3ZpZGVkID0gdHlwZW9mIG1lc3NhZ2UgPT09ICdmdW5jdGlvbicgPyBtZXNzYWdlKCkgOiBtZXNzYWdlO1xuICAgIHZhciB2YWx1ZSA9IHByb3ZpZGVkID8gXCJcIi5jb25jYXQocHJlZml4LCBcIjogXCIpLmNvbmNhdChwcm92aWRlZCkgOiBwcmVmaXg7XG4gICAgdGhyb3cgbmV3IEVycm9yKHZhbHVlKTtcbn1cblxuZXhwb3J0IHsgaW52YXJpYW50IGFzIGRlZmF1bHQgfTtcbiJdLCJuYW1lcyI6W10sImlnbm9yZUxpc3QiOlswXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///(ssr)/./node_modules/tiny-invariant/dist/esm/tiny-invariant.js\n");
/***/ })
};
;

File diff suppressed because one or more lines are too long

View File

@ -22,8 +22,8 @@
/******/ } /******/ }
/******/ // Create a new module (and put it into the cache) /******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = { /******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed /******/ id: moduleId,
/******/ // no module.loaded needed /******/ loaded: false,
/******/ exports: {} /******/ exports: {}
/******/ }; /******/ };
/******/ /******/
@ -36,6 +36,9 @@
/******/ if(threw) delete __webpack_module_cache__[moduleId]; /******/ if(threw) delete __webpack_module_cache__[moduleId];
/******/ } /******/ }
/******/ /******/
/******/ // Flag the module as loaded
/******/ module.loaded = true;
/******/
/******/ // Return the exports of the module /******/ // Return the exports of the module
/******/ return module.exports; /******/ return module.exports;
/******/ } /******/ }
@ -61,6 +64,36 @@
/******/ }; /******/ };
/******/ })(); /******/ })();
/******/ /******/
/******/ /* webpack/runtime/create fake namespace object */
/******/ (() => {
/******/ var getProto = Object.getPrototypeOf ? (obj) => (Object.getPrototypeOf(obj)) : (obj) => (obj.__proto__);
/******/ var leafPrototypes;
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 16: return value when it's Promise-like
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = this(value);
/******/ if(mode & 8) return value;
/******/ if(typeof value === 'object' && value) {
/******/ if((mode & 4) && value.__esModule) return value;
/******/ if((mode & 16) && typeof value.then === 'function') return value;
/******/ }
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ var def = {};
/******/ leafPrototypes = leafPrototypes || [null, getProto({}), getProto([]), getProto(getProto)];
/******/ for(var current = mode & 2 && value; typeof current == 'object' && !~leafPrototypes.indexOf(current); current = getProto(current)) {
/******/ Object.getOwnPropertyNames(current).forEach((key) => (def[key] = () => (value[key])));
/******/ }
/******/ def['default'] = () => (value);
/******/ __webpack_require__.d(ns, def);
/******/ return ns;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/define property getters */ /******/ /* webpack/runtime/define property getters */
/******/ (() => { /******/ (() => {
/******/ // define getter functions for harmony exports /******/ // define getter functions for harmony exports
@ -95,6 +128,11 @@
/******/ }; /******/ };
/******/ })(); /******/ })();
/******/ /******/
/******/ /* webpack/runtime/getFullHash */
/******/ (() => {
/******/ __webpack_require__.h = () => ("5e2e688d9298ba36")
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */ /******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => { /******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
@ -111,6 +149,15 @@
/******/ }; /******/ };
/******/ })(); /******/ })();
/******/ /******/
/******/ /* webpack/runtime/node module decorator */
/******/ (() => {
/******/ __webpack_require__.nmd = (module) => {
/******/ module.paths = [];
/******/ if (!module.children) module.children = [];
/******/ return module;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/startup entrypoint */ /******/ /* webpack/runtime/startup entrypoint */
/******/ (() => { /******/ (() => {
/******/ __webpack_require__.X = (result, chunkIds, fn) => { /******/ __webpack_require__.X = (result, chunkIds, fn) => {

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

View File

@ -89,6 +89,48 @@
/******/ }; /******/ };
/******/ })(); /******/ })();
/******/ /******/
/******/ /* webpack/runtime/compat get default export */
/******/ (() => {
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = (module) => {
/******/ var getter = module && module.__esModule ?
/******/ () => (module['default']) :
/******/ () => (module);
/******/ __webpack_require__.d(getter, { a: getter });
/******/ return getter;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/create fake namespace object */
/******/ (() => {
/******/ var getProto = Object.getPrototypeOf ? (obj) => (Object.getPrototypeOf(obj)) : (obj) => (obj.__proto__);
/******/ var leafPrototypes;
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 16: return value when it's Promise-like
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = this(value);
/******/ if(mode & 8) return value;
/******/ if(typeof value === 'object' && value) {
/******/ if((mode & 4) && value.__esModule) return value;
/******/ if((mode & 16) && typeof value.then === 'function') return value;
/******/ }
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ var def = {};
/******/ leafPrototypes = leafPrototypes || [null, getProto({}), getProto([]), getProto(getProto)];
/******/ for(var current = mode & 2 && value; typeof current == 'object' && !~leafPrototypes.indexOf(current); current = getProto(current)) {
/******/ Object.getOwnPropertyNames(current).forEach((key) => (def[key] = () => (value[key])));
/******/ }
/******/ def['default'] = () => (value);
/******/ __webpack_require__.d(ns, def);
/******/ return ns;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/define property getters */ /******/ /* webpack/runtime/define property getters */
/******/ (() => { /******/ (() => {
/******/ // define getter functions for harmony exports /******/ // define getter functions for harmony exports
@ -148,7 +190,7 @@
/******/ /******/
/******/ /* webpack/runtime/getFullHash */ /******/ /* webpack/runtime/getFullHash */
/******/ (() => { /******/ (() => {
/******/ __webpack_require__.h = () => ("46496372886b1561") /******/ __webpack_require__.h = () => ("e0cd64fa6e96bf0c")
/******/ })(); /******/ })();
/******/ /******/
/******/ /* webpack/runtime/global */ /******/ /* webpack/runtime/global */

View File

@ -0,0 +1,93 @@
/*!*******************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************!*\
!*** css ./node_modules/next/dist/build/webpack/loaders/css-loader/src/index.js??ruleSet[1].rules[13].oneOf[2].use[1]!./node_modules/next/dist/build/webpack/loaders/next-font-loader/index.js??ruleSet[1].rules[13].oneOf[2].use[2]!./node_modules/next/font/google/target.css?{"path":"src/app/layout.tsx","import":"Geist","arguments":[{"variable":"--font-geist-sans","subsets":["latin"]}],"variableName":"geistSans"} ***!
\*******************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************/
/* cyrillic */
@font-face {
font-family: 'Geist';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(/_next/static/media/8d697b304b401681-s.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* latin-ext */
@font-face {
font-family: 'Geist';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(/_next/static/media/ba015fad6dcf6784-s.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Geist';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(/_next/static/media/4cf2300e9c8272f7-s.p.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}@font-face {font-family: 'Geist Fallback';src: local("Arial");ascent-override: 95.94%;descent-override: 28.16%;line-gap-override: 0.00%;size-adjust: 104.76%
}.__className_188709 {font-family: 'Geist', 'Geist Fallback';font-style: normal
}.__variable_188709 {--font-geist-sans: 'Geist', 'Geist Fallback'
}
/*!************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************!*\
!*** css ./node_modules/next/dist/build/webpack/loaders/css-loader/src/index.js??ruleSet[1].rules[13].oneOf[2].use[1]!./node_modules/next/dist/build/webpack/loaders/next-font-loader/index.js??ruleSet[1].rules[13].oneOf[2].use[2]!./node_modules/next/font/google/target.css?{"path":"src/app/layout.tsx","import":"Geist_Mono","arguments":[{"variable":"--font-geist-mono","subsets":["latin"]}],"variableName":"geistMono"} ***!
\************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************/
/* cyrillic */
@font-face {
font-family: 'Geist Mono';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(/_next/static/media/9610d9e46709d722-s.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* latin-ext */
@font-face {
font-family: 'Geist Mono';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(/_next/static/media/747892c23ea88013-s.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Geist Mono';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(/_next/static/media/93f479601ee12b01-s.p.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}@font-face {font-family: 'Geist Mono Fallback';src: local("Arial");ascent-override: 74.67%;descent-override: 21.92%;line-gap-override: 0.00%;size-adjust: 134.59%
}.__className_9a8899 {font-family: 'Geist Mono', 'Geist Mono Fallback';font-style: normal
}.__variable_9a8899 {--font-geist-mono: 'Geist Mono', 'Geist Mono Fallback'
}
/*!*****************************************************************************************************************************************************************************************************************************************************************!*\
!*** css ./node_modules/next/dist/build/webpack/loaders/css-loader/src/index.js??ruleSet[1].rules[13].oneOf[10].use[2]!./node_modules/next/dist/build/webpack/loaders/postcss-loader/src/index.js??ruleSet[1].rules[13].oneOf[10].use[3]!./src/app/globals.css ***!
\*****************************************************************************************************************************************************************************************************************************************************************/
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
{"c":["webpack"],"r":[],"m":[]}

View File

@ -0,0 +1,60 @@
"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/compat get default export */
/******/ (() => {
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = (module) => {
/******/ var getter = module && module.__esModule ?
/******/ () => (module['default']) :
/******/ () => (module);
/******/ __webpack_require__.d(getter, { a: getter });
/******/ return getter;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/create fake namespace object */
/******/ (() => {
/******/ var getProto = Object.getPrototypeOf ? (obj) => (Object.getPrototypeOf(obj)) : (obj) => (obj.__proto__);
/******/ var leafPrototypes;
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 16: return value when it's Promise-like
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = this(value);
/******/ if(mode & 8) return value;
/******/ if(typeof value === 'object' && value) {
/******/ if((mode & 4) && value.__esModule) return value;
/******/ if((mode & 16) && typeof value.then === 'function') return value;
/******/ }
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ var def = {};
/******/ leafPrototypes = leafPrototypes || [null, getProto({}), getProto([]), getProto(getProto)];
/******/ for(var current = mode & 2 && value; typeof current == 'object' && !~leafPrototypes.indexOf(current); current = getProto(current)) {
/******/ Object.getOwnPropertyNames(current).forEach((key) => (def[key] = () => (value[key])));
/******/ }
/******/ def['default'] = () => (value);
/******/ __webpack_require__.d(ns, def);
/******/ return ns;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/getFullHash */
/******/ (() => {
/******/ __webpack_require__.h = () => ("e0cd64fa6e96bf0c")
/******/ })();
/******/
/******/ }
);

File diff suppressed because one or more lines are too long

84
.next/types/app/layout.ts Normal file
View File

@ -0,0 +1,84 @@
// File: /Users/mattbruce/Documents/Projects/OpenClaw/Web/heartbeat-monitor/src/app/layout.tsx
import * as entry from '../../../src/app/layout.js'
import type { ResolvingMetadata, ResolvingViewport } from 'next/dist/lib/metadata/types/metadata-interface.js'
type TEntry = typeof import('../../../src/app/layout.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<{
default: 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
metadata?: any
generateMetadata?: Function
viewport?: any
generateViewport?: Function
experimental_ppr?: boolean
}, TEntry, ''>>()
// Check the prop type of the entry function
checkFields<Diff<LayoutProps, FirstArg<TEntry['default']>, 'default'>>()
// Check the arguments and return type of the generateMetadata function
if ('generateMetadata' in entry) {
checkFields<Diff<LayoutProps, FirstArg<MaybeField<TEntry, 'generateMetadata'>>, 'generateMetadata'>>()
checkFields<Diff<ResolvingMetadata, SecondArg<MaybeField<TEntry, 'generateMetadata'>>, 'generateMetadata'>>()
}
// Check the arguments and return type of the generateViewport function
if ('generateViewport' in entry) {
checkFields<Diff<LayoutProps, FirstArg<MaybeField<TEntry, 'generateViewport'>>, 'generateViewport'>>()
checkFields<Diff<ResolvingViewport, SecondArg<MaybeField<TEntry, 'generateViewport'>>, 'generateViewport'>>()
}
// 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
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__'

84
.next/types/app/page.ts Normal file
View File

@ -0,0 +1,84 @@
// File: /Users/mattbruce/Documents/Projects/OpenClaw/Web/heartbeat-monitor/src/app/page.tsx
import * as entry from '../../../src/app/page.js'
import type { ResolvingMetadata, ResolvingViewport } from 'next/dist/lib/metadata/types/metadata-interface.js'
type TEntry = typeof import('../../../src/app/page.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<{
default: 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
metadata?: any
generateMetadata?: Function
viewport?: any
generateViewport?: Function
experimental_ppr?: boolean
}, TEntry, ''>>()
// Check the prop type of the entry function
checkFields<Diff<PageProps, FirstArg<TEntry['default']>, 'default'>>()
// Check the arguments and return type of the generateMetadata function
if ('generateMetadata' in entry) {
checkFields<Diff<PageProps, FirstArg<MaybeField<TEntry, 'generateMetadata'>>, 'generateMetadata'>>()
checkFields<Diff<ResolvingMetadata, SecondArg<MaybeField<TEntry, 'generateMetadata'>>, 'generateMetadata'>>()
}
// Check the arguments and return type of the generateViewport function
if ('generateViewport' in entry) {
checkFields<Diff<PageProps, FirstArg<MaybeField<TEntry, 'generateViewport'>>, 'generateViewport'>>()
checkFields<Diff<ResolvingViewport, SecondArg<MaybeField<TEntry, 'generateViewport'>>, 'generateViewport'>>()
}
// 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
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__'

View File

@ -1,21 +1,56 @@
@import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600;700&family=Inter:wght@300;400;500;600;700&display=swap');
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
:root { :root {
--background: #ffffff; --background: #0F172A;
--foreground: #171717; --foreground: #F8FAFC;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
} }
body { body {
color: var(--foreground); color: var(--foreground);
background: var(--background); background: var(--background);
font-family: Arial, Helvetica, sans-serif; font-family: 'Inter', system-ui, -apple-system, sans-serif;
}
.font-mono {
font-family: 'Fira Code', monospace;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #1E293B;
}
::-webkit-scrollbar-thumb {
background: #334155;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #475569;
}
/* Smooth transitions */
* {
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
/* Focus visible styles */
:focus-visible {
outline: 2px solid #22C55E;
outline-offset: 2px;
}
/* Selection color */
::selection {
background: rgba(34, 197, 94, 0.3);
color: #F8FAFC;
} }

View File

@ -1,20 +1,9 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Heartbeat Monitor", title: "Heartbeat Monitor",
description: "Monitor all your local web apps", description: "Real-time monitoring dashboard for your web applications",
}; };
export default function RootLayout({ export default function RootLayout({
@ -23,12 +12,8 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="en"> <html lang="en" className="dark">
<body <body className="antialiased">{children}</body>
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html> </html>
); );
} }

View File

@ -1,8 +1,8 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Activity, Plus, Play, Square, Trash2, Edit2, RefreshCw, Server, Clock, TrendingUp, AlertCircle } from "lucide-react"; import { Activity, Plus, RefreshCw, Trash2, Server, Clock, TrendingUp, AlertCircle, CheckCircle2, XCircle, ExternalLink, LayoutDashboard, Settings, Bell } from "lucide-react";
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts"; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from "recharts";
interface App { interface App {
id: string; id: string;
@ -24,6 +24,12 @@ interface StatusEntry {
responseTime?: number; responseTime?: number;
} }
interface UptimeData {
time: string;
uptime: number;
responseTime: number;
}
export default function HeartbeatMonitor() { export default function HeartbeatMonitor() {
const [apps, setApps] = useState<App[]>([]); const [apps, setApps] = useState<App[]>([]);
const [status, setStatus] = useState<StatusEntry[]>([]); const [status, setStatus] = useState<StatusEntry[]>([]);
@ -31,6 +37,7 @@ export default function HeartbeatMonitor() {
const [checking, setChecking] = useState<string | null>(null); const [checking, setChecking] = useState<string | null>(null);
const [showAddApp, setShowAddApp] = useState(false); const [showAddApp, setShowAddApp] = useState(false);
const [selectedApp, setSelectedApp] = useState<App | null>(null); const [selectedApp, setSelectedApp] = useState<App | null>(null);
const [activeTab, setActiveTab] = useState("dashboard");
const [newApp, setNewApp] = useState<Partial<App>>({ const [newApp, setNewApp] = useState<Partial<App>>({
name: "", name: "",
description: "", description: "",
@ -39,13 +46,13 @@ export default function HeartbeatMonitor() {
path: "", path: "",
command: "npm run dev", command: "npm run dev",
category: "Other", category: "Other",
color: "#3b82f6", color: "#22C55E",
enabled: true, enabled: true,
}); });
useEffect(() => { useEffect(() => {
fetchData(); fetchData();
const interval = setInterval(fetchData, 30000); // Refresh every 30s const interval = setInterval(fetchData, 30000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []);
@ -66,7 +73,7 @@ export default function HeartbeatMonitor() {
setChecking(app.id); setChecking(app.id);
try { try {
const start = Date.now(); const start = Date.now();
const res = await fetch(app.url, { method: "HEAD", mode: "no-cors" }); await fetch(app.url, { method: "HEAD", mode: "no-cors" });
const responseTime = Date.now() - start; const responseTime = Date.now() - start;
const entry: StatusEntry = { const entry: StatusEntry = {
@ -83,7 +90,7 @@ export default function HeartbeatMonitor() {
}); });
fetchData(); fetchData();
} catch (err) { } catch {
const entry: StatusEntry = { const entry: StatusEntry = {
appId: app.id, appId: app.id,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
@ -102,12 +109,6 @@ export default function HeartbeatMonitor() {
} }
} }
async function restartApp(app: App) {
// This would need to be implemented with a server-side process runner
// For now, just check status
await checkApp(app);
}
async function addApp(e: React.FormEvent) { async function addApp(e: React.FormEvent) {
e.preventDefault(); e.preventDefault();
if (!newApp.name || !newApp.url) return; if (!newApp.name || !newApp.url) return;
@ -126,7 +127,7 @@ export default function HeartbeatMonitor() {
path: "", path: "",
command: "npm run dev", command: "npm run dev",
category: "Other", category: "Other",
color: "#3b82f6", color: "#22C55E",
enabled: true, enabled: true,
}); });
setShowAddApp(false); setShowAddApp(false);
@ -159,47 +160,96 @@ export default function HeartbeatMonitor() {
return Math.round((upCount / entries.length) * 100); return Math.round((upCount / entries.length) * 100);
} }
function generateUptimeData(appId: string): UptimeData[] {
const appStatus = status.filter((s) => s.appId === appId).slice(-24);
return appStatus.map((s, i) => ({
time: new Date(s.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
uptime: s.status === "up" ? 100 : 0,
responseTime: s.responseTime || 0,
}));
}
const categories = Array.from(new Set(apps.map((a) => a.category))); const categories = Array.from(new Set(apps.map((a) => a.category)));
const totalApps = apps.length; const totalApps = apps.length;
const onlineApps = apps.filter((app) => { const onlineApps = apps.filter((app) => {
const { latest } = getAppStatus(app.id); const { latest } = getAppStatus(app.id);
return latest?.status === "up"; return latest?.status === "up";
}).length; }).length;
const offlineApps = totalApps - onlineApps;
const overallHealth = totalApps > 0 ? Math.round((onlineApps / totalApps) * 100) : 100;
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-slate-950 flex items-center justify-center"> <div className="min-h-screen bg-[#0F172A] flex items-center justify-center">
<div className="text-slate-400">Loading...</div> <div className="flex items-center gap-3 text-slate-400">
<div className="w-8 h-8 border-2 border-emerald-500/20 border-t-emerald-500 rounded-full animate-spin" />
<span className="font-mono">Loading monitor...</span>
</div>
</div> </div>
); );
} }
return ( return (
<div className="min-h-screen bg-slate-950 text-slate-100"> <div className="min-h-screen bg-[#0F172A] text-slate-100 font-sans">
{/* Header */} {/* Header */}
<header className="border-b border-slate-800 bg-slate-900/50"> <header className="border-b border-slate-800/50 bg-slate-900/50 backdrop-blur-xl sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 py-4"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between h-16">
{/* Logo */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Activity className="w-8 h-8 text-green-500" /> <div className="relative">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-emerald-500 to-emerald-600 flex items-center justify-center shadow-lg shadow-emerald-500/20">
<Activity className="w-5 h-5 text-white" />
</div>
<div className="absolute -bottom-0.5 -right-0.5 w-3 h-3 bg-emerald-400 rounded-full animate-pulse" />
</div>
<div> <div>
<h1 className="text-2xl font-bold">Heartbeat Monitor</h1> <h1 className="text-lg font-semibold text-white tracking-tight">Heartbeat Monitor</h1>
<p className="text-sm text-slate-400">Track all your local web apps</p> <p className="text-xs text-slate-400 font-mono">System Status Dashboard</p>
</div> </div>
</div> </div>
<div className="flex items-center gap-4">
<div className="text-right"> {/* Nav Tabs */}
<div className="text-2xl font-bold text-green-400"> <nav className="hidden md:flex items-center gap-1 bg-slate-800/50 rounded-lg p-1">
{onlineApps}/{totalApps} <button
</div> onClick={() => setActiveTab("dashboard")}
<div className="text-xs text-slate-400">Apps Online</div> className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-all ${
</div> activeTab === "dashboard"
? "bg-slate-700 text-white"
: "text-slate-400 hover:text-white hover:bg-slate-700/50"
}`}
>
<LayoutDashboard className="w-4 h-4" />
Dashboard
</button>
<button
onClick={() => setActiveTab("settings")}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-all ${
activeTab === "settings"
? "bg-slate-700 text-white"
: "text-slate-400 hover:text-white hover:bg-slate-700/50"
}`}
>
<Settings className="w-4 h-4" />
Settings
</button>
</nav>
{/* Actions */}
<div className="flex items-center gap-3">
<button
onClick={fetchData}
className="p-2 text-slate-400 hover:text-white hover:bg-slate-800 rounded-lg transition-all"
title="Refresh"
>
<RefreshCw className="w-5 h-5" />
</button>
<button <button
onClick={() => setShowAddApp(true)} onClick={() => setShowAddApp(true)}
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors" className="flex items-center gap-2 bg-emerald-500 hover:bg-emerald-600 text-white px-4 py-2 rounded-lg font-medium transition-all shadow-lg shadow-emerald-500/20"
> >
<Plus className="w-4 h-4" /> <Plus className="w-4 h-4" />
Add App <span className="hidden sm:inline">Add App</span>
</button> </button>
</div> </div>
</div> </div>
@ -207,283 +257,322 @@ export default function HeartbeatMonitor() {
</header> </header>
{/* Main Content */} {/* Main Content */}
<main className="max-w-7xl mx-auto px-4 py-6"> <main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Stats Overview */} {activeTab === "dashboard" && (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6"> <>
<div className="bg-slate-900 rounded-lg p-4 border border-slate-800"> {/* Stats Grid */}
<div className="flex items-center gap-3"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<Server className="w-5 h-5 text-blue-400" /> {/* Total Apps */}
<div> <div className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-slate-800/80 to-slate-900/80 border border-slate-700/50 p-6">
<div className="text-2xl font-bold">{totalApps}</div> <div className="absolute top-0 right-0 w-32 h-32 bg-blue-500/10 rounded-full blur-3xl -mr-16 -mt-16" />
<div className="text-xs text-slate-400">Total Apps</div> <div className="relative">
</div> <div className="flex items-center gap-3 mb-2">
</div> <div className="w-10 h-10 rounded-xl bg-blue-500/20 flex items-center justify-center">
</div> <Server className="w-5 h-5 text-blue-400" />
<div className="bg-slate-900 rounded-lg p-4 border border-slate-800">
<div className="flex items-center gap-3">
<Activity className="w-5 h-5 text-green-400" />
<div>
<div className="text-2xl font-bold text-green-400">{onlineApps}</div>
<div className="text-xs text-slate-400">Online</div>
</div>
</div>
</div>
<div className="bg-slate-900 rounded-lg p-4 border border-slate-800">
<div className="flex items-center gap-3">
<AlertCircle className="w-5 h-5 text-red-400" />
<div>
<div className="text-2xl font-bold text-red-400">{totalApps - onlineApps}</div>
<div className="text-xs text-slate-400">Offline</div>
</div>
</div>
</div>
<div className="bg-slate-900 rounded-lg p-4 border border-slate-800">
<div className="flex items-center gap-3">
<Clock className="w-5 h-5 text-purple-400" />
<div>
<div className="text-2xl font-bold">
{Math.round((onlineApps / (totalApps || 1)) * 100)}%
</div>
<div className="text-xs text-slate-400">Health</div>
</div>
</div>
</div>
</div>
{/* Apps by Category */}
{categories.map((category) => (
<div key={category} className="mb-8">
<h2 className="text-lg font-semibold text-slate-300 mb-4 flex items-center gap-2">
<TrendingUp className="w-5 h-5" />
{category}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{apps
.filter((app) => app.category === category)
.map((app) => {
const { latest, history, uptime } = getAppStatus(app.id);
const isUp = latest?.status === "up";
return (
<div
key={app.id}
className={`bg-slate-900 rounded-lg border p-4 transition-all cursor-pointer hover:border-slate-600 ${
isUp ? "border-green-900/50" : "border-red-900/50"
}`}
onClick={() => setSelectedApp(app)}
>
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: app.color }}
/>
<div>
<h3 className="font-semibold text-white">{app.name}</h3>
<p className="text-xs text-slate-400">{app.description}</p>
</div>
</div>
<div
className={`w-2 h-2 rounded-full ${
isUp ? "bg-green-500 animate-pulse" : "bg-red-500"
}`}
/>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-slate-400">URL</span>
<a
href={app.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 hover:underline truncate max-w-[150px]"
onClick={(e) => e.stopPropagation()}
>
{app.url}
</a>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-slate-400">Port</span>
<span className="text-slate-300">{app.port}</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-slate-400">Uptime</span>
<span className={uptime > 90 ? "text-green-400" : "text-yellow-400"}>
{uptime}%
</span>
</div>
{latest?.responseTime && (
<div className="flex items-center justify-between text-sm">
<span className="text-slate-400">Response</span>
<span className="text-slate-300">{latest.responseTime}ms</span>
</div>
)}
</div>
{/* Mini sparkline */}
{history.length > 1 && (
<div className="mt-3 h-10">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={history.map((h, i) => ({ i, status: h.status === "up" ? 1 : 0 }))}>
<Line
type="step"
dataKey="status"
stroke={isUp ? "#22c55e" : "#ef4444"}
strokeWidth={2}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
</div>
)}
<div className="flex gap-2 mt-4">
<button
onClick={(e) => {
e.stopPropagation();
checkApp(app);
}}
disabled={checking === app.id}
className="flex-1 bg-slate-800 hover:bg-slate-700 text-slate-300 px-3 py-1.5 rounded text-sm flex items-center justify-center gap-1 transition-colors"
>
<RefreshCw className={`w-3 h-3 ${checking === app.id ? "animate-spin" : ""}`} />
Check
</button>
<button
onClick={(e) => {
e.stopPropagation();
restartApp(app);
}}
className="flex-1 bg-slate-800 hover:bg-slate-700 text-slate-300 px-3 py-1.5 rounded text-sm flex items-center justify-center gap-1 transition-colors"
>
<Play className="w-3 h-3" />
Restart
</button>
<button
onClick={(e) => {
e.stopPropagation();
deleteApp(app.id);
}}
className="bg-slate-800 hover:bg-red-900/30 text-slate-400 hover:text-red-400 px-3 py-1.5 rounded text-sm transition-colors"
>
<Trash2 className="w-3 h-3" />
</button>
</div>
</div> </div>
); <span className="text-sm font-medium text-slate-400">Total Apps</span>
})} </div>
<div className="text-3xl font-bold text-white font-mono">{totalApps}</div>
</div>
</div>
{/* Online */}
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-slate-800/80 to-slate-900/80 border border-slate-700/50 p-6">
<div className="absolute top-0 right-0 w-32 h-32 bg-emerald-500/10 rounded-full blur-3xl -mr-16 -mt-16" />
<div className="relative">
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-xl bg-emerald-500/20 flex items-center justify-center">
<CheckCircle2 className="w-5 h-5 text-emerald-400" />
</div>
<span className="text-sm font-medium text-slate-400">Online</span>
</div>
<div className="text-3xl font-bold text-emerald-400 font-mono">{onlineApps}</div>
</div>
</div>
{/* Offline */}
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-slate-800/80 to-slate-900/80 border border-slate-700/50 p-6">
<div className="absolute top-0 right-0 w-32 h-32 bg-red-500/10 rounded-full blur-3xl -mr-16 -mt-16" />
<div className="relative">
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-xl bg-red-500/20 flex items-center justify-center">
<XCircle className="w-5 h-5 text-red-400" />
</div>
<span className="text-sm font-medium text-slate-400">Offline</span>
</div>
<div className="text-3xl font-bold text-red-400 font-mono">{offlineApps}</div>
</div>
</div>
{/* Health */}
<div className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-slate-800/80 to-slate-900/80 border border-slate-700/50 p-6">
<div className="absolute top-0 right-0 w-32 h-32 bg-purple-500/10 rounded-full blur-3xl -mr-16 -mt-16" />
<div className="relative">
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-xl bg-purple-500/20 flex items-center justify-center">
<TrendingUp className="w-5 h-5 text-purple-400" />
</div>
<span className="text-sm font-medium text-slate-400">Health</span>
</div>
<div className="text-3xl font-bold text-purple-400 font-mono">{overallHealth}%</div>
</div>
</div>
</div>
{/* Apps List */}
<div className="space-y-6">
{categories.map((category) => (
<section key={category}>
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-emerald-500" />
{category}
<span className="text-sm font-normal text-slate-500">
({apps.filter((a) => a.category === category).length})
</span>
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
{apps
.filter((app) => app.category === category)
.map((app) => {
const { latest, history, uptime } = getAppStatus(app.id);
const isUp = latest?.status === "up";
const uptimeData = generateUptimeData(app.id);
return (
<div
key={app.id}
className={`group relative rounded-2xl border p-5 transition-all hover:scale-[1.02] cursor-pointer ${
isUp
? "bg-slate-800/50 border-slate-700/50 hover:border-emerald-500/30"
: "bg-slate-800/50 border-red-500/30 hover:border-red-500/50"
}`}
onClick={() => setSelectedApp(app)}
>
{/* Status Indicator */}
<div className="absolute top-4 right-4 flex items-center gap-2">
<div
className={`w-2 h-2 rounded-full ${
isUp ? "bg-emerald-500 animate-pulse" : "bg-red-500"
}`}
/>
<span className={`text-xs font-mono ${isUp ? "text-emerald-400" : "text-red-400"}`}>
{isUp ? "ONLINE" : "OFFLINE"}
</span>
</div>
{/* App Header */}
<div className="flex items-start gap-4 mb-4">
<div
className="w-12 h-12 rounded-xl flex items-center justify-center text-xl"
style={{
background: `linear-gradient(135deg, ${app.color}20, ${app.color}40)`,
border: `1px solid ${app.color}30`,
}}
>
<span style={{ color: app.color }}>
{app.name.charAt(0).toUpperCase()}
</span>
</div>
<div className="flex-1 min-w-0 pr-16">
<h3 className="font-semibold text-white truncate">{app.name}</h3>
<p className="text-sm text-slate-400 truncate">{app.description}</p>
</div>
</div>
{/* Metrics */}
<div className="grid grid-cols-3 gap-4 mb-4">
<div>
<p className="text-xs text-slate-500 mb-1">Uptime</p>
<p className={`text-lg font-mono font-semibold ${uptime >= 90 ? "text-emerald-400" : "text-yellow-400"}`}>
{uptime}%
</p>
</div>
<div>
<p className="text-xs text-slate-500 mb-1">Port</p>
<p className="text-lg font-mono font-semibold text-white">{app.port}</p>
</div>
<div>
<p className="text-xs text-slate-500 mb-1">Response</p>
<p className="text-lg font-mono font-semibold text-white">
{latest?.responseTime ? `${latest.responseTime}ms` : "--"}
</p>
</div>
</div>
{/* Sparkline Chart */}
{uptimeData.length > 1 && (
<div className="h-16 mb-4">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={uptimeData}>
<defs>
<linearGradient id={`gradient-${app.id}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={isUp ? "#22C55E" : "#EF4444"} stopOpacity={0.3} />
<stop offset="95%" stopColor={isUp ? "#22C55E" : "#EF4444"} stopOpacity={0} />
</linearGradient>
</defs>
<Area
type="monotone"
dataKey="uptime"
stroke={isUp ? "#22C55E" : "#EF4444"}
strokeWidth={2}
fill={`url(#gradient-${app.id})`}
/>
</AreaChart>
</ResponsiveContainer>
</div>
)}
{/* Actions */}
<div className="flex items-center gap-2">
<button
onClick={(e) => {
e.stopPropagation();
checkApp(app);
}}
disabled={checking === app.id}
className="flex-1 bg-slate-700/50 hover:bg-slate-700 text-slate-300 px-3 py-2 rounded-lg text-sm font-medium transition-all flex items-center justify-center gap-2"
>
<RefreshCw className={`w-4 h-4 ${checking === app.id ? "animate-spin" : ""}`} />
Check
</button>
<a
href={app.url}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="p-2 bg-slate-700/50 hover:bg-slate-700 text-slate-300 rounded-lg transition-all"
>
<ExternalLink className="w-4 h-4" />
</a>
<button
onClick={(e) => {
e.stopPropagation();
deleteApp(app.id);
}}
className="p-2 bg-slate-700/50 hover:bg-red-500/20 hover:text-red-400 text-slate-300 rounded-lg transition-all"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
);
})}
</div>
</section>
))}
</div>
</>
)}
{activeTab === "settings" && (
<div className="max-w-2xl mx-auto">
<div className="bg-slate-800/50 rounded-2xl border border-slate-700/50 p-8">
<h2 className="text-xl font-semibold text-white mb-6">Settings</h2>
<div className="space-y-4 text-slate-400">
<p>Monitor settings and configuration options will be available here.</p>
<p className="text-sm">Current version: 1.0.0</p>
</div>
</div> </div>
</div> </div>
))} )}
</main> </main>
{/* Add App Modal */} {/* Add App Modal */}
{showAddApp && ( {showAddApp && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-slate-900 rounded-lg border border-slate-800 p-6 w-full max-w-lg"> <div className="bg-slate-800 rounded-2xl border border-slate-700/50 p-6 w-full max-w-lg max-h-[90vh] overflow-y-auto">
<h2 className="text-xl font-bold mb-4">Add New App</h2> <div className="flex items-center justify-between mb-6">
<form onSubmit={addApp} className="space-y-4"> <h2 className="text-xl font-semibold text-white">Add New App</h2>
<button
onClick={() => setShowAddApp(false)}
className="text-slate-400 hover:text-white transition-colors"
>
<XCircle className="w-6 h-6" />
</button>
</div>
<form onSubmit={addApp} className="space-y-5">
<div> <div>
<label className="block text-sm text-slate-400 mb-1">Name</label> <label className="block text-sm font-medium text-slate-400 mb-2">Name</label>
<input <input
type="text" type="text"
value={newApp.name} value={newApp.name}
onChange={(e) => setNewApp({ ...newApp, name: e.target.value })} onChange={(e) => setNewApp({ ...newApp, name: e.target.value })}
className="w-full bg-slate-800 border border-slate-700 rounded px-3 py-2 text-white" className="w-full bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-white focus:outline-none focus:border-emerald-500 transition-colors"
placeholder="My App" placeholder="My App"
required required
/> />
</div> </div>
<div> <div>
<label className="block text-sm text-slate-400 mb-1">Description</label> <label className="block text-sm font-medium text-slate-400 mb-2">Description</label>
<input <input
type="text" type="text"
value={newApp.description} value={newApp.description}
onChange={(e) => setNewApp({ ...newApp, description: e.target.value })} onChange={(e) => setNewApp({ ...newApp, description: e.target.value })}
className="w-full bg-slate-800 border border-slate-700 rounded px-3 py-2 text-white" className="w-full bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-white focus:outline-none focus:border-emerald-500 transition-colors"
placeholder="What this app does" placeholder="Brief description"
/> />
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm text-slate-400 mb-1">URL</label> <label className="block text-sm font-medium text-slate-400 mb-2">URL</label>
<input <input
type="text" type="text"
value={newApp.url} value={newApp.url}
onChange={(e) => setNewApp({ ...newApp, url: e.target.value })} onChange={(e) => setNewApp({ ...newApp, url: e.target.value })}
className="w-full bg-slate-800 border border-slate-700 rounded px-3 py-2 text-white" className="w-full bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-white focus:outline-none focus:border-emerald-500 transition-colors"
placeholder="http://localhost:3000" placeholder="http://localhost:3000"
required required
/> />
</div> </div>
<div> <div>
<label className="block text-sm text-slate-400 mb-1">Port</label> <label className="block text-sm font-medium text-slate-400 mb-2">Port</label>
<input <input
type="number" type="number"
value={newApp.port} value={newApp.port}
onChange={(e) => setNewApp({ ...newApp, port: parseInt(e.target.value) })} onChange={(e) => setNewApp({ ...newApp, port: parseInt(e.target.value) })}
className="w-full bg-slate-800 border border-slate-700 rounded px-3 py-2 text-white" className="w-full bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-white focus:outline-none focus:border-emerald-500 transition-colors"
required required
/> />
</div> </div>
</div> </div>
<div> <div>
<label className="block text-sm text-slate-400 mb-1">Project Path</label> <label className="block text-sm font-medium text-slate-400 mb-2">Category</label>
<input <select
type="text" value={newApp.category}
value={newApp.path} onChange={(e) => setNewApp({ ...newApp, category: e.target.value })}
onChange={(e) => setNewApp({ ...newApp, path: e.target.value })} className="w-full bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-white focus:outline-none focus:border-emerald-500 transition-colors"
className="w-full bg-slate-800 border border-slate-700 rounded px-3 py-2 text-white" >
placeholder="/Users/.../my-app" <option>Productivity</option>
/> <option>Backup</option>
<option>Monitoring</option>
<option>Development</option>
<option>Other</option>
</select>
</div> </div>
<div> <div>
<label className="block text-sm text-slate-400 mb-1">Start Command</label> <label className="block text-sm font-medium text-slate-400 mb-2">Color</label>
<input <div className="flex items-center gap-3">
type="text"
value={newApp.command}
onChange={(e) => setNewApp({ ...newApp, command: e.target.value })}
className="w-full bg-slate-800 border border-slate-700 rounded px-3 py-2 text-white"
placeholder="npm run dev"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm text-slate-400 mb-1">Category</label>
<select
value={newApp.category}
onChange={(e) => setNewApp({ ...newApp, category: e.target.value })}
className="w-full bg-slate-800 border border-slate-700 rounded px-3 py-2 text-white"
>
<option>Productivity</option>
<option>Backup</option>
<option>Monitoring</option>
<option>Development</option>
<option>Other</option>
</select>
</div>
<div>
<label className="block text-sm text-slate-400 mb-1">Color</label>
<input <input
type="color" type="color"
value={newApp.color} value={newApp.color}
onChange={(e) => setNewApp({ ...newApp, color: e.target.value })} onChange={(e) => setNewApp({ ...newApp, color: e.target.value })}
className="w-full h-10 bg-slate-800 border border-slate-700 rounded" className="w-12 h-12 rounded-xl bg-slate-900 border border-slate-700 cursor-pointer"
/> />
<span className="text-slate-500 font-mono">{newApp.color}</span>
</div> </div>
</div> </div>
<div className="flex gap-3 pt-4"> <div className="flex gap-3 pt-4">
<button <button
type="button" type="button"
onClick={() => setShowAddApp(false)} onClick={() => setShowAddApp(false)}
className="flex-1 bg-slate-800 hover:bg-slate-700 text-slate-300 px-4 py-2 rounded transition-colors" className="flex-1 bg-slate-700 hover:bg-slate-600 text-slate-300 px-4 py-3 rounded-xl font-medium transition-all"
> >
Cancel Cancel
</button> </button>
<button <button
type="submit" type="submit"
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded transition-colors" className="flex-1 bg-emerald-500 hover:bg-emerald-600 text-white px-4 py-3 rounded-xl font-medium transition-all shadow-lg shadow-emerald-500/20"
> >
Add App Add App
</button> </button>
@ -492,6 +581,113 @@ export default function HeartbeatMonitor() {
</div> </div>
</div> </div>
)} )}
{/* App Detail Modal */}
{selectedApp && (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-slate-800 rounded-2xl border border-slate-700/50 p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-4">
<div
className="w-14 h-14 rounded-xl flex items-center justify-center text-2xl font-bold"
style={{
background: `linear-gradient(135deg, ${selectedApp.color}20, ${selectedApp.color}40)`,
border: `1px solid ${selectedApp.color}30`,
}}
>
<span style={{ color: selectedApp.color }}>
{selectedApp.name.charAt(0).toUpperCase()}
</span>
</div>
<div>
<h2 className="text-xl font-semibold text-white">{selectedApp.name}</h2>
<p className="text-slate-400">{selectedApp.description}</p>
</div>
</div>
<button
onClick={() => setSelectedApp(null)}
className="text-slate-400 hover:text-white transition-colors"
>
<XCircle className="w-6 h-6" />
</button>
</div>
<div className="space-y-6">
<div className="grid grid-cols-2 gap-4">
<div className="bg-slate-900/50 rounded-xl p-4">
<p className="text-sm text-slate-500 mb-1">URL</p>
<a
href={selectedApp.url}
target="_blank"
rel="noopener noreferrer"
className="text-emerald-400 hover:text-emerald-300 font-mono text-sm break-all"
>
{selectedApp.url}
</a>
</div>
<div className="bg-slate-900/50 rounded-xl p-4">
<p className="text-sm text-slate-500 mb-1">Port</p>
<p className="text-white font-mono">{selectedApp.port}</p>
</div>
</div>
<div>
<h3 className="text-sm font-medium text-slate-400 mb-3">Uptime History</h3>
<div className="h-48 bg-slate-900/50 rounded-xl p-4">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={generateUptimeData(selectedApp.id)}>
<defs>
<linearGradient id="detailGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#22C55E" stopOpacity={0.3} />
<stop offset="95%" stopColor="#22C55E" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
<XAxis dataKey="time" stroke="#64748B" fontSize={12} />
<YAxis stroke="#64748B" fontSize={12} />
<Tooltip
contentStyle={{
backgroundColor: "#1E293B",
border: "1px solid #334155",
borderRadius: "8px",
}}
/>
<Area
type="monotone"
dataKey="uptime"
stroke="#22C55E"
strokeWidth={2}
fill="url(#detailGradient)"
/>
</AreaChart>
</ResponsiveContainer>
</div>
</div>
<div className="flex gap-3">
<button
onClick={() => {
checkApp(selectedApp);
setSelectedApp(null);
}}
className="flex-1 bg-emerald-500 hover:bg-emerald-600 text-white px-4 py-3 rounded-xl font-medium transition-all"
>
Check Now
</button>
<button
onClick={() => {
deleteApp(selectedApp.id);
setSelectedApp(null);
}}
className="flex-1 bg-red-500/20 hover:bg-red-500/30 text-red-400 px-4 py-3 rounded-xl font-medium transition-all"
>
Delete App
</button>
</div>
</div>
</div>
</div>
)}
</div> </div>
); );
} }