Add frontend real-time monitoring, snapshots, and configuration management

Co-authored-by: Ospab <189454929+Ospab@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-10-12 07:41:36 +00:00
parent d743cb2df0
commit b51071fad2
4 changed files with 967 additions and 15 deletions

View File

@@ -12,7 +12,9 @@
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-icons": "^5.5.0",
"react-qr-code": "^2.0.18"
"react-qr-code": "^2.0.18",
"recharts": "^2.15.0",
"socket.io-client": "^4.8.1"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
@@ -279,6 +281,15 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
@@ -1393,6 +1404,12 @@
"win32"
]
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"license": "MIT"
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1438,6 +1455,69 @@
"@babel/types": "^7.28.2"
}
},
"node_modules/@types/d3-array": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
"license": "MIT"
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
"license": "MIT"
},
"node_modules/@types/d3-ease": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
"license": "MIT"
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"license": "MIT",
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-path": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
"license": "MIT"
},
"node_modules/@types/d3-scale": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
"license": "MIT",
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-shape": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
"license": "MIT",
"dependencies": {
"@types/d3-path": "*"
}
},
"node_modules/@types/d3-time": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
"license": "MIT"
},
"node_modules/@types/d3-timer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -2120,6 +2200,15 @@
"node": ">= 6"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -2218,9 +2307,129 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true,
"license": "MIT"
},
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"license": "ISC",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-format": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-path": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"license": "ISC",
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-shape": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
"license": "ISC",
"dependencies": {
"d3-path": "^3.1.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"license": "ISC",
"dependencies": {
"d3-array": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time-format": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"license": "ISC",
"dependencies": {
"d3-time": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -2239,6 +2448,12 @@
}
}
},
"node_modules/decimal.js-light": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
"license": "MIT"
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -2269,6 +2484,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -2304,6 +2529,45 @@
"dev": true,
"license": "MIT"
},
"node_modules/engine.io-client": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1",
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-client/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -2592,6 +2856,12 @@
"node": ">=0.10.0"
}
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"license": "MIT"
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -2599,6 +2869,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-equals": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.3.2.tgz",
"integrity": "sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@@ -3033,6 +3312,15 @@
"node": ">=0.8.19"
}
},
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -3264,6 +3552,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -3374,7 +3668,6 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/mz": {
@@ -3941,6 +4234,37 @@
"react-dom": ">=18"
}
},
"node_modules/react-smooth": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
"integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
"license": "MIT",
"dependencies": {
"fast-equals": "^5.0.1",
"prop-types": "^15.8.1",
"react-transition-group": "^4.4.5"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"license": "BSD-3-Clause",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -3964,6 +4288,44 @@
"node": ">=8.10.0"
}
},
"node_modules/recharts": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.0.tgz",
"integrity": "sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==",
"license": "MIT",
"dependencies": {
"clsx": "^2.0.0",
"eventemitter3": "^4.0.1",
"lodash": "^4.17.21",
"react-is": "^18.3.1",
"react-smooth": "^4.0.0",
"recharts-scale": "^0.4.4",
"tiny-invariant": "^1.3.1",
"victory-vendor": "^36.6.8"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/recharts-scale": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
"integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
"license": "MIT",
"dependencies": {
"decimal.js-light": "^2.4.1"
}
},
"node_modules/recharts/node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@@ -4130,6 +4492,68 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/socket.io-client": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-client/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -4367,6 +4791,12 @@
"node": ">=0.8"
}
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -4547,6 +4977,28 @@
"dev": true,
"license": "MIT"
},
"node_modules/victory-vendor": {
"version": "36.9.2",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
"integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
"license": "MIT AND ISC",
"dependencies": {
"@types/d3-array": "^3.0.3",
"@types/d3-ease": "^3.0.0",
"@types/d3-interpolate": "^3.0.1",
"@types/d3-scale": "^4.0.2",
"@types/d3-shape": "^3.1.0",
"@types/d3-time": "^3.0.0",
"@types/d3-timer": "^3.0.0",
"d3-array": "^3.1.6",
"d3-ease": "^3.0.1",
"d3-interpolate": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-shape": "^3.1.0",
"d3-time": "^3.0.0",
"d3-timer": "^3.0.1"
}
},
"node_modules/vite": {
"version": "7.1.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz",
@@ -4774,6 +5226,35 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

View File

@@ -14,7 +14,9 @@
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-icons": "^5.5.0",
"react-qr-code": "^2.0.18"
"react-qr-code": "^2.0.18",
"recharts": "^2.15.0",
"socket.io-client": "^4.8.1"
},
"devDependencies": {
"@eslint/js": "^9.33.0",

View File

@@ -0,0 +1,76 @@
import { useEffect, useState } from 'react';
import { io, Socket } from 'socket.io-client';
const SOCKET_URL = 'http://localhost:5000';
export function useSocket() {
const [socket, setSocket] = useState<Socket | null>(null);
const [connected, setConnected] = useState(false);
useEffect(() => {
const socketInstance = io(SOCKET_URL, {
transports: ['websocket', 'polling'],
reconnection: true,
reconnectionDelay: 1000,
reconnectionAttempts: 5
});
socketInstance.on('connect', () => {
console.log('WebSocket connected');
setConnected(true);
});
socketInstance.on('disconnect', () => {
console.log('WebSocket disconnected');
setConnected(false);
});
socketInstance.on('connect_error', (error) => {
console.error('WebSocket connection error:', error);
});
setSocket(socketInstance);
return () => {
socketInstance.close();
};
}, []);
return { socket, connected };
}
export function useServerStats(serverId: number | null) {
const { socket, connected } = useSocket();
const [stats, setStats] = useState<any>(null);
const [alerts, setAlerts] = useState<any[]>([]);
useEffect(() => {
if (!socket || !connected || !serverId) return;
// Подписываемся на обновления сервера
socket.emit('subscribe-server', serverId);
// Обработчик обновлений статистики
socket.on('server-stats', (data: any) => {
if (data.serverId === serverId) {
setStats(data.stats);
}
});
// Обработчик алертов
socket.on('server-alerts', (data: any) => {
if (data.serverId === serverId) {
setAlerts(data.alerts);
}
});
// Отписываемся при размонтировании
return () => {
socket.emit('unsubscribe-server', serverId);
socket.off('server-stats');
socket.off('server-alerts');
};
}, [socket, connected, serverId]);
return { stats, alerts, connected };
}

View File

@@ -1,5 +1,9 @@
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import axios, { AxiosError } from 'axios';
import { useServerStats } from '../../hooks/useSocket';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
// Встроенная секция консоли
function ConsoleSection({ serverId }: { serverId: number }) {
@@ -47,8 +51,264 @@ function ConsoleSection({ serverId }: { serverId: number }) {
</div>
);
}
import { useParams } from 'react-router-dom';
import axios, { AxiosError } from 'axios';
// Модальное окно для изменения конфигурации
function ResizeModal({ serverId, onClose, onSuccess }: { serverId: number; onClose: () => void; onSuccess: () => void }) {
const [cores, setCores] = useState('');
const [memory, setMemory] = useState('');
const [disk, setDisk] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const handleResize = async () => {
setLoading(true);
setError('');
try {
const token = localStorage.getItem('access_token');
const headers = token ? { Authorization: `Bearer ${token}` } : {};
const data: any = {};
if (cores) data.cores = Number(cores);
if (memory) data.memory = Number(memory);
if (disk) data.disk = Number(disk);
const res = await axios.put(`http://localhost:5000/api/server/${serverId}/resize`, data, { headers });
if (res.data?.status === 'success') {
onSuccess();
onClose();
} else {
setError('Ошибка изменения конфигурации');
}
} catch (err) {
setError('Ошибка изменения конфигурации');
console.error(err);
} finally {
setLoading(false);
}
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" onClick={onClose}>
<div className="bg-white rounded-3xl shadow-xl p-8 max-w-md w-full" onClick={(e) => e.stopPropagation()}>
<h2 className="text-2xl font-bold mb-6">Изменить конфигурацию</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-semibold mb-2">Количество ядер CPU</label>
<input
type="number"
value={cores}
onChange={(e) => setCores(e.target.value)}
className="w-full px-4 py-2 border rounded-lg"
placeholder="Оставьте пустым, чтобы не менять"
min="1"
/>
</div>
<div>
<label className="block text-sm font-semibold mb-2">RAM (МБ)</label>
<input
type="number"
value={memory}
onChange={(e) => setMemory(e.target.value)}
className="w-full px-4 py-2 border rounded-lg"
placeholder="Например: 2048"
min="512"
/>
</div>
<div>
<label className="block text-sm font-semibold mb-2">Диск (ГБ)</label>
<input
type="number"
value={disk}
onChange={(e) => setDisk(e.target.value)}
className="w-full px-4 py-2 border rounded-lg"
placeholder="Например: 40"
min="10"
/>
</div>
{error && <div className="text-red-500">{error}</div>}
<div className="flex gap-4">
<button
onClick={handleResize}
disabled={loading}
className="flex-1 bg-ospab-primary text-white px-6 py-3 rounded-full font-bold disabled:opacity-50"
>
{loading ? 'Изменение...' : 'Применить'}
</button>
<button
onClick={onClose}
className="flex-1 bg-gray-300 text-gray-700 px-6 py-3 rounded-full font-bold"
>
Отмена
</button>
</div>
</div>
</div>
</div>
);
}
// Компонент для управления снэпшотами
function SnapshotsSection({ serverId }: { serverId: number }) {
const [snapshots, setSnapshots] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [snapName, setSnapName] = useState('');
const [snapDesc, setSnapDesc] = useState('');
const [error, setError] = useState('');
useEffect(() => {
loadSnapshots();
}, [serverId]);
const loadSnapshots = async () => {
setLoading(true);
try {
const token = localStorage.getItem('access_token');
const headers = token ? { Authorization: `Bearer ${token}` } : {};
const res = await axios.get(`http://localhost:5000/api/server/${serverId}/snapshots`, { headers });
if (res.data?.status === 'success') {
setSnapshots(res.data.data || []);
}
} catch (err) {
console.error('Error loading snapshots:', err);
} finally {
setLoading(false);
}
};
const handleCreateSnapshot = async () => {
if (!snapName) {
setError('Введите имя снэпшота');
return;
}
setLoading(true);
setError('');
try {
const token = localStorage.getItem('access_token');
const headers = token ? { Authorization: `Bearer ${token}` } : {};
const res = await axios.post(
`http://localhost:5000/api/server/${serverId}/snapshots`,
{ snapname: snapName, description: snapDesc },
{ headers }
);
if (res.data?.status === 'success') {
setSnapName('');
setSnapDesc('');
loadSnapshots();
} else {
setError('Ошибка создания снэпшота');
}
} catch (err) {
setError('Ошибка создания снэпшота');
console.error(err);
} finally {
setLoading(false);
}
};
const handleRollback = async (snapname: string) => {
if (!confirm(`Восстановить из снэпшота ${snapname}? Текущее состояние будет потеряно.`)) return;
setLoading(true);
try {
const token = localStorage.getItem('access_token');
const headers = token ? { Authorization: `Bearer ${token}` } : {};
await axios.post(
`http://localhost:5000/api/server/${serverId}/snapshots/rollback`,
{ snapname },
{ headers }
);
alert('Снэпшот восстановлен');
} catch (err) {
alert('Ошибка восстановления снэпшота');
console.error(err);
} finally {
setLoading(false);
}
};
const handleDelete = async (snapname: string) => {
if (!confirm(`Удалить снэпшот ${snapname}?`)) return;
setLoading(true);
try {
const token = localStorage.getItem('access_token');
const headers = token ? { Authorization: `Bearer ${token}` } : {};
await axios.delete(
`http://localhost:5000/api/server/${serverId}/snapshots`,
{ data: { snapname }, headers }
);
loadSnapshots();
} catch (err) {
alert('Ошибка удаления снэпшота');
console.error(err);
} finally {
setLoading(false);
}
};
return (
<div className="bg-gray-100 rounded-xl p-6">
<h3 className="text-xl font-bold mb-4">Управление снэпшотами</h3>
<div className="bg-white rounded-lg p-4 mb-4">
<h4 className="font-semibold mb-3">Создать новый снэпшот</h4>
<div className="space-y-3">
<input
type="text"
value={snapName}
onChange={(e) => setSnapName(e.target.value)}
placeholder="Имя снэпшота (например: backup-2024)"
className="w-full px-4 py-2 border rounded-lg"
/>
<input
type="text"
value={snapDesc}
onChange={(e) => setSnapDesc(e.target.value)}
placeholder="Описание (опционально)"
className="w-full px-4 py-2 border rounded-lg"
/>
{error && <div className="text-red-500 text-sm">{error}</div>}
<button
onClick={handleCreateSnapshot}
disabled={loading}
className="bg-ospab-primary text-white px-6 py-2 rounded-full font-bold disabled:opacity-50"
>
{loading ? 'Создание...' : 'Создать снэпшот'}
</button>
</div>
</div>
<div className="bg-white rounded-lg p-4">
<h4 className="font-semibold mb-3">Существующие снэпшоты</h4>
{snapshots.length === 0 ? (
<p className="text-gray-500">Снэпшотов пока нет</p>
) : (
<div className="space-y-2">
{snapshots.map((snap) => (
<div key={snap.name} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div>
<div className="font-semibold">{snap.name}</div>
<div className="text-sm text-gray-600">{snap.description || 'Без описания'}</div>
</div>
<div className="flex gap-2">
<button
onClick={() => handleRollback(snap.name)}
className="bg-blue-500 text-white px-4 py-1 rounded-full text-sm font-semibold hover:bg-blue-600"
>
Восстановить
</button>
<button
onClick={() => handleDelete(snap.name)}
className="bg-red-500 text-white px-4 py-1 rounded-full text-sm font-semibold hover:bg-red-600"
>
Удалить
</button>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}
interface Server {
id: number;
@@ -73,6 +333,8 @@ const TABS = [
{ key: 'console', label: 'Консоль' },
{ key: 'stats', label: 'Статистика' },
{ key: 'manage', label: 'Управление' },
{ key: 'snapshots', label: 'Снэпшоты' },
{ key: 'resize', label: 'Конфигурация' },
{ key: 'security', label: 'Безопасность' },
];
@@ -86,6 +348,10 @@ const ServerPanel: React.FC = () => {
const [newRoot, setNewRoot] = useState<string | null>(null);
const [showRoot, setShowRoot] = useState(false);
const [stats, setStats] = useState<ServerStats | null>(null);
const [showResizeModal, setShowResizeModal] = useState(false);
// Real-time WebSocket stats
const { stats: realtimeStats, alerts, connected } = useServerStats(server?.id || null);
useEffect(() => {
const fetchServer = async () => {
@@ -210,16 +476,105 @@ const ServerPanel: React.FC = () => {
)}
{activeTab === 'stats' && (
<div className="bg-gray-100 rounded-xl p-6">
<div className="mb-2 font-bold">Графики нагрузки</div>
<div className="flex gap-6">
<div className="w-1/2 h-32 bg-white rounded-lg shadow-inner flex flex-col items-center justify-center">
<div className="font-bold text-gray-700">CPU</div>
<div className="text-2xl text-ospab-primary">{stats?.data?.cpu ? (stats.data.cpu * 100).toFixed(1) : '—'}%</div>
<div className="space-y-6">
{/* WebSocket connection status */}
<div className="flex items-center gap-2 mb-4">
<div className={`w-3 h-3 rounded-full ${connected ? 'bg-green-500' : 'bg-red-500'}`}></div>
<span className="text-sm text-gray-600">
{connected ? 'Подключено к live-мониторингу' : 'Нет подключения к мониторингу'}
</span>
</div>
{/* Alerts */}
{alerts.length > 0 && (
<div className="bg-yellow-50 border border-yellow-200 rounded-xl p-4">
<h3 className="font-bold text-yellow-800 mb-2"> Предупреждения</h3>
<div className="space-y-1">
{alerts.map((alert, idx) => (
<div key={idx} className="text-yellow-700 text-sm">
{alert.message}
</div>
))}
</div>
</div>
<div className="w-1/2 h-32 bg-white rounded-lg shadow-inner flex flex-col items-center justify-center">
<div className="font-bold text-gray-700">RAM</div>
<div className="text-2xl text-ospab-primary">{stats?.data?.memory?.usage ? stats.data.memory.usage.toFixed(1) : '—'}%</div>
)}
{/* Real-time stats cards */}
<div className="grid grid-cols-3 gap-4">
<div className="bg-white rounded-xl p-6 shadow-sm">
<div className="font-bold text-gray-700 mb-2">CPU</div>
<div className="text-3xl text-ospab-primary font-bold">
{realtimeStats?.data?.cpu ? (realtimeStats.data.cpu * 100).toFixed(1) : stats?.data?.cpu ? (stats.data.cpu * 100).toFixed(1) : '—'}%
</div>
</div>
<div className="bg-white rounded-xl p-6 shadow-sm">
<div className="font-bold text-gray-700 mb-2">RAM</div>
<div className="text-3xl text-ospab-primary font-bold">
{realtimeStats?.data?.memory?.usage?.toFixed(1) || stats?.data?.memory?.usage?.toFixed(1) || '—'}%
</div>
</div>
<div className="bg-white rounded-xl p-6 shadow-sm">
<div className="font-bold text-gray-700 mb-2">Disk</div>
<div className="text-3xl text-ospab-primary font-bold">
{realtimeStats?.data?.disk?.usage?.toFixed(1) || '—'}%
</div>
</div>
</div>
{/* Charts */}
{realtimeStats?.data?.rrdData && realtimeStats.data.rrdData.length > 0 && (
<div className="bg-white rounded-xl p-6 shadow-sm">
<h3 className="font-bold text-gray-800 mb-4">История использования (последний час)</h3>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={realtimeStats.data.rrdData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="time" hide />
<YAxis />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="cpu" stroke="#8b5cf6" name="CPU %" />
<Line type="monotone" dataKey="mem" stroke="#3b82f6" name="Memory (bytes)" />
</LineChart>
</ResponsiveContainer>
</div>
)}
{/* Detailed stats */}
<div className="bg-gray-100 rounded-xl p-6">
<div className="mb-2 font-bold">Детальная статистика</div>
<div className="grid grid-cols-2 gap-4">
<div className="bg-white rounded-lg p-4">
<div className="text-sm text-gray-600">Memory Used</div>
<div className="text-lg font-semibold">
{realtimeStats?.data?.memory?.used
? `${(realtimeStats.data.memory.used / (1024 * 1024 * 1024)).toFixed(2)} GB`
: '—'}
</div>
</div>
<div className="bg-white rounded-lg p-4">
<div className="text-sm text-gray-600">Memory Max</div>
<div className="text-lg font-semibold">
{realtimeStats?.data?.memory?.max
? `${(realtimeStats.data.memory.max / (1024 * 1024 * 1024)).toFixed(2)} GB`
: '—'}
</div>
</div>
<div className="bg-white rounded-lg p-4">
<div className="text-sm text-gray-600">Network In</div>
<div className="text-lg font-semibold">
{realtimeStats?.data?.network?.in
? `${(realtimeStats.data.network.in / (1024 * 1024)).toFixed(2)} MB`
: '—'}
</div>
</div>
<div className="bg-white rounded-lg p-4">
<div className="text-sm text-gray-600">Network Out</div>
<div className="text-lg font-semibold">
{realtimeStats?.data?.network?.out
? `${(realtimeStats.data.network.out / (1024 * 1024)).toFixed(2)} MB`
: '—'}
</div>
</div>
</div>
</div>
</div>
@@ -233,6 +588,26 @@ const ServerPanel: React.FC = () => {
</div>
)}
{activeTab === 'snapshots' && (
<SnapshotsSection serverId={server.id} />
)}
{activeTab === 'resize' && (
<div className="bg-gray-100 rounded-xl p-6">
<h3 className="text-xl font-bold mb-4">Изменение конфигурации сервера</h3>
<p className="text-gray-600 mb-4">
Вы можете увеличить или уменьшить ресурсы вашего сервера (CPU, RAM, диск).
Изменения вступят в силу после перезапуска сервера.
</p>
<button
onClick={() => setShowResizeModal(true)}
className="bg-ospab-primary text-white px-6 py-3 rounded-full font-bold hover:bg-opacity-90"
>
Изменить конфигурацию
</button>
</div>
)}
{activeTab === 'security' && (
<div className="space-y-4">
<button className="bg-ospab-primary text-white px-6 py-3 rounded-full font-bold" onClick={handleGenerateRoot}>Сгенерировать новый root-пароль</button>
@@ -246,6 +621,24 @@ const ServerPanel: React.FC = () => {
</div>
)}
</div>
{/* Resize Modal */}
{showResizeModal && (
<ResizeModal
serverId={server.id}
onClose={() => setShowResizeModal(false)}
onSuccess={() => {
// Reload server data after resize
const fetchServer = async () => {
const token = localStorage.getItem('access_token');
const headers = token ? { Authorization: `Bearer ${token}` } : {};
const res = await axios.get(`http://localhost:5000/api/server/${id}`, { headers });
setServer(res.data);
};
fetchServer();
}}
/>
)}
</div>
);
};