From facae93e4b934f25374403bcdf5e21e6621c9509 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Thu, 12 Jun 2025 08:04:30 +0000 Subject: [PATCH] feat: Implement dees-statsgrid in DCRouter UI for enhanced stats visualization - Added new readme.statsgrid.md outlining the implementation plan for dees-statsgrid component. - Replaced custom stats cards in ops-view-overview.ts and ops-view-network.ts with dees-statsgrid for better visualization. - Introduced consistent color scheme for success, warning, error, and info states. - Enhanced interactive features including click actions, context menus, and real-time updates. - Developed ops-view-emails.ts for email management with features like composing, searching, and viewing emails. - Integrated mock data generation for emails and network requests to facilitate testing. - Added responsive design elements and improved UI consistency across components. --- package.json | 4 +- pnpm-lock.yaml | 180 ++++--- readme.statsgrid.md | 46 ++ ts_web/elements/index.ts | 3 +- ts_web/elements/ops-dashboard.ts | 11 +- ts_web/elements/ops-view-emails.ts | 697 +++++++++++++++++++++++++++ ts_web/elements/ops-view-network.ts | 520 ++++++++++++++++++++ ts_web/elements/ops-view-overview.ts | 279 +++++++---- ts_web/elements/ops-view-stats.ts | 302 ------------ 9 files changed, 1543 insertions(+), 499 deletions(-) create mode 100644 readme.statsgrid.md create mode 100644 ts_web/elements/ops-view-emails.ts create mode 100644 ts_web/elements/ops-view-network.ts delete mode 100644 ts_web/elements/ops-view-stats.ts diff --git a/package.json b/package.json index 5af68c5..dc3af64 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@git.zone/tsrun": "^1.3.3", "@git.zone/tstest": "^2.3.1", "@git.zone/tswatch": "^2.0.1", - "@types/node": "^22.15.30", + "@types/node": "^24.0.0", "node-forge": "^1.3.1" }, "dependencies": { @@ -29,7 +29,7 @@ "@api.global/typedserver": "^3.0.74", "@api.global/typedsocket": "^3.0.0", "@apiclient.xyz/cloudflare": "^6.4.1", - "@design.estate/dees-catalog": "^1.8.0", + "@design.estate/dees-catalog": "^1.8.1", "@design.estate/dees-element": "^2.0.42", "@push.rocks/projectinfo": "^5.0.1", "@push.rocks/qenv": "^6.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 44322de..f0b21c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: specifier: ^6.4.1 version: 6.4.1 '@design.estate/dees-catalog': - specifier: ^1.8.0 - version: 1.8.0 + specifier: ^1.8.1 + version: 1.8.1 '@design.estate/dees-element': specifier: ^2.0.42 version: 2.0.42 @@ -130,8 +130,8 @@ importers: specifier: ^2.0.1 version: 2.1.0 '@types/node': - specifier: ^22.15.30 - version: 22.15.30 + specifier: ^24.0.0 + version: 24.0.0 node-forge: specifier: ^1.3.1 version: 1.3.1 @@ -344,8 +344,8 @@ packages: '@dabh/diagnostics@2.0.3': resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} - '@design.estate/dees-catalog@1.8.0': - resolution: {integrity: sha512-8xUkp+fPjXR9/GM5fvc+eHogpirkcmW4Ao8vNLgIomejoLRamtIbICJQaVmpra20XJqPkOcnvHwUHyhLckwYJA==} + '@design.estate/dees-catalog@1.8.1': + resolution: {integrity: sha512-luYrGMK5APzw7GgXSe4UpFE5oC3vwzKTTB0etH3o6Au1nxvllhDcWr6S5UkB+WL9lroslBcK8Icyhiq8kIxEHg==} '@design.estate/dees-comms@1.0.27': resolution: {integrity: sha512-GvzTUwkV442LD60T08iqSoqvhA02Mou5lFvvqBPc4yBUiU7cZISqBx+76xvMgMIEI9Dx9JfTl4/2nW8MoVAanw==} @@ -737,68 +737,68 @@ packages: '@mongodb-js/saslprep@1.2.2': resolution: {integrity: sha512-EB0O3SCSNRUFk66iRCpI+cXzIjdswfCs7F6nOC3RAGJ7xr5YhaicvsRwJ9eyzYvYRlCSDUO/c7g4yNulxKC1WA==} - '@napi-rs/canvas-android-arm64@0.1.70': - resolution: {integrity: sha512-I/YOuQ0wbkVYxVaYtCgN42WKTYxNqFA0gTcTrHIGG1jfpDSyZWII/uHcjOo4nzd19io6Y4+/BqP8E5hJgf9OmQ==} + '@napi-rs/canvas-android-arm64@0.1.71': + resolution: {integrity: sha512-cxi3VCotIOS9kNFQI7dcysbVJi106pxryVY1Hi85pX+ZeqahRyeqc/NsLaZ998Ae99+F3HI5X/39G1Y/Byrf0A==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - '@napi-rs/canvas-darwin-arm64@0.1.70': - resolution: {integrity: sha512-4pPGyXetHIHkw2TOJHujt3mkCP8LdDu8+CT15ld9Id39c752RcI0amDHSuMLMQfAjvusA9B5kKxazwjMGjEJpQ==} + '@napi-rs/canvas-darwin-arm64@0.1.71': + resolution: {integrity: sha512-7Y4D/6vIuMLYsVNtRM/w2j0+fB1GyqeOxc7I0BTx8eLP1S6BZE2Rj6zJfdG+zmLEOW0IlHa+VQq1q2MUAjW84w==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@napi-rs/canvas-darwin-x64@0.1.70': - resolution: {integrity: sha512-+2N6Os9LbkmDMHL+raknrUcLQhsXzc5CSXRbXws9C3pv/mjHRVszQ9dhFUUe9FjfPhCJznO6USVdwOtu7pOrzQ==} + '@napi-rs/canvas-darwin-x64@0.1.71': + resolution: {integrity: sha512-Z0IUqxclrYdfVt/SK9nKCzUHTOXKTWiygtO71YCzs0OtxKdNI7GJRJdYG48wXZEDQ/pqTF4F7Ifgtidfc2tYpg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@napi-rs/canvas-linux-arm-gnueabihf@0.1.70': - resolution: {integrity: sha512-QjscX9OaKq/990sVhSMj581xuqLgiaPVMjjYvWaCmAJRkNQ004QfoSMEm3FoTqM4DRoquP8jvuEXScVJsc1rqQ==} + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.71': + resolution: {integrity: sha512-KlpqqCASak5ruY+UIolJgmhMZ9Pa2o1QyaNu648L8sz4WNBbNa+aOT60XCLCL1VIKLv11B3MlNgiOHoYNmDhXQ==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@napi-rs/canvas-linux-arm64-gnu@0.1.70': - resolution: {integrity: sha512-LNakMOwwqwiHIwMpnMAbFRczQMQ7TkkMyATqFCOtUJNlE6LPP/QiUj/mlFrNbUn/hctqShJ60gWEb52ZTALbVw==} + '@napi-rs/canvas-linux-arm64-gnu@0.1.71': + resolution: {integrity: sha512-bdGZCGu8YQNAiu3nkIVVUp6nIn6fPd36IuZsLXTG027E52KyIuZ3obCxehSwjDIUNkFWvmff5D6JYfWwAoioEw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@napi-rs/canvas-linux-arm64-musl@0.1.70': - resolution: {integrity: sha512-wBTOllEYNfJCHOdZj9v8gLzZ4oY3oyPX8MSRvaxPm/s7RfEXxCyZ8OhJ5xAyicsDdbE5YBZqdmaaeP5+xKxvtg==} + '@napi-rs/canvas-linux-arm64-musl@0.1.71': + resolution: {integrity: sha512-1R5sMWe9ur8uM+hAeylBwG0b6UHDR+iWQNgzXmF9vbBYRooQvmDWqpcgytKLJAC0vnWhIkKwqd7yExn7cwczmg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@napi-rs/canvas-linux-riscv64-gnu@0.1.70': - resolution: {integrity: sha512-GVUUPC8TuuFqHip0rxHkUqArQnlzmlXmTEBuXAWdgCv85zTCFH8nOHk/YCF5yo0Z2eOm8nOi90aWs0leJ4OE5Q==} + '@napi-rs/canvas-linux-riscv64-gnu@0.1.71': + resolution: {integrity: sha512-xjjKsipueuG+LdKIk6/uAlqdo+rzGcmNpTZPXdakIT1sHX4NNSnQTzjRaj9Gh96Czjd9G89UWR0KIlE7fwOgFA==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] - '@napi-rs/canvas-linux-x64-gnu@0.1.70': - resolution: {integrity: sha512-/kvUa2lZRwGNyfznSn5t1ShWJnr/m5acSlhTV3eXECafObjl0VBuA1HJw0QrilLpb4Fe0VLywkpD1NsMoVDROQ==} + '@napi-rs/canvas-linux-x64-gnu@0.1.71': + resolution: {integrity: sha512-3s6YpklXDB4OeeULG1XTRyKrKAOo7c3HHEqM9A6N4STSjMaJtzmpp7tB/JTvAFeOeFte6gWN8IwC+7AjGJ6MpQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@napi-rs/canvas-linux-x64-musl@0.1.70': - resolution: {integrity: sha512-aqlv8MLpycoMKRmds7JWCfVwNf1fiZxaU7JwJs9/ExjTD8lX2KjsO7CTeAj5Cl4aEuzxUWbJPUUE2Qu9cZ1vfg==} + '@napi-rs/canvas-linux-x64-musl@0.1.71': + resolution: {integrity: sha512-5v9aCLzCXw7u10ray5juQMdl7TykZSn1X5AIGYwBvTAcKSgrqaR9QkRxp1Lqk3njQmFekOW1SFN9bZ/i/6y6kA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@napi-rs/canvas-win32-x64-msvc@0.1.70': - resolution: {integrity: sha512-Q9QU3WIpwBTVHk4cPfBjGHGU4U0llQYRXgJtFtYqqGNEOKVN4OT6PQ+ve63xwIPODMpZ0HHyj/KLGc9CWc3EtQ==} + '@napi-rs/canvas-win32-x64-msvc@0.1.71': + resolution: {integrity: sha512-oJughk6xjsRIr0Rd9EqjmZmhIMkvcPuXgr3MNn2QexTqn+YFOizrwHS5ha0BDfFl7TEGRvwaDUXBQtu8JKXb8A==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@napi-rs/canvas@0.1.70': - resolution: {integrity: sha512-nD6NGa4JbNYSZYsTnLGrqe9Kn/lCkA4ybXt8sx5ojDqZjr2i0TWAHxx/vhgfjX+i3hCdKWufxYwi7CfXqtITSA==} + '@napi-rs/canvas@0.1.71': + resolution: {integrity: sha512-92ybDocKl6JM48ZpYbj+A7Qt45IaTABDk0y3sDecEQfgdhfNzJtEityqNHoCZ4Vty2dldPkJhxgvOnbrQMXTTA==} engines: {node: '>= 10'} '@oozcitak/dom@1.15.10': @@ -1661,8 +1661,8 @@ packages: '@types/node@18.19.111': resolution: {integrity: sha512-90sGdgA+QLJr1F9X79tQuEut0gEYIfkX9pydI4XGRgvFo9g2JWswefI+WUSUHPYVBHYSEfTEqBxA5hQvAZB3Mw==} - '@types/node@22.15.30': - resolution: {integrity: sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==} + '@types/node@24.0.0': + resolution: {integrity: sha512-yZQa2zm87aRVcqDyH5+4Hv9KYgSdgwX1rFnGvpbzMaC7YAljmhBET93TPiTd3ObwTL+gSpIzPKg5BqVxdCvxKg==} '@types/pidusage@2.0.5': resolution: {integrity: sha512-MIiyZI4/MK9UGUXWt0jJcCZhVw7YdhBuTOuqP/BjuLDLZ2PmmViMIQgZiWxtaMicQfAz/kMrZ5T7PKxFSkTeUA==} @@ -2963,8 +2963,8 @@ packages: leac@0.6.0: resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} - lenis@1.3.3: - resolution: {integrity: sha512-DOopj/UKHS54E9l2g4BOpDUvsyvkd1zkv+ECtHxQ9Fto8ozzKSz7MccqT+KOyG0ABA/OHXZ7l9INx0peUoQ8rQ==} + lenis@1.3.4: + resolution: {integrity: sha512-WIGk8wiV2ABm/T7M+NC+tAV8fjzNJD1J4z11aZ3mTtx7WAZX/4QdCNhBO0g/TqXISA+/3hTbzrPC4FW1nhoNMQ==} peerDependencies: '@nuxt/kit': '>=3.0.0' react: '>=17.0.0' @@ -4194,8 +4194,8 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.8.0: + resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} undici@7.10.0: resolution: {integrity: sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==} @@ -5075,7 +5075,7 @@ snapshots: enabled: 2.0.0 kuler: 2.0.0 - '@design.estate/dees-catalog@1.8.0': + '@design.estate/dees-catalog@1.8.1': dependencies: '@design.estate/dees-domtools': 2.3.2 '@design.estate/dees-element': 2.0.42 @@ -5127,7 +5127,7 @@ snapshots: '@push.rocks/webrequest': 3.0.37 '@push.rocks/websetup': 3.0.19 '@push.rocks/webstore': 2.0.20 - lenis: 1.3.3 + lenis: 1.3.4 lit: 3.3.0 sweet-scroll: 4.0.0 transitivePeerDependencies: @@ -5487,48 +5487,48 @@ snapshots: dependencies: sparse-bitfield: 3.0.3 - '@napi-rs/canvas-android-arm64@0.1.70': + '@napi-rs/canvas-android-arm64@0.1.71': optional: true - '@napi-rs/canvas-darwin-arm64@0.1.70': + '@napi-rs/canvas-darwin-arm64@0.1.71': optional: true - '@napi-rs/canvas-darwin-x64@0.1.70': + '@napi-rs/canvas-darwin-x64@0.1.71': optional: true - '@napi-rs/canvas-linux-arm-gnueabihf@0.1.70': + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.71': optional: true - '@napi-rs/canvas-linux-arm64-gnu@0.1.70': + '@napi-rs/canvas-linux-arm64-gnu@0.1.71': optional: true - '@napi-rs/canvas-linux-arm64-musl@0.1.70': + '@napi-rs/canvas-linux-arm64-musl@0.1.71': optional: true - '@napi-rs/canvas-linux-riscv64-gnu@0.1.70': + '@napi-rs/canvas-linux-riscv64-gnu@0.1.71': optional: true - '@napi-rs/canvas-linux-x64-gnu@0.1.70': + '@napi-rs/canvas-linux-x64-gnu@0.1.71': optional: true - '@napi-rs/canvas-linux-x64-musl@0.1.70': + '@napi-rs/canvas-linux-x64-musl@0.1.71': optional: true - '@napi-rs/canvas-win32-x64-msvc@0.1.70': + '@napi-rs/canvas-win32-x64-msvc@0.1.71': optional: true - '@napi-rs/canvas@0.1.70': + '@napi-rs/canvas@0.1.71': optionalDependencies: - '@napi-rs/canvas-android-arm64': 0.1.70 - '@napi-rs/canvas-darwin-arm64': 0.1.70 - '@napi-rs/canvas-darwin-x64': 0.1.70 - '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.70 - '@napi-rs/canvas-linux-arm64-gnu': 0.1.70 - '@napi-rs/canvas-linux-arm64-musl': 0.1.70 - '@napi-rs/canvas-linux-riscv64-gnu': 0.1.70 - '@napi-rs/canvas-linux-x64-gnu': 0.1.70 - '@napi-rs/canvas-linux-x64-musl': 0.1.70 - '@napi-rs/canvas-win32-x64-msvc': 0.1.70 + '@napi-rs/canvas-android-arm64': 0.1.71 + '@napi-rs/canvas-darwin-arm64': 0.1.71 + '@napi-rs/canvas-darwin-x64': 0.1.71 + '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.71 + '@napi-rs/canvas-linux-arm64-gnu': 0.1.71 + '@napi-rs/canvas-linux-arm64-musl': 0.1.71 + '@napi-rs/canvas-linux-riscv64-gnu': 0.1.71 + '@napi-rs/canvas-linux-x64-gnu': 0.1.71 + '@napi-rs/canvas-linux-x64-musl': 0.1.71 + '@napi-rs/canvas-win32-x64-msvc': 0.1.71 optional: true '@oozcitak/dom@1.15.10': @@ -5784,7 +5784,6 @@ snapshots: - '@mongodb-js/zstd' - '@nuxt/kit' - aws-crt - - bufferutil - encoding - gcp-metadata - kerberos @@ -5793,7 +5792,6 @@ snapshots: - snappy - socks - supports-color - - utf-8-validate - vue '@push.rocks/smartarchive@3.0.8': @@ -7046,27 +7044,27 @@ snapshots: '@types/bn.js@5.2.0': dependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.0 '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.38 - '@types/node': 22.15.30 + '@types/node': 24.0.0 '@types/buffer-json@2.0.3': {} '@types/clean-css@4.2.11': dependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.0 source-map: 0.6.1 '@types/connect@3.4.38': dependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.0 '@types/cors@2.8.18': dependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.0 '@types/debug@4.1.12': dependencies: @@ -7078,7 +7076,7 @@ snapshots: '@types/dns-packet@5.6.5': dependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.0 '@types/elliptic@6.4.18': dependencies: @@ -7086,7 +7084,7 @@ snapshots: '@types/express-serve-static-core@5.0.6': dependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.0 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -7103,30 +7101,30 @@ snapshots: '@types/from2@2.3.5': dependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.0 '@types/fs-extra@11.0.4': dependencies: '@types/jsonfile': 6.1.4 - '@types/node': 22.15.30 + '@types/node': 24.0.0 '@types/fs-extra@9.0.13': dependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.0 '@types/glob@7.2.0': dependencies: '@types/minimatch': 5.1.2 - '@types/node': 22.15.30 + '@types/node': 24.0.0 '@types/glob@8.1.0': dependencies: '@types/minimatch': 5.1.2 - '@types/node': 22.15.30 + '@types/node': 24.0.0 '@types/gunzip-maybe@1.4.2': dependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.0 '@types/hast@3.0.4': dependencies: @@ -7148,16 +7146,16 @@ snapshots: '@types/jsonfile@6.1.4': dependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.0 '@types/jsonwebtoken@9.0.9': dependencies: '@types/ms': 2.1.0 - '@types/node': 22.15.30 + '@types/node': 24.0.0 '@types/mailparser@3.4.6': dependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.0 iconv-lite: 0.6.3 '@types/mdast@4.0.4': @@ -7176,20 +7174,20 @@ snapshots: '@types/node-fetch@2.6.12': dependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.0 form-data: 4.0.2 '@types/node-forge@1.3.11': dependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.0 '@types/node@18.19.111': dependencies: undici-types: 5.26.5 - '@types/node@22.15.30': + '@types/node@24.0.0': dependencies: - undici-types: 6.21.0 + undici-types: 7.8.0 '@types/pidusage@2.0.5': {} @@ -7205,30 +7203,30 @@ snapshots: '@types/s3rver@3.7.4': dependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.0 '@types/semver@7.7.0': {} '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 - '@types/node': 22.15.30 + '@types/node': 24.0.0 '@types/serve-static@1.15.7': dependencies: '@types/http-errors': 2.0.4 - '@types/node': 22.15.30 + '@types/node': 24.0.0 '@types/send': 0.17.4 '@types/symbol-tree@3.2.5': {} '@types/tar-stream@2.2.3': dependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.0 '@types/through2@2.0.41': dependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.0 '@types/triple-beam@1.3.5': {} @@ -7252,18 +7250,18 @@ snapshots: '@types/whatwg-url@8.2.2': dependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.0 '@types/webidl-conversions': 7.0.3 '@types/which@3.0.4': {} '@types/ws@8.18.1': dependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.0 '@types/yauzl@2.10.3': dependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.0 optional: true '@ungap/structured-clone@1.3.0': {} @@ -7835,7 +7833,7 @@ snapshots: engine.io@6.6.4: dependencies: '@types/cors': 2.8.18 - '@types/node': 22.15.30 + '@types/node': 24.0.0 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.7.2 @@ -8663,7 +8661,7 @@ snapshots: leac@0.6.0: {} - lenis@1.3.3: {} + lenis@1.3.4: {} libbase64@1.3.0: {} @@ -9473,7 +9471,7 @@ snapshots: pdfjs-dist@4.10.38: optionalDependencies: - '@napi-rs/canvas': 0.1.70 + '@napi-rs/canvas': 0.1.71 peberminta@0.9.0: {} @@ -10168,7 +10166,7 @@ snapshots: undici-types@5.26.5: {} - undici-types@6.21.0: {} + undici-types@7.8.0: {} undici@7.10.0: {} diff --git a/readme.statsgrid.md b/readme.statsgrid.md new file mode 100644 index 0000000..896beb6 --- /dev/null +++ b/readme.statsgrid.md @@ -0,0 +1,46 @@ +# Plan: Implement dees-statsgrid in DCRouter UI + +Command to reread CLAUDE.md: `Read /home/centraluser/eu.central.ingress-2/CLAUDE.md` + +## Overview +Replace the current stats cards with the new dees-statsgrid component from @design.estate/dees-catalog for better visualization and consistency. + +## Implementation Plan + +### 1. Update Overview View (`ops-view-overview.ts`) +- Replace the custom stats cards with dees-statsgrid +- Use appropriate tile types for different metrics: + - `gauge` for CPU and Memory usage + - `number` for Active Connections, Total Requests, etc. + - `trend` for time-series data like requests over time + +### 2. Update Network View (`ops-view-network.ts`) +- Replace the current stats cards section with dees-statsgrid +- Configure tiles for: + - Active Connections (number) + - Requests/sec (number with trend) + - Throughput In/Out (number with units) + - Protocol distribution (percentage) + +### 3. Create Consistent Color Scheme +- Success/Normal: #22c55e (green) +- Warning: #f59e0b (amber) +- Error/Critical: #ef4444 (red) +- Info: #3b82f6 (blue) + +### 4. Add Interactive Features +- Click actions to show detailed views +- Context menu for refresh, export, etc. +- Real-time updates from metrics data + +### 5. Integration Points +- Connect to existing appstate for data +- Use MetricsManager data for real values +- Update on the 1-second refresh interval + +## Benefits +- Consistent UI component usage +- Better visual hierarchy +- Built-in responsive design +- More visualization options (gauges, trends) +- Reduced custom CSS maintenance \ No newline at end of file diff --git a/ts_web/elements/index.ts b/ts_web/elements/index.ts index 7abf48c..46e90c8 100644 --- a/ts_web/elements/index.ts +++ b/ts_web/elements/index.ts @@ -1,6 +1,7 @@ export * from './ops-dashboard.js'; export * from './ops-view-overview.js'; -export * from './ops-view-stats.js'; +export * from './ops-view-network.js'; +export * from './ops-view-emails.js'; export * from './ops-view-logs.js'; export * from './ops-view-config.js'; export * from './ops-view-security.js'; diff --git a/ts_web/elements/ops-dashboard.ts b/ts_web/elements/ops-dashboard.ts index d4ee0d1..cbdfd69 100644 --- a/ts_web/elements/ops-dashboard.ts +++ b/ts_web/elements/ops-dashboard.ts @@ -13,7 +13,8 @@ import { // Import view components import { OpsViewOverview } from './ops-view-overview.js'; -import { OpsViewStats } from './ops-view-stats.js'; +import { OpsViewNetwork } from './ops-view-network.js'; +import { OpsViewEmails } from './ops-view-emails.js'; import { OpsViewLogs } from './ops-view-logs.js'; import { OpsViewConfig } from './ops-view-config.js'; import { OpsViewSecurity } from './ops-view-security.js'; @@ -84,8 +85,12 @@ export class OpsDashboard extends DeesElement { element: OpsViewOverview, }, { - name: 'Statistics', - element: OpsViewStats, + name: 'Network', + element: OpsViewNetwork, + }, + { + name: 'Emails', + element: OpsViewEmails, }, { name: 'Logs', diff --git a/ts_web/elements/ops-view-emails.ts b/ts_web/elements/ops-view-emails.ts new file mode 100644 index 0000000..b9dfd19 --- /dev/null +++ b/ts_web/elements/ops-view-emails.ts @@ -0,0 +1,697 @@ +import { DeesElement, property, html, customElement, type TemplateResult, css, state, cssManager } from '@design.estate/dees-element'; +import * as appstate from '../appstate.js'; + +declare global { + interface HTMLElementTagNameMap { + 'ops-view-emails': OpsViewEmails; + } +} + +interface IEmail { + id: string; + from: string; + to: string[]; + cc?: string[]; + bcc?: string[]; + subject: string; + body: string; + html?: string; + attachments?: Array<{ + filename: string; + size: number; + contentType: string; + }>; + date: number; + read: boolean; + folder: 'inbox' | 'sent' | 'draft' | 'trash'; + flags?: string[]; + messageId?: string; + inReplyTo?: string; +} + +@customElement('ops-view-emails') +export class OpsViewEmails extends DeesElement { + @state() + private selectedFolder: 'inbox' | 'sent' | 'draft' | 'trash' = 'inbox'; + + @state() + private emails: IEmail[] = []; + + @state() + private selectedEmail: IEmail | null = null; + + @state() + private showCompose = false; + + @state() + private isLoading = false; + + @state() + private searchTerm = ''; + + constructor() { + super(); + this.loadEmails(); + } + + public static styles = [ + cssManager.defaultStyles, + css` + :host { + display: block; + height: 100%; + padding: 24px; + } + + .emailContainer { + display: grid; + grid-template-columns: 250px 1fr; + gap: 24px; + height: calc(100vh - 200px); + } + + .sidebar { + background: white; + border: 1px solid #e9ecef; + border-radius: 8px; + padding: 16px; + display: flex; + flex-direction: column; + gap: 16px; + } + + .folderList { + display: flex; + flex-direction: column; + gap: 8px; + } + + .folderItem { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s; + position: relative; + } + + .folderItem:hover { + background: #f5f5f5; + } + + .folderItem.selected { + background: #e3f2fd; + color: #1976d2; + } + + .folderIcon { + font-size: 18px; + } + + .folderLabel { + flex: 1; + font-weight: 500; + } + + .folderCount { + background: #e0e0e0; + color: #666; + padding: 2px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 600; + } + + .folderItem.selected .folderCount { + background: #1976d2; + color: white; + } + + .mainContent { + background: white; + border: 1px solid #e9ecef; + border-radius: 8px; + overflow: hidden; + display: flex; + flex-direction: column; + } + + .emailToolbar { + display: flex; + align-items: center; + gap: 16px; + padding: 16px; + border-bottom: 1px solid #e9ecef; + background: #fafafa; + } + + .searchBox { + flex: 1; + max-width: 400px; + } + + .emailList { + flex: 1; + overflow-y: auto; + } + + .emailPreview { + background: white; + border: 1px solid #e9ecef; + border-radius: 8px; + overflow: hidden; + } + + .emailHeader { + padding: 20px; + border-bottom: 1px solid #e9ecef; + background: #fafafa; + } + + .emailSubject { + font-size: 20px; + font-weight: 600; + margin-bottom: 12px; + } + + .emailMeta { + display: flex; + gap: 16px; + font-size: 14px; + color: #666; + } + + .emailBody { + padding: 20px; + max-height: 500px; + overflow-y: auto; + } + + .emailActions { + display: flex; + gap: 8px; + padding: 16px; + border-top: 1px solid #e9ecef; + background: #fafafa; + } + + .composeModal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + } + + .composeContent { + background: white; + border-radius: 8px; + width: 90%; + max-width: 800px; + max-height: 90vh; + overflow: hidden; + display: flex; + flex-direction: column; + } + + .composeHeader { + padding: 20px; + border-bottom: 1px solid #e9ecef; + display: flex; + align-items: center; + justify-content: space-between; + } + + .composeTitle { + font-size: 20px; + font-weight: 600; + } + + .composeForm { + flex: 1; + overflow-y: auto; + padding: 20px; + } + + .composeActions { + padding: 20px; + border-top: 1px solid #e9ecef; + display: flex; + gap: 12px; + justify-content: flex-end; + } + + .emptyState { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + color: #999; + } + + .emptyIcon { + font-size: 64px; + margin-bottom: 16px; + opacity: 0.3; + } + + .emptyText { + font-size: 18px; + } + `, + ]; + + public render() { + return html` + Emails + +
+ + + + +
+ +
+ this.searchTerm = (e.target as any).value} + > + + + + this.refreshEmails()}> + ${this.isLoading ? html`` : html``} + + + this.markAllAsRead()}> + + Mark all read + +
+ + + ${this.selectedEmail ? this.renderEmailPreview() : this.renderEmailList()} +
+
+ + + ${this.showCompose ? this.renderComposeModal() : ''} + `; + } + + private renderFolderItem(folder: string, icon: string, label: string, count: number) { + return html` +
this.selectFolder(folder as any)} + > + + ${label} + ${count > 0 ? html`${count}` : ''} +
+ `; + } + + private renderEmailList() { + const filteredEmails = this.getFilteredEmails(); + + if (filteredEmails.length === 0) { + return html` +
+ +
No emails in ${this.selectedFolder}
+
+ `; + } + + return html` +
+ ({ + 'Status': html``, + From: email.from, + Subject: html`${email.subject}`, + Date: this.formatDate(email.date), + 'Attach': html` + ${email.attachments?.length ? html`` : ''} + `, + })} + .dataActions=${[ + { + name: 'Read', + iconName: 'eye', + type: ['doubleClick', 'inRow'], + actionFunc: async (actionData) => { + this.selectedEmail = actionData.item; + if (!actionData.item.read) { + this.markAsRead(actionData.item.id); + } + } + }, + { + name: 'Reply', + iconName: 'reply', + type: ['contextmenu'], + actionFunc: async (actionData) => { + this.replyToEmail(actionData.item); + } + }, + { + name: 'Forward', + iconName: 'share', + type: ['contextmenu'], + actionFunc: async (actionData) => { + this.forwardEmail(actionData.item); + } + }, + { + name: 'Delete', + iconName: 'trash', + type: ['contextmenu'], + actionFunc: async (actionData) => { + this.deleteEmail(actionData.item.id); + } + } + ]} + .selectionMode=${'single'} + > +
+ `; + } + + private renderEmailPreview() { + if (!this.selectedEmail) return ''; + + return html` +
+
+
${this.selectedEmail.subject}
+
+ From: ${this.selectedEmail.from} + To: ${this.selectedEmail.to.join(', ')} + Date: ${this.formatDate(this.selectedEmail.date)} +
+
+ +
+ ${this.selectedEmail.html ? + html`
` : + html`
${this.selectedEmail.body}
` + } +
+ +
+ this.selectedEmail = null}> + + Back + + this.replyToEmail(this.selectedEmail!)}> + + Reply + + this.replyAllToEmail(this.selectedEmail!)}> + + Reply All + + this.forwardEmail(this.selectedEmail!)}> + + Forward + + this.deleteEmail(this.selectedEmail!.id)} type="danger"> + + Delete + +
+
+ `; + } + + private renderComposeModal() { + return html` +
{ + if (e.target === e.currentTarget) this.showCompose = false; + }}> +
+
+
New Email
+ this.showCompose = false} type="ghost"> + + +
+ +
+ this.sendEmail(e.detail)}> + + + + + + + + + + + + +
+ this.showCompose = false} type="secondary"> + Cancel + + + + Send Email + +
+
+
+
+
+ `; + } + + private getFilteredEmails(): IEmail[] { + let emails = this.emails.filter(e => e.folder === this.selectedFolder); + + if (this.searchTerm) { + const search = this.searchTerm.toLowerCase(); + emails = emails.filter(e => + e.subject.toLowerCase().includes(search) || + e.from.toLowerCase().includes(search) || + e.body.toLowerCase().includes(search) + ); + } + + return emails.sort((a, b) => b.date - a.date); + } + + private getEmailCount(folder: string): number { + return this.emails.filter(e => e.folder === folder && !e.read).length; + } + + private selectFolder(folder: 'inbox' | 'sent' | 'draft' | 'trash') { + this.selectedFolder = folder; + this.selectedEmail = null; + } + + private formatDate(timestamp: number): string { + const date = new Date(timestamp); + const now = new Date(); + const diff = now.getTime() - date.getTime(); + const hours = diff / (1000 * 60 * 60); + + if (hours < 24) { + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + } else if (hours < 168) { // 7 days + return date.toLocaleDateString([], { weekday: 'short' }); + } else { + return date.toLocaleDateString([], { month: 'short', day: 'numeric' }); + } + } + + private async loadEmails() { + // TODO: Load real emails from server + // For now, generate mock data + this.generateMockEmails(); + } + + private async refreshEmails() { + this.isLoading = true; + await this.loadEmails(); + this.isLoading = false; + } + + private async sendEmail(formData: any) { + + try { + // TODO: Implement actual email sending + console.log('Sending email:', formData); + + // Add to sent folder + const newEmail: IEmail = { + id: `email-${Date.now()}`, + from: 'me@dcrouter.local', + to: formData.to, + cc: formData.cc || [], + bcc: formData.bcc || [], + subject: formData.subject, + body: formData.body, + date: Date.now(), + read: true, + folder: 'sent', + }; + + this.emails = [...this.emails, newEmail]; + this.showCompose = false; + + // TODO: Implement toast notification when DeesToast.show is available + console.log('Email sent successfully'); + } catch (error: any) { + // TODO: Implement toast notification when DeesToast.show is available + console.error('Failed to send email', error); + } + } + + private async markAsRead(emailId: string) { + const email = this.emails.find(e => e.id === emailId); + if (email) { + email.read = true; + this.emails = [...this.emails]; + } + } + + private async markAllAsRead() { + this.emails = this.emails.map(e => + e.folder === this.selectedFolder ? { ...e, read: true } : e + ); + } + + private async deleteEmail(emailId: string) { + const email = this.emails.find(e => e.id === emailId); + if (email) { + if (email.folder === 'trash') { + // Permanently delete + this.emails = this.emails.filter(e => e.id !== emailId); + } else { + // Move to trash + email.folder = 'trash'; + this.emails = [...this.emails]; + } + + if (this.selectedEmail?.id === emailId) { + this.selectedEmail = null; + } + } + } + + private async replyToEmail(email: IEmail) { + // TODO: Open compose with reply context + this.showCompose = true; + } + + private async replyAllToEmail(email: IEmail) { + // TODO: Open compose with reply all context + this.showCompose = true; + } + + private async forwardEmail(email: IEmail) { + // TODO: Open compose with forward context + this.showCompose = true; + } + + private generateMockEmails() { + const subjects = [ + 'Server Alert: High CPU Usage', + 'Daily Report - Network Activity', + 'Security Update Required', + 'New User Registration', + 'Backup Completed Successfully', + 'DNS Query Spike Detected', + 'SSL Certificate Renewal Notice', + 'Monthly Usage Summary', + ]; + + const senders = [ + 'monitoring@dcrouter.local', + 'alerts@system.local', + 'admin@company.com', + 'noreply@service.com', + 'support@vendor.com', + ]; + + const bodies = [ + 'This is an automated alert regarding your server status.', + 'Please review the attached report for detailed information.', + 'Action required: Update your security settings.', + 'Your daily summary is ready for review.', + 'All systems are operating normally.', + ]; + + this.emails = Array.from({ length: 50 }, (_, i) => ({ + id: `email-${i}`, + from: senders[Math.floor(Math.random() * senders.length)], + to: ['admin@dcrouter.local'], + subject: subjects[Math.floor(Math.random() * subjects.length)], + body: bodies[Math.floor(Math.random() * bodies.length)], + date: Date.now() - (i * 3600000), // 1 hour apart + read: Math.random() > 0.3, + folder: i < 40 ? 'inbox' : i < 45 ? 'sent' : 'trash', + attachments: Math.random() > 0.8 ? [{ + filename: 'report.pdf', + size: 1024 * 1024, + contentType: 'application/pdf', + }] : undefined, + })); + } +} \ No newline at end of file diff --git a/ts_web/elements/ops-view-network.ts b/ts_web/elements/ops-view-network.ts new file mode 100644 index 0000000..627fa6b --- /dev/null +++ b/ts_web/elements/ops-view-network.ts @@ -0,0 +1,520 @@ +import { DeesElement, property, html, customElement, type TemplateResult, css, state, cssManager } from '@design.estate/dees-element'; +import * as appstate from '../appstate.js'; +import { viewHostCss } from './shared/css.js'; +import { type IStatsTile } from '@design.estate/dees-catalog'; + +declare global { + interface HTMLElementTagNameMap { + 'ops-view-network': OpsViewNetwork; + } +} + +interface INetworkRequest { + id: string; + timestamp: number; + method: string; + url: string; + hostname: string; + port: number; + protocol: 'http' | 'https' | 'tcp' | 'udp'; + statusCode?: number; + duration: number; + bytesIn: number; + bytesOut: number; + remoteIp: string; + route?: string; +} + +@customElement('ops-view-network') +export class OpsViewNetwork extends DeesElement { + @state() + private statsState = appstate.statsStatePart.getState(); + + @state() + private selectedTimeRange: '1m' | '5m' | '15m' | '1h' | '24h' = '5m'; + + @state() + private selectedProtocol: 'all' | 'http' | 'https' | 'smtp' | 'dns' = 'all'; + + @state() + private networkRequests: INetworkRequest[] = []; + + @state() + private trafficData: Array<{ x: number; y: number }> = []; + + @state() + private isLoading = false; + + constructor() { + super(); + this.subscribeToStateParts(); + this.generateMockData(); // TODO: Replace with real data from metrics + } + + private subscribeToStateParts() { + appstate.statsStatePart.state.subscribe((state) => { + this.statsState = state; + this.updateNetworkData(); + }); + } + + public static styles = [ + cssManager.defaultStyles, + viewHostCss, + css` + :host { + display: block; + padding: 24px; + } + + .networkContainer { + display: flex; + flex-direction: column; + gap: 24px; + } + + .controlBar { + background: white; + border: 1px solid #e9ecef; + border-radius: 8px; + padding: 16px; + display: flex; + gap: 16px; + align-items: center; + flex-wrap: wrap; + } + + .controlGroup { + display: flex; + gap: 8px; + align-items: center; + } + + .controlLabel { + font-size: 14px; + color: #666; + margin-right: 8px; + } + + dees-statsgrid { + margin-bottom: 24px; + } + + .chartSection { + background: white; + border: 1px solid #e9ecef; + border-radius: 8px; + padding: 24px; + } + + .tableSection { + background: white; + border: 1px solid #e9ecef; + border-radius: 8px; + overflow: hidden; + } + + .protocolBadge { + display: inline-flex; + align-items: center; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; + } + + .protocolBadge.http { + background: #e3f2fd; + color: #1976d2; + } + + .protocolBadge.https { + background: #e8f5e9; + color: #388e3c; + } + + .protocolBadge.tcp { + background: #fff3e0; + color: #f57c00; + } + + .protocolBadge.smtp { + background: #f3e5f5; + color: #7b1fa2; + } + + .protocolBadge.dns { + background: #e0f2f1; + color: #00796b; + } + + .statusBadge { + display: inline-flex; + align-items: center; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; + } + + .statusBadge.success { + background: #e8f5e9; + color: #388e3c; + } + + .statusBadge.error { + background: #ffebee; + color: #d32f2f; + } + + .statusBadge.warning { + background: #fff3e0; + color: #f57c00; + } + `, + ]; + + public render() { + return html` + Network Activity + +
+ +
+
+ Time Range: + + ${(['1m', '5m', '15m', '1h', '24h'] as const).map(range => html` + this.selectedTimeRange = range} + .type=${this.selectedTimeRange === range ? 'highlighted' : 'normal'} + > + ${range} + + `)} + +
+ +
+ Protocol: + this.selectedProtocol = e.detail.key} + > +
+ +
+ this.refreshData()} + .disabled=${this.isLoading} + > + ${this.isLoading ? html`` : 'Refresh'} + +
+
+ + + ${this.renderNetworkStats()} + + +
+ +
+ + +
+ ({ + Time: new Date(req.timestamp).toLocaleTimeString(), + Protocol: html`${req.protocol.toUpperCase()}`, + Method: req.method, + 'Host:Port': `${req.hostname}:${req.port}`, + Path: this.truncateUrl(req.url), + Status: this.renderStatus(req.statusCode), + Duration: `${req.duration}ms`, + 'In/Out': `${this.formatBytes(req.bytesIn)} / ${this.formatBytes(req.bytesOut)}`, + 'Remote IP': req.remoteIp, + })} + .dataActions=${[ + { + name: 'View Details', + iconName: 'magnifyingGlass', + type: ['inRow', 'doubleClick', 'contextmenu'], + actionFunc: async (actionData) => { + await this.showRequestDetails(actionData.item); + } + } + ]} + heading1="Recent Network Activity" + heading2="Last ${this.selectedTimeRange} of network requests" + searchable + .pagination=${true} + .paginationSize=${50} + dataName="request" + > +
+
+ `; + } + + private async showRequestDetails(request: INetworkRequest) { + const { DeesModal } = await import('@design.estate/dees-catalog'); + + await DeesModal.createAndShow({ + heading: 'Request Details', + content: html` +
+ +
+ `, + menuOptions: [ + { + name: 'Copy Request ID', + iconName: 'copy', + action: async () => { + await navigator.clipboard.writeText(request.id); + // TODO: Implement toast notification when DeesToast.show is available + console.log('Request ID copied to clipboard'); + } + } + ] + }); + } + + private getFilteredRequests(): INetworkRequest[] { + if (this.selectedProtocol === 'all') { + return this.networkRequests; + } + + // Map protocol filter to actual protocol values + const protocolMap: Record = { + 'http': ['http'], + 'https': ['https'], + 'smtp': ['tcp'], // SMTP runs over TCP + 'dns': ['udp'], // DNS typically runs over UDP + }; + + const allowedProtocols = protocolMap[this.selectedProtocol] || [this.selectedProtocol]; + return this.networkRequests.filter(req => allowedProtocols.includes(req.protocol)); + } + + private renderStatus(statusCode?: number): TemplateResult { + if (!statusCode) { + return html`N/A`; + } + + const statusClass = statusCode >= 200 && statusCode < 300 ? 'success' : + statusCode >= 400 ? 'error' : 'warning'; + + return html`${statusCode}`; + } + + private truncateUrl(url: string, maxLength = 50): string { + if (url.length <= maxLength) return url; + return url.substring(0, maxLength - 3) + '...'; + } + + private getProtocolLabel(protocol: string): string { + const labels: Record = { + 'all': 'All Protocols', + 'http': 'HTTP', + 'https': 'HTTPS', + 'smtp': 'SMTP', + 'dns': 'DNS', + }; + return labels[protocol] || protocol.toUpperCase(); + } + + private formatNumber(num: number): string { + if (num >= 1000000) { + return (num / 1000000).toFixed(1) + 'M'; + } else if (num >= 1000) { + return (num / 1000).toFixed(1) + 'K'; + } + return num.toFixed(0); + } + + private formatBytes(bytes: number): string { + const units = ['B', 'KB', 'MB', 'GB']; + let size = bytes; + let unitIndex = 0; + + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex++; + } + + return `${size.toFixed(1)} ${units[unitIndex]}`; + } + + private calculateRequestsPerSecond(): number { + // TODO: Calculate from real data based on connection metrics + // For now, return a calculated value based on active connections + return Math.floor((this.statsState.serverStats?.activeConnections || 0) * 0.8); + } + + private calculateThroughput(): { in: number; out: number } { + // TODO: Calculate from real connection data + // For now, return estimated values + const activeConnections = this.statsState.serverStats?.activeConnections || 0; + return { + in: activeConnections * 1024 * 10, // 10KB per connection estimate + out: activeConnections * 1024 * 50, // 50KB per connection estimate + }; + } + + private renderNetworkStats(): TemplateResult { + const reqPerSec = this.calculateRequestsPerSecond(); + const throughput = this.calculateThroughput(); + const activeConnections = this.statsState.serverStats?.activeConnections || 0; + + // Generate trend data for requests per second + const trendData = Array.from({ length: 20 }, (_, i) => + Math.max(0, reqPerSec + (Math.random() - 0.5) * 10) + ); + + const tiles: IStatsTile[] = [ + { + id: 'connections', + title: 'Active Connections', + value: activeConnections, + type: 'number', + icon: 'plug', + color: activeConnections > 100 ? '#f59e0b' : '#22c55e', + description: `Total: ${this.statsState.serverStats?.totalConnections || 0}`, + actions: [ + { + name: 'View Details', + iconName: 'magnifyingGlass', + action: async () => { + // TODO: Show connection details + }, + }, + ], + }, + { + id: 'requests', + title: 'Requests/sec', + value: reqPerSec, + type: 'trend', + icon: 'chartLine', + color: '#3b82f6', + trendData: trendData, + description: `${this.formatNumber(reqPerSec)} req/s`, + }, + { + id: 'throughputIn', + title: 'Throughput In', + value: this.formatBytes(throughput.in), + unit: '/s', + type: 'number', + icon: 'download', + color: '#22c55e', + }, + { + id: 'throughputOut', + title: 'Throughput Out', + value: this.formatBytes(throughput.out), + unit: '/s', + type: 'number', + icon: 'upload', + color: '#8b5cf6', + }, + ]; + + return html` + { + // TODO: Export network data + // TODO: Implement toast notification when DeesToast.show is available + console.log('Export feature coming soon'); + }, + }, + ]} + > + `; + } + + private async refreshData() { + this.isLoading = true; + await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null); + await this.updateNetworkData(); + this.isLoading = false; + } + + private async updateNetworkData() { + // TODO: Fetch real network data from the server + // For now, using mock data + this.generateMockData(); + } + + private generateMockData() { + // Generate mock network requests + const now = Date.now(); + const protocols: Array<'http' | 'https' | 'tcp' | 'udp'> = ['http', 'https', 'tcp', 'udp']; + const methods = ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS']; + const hosts = ['api.example.com', 'app.local', 'mail.server.com', 'dns.resolver.net']; + + this.networkRequests = Array.from({ length: 100 }, (_, i) => ({ + id: `req-${i}`, + timestamp: now - (i * 5000), // 5 seconds apart + method: methods[Math.floor(Math.random() * methods.length)], + url: `/api/v1/resource/${Math.floor(Math.random() * 100)}`, + hostname: hosts[Math.floor(Math.random() * hosts.length)], + port: Math.random() > 0.5 ? 443 : 80, + protocol: protocols[Math.floor(Math.random() * protocols.length)], + statusCode: Math.random() > 0.8 ? 404 : 200, + duration: Math.floor(Math.random() * 500), + bytesIn: Math.floor(Math.random() * 10000), + bytesOut: Math.floor(Math.random() * 50000), + remoteIp: `192.168.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`, + route: 'main-route', + })); + + // Generate traffic data for chart + this.trafficData = Array.from({ length: 60 }, (_, i) => ({ + x: now - (i * 60000), // 1 minute intervals + y: Math.floor(Math.random() * 100) + 50, + })).reverse(); + } +} \ No newline at end of file diff --git a/ts_web/elements/ops-view-overview.ts b/ts_web/elements/ops-view-overview.ts index 1e9c38f..43cf18c 100644 --- a/ts_web/elements/ops-view-overview.ts +++ b/ts_web/elements/ops-view-overview.ts @@ -9,7 +9,9 @@ import { state, css, cssManager, + type TemplateResult, } from '@design.estate/dees-element'; +import { type IStatsTile } from '@design.estate/dees-catalog'; @customElement('ops-view-overview') export class OpsViewOverview extends DeesElement { @@ -38,39 +40,13 @@ export class OpsViewOverview extends DeesElement { cssManager.defaultStyles, shared.viewHostCss, css` - .statsGrid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - grid-gap: 16px; - margin-bottom: 40px; - } - - .statCard { - background: #f8f9fa; - border: 1px solid #e9ecef; - border-radius: 8px; - padding: 24px; - } - - .statCard h3 { - margin: 0 0 16px 0; - font-size: 18px; + h2 { + margin: 32px 0 16px 0; + font-size: 24px; font-weight: 600; color: #333; } - .statValue { - font-size: 32px; - font-weight: 700; - color: #2196F3; - margin-bottom: 8px; - } - - .statLabel { - font-size: 14px; - color: #666; - } - .chartGrid { display: grid; grid-template-columns: repeat(2, 1fr); @@ -92,6 +68,10 @@ export class OpsViewOverview extends DeesElement { color: #c00; margin: 16px 0; } + + dees-statsgrid { + margin-bottom: 32px; + } `, ]; @@ -109,79 +89,11 @@ export class OpsViewOverview extends DeesElement { Error loading statistics: ${this.statsState.error} ` : html` -
- ${this.statsState.serverStats ? html` -
-

Server Status

-
${this.statsState.serverStats.uptime ? 'Online' : 'Offline'}
-
Uptime: ${this.formatUptime(this.statsState.serverStats.uptime)}
-
- -
-

Connections

-
${this.statsState.serverStats.activeConnections}
-
Active connections
-
+ ${this.renderServerStats()} -
-

Memory Usage

-
${this.formatBytes(this.statsState.serverStats.memoryUsage.heapUsed)}
-
of ${this.formatBytes(this.statsState.serverStats.memoryUsage.heapTotal)}
-
+ ${this.renderEmailStats()} -
-

CPU Usage

-
${Math.round((this.statsState.serverStats.cpuUsage.user + this.statsState.serverStats.cpuUsage.system) / 2)}%
-
Average load
-
- ` : ''} -
- - ${this.statsState.emailStats ? html` -

Email Statistics

-
-
-

Emails Sent

-
${this.statsState.emailStats.sent}
-
Total sent
-
- -
-

Emails Received

-
${this.statsState.emailStats.received}
-
Total received
-
- -
-

Failed Deliveries

-
${this.statsState.emailStats.failed}
-
Delivery failures
-
- -
-

Queued

-
${this.statsState.emailStats.queued}
-
In queue
-
-
- ` : ''} - - ${this.statsState.dnsStats ? html` -

DNS Statistics

-
-
-

DNS Queries

-
${this.statsState.dnsStats.totalQueries}
-
Total queries handled
-
- -
-

Cache Hit Rate

-
${Math.round(this.statsState.dnsStats.cacheHitRate * 100)}%
-
Cache efficiency
-
-
- ` : ''} + ${this.renderDnsStats()}
@@ -222,4 +134,171 @@ export class OpsViewOverview extends DeesElement { return `${size.toFixed(1)} ${units[unitIndex]}`; } + + private renderServerStats(): TemplateResult { + if (!this.statsState.serverStats) return html``; + + const cpuUsage = Math.round((this.statsState.serverStats.cpuUsage.user + this.statsState.serverStats.cpuUsage.system) / 2); + const memoryUsage = Math.round((this.statsState.serverStats.memoryUsage.heapUsed / this.statsState.serverStats.memoryUsage.heapTotal) * 100); + + const tiles: IStatsTile[] = [ + { + id: 'status', + title: 'Server Status', + value: this.statsState.serverStats.uptime ? 'Online' : 'Offline', + type: 'text', + icon: 'server', + color: this.statsState.serverStats.uptime ? '#22c55e' : '#ef4444', + description: `Uptime: ${this.formatUptime(this.statsState.serverStats.uptime)}`, + }, + { + id: 'connections', + title: 'Active Connections', + value: this.statsState.serverStats.activeConnections, + type: 'number', + icon: 'networkWired', + color: '#3b82f6', + description: `Total: ${this.statsState.serverStats.totalConnections}`, + }, + { + id: 'cpu', + title: 'CPU Usage', + value: cpuUsage, + type: 'gauge', + icon: 'microchip', + gaugeOptions: { + min: 0, + max: 100, + thresholds: [ + { value: 0, color: '#22c55e' }, + { value: 60, color: '#f59e0b' }, + { value: 80, color: '#ef4444' }, + ], + }, + }, + { + id: 'memory', + title: 'Memory Usage', + value: memoryUsage, + type: 'percentage', + icon: 'memory', + color: memoryUsage > 80 ? '#ef4444' : memoryUsage > 60 ? '#f59e0b' : '#22c55e', + description: `${this.formatBytes(this.statsState.serverStats.memoryUsage.heapUsed)} / ${this.formatBytes(this.statsState.serverStats.memoryUsage.heapTotal)}`, + }, + ]; + + return html` + { + await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null); + }, + }, + ]} + > + `; + } + + private renderEmailStats(): TemplateResult { + if (!this.statsState.emailStats) return html``; + + const deliveryRate = this.statsState.emailStats.deliveryRate || 0; + const bounceRate = this.statsState.emailStats.bounceRate || 0; + + const tiles: IStatsTile[] = [ + { + id: 'sent', + title: 'Emails Sent', + value: this.statsState.emailStats.sent, + type: 'number', + icon: 'paperPlane', + color: '#22c55e', + description: `Delivery rate: ${(deliveryRate * 100).toFixed(1)}%`, + }, + { + id: 'received', + title: 'Emails Received', + value: this.statsState.emailStats.received, + type: 'number', + icon: 'envelope', + color: '#3b82f6', + }, + { + id: 'queued', + title: 'Queued', + value: this.statsState.emailStats.queued, + type: 'number', + icon: 'clock', + color: '#f59e0b', + description: 'Pending delivery', + }, + { + id: 'failed', + title: 'Failed', + value: this.statsState.emailStats.failed, + type: 'number', + icon: 'triangleExclamation', + color: '#ef4444', + description: `Bounce rate: ${(bounceRate * 100).toFixed(1)}%`, + }, + ]; + + return html` +

Email Statistics

+ + `; + } + + private renderDnsStats(): TemplateResult { + if (!this.statsState.dnsStats) return html``; + + const cacheHitRate = Math.round(this.statsState.dnsStats.cacheHitRate * 100); + + const tiles: IStatsTile[] = [ + { + id: 'queries', + title: 'DNS Queries', + value: this.statsState.dnsStats.totalQueries, + type: 'number', + icon: 'globe', + color: '#3b82f6', + description: 'Total queries handled', + }, + { + id: 'cacheRate', + title: 'Cache Hit Rate', + value: cacheHitRate, + type: 'percentage', + icon: 'database', + color: cacheHitRate > 80 ? '#22c55e' : cacheHitRate > 60 ? '#f59e0b' : '#ef4444', + description: `${this.statsState.dnsStats.cacheHits} hits / ${this.statsState.dnsStats.cacheMisses} misses`, + }, + { + id: 'domains', + title: 'Active Domains', + value: this.statsState.dnsStats.activeDomains, + type: 'number', + icon: 'sitemap', + color: '#8b5cf6', + }, + { + id: 'responseTime', + title: 'Avg Response Time', + value: this.statsState.dnsStats.averageResponseTime.toFixed(1), + unit: 'ms', + type: 'number', + icon: 'clockRotateLeft', + color: this.statsState.dnsStats.averageResponseTime < 50 ? '#22c55e' : '#f59e0b', + }, + ]; + + return html` +

DNS Statistics

+ + `; + } } \ No newline at end of file diff --git a/ts_web/elements/ops-view-stats.ts b/ts_web/elements/ops-view-stats.ts deleted file mode 100644 index b343164..0000000 --- a/ts_web/elements/ops-view-stats.ts +++ /dev/null @@ -1,302 +0,0 @@ -import * as plugins from '../plugins.js'; -import * as shared from './shared/index.js'; -import * as appstate from '../appstate.js'; - -import { - DeesElement, - customElement, - html, - state, - css, - cssManager, -} from '@design.estate/dees-element'; - -@customElement('ops-view-stats') -export class OpsViewStats extends DeesElement { - @state() - private statsState: appstate.IStatsState = { - serverStats: null, - emailStats: null, - dnsStats: null, - securityMetrics: null, - lastUpdated: 0, - isLoading: false, - error: null, - }; - - @state() - private uiState: appstate.IUiState = { - activeView: 'dashboard', - sidebarCollapsed: false, - autoRefresh: true, - refreshInterval: 30000, - theme: 'light', - }; - - constructor() { - super(); - const statsSubscription = appstate.statsStatePart - .select((stateArg) => stateArg) - .subscribe((statsState) => { - this.statsState = statsState; - }); - this.rxSubscriptions.push(statsSubscription); - - const uiSubscription = appstate.uiStatePart - .select((stateArg) => stateArg) - .subscribe((uiState) => { - this.uiState = uiState; - }); - this.rxSubscriptions.push(uiSubscription); - } - - public static styles = [ - cssManager.defaultStyles, - shared.viewHostCss, - css` - .controls { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 24px; - padding: 16px; - background: #f8f9fa; - border-radius: 8px; - } - - .refreshButton { - display: flex; - align-items: center; - gap: 8px; - } - - .lastUpdated { - font-size: 14px; - color: #666; - } - - .statsSection { - margin-bottom: 48px; - } - - .sectionTitle { - font-size: 24px; - font-weight: 600; - margin-bottom: 24px; - color: #333; - } - - .metricsGrid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 16px; - } - - .metricCard { - background: white; - border: 1px solid #e9ecef; - border-radius: 8px; - padding: 20px; - transition: all 0.2s ease; - } - - .metricCard:hover { - box-shadow: 0 4px 12px rgba(0,0,0,0.1); - transform: translateY(-2px); - } - - .metricLabel { - font-size: 14px; - color: #666; - margin-bottom: 8px; - } - - .metricValue { - font-size: 28px; - font-weight: 700; - color: #2196F3; - } - - .metricUnit { - font-size: 16px; - color: #999; - margin-left: 4px; - } - - .chartContainer { - background: white; - border: 1px solid #e9ecef; - border-radius: 8px; - padding: 24px; - margin-top: 24px; - } - `, - ]; - - public render() { - return html` - Statistics - -
-
- appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null)} - .disabled=${this.statsState.isLoading} - > - ${this.statsState.isLoading ? html`` : 'Refresh'} - - appstate.uiStatePart.dispatchAction(appstate.toggleAutoRefreshAction, null)} - .type=${this.uiState.autoRefresh ? 'highlighted' : 'normal'} - > - Auto-refresh: ${this.uiState.autoRefresh ? 'ON' : 'OFF'} - -
-
- ${this.statsState.lastUpdated ? html` - Last updated: ${new Date(this.statsState.lastUpdated).toLocaleTimeString()} - ` : ''} -
-
- - ${this.statsState.serverStats ? html` -
-

Server Metrics

-
-
-
Uptime
-
${this.formatUptime(this.statsState.serverStats.uptime)}
-
-
-
CPU Usage
-
${Math.round((this.statsState.serverStats.cpuUsage.user + this.statsState.serverStats.cpuUsage.system) / 2)}%
-
-
-
Memory Used
-
${this.formatBytes(this.statsState.serverStats.memoryUsage.heapUsed)}
-
-
-
Active Connections
-
${this.statsState.serverStats.activeConnections}
-
-
- -
- -
-
- ` : ''} - - ${this.statsState.emailStats ? html` -
-

Email Statistics

- ({ - Metric: item.metric, - Value: `${item.value} ${item.unit}`, - })} - > -
- ` : ''} - - ${this.statsState.dnsStats ? html` -
-

DNS Statistics

-
-
-
Total Queries
-
${this.formatNumber(this.statsState.dnsStats.totalQueries)}
-
-
-
Cache Hit Rate
-
${Math.round(this.statsState.dnsStats.cacheHitRate * 100)}%
-
-
-
Average Response Time
-
${this.statsState.dnsStats.averageResponseTime}ms
-
-
-
Domains Configured
-
${this.statsState.dnsStats.activeDomains}
-
-
-
- ` : ''} - - ${this.statsState.securityMetrics ? html` -
-

Security Metrics

- ({ - 'Security Metric': item.metric, - 'Count': item.value, - 'Severity': item.severity, - })} - > -
- ` : ''} - `; - } - - private formatUptime(seconds: number): string { - const days = Math.floor(seconds / 86400); - const hours = Math.floor((seconds % 86400) / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - const secs = Math.floor(seconds % 60); - - if (days > 0) { - return `${days}d ${hours}h ${minutes}m ${secs}s`; - } else if (hours > 0) { - return `${hours}h ${minutes}m ${secs}s`; - } else if (minutes > 0) { - return `${minutes}m ${secs}s`; - } else { - return `${secs}s`; - } - } - - private formatBytes(bytes: number): string { - const units = ['B', 'KB', 'MB', 'GB', 'TB']; - let size = bytes; - let unitIndex = 0; - - while (size >= 1024 && unitIndex < units.length - 1) { - size /= 1024; - unitIndex++; - } - - return `${size.toFixed(1)} ${units[unitIndex]}`; - } - - private formatNumber(num: number): string { - if (num >= 1000000) { - return `${(num / 1000000).toFixed(1)}M`; - } else if (num >= 1000) { - return `${(num / 1000).toFixed(1)}K`; - } - return num.toString(); - } -} \ No newline at end of file