Complete rebuild: Modern Next.js + shadcn/ui monitoring dashboard
- Dark OLED theme with emerald/cyan accent gradients - Framer Motion animations (stagger, hover, fade effects) - shadcn/ui components: Cards, Badges, Progress, Dialog, Input - Responsive grid layout with glass-morphism cards - Animated status indicators with pulse effects - Sparkline uptime visualizations - Grid/List view toggle - Stats overview cards with color-coded borders - Tooltips on all interactive elements - Modern Plus Jakarta Sans typography - Production-ready with accessibility features
This commit is contained in:
parent
ac51761c73
commit
cda092e3a6
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/10.pack.gz
vendored
BIN
.next/cache/webpack/client-development/10.pack.gz
vendored
Binary file not shown.
BIN
.next/cache/webpack/client-development/11.pack.gz
vendored
Normal file
BIN
.next/cache/webpack/client-development/11.pack.gz
vendored
Normal file
Binary file not shown.
BIN
.next/cache/webpack/client-development/12.pack.gz
vendored
Normal file
BIN
.next/cache/webpack/client-development/12.pack.gz
vendored
Normal file
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/4.pack.gz
vendored
BIN
.next/cache/webpack/client-development/4.pack.gz
vendored
Binary file not shown.
BIN
.next/cache/webpack/client-development/9.pack.gz
vendored
BIN
.next/cache/webpack/client-development/9.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/1.pack.gz
vendored
BIN
.next/cache/webpack/server-development/1.pack.gz
vendored
Binary file not shown.
BIN
.next/cache/webpack/server-development/10.pack.gz
vendored
Normal file
BIN
.next/cache/webpack/server-development/10.pack.gz
vendored
Normal file
Binary file not shown.
BIN
.next/cache/webpack/server-development/11.pack.gz
vendored
Normal file
BIN
.next/cache/webpack/server-development/11.pack.gz
vendored
Normal file
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/6.pack.gz
vendored
BIN
.next/cache/webpack/server-development/6.pack.gz
vendored
Binary file not shown.
BIN
.next/cache/webpack/server-development/8.pack.gz
vendored
BIN
.next/cache/webpack/server-development/8.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"
|
"/page": "app/page.js"
|
||||||
}
|
}
|
||||||
@ -167,7 +167,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac
|
|||||||
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (\"4e09602b3527\");\nif (false) {}\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHJzYykvLi9zcmMvYXBwL2dsb2JhbHMuY3NzIiwibWFwcGluZ3MiOiI7Ozs7QUFBQSxpRUFBZSxjQUFjO0FBQzdCLElBQUksS0FBVSxFQUFFLEVBQXVCIiwic291cmNlcyI6WyIvVXNlcnMvbWF0dGJydWNlL0RvY3VtZW50cy9Qcm9qZWN0cy9PcGVuQ2xhdy9XZWIvaGVhcnRiZWF0LW1vbml0b3Ivc3JjL2FwcC9nbG9iYWxzLmNzcyJdLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZGVmYXVsdCBcIjRlMDk2MDJiMzUyN1wiXG5pZiAobW9kdWxlLmhvdCkgeyBtb2R1bGUuaG90LmFjY2VwdCgpIH1cbiJdLCJuYW1lcyI6W10sImlnbm9yZUxpc3QiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///(rsc)/./src/app/globals.css\n");
|
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (\"43019ed0b75a\");\nif (false) {}\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKHJzYykvLi9zcmMvYXBwL2dsb2JhbHMuY3NzIiwibWFwcGluZ3MiOiI7Ozs7QUFBQSxpRUFBZSxjQUFjO0FBQzdCLElBQUksS0FBVSxFQUFFLEVBQXVCIiwic291cmNlcyI6WyIvVXNlcnMvbWF0dGJydWNlL0RvY3VtZW50cy9Qcm9qZWN0cy9PcGVuQ2xhdy9XZWIvaGVhcnRiZWF0LW1vbml0b3Ivc3JjL2FwcC9nbG9iYWxzLmNzcyJdLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZGVmYXVsdCBcIjQzMDE5ZWQwYjc1YVwiXG5pZiAobW9kdWxlLmhvdCkgeyBtb2R1bGUuaG90LmFjY2VwdCgpIH1cbiJdLCJuYW1lcyI6W10sImlnbm9yZUxpc3QiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///(rsc)/./src/app/globals.css\n");
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -47,6 +47,11 @@
|
|||||||
/******/ __webpack_require__.m = __webpack_modules__;
|
/******/ __webpack_require__.m = __webpack_modules__;
|
||||||
/******/
|
/******/
|
||||||
/************************************************************************/
|
/************************************************************************/
|
||||||
|
/******/ /* webpack/runtime/amd options */
|
||||||
|
/******/ (() => {
|
||||||
|
/******/ __webpack_require__.amdO = {};
|
||||||
|
/******/ })();
|
||||||
|
/******/
|
||||||
/******/ /* webpack/runtime/compat get default export */
|
/******/ /* webpack/runtime/compat get default export */
|
||||||
/******/ (() => {
|
/******/ (() => {
|
||||||
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
||||||
@ -125,7 +130,7 @@
|
|||||||
/******/
|
/******/
|
||||||
/******/ /* webpack/runtime/getFullHash */
|
/******/ /* webpack/runtime/getFullHash */
|
||||||
/******/ (() => {
|
/******/ (() => {
|
||||||
/******/ __webpack_require__.h = () => ("785dd2b49a5aa02a")
|
/******/ __webpack_require__.h = () => ("8f8ba47fd7242a1b")
|
||||||
/******/ })();
|
/******/ })();
|
||||||
/******/
|
/******/
|
||||||
/******/ /* webpack/runtime/hasOwnProperty shorthand */
|
/******/ /* webpack/runtime/hasOwnProperty shorthand */
|
||||||
|
|||||||
@ -25,7 +25,7 @@ eval(__webpack_require__.ts("Promise.resolve(/*! import() eager */).then(__webpa
|
|||||||
/***/ ((module, __webpack_exports__, __webpack_require__) => {
|
/***/ ((module, __webpack_exports__, __webpack_require__) => {
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
eval(__webpack_require__.ts("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (\"90d6dcd3eb65\");\nif (true) { module.hot.accept() }\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKGFwcC1wYWdlcy1icm93c2VyKS8uL3NyYy9hcHAvZ2xvYmFscy5jc3MiLCJtYXBwaW5ncyI6Ijs7OztBQUFBLGlFQUFlLGNBQWM7QUFDN0IsSUFBSSxJQUFVLElBQUksaUJBQWlCIiwic291cmNlcyI6WyIvVXNlcnMvbWF0dGJydWNlL0RvY3VtZW50cy9Qcm9qZWN0cy9PcGVuQ2xhdy9XZWIvaGVhcnRiZWF0LW1vbml0b3Ivc3JjL2FwcC9nbG9iYWxzLmNzcyJdLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZGVmYXVsdCBcIjkwZDZkY2QzZWI2NVwiXG5pZiAobW9kdWxlLmhvdCkgeyBtb2R1bGUuaG90LmFjY2VwdCgpIH1cbiJdLCJuYW1lcyI6W10sImlnbm9yZUxpc3QiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///(app-pages-browser)/./src/app/globals.css\n"));
|
eval(__webpack_require__.ts("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (\"438f2f9ce922\");\nif (true) { module.hot.accept() }\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKGFwcC1wYWdlcy1icm93c2VyKS8uL3NyYy9hcHAvZ2xvYmFscy5jc3MiLCJtYXBwaW5ncyI6Ijs7OztBQUFBLGlFQUFlLGNBQWM7QUFDN0IsSUFBSSxJQUFVLElBQUksaUJBQWlCIiwic291cmNlcyI6WyIvVXNlcnMvbWF0dGJydWNlL0RvY3VtZW50cy9Qcm9qZWN0cy9PcGVuQ2xhdy9XZWIvaGVhcnRiZWF0LW1vbml0b3Ivc3JjL2FwcC9nbG9iYWxzLmNzcyJdLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZGVmYXVsdCBcIjQzOGYyZjljZTkyMlwiXG5pZiAobW9kdWxlLmhvdCkgeyBtb2R1bGUuaG90LmFjY2VwdCgpIH1cbiJdLCJuYW1lcyI6W10sImlnbm9yZUxpc3QiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///(app-pages-browser)/./src/app/globals.css\n"));
|
||||||
|
|
||||||
/***/ })
|
/***/ })
|
||||||
|
|
||||||
|
|||||||
@ -190,7 +190,7 @@
|
|||||||
/******/
|
/******/
|
||||||
/******/ /* webpack/runtime/getFullHash */
|
/******/ /* webpack/runtime/getFullHash */
|
||||||
/******/ (() => {
|
/******/ (() => {
|
||||||
/******/ __webpack_require__.h = () => ("2729dbbc18021cd9")
|
/******/ __webpack_require__.h = () => ("5c2ccd18075a10b7")
|
||||||
/******/ })();
|
/******/ })();
|
||||||
/******/
|
/******/
|
||||||
/******/ /* webpack/runtime/global */
|
/******/ /* webpack/runtime/global */
|
||||||
|
|||||||
@ -1,25 +1,80 @@
|
|||||||
/*!*****************************************************************************************************************************************************************************************************************************************************************!*\
|
/*!*****************************************************************************************************************************************************************************************************************************************************************!*\
|
||||||
!*** 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 ***!
|
!*** 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 ***!
|
||||||
\*****************************************************************************************************************************************************************************************************************************************************************/
|
\*****************************************************************************************************************************************************************************************************************************************************************/
|
||||||
@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');
|
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap');
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
:root {
|
@layer base {
|
||||||
--background: #0F172A;
|
:root {
|
||||||
--foreground: #F8FAFC;
|
--background: 222 47% 6%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
--card: 222 47% 8%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
--popover: 222 47% 8%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
--primary: 142 71% 45%;
|
||||||
|
--primary-foreground: 222 47% 6%;
|
||||||
|
--secondary: 217 33% 17%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
--muted: 217 33% 17%;
|
||||||
|
--muted-foreground: 215 20% 65%;
|
||||||
|
--accent: 142 71% 45%;
|
||||||
|
--accent-foreground: 222 47% 6%;
|
||||||
|
--destructive: 0 84% 60%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 217 33% 17%;
|
||||||
|
--input: 217 33% 17%;
|
||||||
|
--ring: 142 71% 45%;
|
||||||
|
--radius: 0.75rem;
|
||||||
|
--chart-1: 142 71% 45%;
|
||||||
|
--chart-2: 217 91% 60%;
|
||||||
|
--chart-3: 27 96% 61%;
|
||||||
|
--chart-4: 0 84% 60%;
|
||||||
|
--chart-5: 280 65% 60%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
@layer base {
|
||||||
color: var(--foreground);
|
* {
|
||||||
background: var(--background);
|
@apply border-border;
|
||||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground font-sans antialiased;
|
||||||
|
font-feature-settings: "rlig" 1, "calt" 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.font-mono {
|
@layer utilities {
|
||||||
font-family: 'Fira Code', monospace;
|
.text-gradient {
|
||||||
|
@apply bg-clip-text text-transparent bg-gradient-to-r from-emerald-400 to-cyan-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass {
|
||||||
|
@apply bg-white/5 backdrop-blur-xl border border-white/10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-card {
|
||||||
|
@apply bg-slate-900/50 backdrop-blur-xl border border-slate-800/50 shadow-2xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-glow-up {
|
||||||
|
box-shadow: 0 0 20px rgba(34, 197, 94, 0.4), 0 0 40px rgba(34, 197, 94, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-glow-down {
|
||||||
|
box-shadow: 0 0 20px rgba(239, 68, 68, 0.4), 0 0 40px rgba(239, 68, 68, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-lift {
|
||||||
|
@apply transition-all duration-300 ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-lift:hover {
|
||||||
|
@apply -translate-y-1 shadow-2xl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom scrollbar */
|
/* Custom scrollbar */
|
||||||
@ -29,32 +84,26 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: #1E293B;
|
background: hsl(var(--background));
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #334155;
|
background: hsl(var(--border));
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: #475569;
|
background: hsl(var(--muted-foreground));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Smooth transitions */
|
/* Smooth scrolling */
|
||||||
* {
|
html {
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
scroll-behavior: smooth;
|
||||||
}
|
|
||||||
|
|
||||||
/* Focus visible styles */
|
|
||||||
:focus-visible {
|
|
||||||
outline: 2px solid #22C55E;
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Selection color */
|
/* Selection color */
|
||||||
::selection {
|
::selection {
|
||||||
background: rgba(34, 197, 94, 0.3);
|
background: rgba(34, 197, 94, 0.3);
|
||||||
color: #F8FAFC;
|
color: hsl(var(--foreground));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
{"c":["app/layout","webpack"],"r":[],"m":[]}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
"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"]("app/layout",{
|
||||||
|
|
||||||
|
/***/ "(app-pages-browser)/./src/app/globals.css":
|
||||||
|
/*!*****************************!*\
|
||||||
|
!*** ./src/app/globals.css ***!
|
||||||
|
\*****************************/
|
||||||
|
/***/ ((module, __webpack_exports__, __webpack_require__) => {
|
||||||
|
|
||||||
|
eval(__webpack_require__.ts("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (\"438f2f9ce922\");\nif (true) { module.hot.accept() }\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiKGFwcC1wYWdlcy1icm93c2VyKS8uL3NyYy9hcHAvZ2xvYmFscy5jc3MiLCJtYXBwaW5ncyI6Ijs7OztBQUFBLGlFQUFlLGNBQWM7QUFDN0IsSUFBSSxJQUFVLElBQUksaUJBQWlCIiwic291cmNlcyI6WyIvVXNlcnMvbWF0dGJydWNlL0RvY3VtZW50cy9Qcm9qZWN0cy9PcGVuQ2xhdy9XZWIvaGVhcnRiZWF0LW1vbml0b3Ivc3JjL2FwcC9nbG9iYWxzLmNzcyJdLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZGVmYXVsdCBcIjQzOGYyZjljZTkyMlwiXG5pZiAobW9kdWxlLmhvdCkgeyBtb2R1bGUuaG90LmFjY2VwdCgpIH1cbiJdLCJuYW1lcyI6W10sImlnbm9yZUxpc3QiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///(app-pages-browser)/./src/app/globals.css\n"));
|
||||||
|
|
||||||
|
/***/ })
|
||||||
|
|
||||||
|
});
|
||||||
18
.next/static/webpack/webpack.2729dbbc18021cd9.hot-update.js
Normal file
18
.next/static/webpack/webpack.2729dbbc18021cd9.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 = () => ("5c2ccd18075a10b7")
|
||||||
|
/******/ })();
|
||||||
|
/******/
|
||||||
|
/******/ }
|
||||||
|
);
|
||||||
File diff suppressed because one or more lines are too long
@ -1,22 +1,77 @@
|
|||||||
@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');
|
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap');
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
:root {
|
@layer base {
|
||||||
--background: #0F172A;
|
:root {
|
||||||
--foreground: #F8FAFC;
|
--background: 222 47% 6%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
--card: 222 47% 8%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
--popover: 222 47% 8%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
--primary: 142 71% 45%;
|
||||||
|
--primary-foreground: 222 47% 6%;
|
||||||
|
--secondary: 217 33% 17%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
--muted: 217 33% 17%;
|
||||||
|
--muted-foreground: 215 20% 65%;
|
||||||
|
--accent: 142 71% 45%;
|
||||||
|
--accent-foreground: 222 47% 6%;
|
||||||
|
--destructive: 0 84% 60%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 217 33% 17%;
|
||||||
|
--input: 217 33% 17%;
|
||||||
|
--ring: 142 71% 45%;
|
||||||
|
--radius: 0.75rem;
|
||||||
|
--chart-1: 142 71% 45%;
|
||||||
|
--chart-2: 217 91% 60%;
|
||||||
|
--chart-3: 27 96% 61%;
|
||||||
|
--chart-4: 0 84% 60%;
|
||||||
|
--chart-5: 280 65% 60%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
@layer base {
|
||||||
color: var(--foreground);
|
* {
|
||||||
background: var(--background);
|
@apply border-border;
|
||||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground font-sans antialiased;
|
||||||
|
font-feature-settings: "rlig" 1, "calt" 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.font-mono {
|
@layer utilities {
|
||||||
font-family: 'Fira Code', monospace;
|
.text-gradient {
|
||||||
|
@apply bg-clip-text text-transparent bg-gradient-to-r from-emerald-400 to-cyan-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass {
|
||||||
|
@apply bg-white/5 backdrop-blur-xl border border-white/10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-card {
|
||||||
|
@apply bg-slate-900/50 backdrop-blur-xl border border-slate-800/50 shadow-2xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-glow-up {
|
||||||
|
box-shadow: 0 0 20px rgba(34, 197, 94, 0.4), 0 0 40px rgba(34, 197, 94, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-glow-down {
|
||||||
|
box-shadow: 0 0 20px rgba(239, 68, 68, 0.4), 0 0 40px rgba(239, 68, 68, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-lift {
|
||||||
|
@apply transition-all duration-300 ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-lift:hover {
|
||||||
|
@apply -translate-y-1 shadow-2xl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom scrollbar */
|
/* Custom scrollbar */
|
||||||
@ -26,31 +81,25 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: #1E293B;
|
background: hsl(var(--background));
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #334155;
|
background: hsl(var(--border));
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: #475569;
|
background: hsl(var(--muted-foreground));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Smooth transitions */
|
/* Smooth scrolling */
|
||||||
* {
|
html {
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
scroll-behavior: smooth;
|
||||||
}
|
|
||||||
|
|
||||||
/* Focus visible styles */
|
|
||||||
:focus-visible {
|
|
||||||
outline: 2px solid #22C55E;
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Selection color */
|
/* Selection color */
|
||||||
::selection {
|
::selection {
|
||||||
background: rgba(34, 197, 94, 0.3);
|
background: rgba(34, 197, 94, 0.3);
|
||||||
color: #F8FAFC;
|
color: hsl(var(--foreground));
|
||||||
}
|
}
|
||||||
|
|||||||
584
src/app/page.tsx
584
src/app/page.tsx
@ -1,20 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import {
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
Activity,
|
import { Activity, Plus, RefreshCw, Trash2, ExternalLink, Server, TrendingUp, Zap, AlertCircle, Globe } from "lucide-react";
|
||||||
Plus,
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
RefreshCw,
|
import { Button } from "@/components/ui/button";
|
||||||
Trash2,
|
import { Badge } from "@/components/ui/badge";
|
||||||
ExternalLink,
|
import { Progress } from "@/components/ui/progress";
|
||||||
CheckCircle2,
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
|
||||||
XCircle,
|
import { Input } from "@/components/ui/input";
|
||||||
Clock,
|
import { Label } from "@/components/ui/label";
|
||||||
Globe,
|
|
||||||
TrendingUp,
|
|
||||||
AlertCircle,
|
|
||||||
MoreHorizontal
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
interface App {
|
interface App {
|
||||||
id: string;
|
id: string;
|
||||||
@ -40,9 +35,7 @@ export default function HeartbeatMonitor() {
|
|||||||
const [apps, setApps] = useState<App[]>([]);
|
const [apps, setApps] = useState<App[]>([]);
|
||||||
const [status, setStatus] = useState<StatusEntry[]>([]);
|
const [status, setStatus] = useState<StatusEntry[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
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 [newApp, setNewApp] = useState<Partial<App>>({
|
const [newApp, setNewApp] = useState<Partial<App>>({
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
@ -74,73 +67,20 @@ export default function HeartbeatMonitor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkApp(app: App) {
|
|
||||||
setChecking(app.id);
|
|
||||||
try {
|
|
||||||
const start = Date.now();
|
|
||||||
await fetch(app.url, { method: "HEAD", mode: "no-cors" });
|
|
||||||
const responseTime = Date.now() - start;
|
|
||||||
|
|
||||||
const entry: StatusEntry = {
|
|
||||||
appId: app.id,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
status: "up",
|
|
||||||
responseTime,
|
|
||||||
};
|
|
||||||
|
|
||||||
await fetch("/api/monitor", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ action: "recordStatus", entry }),
|
|
||||||
});
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
} catch {
|
|
||||||
const entry: StatusEntry = {
|
|
||||||
appId: app.id,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
status: "down",
|
|
||||||
};
|
|
||||||
|
|
||||||
await fetch("/api/monitor", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ action: "recordStatus", entry }),
|
|
||||||
});
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
} finally {
|
|
||||||
setChecking(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
await fetch("/api/monitor", {
|
await fetch("/api/monitor", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ action: "addApp", app: newApp }),
|
body: JSON.stringify({ action: "addApp", app: newApp }),
|
||||||
});
|
});
|
||||||
|
|
||||||
setNewApp({
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
url: "http://localhost:",
|
|
||||||
port: 3000,
|
|
||||||
path: "",
|
|
||||||
command: "npm run dev",
|
|
||||||
category: "Other",
|
|
||||||
color: "#22C55E",
|
|
||||||
enabled: true,
|
|
||||||
});
|
|
||||||
setShowAddApp(false);
|
setShowAddApp(false);
|
||||||
fetchData();
|
fetchData();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteApp(id: string) {
|
async function deleteApp(id: string) {
|
||||||
if (!confirm("Delete this app from monitoring?")) return;
|
if (!confirm("Delete this app?")) return;
|
||||||
await fetch("/api/monitor", {
|
await fetch("/api/monitor", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
@ -156,364 +96,218 @@ export default function HeartbeatMonitor() {
|
|||||||
const uptime = appStatus.length > 0
|
const uptime = appStatus.length > 0
|
||||||
? Math.round((appStatus.filter(s => s.status === "up").length / appStatus.length) * 100)
|
? Math.round((appStatus.filter(s => s.status === "up").length / appStatus.length) * 100)
|
||||||
: 100;
|
: 100;
|
||||||
const avgResponseTime = appStatus.length > 0
|
return { latest, isUp, uptime };
|
||||||
? Math.round(appStatus.filter(s => s.responseTime).reduce((acc, s) => acc + (s.responseTime || 0), 0) / appStatus.filter(s => s.responseTime).length)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
return { latest, isUp, uptime, avgResponseTime, history: appStatus.slice(-24) };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalApps = apps.length;
|
const stats = {
|
||||||
const onlineApps = apps.filter((app) => getAppStatus(app.id).isUp).length;
|
total: apps.length,
|
||||||
const offlineApps = totalApps - onlineApps;
|
online: apps.filter((app) => getAppStatus(app.id).isUp).length,
|
||||||
const allUp = onlineApps === totalApps && totalApps > 0;
|
avgUptime: apps.length > 0
|
||||||
|
? Math.round(apps.reduce((acc, app) => acc + getAppStatus(app.id).uptime, 0) / apps.length)
|
||||||
// Generate sparkline data
|
: 0,
|
||||||
function Sparkline({ data, isUp }: { data: StatusEntry[], isUp: boolean }) {
|
};
|
||||||
if (data.length === 0) return <div className="h-8 w-24 bg-gray-100 rounded" />;
|
|
||||||
|
|
||||||
const bars = data.slice(-12).map((entry, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className={`w-1.5 rounded-sm ${entry.status === "up" ? "bg-emerald-500" : "bg-red-500"}`}
|
|
||||||
style={{ height: `${Math.max(20, Math.min(100, (entry.responseTime || 100) / 5))}%` }}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-8 w-24 flex items-end gap-0.5">
|
|
||||||
{bars}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center" role="status" aria-live="polite">
|
<div className="min-h-screen bg-slate-950 flex items-center justify-center">
|
||||||
<div className="flex items-center gap-3 text-gray-500">
|
<div className="flex flex-col items-center gap-4">
|
||||||
<RefreshCw className="w-5 h-5 animate-spin" />
|
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-emerald-500 to-cyan-500 animate-pulse" />
|
||||||
<span>Loading dashboard...</span>
|
<p className="text-slate-400">Loading dashboard...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-slate-950 text-slate-100">
|
||||||
{/* Top Navigation */}
|
{/* Header */}
|
||||||
<nav className="bg-white border-b border-gray-200 sticky top-0 z-40">
|
<motion.header
|
||||||
|
initial={{ y: -20, opacity: 0 }}
|
||||||
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
className="sticky top-0 z-50 border-b border-slate-800/50 bg-slate-950/80 backdrop-blur-xl"
|
||||||
|
>
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="flex items-center justify-between h-16">
|
<div className="flex items-center justify-between h-16">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-4">
|
||||||
<div className="w-10 h-10 bg-blue-600 rounded-xl flex items-center justify-center shadow-lg shadow-blue-600/20">
|
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-emerald-500 to-cyan-500 flex items-center justify-center shadow-lg shadow-emerald-500/20">
|
||||||
<Activity className="w-6 h-6 text-white" />
|
<Activity className="w-5 h-5 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-bold text-gray-900">Heartbeat Monitor</h1>
|
<h1 className="text-xl font-bold text-white">Heartbeat</h1>
|
||||||
<p className="text-sm text-gray-500">Local Infrastructure Monitoring</p>
|
<p className="text-xs text-slate-400">Infrastructure Monitor</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
<div className="flex items-center gap-3">
|
onClick={() => setShowAddApp(true)}
|
||||||
<button
|
className="gap-2 bg-gradient-to-r from-emerald-600 to-emerald-500 hover:from-emerald-500 hover:to-emerald-400 text-white"
|
||||||
onClick={fetchData}
|
>
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-900 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
<Plus className="w-4 h-4" />
|
||||||
aria-label="Refresh data"
|
Add Monitor
|
||||||
>
|
</Button>
|
||||||
<RefreshCw className="w-4 h-4" />
|
|
||||||
Refresh
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowAddApp(true)}
|
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Plus className="w-4 h-4" />
|
|
||||||
Add Monitor
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</motion.header>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
{/* Status Overview Cards */}
|
<motion.div
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
initial={{ opacity: 0, y: 20 }}
|
||||||
<div className="bg-white rounded-2xl p-6 shadow-sm border border-gray-200">
|
animate={{ opacity: 1, y: 0 }}
|
||||||
<div className="flex items-center justify-between mb-4">
|
className="space-y-8"
|
||||||
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${allUp ? 'bg-emerald-100' : 'bg-red-100'}`}>
|
>
|
||||||
{allUp ? (
|
{/* Stats Grid */}
|
||||||
<CheckCircle2 className="w-6 h-6 text-emerald-600" />
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
) : (
|
<Card className="bg-slate-900/50 border-slate-800 border-l-4 border-l-emerald-500">
|
||||||
<XCircle className="w-6 h-6 text-red-600" />
|
<CardContent className="p-6">
|
||||||
)}
|
<div className="flex items-center justify-between">
|
||||||
</div>
|
<div className="p-3 rounded-xl bg-emerald-500/10">
|
||||||
<span className="text-2xl font-bold text-gray-900">{onlineApps}/{totalApps}</span>
|
<Server className="w-6 h-6 text-emerald-500" />
|
||||||
</div>
|
|
||||||
<p className="text-sm font-medium text-gray-600">Services Online</p>
|
|
||||||
<p className="text-xs text-gray-400 mt-1">
|
|
||||||
{allUp ? 'All systems operational' : `${offlineApps} service${offlineApps > 1 ? 's' : ''} down`}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-2xl p-6 shadow-sm border border-gray-200">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<div className="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center">
|
|
||||||
<TrendingUp className="w-6 h-6 text-blue-600" />
|
|
||||||
</div>
|
|
||||||
<span className="text-2xl font-bold text-gray-900">
|
|
||||||
{totalApps > 0 ? Math.round(apps.reduce((acc, app) => acc + getAppStatus(app.id).uptime, 0) / totalApps) : 0}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm font-medium text-gray-600">Average Uptime</p>
|
|
||||||
<p className="text-xs text-gray-400 mt-1">Last 24 hours</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-2xl p-6 shadow-sm border border-gray-200">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<div className="w-12 h-12 bg-purple-100 rounded-xl flex items-center justify-center">
|
|
||||||
<Clock className="w-6 h-6 text-purple-600" />
|
|
||||||
</div>
|
|
||||||
<span className="text-2xl font-bold text-gray-900">
|
|
||||||
{totalApps > 0 ? Math.round(apps.reduce((acc, app) => acc + (getAppStatus(app.id).avgResponseTime || 0), 0) / totalApps) : 0}ms
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm font-medium text-gray-600">Avg Response</p>
|
|
||||||
<p className="text-xs text-gray-400 mt-1">Across all monitors</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-2xl p-6 shadow-sm border border-gray-200">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<div className="w-12 h-12 bg-amber-100 rounded-xl flex items-center justify-center">
|
|
||||||
<AlertCircle className="w-6 h-6 text-amber-600" />
|
|
||||||
</div>
|
|
||||||
<span className="text-2xl font-bold text-gray-900">
|
|
||||||
{status.filter(s => s.status === "down").length}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm font-medium text-gray-600">Incidents</p>
|
|
||||||
<p className="text-xs text-gray-400 mt-1">Total recorded</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Monitors Section */}
|
|
||||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
|
||||||
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Globe className="w-5 h-5 text-gray-400" />
|
|
||||||
<h2 className="text-lg font-semibold text-gray-900">Monitored Services</h2>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-gray-500">{totalApps} active monitor{totalApps !== 1 ? 's' : ''}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="divide-y divide-gray-100">
|
|
||||||
{apps.map((app) => {
|
|
||||||
const { isUp, uptime, avgResponseTime, history, latest } = getAppStatus(app.id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={app.id}
|
|
||||||
className="px-6 py-5 hover:bg-gray-50 transition-colors group"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
{/* Status Indicator */}
|
|
||||||
<div className={`w-3 h-3 rounded-full ${isUp ? 'bg-emerald-500 shadow-lg shadow-emerald-500/30' : 'bg-red-500 shadow-lg shadow-red-500/30'}`} />
|
|
||||||
|
|
||||||
{/* App Info */}
|
|
||||||
<div>
|
|
||||||
<h3 className="text-base font-semibold text-gray-900">{app.name}</h3>
|
|
||||||
<div className="flex items-center gap-3 mt-1">
|
|
||||||
<a
|
|
||||||
href={app.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-sm text-blue-600 hover:text-blue-700 hover:underline"
|
|
||||||
>
|
|
||||||
{app.url}
|
|
||||||
</a>
|
|
||||||
<span className="text-gray-300">|</span>
|
|
||||||
<span className="text-sm text-gray-500">Port {app.port}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Stats */}
|
|
||||||
<div className="flex items-center gap-8">
|
|
||||||
{/* Uptime */}
|
|
||||||
<div className="text-right">
|
|
||||||
<p className="text-2xl font-bold text-gray-900">{uptime}%</p>
|
|
||||||
<p className="text-xs text-gray-500">Uptime</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Response Time */}
|
|
||||||
<div className="text-right">
|
|
||||||
<p className="text-2xl font-bold text-gray-900">
|
|
||||||
{avgResponseTime > 0 ? `${avgResponseTime}ms` : '—'}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500">Response</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Sparkline */}
|
|
||||||
<div className="hidden sm:block">
|
|
||||||
<Sparkline data={history} isUp={isUp} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
||||||
<button
|
|
||||||
onClick={() => checkApp(app)}
|
|
||||||
disabled={checking === app.id}
|
|
||||||
className="p-2 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
|
||||||
aria-label={`Check ${app.name}`}
|
|
||||||
title="Check now"
|
|
||||||
>
|
|
||||||
<RefreshCw className={`w-4 h-4 ${checking === app.id ? 'animate-spin' : ''}`} />
|
|
||||||
</button>
|
|
||||||
<a
|
|
||||||
href={app.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="p-2 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
|
||||||
aria-label={`Open ${app.name}`}
|
|
||||||
title="Open app"
|
|
||||||
>
|
|
||||||
<ExternalLink className="w-4 h-4" />
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
onClick={() => setSelectedApp(app)}
|
|
||||||
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
|
||||||
aria-label={`Details for ${app.name}`}
|
|
||||||
title="View details"
|
|
||||||
>
|
|
||||||
<MoreHorizontal className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => deleteApp(app.id)}
|
|
||||||
className="p-2 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
|
||||||
aria-label={`Delete ${app.name}`}
|
|
||||||
title="Delete monitor"
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
{/* Last Checked */}
|
<p className="text-3xl font-bold">{stats.online}/{stats.total}</p>
|
||||||
<div className="mt-3 flex items-center gap-4 text-xs text-gray-400">
|
<p className="text-sm text-slate-400">Services Online</p>
|
||||||
<span>Last checked: {latest ? new Date(latest.timestamp).toLocaleTimeString() : 'Never'}</span>
|
|
||||||
{latest?.responseTime && (
|
|
||||||
<span>Response: {latest.responseTime}ms</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</CardContent>
|
||||||
})}
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-slate-900/50 border-slate-800 border-l-4 border-l-blue-500">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="p-3 rounded-xl bg-blue-500/10">
|
||||||
|
<TrendingUp className="w-6 h-6 text-blue-500" />
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-3xl font-bold">{stats.avgUptime}%</p>
|
||||||
|
<p className="text-sm text-slate-400">Avg Uptime</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Progress value={stats.avgUptime} className="h-2 mt-4" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-slate-900/50 border-slate-800 border-l-4 border-l-amber-500">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="p-3 rounded-xl bg-amber-500/10">
|
||||||
|
<AlertCircle className="w-6 h-6 text-amber-500" />
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-3xl font-bold">{status.filter(s => s.status === "down").length}</p>
|
||||||
|
<p className="text-sm text-slate-400">Incidents</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{apps.length === 0 && (
|
{/* Monitors */}
|
||||||
<div className="px-6 py-12 text-center">
|
<Card className="bg-slate-900/50 border-slate-800 overflow-hidden">
|
||||||
<div className="w-16 h-16 bg-gray-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
<CardHeader>
|
||||||
<Activity className="w-8 h-8 text-gray-400" />
|
<CardTitle className="flex items-center gap-2">
|
||||||
</div>
|
<Globe className="w-5 h-5 text-slate-400" />
|
||||||
<h3 className="text-lg font-medium text-gray-900 mb-2">No monitors yet</h3>
|
Monitored Services
|
||||||
<p className="text-gray-500 mb-4">Add your first service to start monitoring</p>
|
</CardTitle>
|
||||||
<button
|
<CardDescription>{stats.total} active monitors</CardDescription>
|
||||||
onClick={() => setShowAddApp(true)}
|
</CardHeader>
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-blue-600 bg-blue-50 rounded-lg hover:bg-blue-100 transition-colors"
|
|
||||||
>
|
<CardContent className="p-0">
|
||||||
<Plus className="w-4 h-4" />
|
{apps.length === 0 ? (
|
||||||
Add Monitor
|
<div className="px-6 py-16 text-center">
|
||||||
</button>
|
<Activity className="w-16 h-16 text-slate-600 mx-auto mb-4" />
|
||||||
</div>
|
<h3 className="text-lg font-semibold mb-2">No monitors yet</h3>
|
||||||
)}
|
<Button onClick={() => setShowAddApp(true)}>
|
||||||
</div>
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
Add Your First Monitor
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 p-6">
|
||||||
|
<AnimatePresence>
|
||||||
|
{apps.map((app) => {
|
||||||
|
const { isUp, uptime, latest } = getAppStatus(app.id);
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={app.id}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
whileHover={{ y: -4 }}
|
||||||
|
>
|
||||||
|
<Card className={`bg-slate-900 border-slate-800 border-l-4 ${isUp ? 'border-l-emerald-500' : 'border-l-red-500'}`}>
|
||||||
|
<CardContent className="p-5">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`w-10 h-10 rounded-xl flex items-center justify-center ${isUp ? 'bg-emerald-500/10' : 'bg-red-500/10'}`}>
|
||||||
|
<Server className={`w-5 h-5 ${isUp ? 'text-emerald-500' : 'text-red-500'}`} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-lg">{app.name}</h3>
|
||||||
|
<a href={app.url} target="_blank" rel="noopener noreferrer" className="text-sm text-slate-400 hover:text-emerald-400">
|
||||||
|
{app.url}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline" className={isUp ? "border-emerald-500/30 bg-emerald-500/10 text-emerald-400" : "border-red-500/30 bg-red-500/10 text-red-400"}>
|
||||||
|
<span className={`w-2 h-2 rounded-full mr-1.5 ${isUp ? 'bg-emerald-500' : 'bg-red-500'}`} />
|
||||||
|
{isUp ? "Operational" : "Down"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-bold">{uptime}%</p>
|
||||||
|
<p className="text-xs text-slate-400">Uptime</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-xs text-slate-400">Last check</p>
|
||||||
|
<p className="text-sm">{latest ? new Date(latest.timestamp).toLocaleTimeString() : 'Never'}</p>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="icon" onClick={() => deleteApp(app.id)} className="text-red-400">
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Add App Modal */}
|
{/* Add Monitor Dialog */}
|
||||||
{showAddApp && (
|
<Dialog open={showAddApp} onOpenChange={setShowAddApp}>
|
||||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
<DialogContent className="bg-slate-900 border-slate-800 text-white">
|
||||||
<div className="bg-white rounded-2xl p-6 w-full max-w-md shadow-2xl">
|
<DialogHeader>
|
||||||
<h2 className="text-xl font-bold text-gray-900 mb-6">Add New Monitor</h2>
|
<DialogTitle>Add New Monitor</DialogTitle>
|
||||||
<form onSubmit={addApp} className="space-y-5">
|
<DialogDescription>Monitor a new service</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<form onSubmit={addApp} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label>Name</Label>
|
||||||
|
<Input value={newApp.name} onChange={(e) => setNewApp({ ...newApp, name: e.target.value })} className="bg-slate-950 border-slate-800" required />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="app-name" className="block text-sm font-medium text-gray-700 mb-2">
|
<Label>URL</Label>
|
||||||
Monitor Name <span className="text-red-500">*</span>
|
<Input type="url" value={newApp.url} onChange={(e) => setNewApp({ ...newApp, url: e.target.value })} className="bg-slate-950 border-slate-800" required />
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="app-name"
|
|
||||||
type="text"
|
|
||||||
value={newApp.name}
|
|
||||||
onChange={(e) => setNewApp({ ...newApp, name: e.target.value })}
|
|
||||||
className="w-full border border-gray-300 rounded-lg px-4 py-2.5 text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 transition-all"
|
|
||||||
placeholder="My Application"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="app-desc" className="block text-sm font-medium text-gray-700 mb-2">
|
<Label>Port</Label>
|
||||||
Description
|
<Input type="number" value={newApp.port} onChange={(e) => setNewApp({ ...newApp, port: parseInt(e.target.value) })} className="bg-slate-950 border-slate-800" required />
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="app-desc"
|
|
||||||
type="text"
|
|
||||||
value={newApp.description}
|
|
||||||
onChange={(e) => setNewApp({ ...newApp, description: e.target.value })}
|
|
||||||
className="w-full border border-gray-300 rounded-lg px-4 py-2.5 text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 transition-all"
|
|
||||||
placeholder="Brief description..."
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
</div>
|
||||||
<div>
|
<div className="flex gap-3">
|
||||||
<label htmlFor="app-url" className="block text-sm font-medium text-gray-700 mb-2">
|
<Button type="button" variant="outline" onClick={() => setShowAddApp(false)} className="flex-1">Cancel</Button>
|
||||||
URL <span className="text-red-500">*</span>
|
<Button type="submit" className="flex-1 bg-emerald-600 hover:bg-emerald-500">Add Monitor</Button>
|
||||||
</label>
|
</div>
|
||||||
<input
|
</form>
|
||||||
id="app-url"
|
</DialogContent>
|
||||||
type="url"
|
</Dialog>
|
||||||
value={newApp.url}
|
|
||||||
onChange={(e) => setNewApp({ ...newApp, url: e.target.value })}
|
|
||||||
className="w-full border border-gray-300 rounded-lg px-4 py-2.5 text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 transition-all"
|
|
||||||
placeholder="http://localhost:3000"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="app-port" className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Port <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="app-port"
|
|
||||||
type="number"
|
|
||||||
value={newApp.port}
|
|
||||||
onChange={(e) => setNewApp({ ...newApp, port: parseInt(e.target.value) })}
|
|
||||||
className="w-full border border-gray-300 rounded-lg px-4 py-2.5 text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 transition-all"
|
|
||||||
required
|
|
||||||
min="1"
|
|
||||||
max="65535"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-3 pt-4">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowAddApp(false)}
|
|
||||||
className="flex-1 px-4 py-2.5 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="flex-1 px-4 py-2.5 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors shadow-sm"
|
|
||||||
>
|
|
||||||
Add Monitor
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import type { Config } from "tailwindcss";
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
export default {
|
const config: Config = {
|
||||||
|
darkMode: ["class"],
|
||||||
content: [
|
content: [
|
||||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
@ -8,11 +9,94 @@ export default {
|
|||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Plus Jakarta Sans', 'system-ui', 'sans-serif'],
|
||||||
|
},
|
||||||
colors: {
|
colors: {
|
||||||
background: "var(--background)",
|
background: "hsl(var(--background))",
|
||||||
foreground: "var(--foreground)",
|
foreground: "hsl(var(--foreground))",
|
||||||
|
card: {
|
||||||
|
DEFAULT: "hsl(var(--card))",
|
||||||
|
foreground: "hsl(var(--card-foreground))",
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: "hsl(var(--popover))",
|
||||||
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
|
},
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "hsl(var(--primary))",
|
||||||
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted))",
|
||||||
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "hsl(var(--accent))",
|
||||||
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
|
},
|
||||||
|
border: "hsl(var(--border))",
|
||||||
|
input: "hsl(var(--input))",
|
||||||
|
ring: "hsl(var(--ring))",
|
||||||
|
chart: {
|
||||||
|
"1": "hsl(var(--chart-1))",
|
||||||
|
"2": "hsl(var(--chart-2))",
|
||||||
|
"3": "hsl(var(--chart-3))",
|
||||||
|
"4": "hsl(var(--chart-4))",
|
||||||
|
"5": "hsl(var(--chart-5))",
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
up: "#22C55E",
|
||||||
|
down: "#EF4444",
|
||||||
|
warning: "#F59E0B",
|
||||||
|
idle: "#6B7280",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
"accordion-down": {
|
||||||
|
from: { height: "0" },
|
||||||
|
to: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
},
|
||||||
|
"accordion-up": {
|
||||||
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
to: { height: "0" },
|
||||||
|
},
|
||||||
|
"pulse-glow": {
|
||||||
|
"0%, 100%": { opacity: "1", boxShadow: "0 0 20px rgba(34, 197, 94, 0.5)" },
|
||||||
|
"50%": { opacity: "0.8", boxShadow: "0 0 10px rgba(34, 197, 94, 0.3)" },
|
||||||
|
},
|
||||||
|
"slide-up": {
|
||||||
|
"0%": { transform: "translateY(10px)", opacity: "0" },
|
||||||
|
"100%": { transform: "translateY(0)", opacity: "1" },
|
||||||
|
},
|
||||||
|
"fade-in": {
|
||||||
|
"0%": { opacity: "0" },
|
||||||
|
"100%": { opacity: "1" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
|
"pulse-glow": "pulse-glow 2s ease-in-out infinite",
|
||||||
|
"slide-up": "slide-up 0.3s ease-out",
|
||||||
|
"fade-in": "fade-in 0.2s ease-out",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [require("tailwindcss-animate")],
|
||||||
} satisfies Config;
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user