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.
This commit is contained in:
Juergen Kunz
2025-06-12 08:04:30 +00:00
parent 0eb4963247
commit facae93e4b
9 changed files with 1543 additions and 499 deletions

View File

@ -20,7 +20,7 @@
"@git.zone/tsrun": "^1.3.3", "@git.zone/tsrun": "^1.3.3",
"@git.zone/tstest": "^2.3.1", "@git.zone/tstest": "^2.3.1",
"@git.zone/tswatch": "^2.0.1", "@git.zone/tswatch": "^2.0.1",
"@types/node": "^22.15.30", "@types/node": "^24.0.0",
"node-forge": "^1.3.1" "node-forge": "^1.3.1"
}, },
"dependencies": { "dependencies": {
@ -29,7 +29,7 @@
"@api.global/typedserver": "^3.0.74", "@api.global/typedserver": "^3.0.74",
"@api.global/typedsocket": "^3.0.0", "@api.global/typedsocket": "^3.0.0",
"@apiclient.xyz/cloudflare": "^6.4.1", "@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", "@design.estate/dees-element": "^2.0.42",
"@push.rocks/projectinfo": "^5.0.1", "@push.rocks/projectinfo": "^5.0.1",
"@push.rocks/qenv": "^6.1.0", "@push.rocks/qenv": "^6.1.0",

180
pnpm-lock.yaml generated
View File

@ -24,8 +24,8 @@ importers:
specifier: ^6.4.1 specifier: ^6.4.1
version: 6.4.1 version: 6.4.1
'@design.estate/dees-catalog': '@design.estate/dees-catalog':
specifier: ^1.8.0 specifier: ^1.8.1
version: 1.8.0 version: 1.8.1
'@design.estate/dees-element': '@design.estate/dees-element':
specifier: ^2.0.42 specifier: ^2.0.42
version: 2.0.42 version: 2.0.42
@ -130,8 +130,8 @@ importers:
specifier: ^2.0.1 specifier: ^2.0.1
version: 2.1.0 version: 2.1.0
'@types/node': '@types/node':
specifier: ^22.15.30 specifier: ^24.0.0
version: 22.15.30 version: 24.0.0
node-forge: node-forge:
specifier: ^1.3.1 specifier: ^1.3.1
version: 1.3.1 version: 1.3.1
@ -344,8 +344,8 @@ packages:
'@dabh/diagnostics@2.0.3': '@dabh/diagnostics@2.0.3':
resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
'@design.estate/dees-catalog@1.8.0': '@design.estate/dees-catalog@1.8.1':
resolution: {integrity: sha512-8xUkp+fPjXR9/GM5fvc+eHogpirkcmW4Ao8vNLgIomejoLRamtIbICJQaVmpra20XJqPkOcnvHwUHyhLckwYJA==} resolution: {integrity: sha512-luYrGMK5APzw7GgXSe4UpFE5oC3vwzKTTB0etH3o6Au1nxvllhDcWr6S5UkB+WL9lroslBcK8Icyhiq8kIxEHg==}
'@design.estate/dees-comms@1.0.27': '@design.estate/dees-comms@1.0.27':
resolution: {integrity: sha512-GvzTUwkV442LD60T08iqSoqvhA02Mou5lFvvqBPc4yBUiU7cZISqBx+76xvMgMIEI9Dx9JfTl4/2nW8MoVAanw==} resolution: {integrity: sha512-GvzTUwkV442LD60T08iqSoqvhA02Mou5lFvvqBPc4yBUiU7cZISqBx+76xvMgMIEI9Dx9JfTl4/2nW8MoVAanw==}
@ -737,68 +737,68 @@ packages:
'@mongodb-js/saslprep@1.2.2': '@mongodb-js/saslprep@1.2.2':
resolution: {integrity: sha512-EB0O3SCSNRUFk66iRCpI+cXzIjdswfCs7F6nOC3RAGJ7xr5YhaicvsRwJ9eyzYvYRlCSDUO/c7g4yNulxKC1WA==} resolution: {integrity: sha512-EB0O3SCSNRUFk66iRCpI+cXzIjdswfCs7F6nOC3RAGJ7xr5YhaicvsRwJ9eyzYvYRlCSDUO/c7g4yNulxKC1WA==}
'@napi-rs/canvas-android-arm64@0.1.70': '@napi-rs/canvas-android-arm64@0.1.71':
resolution: {integrity: sha512-I/YOuQ0wbkVYxVaYtCgN42WKTYxNqFA0gTcTrHIGG1jfpDSyZWII/uHcjOo4nzd19io6Y4+/BqP8E5hJgf9OmQ==} resolution: {integrity: sha512-cxi3VCotIOS9kNFQI7dcysbVJi106pxryVY1Hi85pX+ZeqahRyeqc/NsLaZ998Ae99+F3HI5X/39G1Y/Byrf0A==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [android] os: [android]
'@napi-rs/canvas-darwin-arm64@0.1.70': '@napi-rs/canvas-darwin-arm64@0.1.71':
resolution: {integrity: sha512-4pPGyXetHIHkw2TOJHujt3mkCP8LdDu8+CT15ld9Id39c752RcI0amDHSuMLMQfAjvusA9B5kKxazwjMGjEJpQ==} resolution: {integrity: sha512-7Y4D/6vIuMLYsVNtRM/w2j0+fB1GyqeOxc7I0BTx8eLP1S6BZE2Rj6zJfdG+zmLEOW0IlHa+VQq1q2MUAjW84w==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@napi-rs/canvas-darwin-x64@0.1.70': '@napi-rs/canvas-darwin-x64@0.1.71':
resolution: {integrity: sha512-+2N6Os9LbkmDMHL+raknrUcLQhsXzc5CSXRbXws9C3pv/mjHRVszQ9dhFUUe9FjfPhCJznO6USVdwOtu7pOrzQ==} resolution: {integrity: sha512-Z0IUqxclrYdfVt/SK9nKCzUHTOXKTWiygtO71YCzs0OtxKdNI7GJRJdYG48wXZEDQ/pqTF4F7Ifgtidfc2tYpg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.70': '@napi-rs/canvas-linux-arm-gnueabihf@0.1.71':
resolution: {integrity: sha512-QjscX9OaKq/990sVhSMj581xuqLgiaPVMjjYvWaCmAJRkNQ004QfoSMEm3FoTqM4DRoquP8jvuEXScVJsc1rqQ==} resolution: {integrity: sha512-KlpqqCASak5ruY+UIolJgmhMZ9Pa2o1QyaNu648L8sz4WNBbNa+aOT60XCLCL1VIKLv11B3MlNgiOHoYNmDhXQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
'@napi-rs/canvas-linux-arm64-gnu@0.1.70': '@napi-rs/canvas-linux-arm64-gnu@0.1.71':
resolution: {integrity: sha512-LNakMOwwqwiHIwMpnMAbFRczQMQ7TkkMyATqFCOtUJNlE6LPP/QiUj/mlFrNbUn/hctqShJ60gWEb52ZTALbVw==} resolution: {integrity: sha512-bdGZCGu8YQNAiu3nkIVVUp6nIn6fPd36IuZsLXTG027E52KyIuZ3obCxehSwjDIUNkFWvmff5D6JYfWwAoioEw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@napi-rs/canvas-linux-arm64-musl@0.1.70': '@napi-rs/canvas-linux-arm64-musl@0.1.71':
resolution: {integrity: sha512-wBTOllEYNfJCHOdZj9v8gLzZ4oY3oyPX8MSRvaxPm/s7RfEXxCyZ8OhJ5xAyicsDdbE5YBZqdmaaeP5+xKxvtg==} resolution: {integrity: sha512-1R5sMWe9ur8uM+hAeylBwG0b6UHDR+iWQNgzXmF9vbBYRooQvmDWqpcgytKLJAC0vnWhIkKwqd7yExn7cwczmg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@napi-rs/canvas-linux-riscv64-gnu@0.1.70': '@napi-rs/canvas-linux-riscv64-gnu@0.1.71':
resolution: {integrity: sha512-GVUUPC8TuuFqHip0rxHkUqArQnlzmlXmTEBuXAWdgCv85zTCFH8nOHk/YCF5yo0Z2eOm8nOi90aWs0leJ4OE5Q==} resolution: {integrity: sha512-xjjKsipueuG+LdKIk6/uAlqdo+rzGcmNpTZPXdakIT1sHX4NNSnQTzjRaj9Gh96Czjd9G89UWR0KIlE7fwOgFA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
'@napi-rs/canvas-linux-x64-gnu@0.1.70': '@napi-rs/canvas-linux-x64-gnu@0.1.71':
resolution: {integrity: sha512-/kvUa2lZRwGNyfznSn5t1ShWJnr/m5acSlhTV3eXECafObjl0VBuA1HJw0QrilLpb4Fe0VLywkpD1NsMoVDROQ==} resolution: {integrity: sha512-3s6YpklXDB4OeeULG1XTRyKrKAOo7c3HHEqM9A6N4STSjMaJtzmpp7tB/JTvAFeOeFte6gWN8IwC+7AjGJ6MpQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@napi-rs/canvas-linux-x64-musl@0.1.70': '@napi-rs/canvas-linux-x64-musl@0.1.71':
resolution: {integrity: sha512-aqlv8MLpycoMKRmds7JWCfVwNf1fiZxaU7JwJs9/ExjTD8lX2KjsO7CTeAj5Cl4aEuzxUWbJPUUE2Qu9cZ1vfg==} resolution: {integrity: sha512-5v9aCLzCXw7u10ray5juQMdl7TykZSn1X5AIGYwBvTAcKSgrqaR9QkRxp1Lqk3njQmFekOW1SFN9bZ/i/6y6kA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@napi-rs/canvas-win32-x64-msvc@0.1.70': '@napi-rs/canvas-win32-x64-msvc@0.1.71':
resolution: {integrity: sha512-Q9QU3WIpwBTVHk4cPfBjGHGU4U0llQYRXgJtFtYqqGNEOKVN4OT6PQ+ve63xwIPODMpZ0HHyj/KLGc9CWc3EtQ==} resolution: {integrity: sha512-oJughk6xjsRIr0Rd9EqjmZmhIMkvcPuXgr3MNn2QexTqn+YFOizrwHS5ha0BDfFl7TEGRvwaDUXBQtu8JKXb8A==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@napi-rs/canvas@0.1.70': '@napi-rs/canvas@0.1.71':
resolution: {integrity: sha512-nD6NGa4JbNYSZYsTnLGrqe9Kn/lCkA4ybXt8sx5ojDqZjr2i0TWAHxx/vhgfjX+i3hCdKWufxYwi7CfXqtITSA==} resolution: {integrity: sha512-92ybDocKl6JM48ZpYbj+A7Qt45IaTABDk0y3sDecEQfgdhfNzJtEityqNHoCZ4Vty2dldPkJhxgvOnbrQMXTTA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
'@oozcitak/dom@1.15.10': '@oozcitak/dom@1.15.10':
@ -1661,8 +1661,8 @@ packages:
'@types/node@18.19.111': '@types/node@18.19.111':
resolution: {integrity: sha512-90sGdgA+QLJr1F9X79tQuEut0gEYIfkX9pydI4XGRgvFo9g2JWswefI+WUSUHPYVBHYSEfTEqBxA5hQvAZB3Mw==} resolution: {integrity: sha512-90sGdgA+QLJr1F9X79tQuEut0gEYIfkX9pydI4XGRgvFo9g2JWswefI+WUSUHPYVBHYSEfTEqBxA5hQvAZB3Mw==}
'@types/node@22.15.30': '@types/node@24.0.0':
resolution: {integrity: sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==} resolution: {integrity: sha512-yZQa2zm87aRVcqDyH5+4Hv9KYgSdgwX1rFnGvpbzMaC7YAljmhBET93TPiTd3ObwTL+gSpIzPKg5BqVxdCvxKg==}
'@types/pidusage@2.0.5': '@types/pidusage@2.0.5':
resolution: {integrity: sha512-MIiyZI4/MK9UGUXWt0jJcCZhVw7YdhBuTOuqP/BjuLDLZ2PmmViMIQgZiWxtaMicQfAz/kMrZ5T7PKxFSkTeUA==} resolution: {integrity: sha512-MIiyZI4/MK9UGUXWt0jJcCZhVw7YdhBuTOuqP/BjuLDLZ2PmmViMIQgZiWxtaMicQfAz/kMrZ5T7PKxFSkTeUA==}
@ -2963,8 +2963,8 @@ packages:
leac@0.6.0: leac@0.6.0:
resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==}
lenis@1.3.3: lenis@1.3.4:
resolution: {integrity: sha512-DOopj/UKHS54E9l2g4BOpDUvsyvkd1zkv+ECtHxQ9Fto8ozzKSz7MccqT+KOyG0ABA/OHXZ7l9INx0peUoQ8rQ==} resolution: {integrity: sha512-WIGk8wiV2ABm/T7M+NC+tAV8fjzNJD1J4z11aZ3mTtx7WAZX/4QdCNhBO0g/TqXISA+/3hTbzrPC4FW1nhoNMQ==}
peerDependencies: peerDependencies:
'@nuxt/kit': '>=3.0.0' '@nuxt/kit': '>=3.0.0'
react: '>=17.0.0' react: '>=17.0.0'
@ -4194,8 +4194,8 @@ packages:
undici-types@5.26.5: undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
undici-types@6.21.0: undici-types@7.8.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==}
undici@7.10.0: undici@7.10.0:
resolution: {integrity: sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==} resolution: {integrity: sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==}
@ -5075,7 +5075,7 @@ snapshots:
enabled: 2.0.0 enabled: 2.0.0
kuler: 2.0.0 kuler: 2.0.0
'@design.estate/dees-catalog@1.8.0': '@design.estate/dees-catalog@1.8.1':
dependencies: dependencies:
'@design.estate/dees-domtools': 2.3.2 '@design.estate/dees-domtools': 2.3.2
'@design.estate/dees-element': 2.0.42 '@design.estate/dees-element': 2.0.42
@ -5127,7 +5127,7 @@ snapshots:
'@push.rocks/webrequest': 3.0.37 '@push.rocks/webrequest': 3.0.37
'@push.rocks/websetup': 3.0.19 '@push.rocks/websetup': 3.0.19
'@push.rocks/webstore': 2.0.20 '@push.rocks/webstore': 2.0.20
lenis: 1.3.3 lenis: 1.3.4
lit: 3.3.0 lit: 3.3.0
sweet-scroll: 4.0.0 sweet-scroll: 4.0.0
transitivePeerDependencies: transitivePeerDependencies:
@ -5487,48 +5487,48 @@ snapshots:
dependencies: dependencies:
sparse-bitfield: 3.0.3 sparse-bitfield: 3.0.3
'@napi-rs/canvas-android-arm64@0.1.70': '@napi-rs/canvas-android-arm64@0.1.71':
optional: true optional: true
'@napi-rs/canvas-darwin-arm64@0.1.70': '@napi-rs/canvas-darwin-arm64@0.1.71':
optional: true optional: true
'@napi-rs/canvas-darwin-x64@0.1.70': '@napi-rs/canvas-darwin-x64@0.1.71':
optional: true optional: true
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.70': '@napi-rs/canvas-linux-arm-gnueabihf@0.1.71':
optional: true optional: true
'@napi-rs/canvas-linux-arm64-gnu@0.1.70': '@napi-rs/canvas-linux-arm64-gnu@0.1.71':
optional: true optional: true
'@napi-rs/canvas-linux-arm64-musl@0.1.70': '@napi-rs/canvas-linux-arm64-musl@0.1.71':
optional: true optional: true
'@napi-rs/canvas-linux-riscv64-gnu@0.1.70': '@napi-rs/canvas-linux-riscv64-gnu@0.1.71':
optional: true optional: true
'@napi-rs/canvas-linux-x64-gnu@0.1.70': '@napi-rs/canvas-linux-x64-gnu@0.1.71':
optional: true optional: true
'@napi-rs/canvas-linux-x64-musl@0.1.70': '@napi-rs/canvas-linux-x64-musl@0.1.71':
optional: true optional: true
'@napi-rs/canvas-win32-x64-msvc@0.1.70': '@napi-rs/canvas-win32-x64-msvc@0.1.71':
optional: true optional: true
'@napi-rs/canvas@0.1.70': '@napi-rs/canvas@0.1.71':
optionalDependencies: optionalDependencies:
'@napi-rs/canvas-android-arm64': 0.1.70 '@napi-rs/canvas-android-arm64': 0.1.71
'@napi-rs/canvas-darwin-arm64': 0.1.70 '@napi-rs/canvas-darwin-arm64': 0.1.71
'@napi-rs/canvas-darwin-x64': 0.1.70 '@napi-rs/canvas-darwin-x64': 0.1.71
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.70 '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.71
'@napi-rs/canvas-linux-arm64-gnu': 0.1.70 '@napi-rs/canvas-linux-arm64-gnu': 0.1.71
'@napi-rs/canvas-linux-arm64-musl': 0.1.70 '@napi-rs/canvas-linux-arm64-musl': 0.1.71
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.70 '@napi-rs/canvas-linux-riscv64-gnu': 0.1.71
'@napi-rs/canvas-linux-x64-gnu': 0.1.70 '@napi-rs/canvas-linux-x64-gnu': 0.1.71
'@napi-rs/canvas-linux-x64-musl': 0.1.70 '@napi-rs/canvas-linux-x64-musl': 0.1.71
'@napi-rs/canvas-win32-x64-msvc': 0.1.70 '@napi-rs/canvas-win32-x64-msvc': 0.1.71
optional: true optional: true
'@oozcitak/dom@1.15.10': '@oozcitak/dom@1.15.10':
@ -5784,7 +5784,6 @@ snapshots:
- '@mongodb-js/zstd' - '@mongodb-js/zstd'
- '@nuxt/kit' - '@nuxt/kit'
- aws-crt - aws-crt
- bufferutil
- encoding - encoding
- gcp-metadata - gcp-metadata
- kerberos - kerberos
@ -5793,7 +5792,6 @@ snapshots:
- snappy - snappy
- socks - socks
- supports-color - supports-color
- utf-8-validate
- vue - vue
'@push.rocks/smartarchive@3.0.8': '@push.rocks/smartarchive@3.0.8':
@ -7046,27 +7044,27 @@ snapshots:
'@types/bn.js@5.2.0': '@types/bn.js@5.2.0':
dependencies: dependencies:
'@types/node': 22.15.30 '@types/node': 24.0.0
'@types/body-parser@1.19.5': '@types/body-parser@1.19.5':
dependencies: dependencies:
'@types/connect': 3.4.38 '@types/connect': 3.4.38
'@types/node': 22.15.30 '@types/node': 24.0.0
'@types/buffer-json@2.0.3': {} '@types/buffer-json@2.0.3': {}
'@types/clean-css@4.2.11': '@types/clean-css@4.2.11':
dependencies: dependencies:
'@types/node': 22.15.30 '@types/node': 24.0.0
source-map: 0.6.1 source-map: 0.6.1
'@types/connect@3.4.38': '@types/connect@3.4.38':
dependencies: dependencies:
'@types/node': 22.15.30 '@types/node': 24.0.0
'@types/cors@2.8.18': '@types/cors@2.8.18':
dependencies: dependencies:
'@types/node': 22.15.30 '@types/node': 24.0.0
'@types/debug@4.1.12': '@types/debug@4.1.12':
dependencies: dependencies:
@ -7078,7 +7076,7 @@ snapshots:
'@types/dns-packet@5.6.5': '@types/dns-packet@5.6.5':
dependencies: dependencies:
'@types/node': 22.15.30 '@types/node': 24.0.0
'@types/elliptic@6.4.18': '@types/elliptic@6.4.18':
dependencies: dependencies:
@ -7086,7 +7084,7 @@ snapshots:
'@types/express-serve-static-core@5.0.6': '@types/express-serve-static-core@5.0.6':
dependencies: dependencies:
'@types/node': 22.15.30 '@types/node': 24.0.0
'@types/qs': 6.14.0 '@types/qs': 6.14.0
'@types/range-parser': 1.2.7 '@types/range-parser': 1.2.7
'@types/send': 0.17.4 '@types/send': 0.17.4
@ -7103,30 +7101,30 @@ snapshots:
'@types/from2@2.3.5': '@types/from2@2.3.5':
dependencies: dependencies:
'@types/node': 22.15.30 '@types/node': 24.0.0
'@types/fs-extra@11.0.4': '@types/fs-extra@11.0.4':
dependencies: dependencies:
'@types/jsonfile': 6.1.4 '@types/jsonfile': 6.1.4
'@types/node': 22.15.30 '@types/node': 24.0.0
'@types/fs-extra@9.0.13': '@types/fs-extra@9.0.13':
dependencies: dependencies:
'@types/node': 22.15.30 '@types/node': 24.0.0
'@types/glob@7.2.0': '@types/glob@7.2.0':
dependencies: dependencies:
'@types/minimatch': 5.1.2 '@types/minimatch': 5.1.2
'@types/node': 22.15.30 '@types/node': 24.0.0
'@types/glob@8.1.0': '@types/glob@8.1.0':
dependencies: dependencies:
'@types/minimatch': 5.1.2 '@types/minimatch': 5.1.2
'@types/node': 22.15.30 '@types/node': 24.0.0
'@types/gunzip-maybe@1.4.2': '@types/gunzip-maybe@1.4.2':
dependencies: dependencies:
'@types/node': 22.15.30 '@types/node': 24.0.0
'@types/hast@3.0.4': '@types/hast@3.0.4':
dependencies: dependencies:
@ -7148,16 +7146,16 @@ snapshots:
'@types/jsonfile@6.1.4': '@types/jsonfile@6.1.4':
dependencies: dependencies:
'@types/node': 22.15.30 '@types/node': 24.0.0
'@types/jsonwebtoken@9.0.9': '@types/jsonwebtoken@9.0.9':
dependencies: dependencies:
'@types/ms': 2.1.0 '@types/ms': 2.1.0
'@types/node': 22.15.30 '@types/node': 24.0.0
'@types/mailparser@3.4.6': '@types/mailparser@3.4.6':
dependencies: dependencies:
'@types/node': 22.15.30 '@types/node': 24.0.0
iconv-lite: 0.6.3 iconv-lite: 0.6.3
'@types/mdast@4.0.4': '@types/mdast@4.0.4':
@ -7176,20 +7174,20 @@ snapshots:
'@types/node-fetch@2.6.12': '@types/node-fetch@2.6.12':
dependencies: dependencies:
'@types/node': 22.15.30 '@types/node': 24.0.0
form-data: 4.0.2 form-data: 4.0.2
'@types/node-forge@1.3.11': '@types/node-forge@1.3.11':
dependencies: dependencies:
'@types/node': 22.15.30 '@types/node': 24.0.0
'@types/node@18.19.111': '@types/node@18.19.111':
dependencies: dependencies:
undici-types: 5.26.5 undici-types: 5.26.5
'@types/node@22.15.30': '@types/node@24.0.0':
dependencies: dependencies:
undici-types: 6.21.0 undici-types: 7.8.0
'@types/pidusage@2.0.5': {} '@types/pidusage@2.0.5': {}
@ -7205,30 +7203,30 @@ snapshots:
'@types/s3rver@3.7.4': '@types/s3rver@3.7.4':
dependencies: dependencies:
'@types/node': 22.15.30 '@types/node': 24.0.0
'@types/semver@7.7.0': {} '@types/semver@7.7.0': {}
'@types/send@0.17.4': '@types/send@0.17.4':
dependencies: dependencies:
'@types/mime': 1.3.5 '@types/mime': 1.3.5
'@types/node': 22.15.30 '@types/node': 24.0.0
'@types/serve-static@1.15.7': '@types/serve-static@1.15.7':
dependencies: dependencies:
'@types/http-errors': 2.0.4 '@types/http-errors': 2.0.4
'@types/node': 22.15.30 '@types/node': 24.0.0
'@types/send': 0.17.4 '@types/send': 0.17.4
'@types/symbol-tree@3.2.5': {} '@types/symbol-tree@3.2.5': {}
'@types/tar-stream@2.2.3': '@types/tar-stream@2.2.3':
dependencies: dependencies:
'@types/node': 22.15.30 '@types/node': 24.0.0
'@types/through2@2.0.41': '@types/through2@2.0.41':
dependencies: dependencies:
'@types/node': 22.15.30 '@types/node': 24.0.0
'@types/triple-beam@1.3.5': {} '@types/triple-beam@1.3.5': {}
@ -7252,18 +7250,18 @@ snapshots:
'@types/whatwg-url@8.2.2': '@types/whatwg-url@8.2.2':
dependencies: dependencies:
'@types/node': 22.15.30 '@types/node': 24.0.0
'@types/webidl-conversions': 7.0.3 '@types/webidl-conversions': 7.0.3
'@types/which@3.0.4': {} '@types/which@3.0.4': {}
'@types/ws@8.18.1': '@types/ws@8.18.1':
dependencies: dependencies:
'@types/node': 22.15.30 '@types/node': 24.0.0
'@types/yauzl@2.10.3': '@types/yauzl@2.10.3':
dependencies: dependencies:
'@types/node': 22.15.30 '@types/node': 24.0.0
optional: true optional: true
'@ungap/structured-clone@1.3.0': {} '@ungap/structured-clone@1.3.0': {}
@ -7835,7 +7833,7 @@ snapshots:
engine.io@6.6.4: engine.io@6.6.4:
dependencies: dependencies:
'@types/cors': 2.8.18 '@types/cors': 2.8.18
'@types/node': 22.15.30 '@types/node': 24.0.0
accepts: 1.3.8 accepts: 1.3.8
base64id: 2.0.0 base64id: 2.0.0
cookie: 0.7.2 cookie: 0.7.2
@ -8663,7 +8661,7 @@ snapshots:
leac@0.6.0: {} leac@0.6.0: {}
lenis@1.3.3: {} lenis@1.3.4: {}
libbase64@1.3.0: {} libbase64@1.3.0: {}
@ -9473,7 +9471,7 @@ snapshots:
pdfjs-dist@4.10.38: pdfjs-dist@4.10.38:
optionalDependencies: optionalDependencies:
'@napi-rs/canvas': 0.1.70 '@napi-rs/canvas': 0.1.71
peberminta@0.9.0: {} peberminta@0.9.0: {}
@ -10168,7 +10166,7 @@ snapshots:
undici-types@5.26.5: {} undici-types@5.26.5: {}
undici-types@6.21.0: {} undici-types@7.8.0: {}
undici@7.10.0: {} undici@7.10.0: {}

46
readme.statsgrid.md Normal file
View File

@ -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

View File

@ -1,6 +1,7 @@
export * from './ops-dashboard.js'; export * from './ops-dashboard.js';
export * from './ops-view-overview.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-logs.js';
export * from './ops-view-config.js'; export * from './ops-view-config.js';
export * from './ops-view-security.js'; export * from './ops-view-security.js';

View File

@ -13,7 +13,8 @@ import {
// Import view components // Import view components
import { OpsViewOverview } from './ops-view-overview.js'; 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 { OpsViewLogs } from './ops-view-logs.js';
import { OpsViewConfig } from './ops-view-config.js'; import { OpsViewConfig } from './ops-view-config.js';
import { OpsViewSecurity } from './ops-view-security.js'; import { OpsViewSecurity } from './ops-view-security.js';
@ -84,8 +85,12 @@ export class OpsDashboard extends DeesElement {
element: OpsViewOverview, element: OpsViewOverview,
}, },
{ {
name: 'Statistics', name: 'Network',
element: OpsViewStats, element: OpsViewNetwork,
},
{
name: 'Emails',
element: OpsViewEmails,
}, },
{ {
name: 'Logs', name: 'Logs',

View File

@ -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`
<ops-sectionheading>Emails</ops-sectionheading>
<div class="emailContainer">
<!-- Sidebar -->
<div class="sidebar">
<dees-button @click=${() => this.showCompose = true} type="highlighted">
<dees-icon name="penToSquare" slot="iconSlot"></dees-icon>
Compose
</dees-button>
<div class="folderList">
${this.renderFolderItem('inbox', 'inbox', 'Inbox', this.getEmailCount('inbox'))}
${this.renderFolderItem('sent', 'paperPlane', 'Sent', this.getEmailCount('sent'))}
${this.renderFolderItem('draft', 'file', 'Drafts', this.getEmailCount('draft'))}
${this.renderFolderItem('trash', 'trash', 'Trash', this.getEmailCount('trash'))}
</div>
</div>
<!-- Main Content -->
<div class="mainContent">
<!-- Toolbar -->
<div class="emailToolbar">
<dees-input-text
class="searchBox"
placeholder="Search emails..."
.value=${this.searchTerm}
@input=${(e: Event) => this.searchTerm = (e.target as any).value}
>
<dees-icon name="magnifyingGlass" slot="iconSlot"></dees-icon>
</dees-input-text>
<dees-button @click=${() => this.refreshEmails()}>
${this.isLoading ? html`<dees-spinner size="small"></dees-spinner>` : html`<dees-icon name="arrowsRotate"></dees-icon>`}
</dees-button>
<dees-button @click=${() => this.markAllAsRead()}>
<dees-icon name="envelopeOpen"></dees-icon>
Mark all read
</dees-button>
</div>
<!-- Email List or Preview -->
${this.selectedEmail ? this.renderEmailPreview() : this.renderEmailList()}
</div>
</div>
<!-- Compose Modal -->
${this.showCompose ? this.renderComposeModal() : ''}
`;
}
private renderFolderItem(folder: string, icon: string, label: string, count: number) {
return html`
<div
class="folderItem ${this.selectedFolder === folder ? 'selected' : ''}"
@click=${() => this.selectFolder(folder as any)}
>
<dees-icon class="folderIcon" name="${icon}"></dees-icon>
<span class="folderLabel">${label}</span>
${count > 0 ? html`<span class="folderCount">${count}</span>` : ''}
</div>
`;
}
private renderEmailList() {
const filteredEmails = this.getFilteredEmails();
if (filteredEmails.length === 0) {
return html`
<div class="emptyState">
<dees-icon class="emptyIcon" name="envelope"></dees-icon>
<div class="emptyText">No emails in ${this.selectedFolder}</div>
</div>
`;
}
return html`
<div class="emailList">
<dees-table
.data=${filteredEmails}
.displayFunction=${(email: IEmail) => ({
'Status': html`<dees-icon name="${email.read ? 'envelopeOpen' : 'envelope'}" style="color: ${email.read ? '#999' : '#1976d2'}"></dees-icon>`,
From: email.from,
Subject: html`<strong style="${!email.read ? 'font-weight: 600' : ''}">${email.subject}</strong>`,
Date: this.formatDate(email.date),
'Attach': html`
${email.attachments?.length ? html`<dees-icon name="paperclip" style="color: #666"></dees-icon>` : ''}
`,
})}
.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'}
></dees-table>
</div>
`;
}
private renderEmailPreview() {
if (!this.selectedEmail) return '';
return html`
<div class="emailPreview">
<div class="emailHeader">
<div class="emailSubject">${this.selectedEmail.subject}</div>
<div class="emailMeta">
<span><strong>From:</strong> ${this.selectedEmail.from}</span>
<span><strong>To:</strong> ${this.selectedEmail.to.join(', ')}</span>
<span><strong>Date:</strong> ${this.formatDate(this.selectedEmail.date)}</span>
</div>
</div>
<div class="emailBody">
${this.selectedEmail.html ?
html`<div .innerHTML=${this.selectedEmail.html}></div>` :
html`<pre style="white-space: pre-wrap; font-family: inherit;">${this.selectedEmail.body}</pre>`
}
</div>
<div class="emailActions">
<dees-button @click=${() => this.selectedEmail = null}>
<dees-icon name="arrowLeft"></dees-icon>
Back
</dees-button>
<dees-button @click=${() => this.replyToEmail(this.selectedEmail!)}>
<dees-icon name="reply"></dees-icon>
Reply
</dees-button>
<dees-button @click=${() => this.replyAllToEmail(this.selectedEmail!)}>
<dees-icon name="replyAll"></dees-icon>
Reply All
</dees-button>
<dees-button @click=${() => this.forwardEmail(this.selectedEmail!)}>
<dees-icon name="share"></dees-icon>
Forward
</dees-button>
<dees-button @click=${() => this.deleteEmail(this.selectedEmail!.id)} type="danger">
<dees-icon name="trash"></dees-icon>
Delete
</dees-button>
</div>
</div>
`;
}
private renderComposeModal() {
return html`
<div class="composeModal" @click=${(e: Event) => {
if (e.target === e.currentTarget) this.showCompose = false;
}}>
<div class="composeContent">
<div class="composeHeader">
<div class="composeTitle">New Email</div>
<dees-button @click=${() => this.showCompose = false} type="ghost">
<dees-icon name="xmark"></dees-icon>
</dees-button>
</div>
<div class="composeForm">
<dees-form @formData=${(e: CustomEvent) => this.sendEmail(e.detail)}>
<dees-input-tags
key="to"
label="To"
placeholder="Enter recipient email addresses..."
required
></dees-input-tags>
<dees-input-tags
key="cc"
label="CC"
placeholder="Enter CC recipients..."
></dees-input-tags>
<dees-input-tags
key="bcc"
label="BCC"
placeholder="Enter BCC recipients..."
></dees-input-tags>
<dees-input-text
key="subject"
label="Subject"
placeholder="Enter email subject..."
required
></dees-input-text>
<dees-editor
key="body"
label="Message"
.mode=${'markdown'}
.height=${300}
required
></dees-editor>
<dees-input-fileupload
key="attachments"
label="Attachments"
multiple
></dees-input-fileupload>
<div class="composeActions">
<dees-button @click=${() => this.showCompose = false} type="secondary">
Cancel
</dees-button>
<dees-form-submit>
<dees-icon name="paperPlane"></dees-icon>
Send Email
</dees-form-submit>
</div>
</dees-form>
</div>
</div>
</div>
`;
}
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,
}));
}
}

View File

@ -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`
<ops-sectionheading>Network Activity</ops-sectionheading>
<div class="networkContainer">
<!-- Control Bar -->
<div class="controlBar">
<div class="controlGroup">
<span class="controlLabel">Time Range:</span>
<dees-button-group>
${(['1m', '5m', '15m', '1h', '24h'] as const).map(range => html`
<dees-button
@click=${() => this.selectedTimeRange = range}
.type=${this.selectedTimeRange === range ? 'highlighted' : 'normal'}
>
${range}
</dees-button>
`)}
</dees-button-group>
</div>
<div class="controlGroup">
<span class="controlLabel">Protocol:</span>
<dees-input-dropdown
.options=${[
{ key: 'all', label: 'All Protocols' },
{ key: 'http', label: 'HTTP' },
{ key: 'https', label: 'HTTPS' },
{ key: 'smtp', label: 'SMTP' },
{ key: 'dns', label: 'DNS' },
]}
.selectedOption=${{ key: this.selectedProtocol, label: this.getProtocolLabel(this.selectedProtocol) }}
@selectedOption=${(e: CustomEvent) => this.selectedProtocol = e.detail.key}
></dees-input-dropdown>
</div>
<div style="margin-left: auto;">
<dees-button
@click=${() => this.refreshData()}
.disabled=${this.isLoading}
>
${this.isLoading ? html`<dees-spinner size="small"></dees-spinner>` : 'Refresh'}
</dees-button>
</div>
</div>
<!-- Stats Grid -->
${this.renderNetworkStats()}
<!-- Traffic Chart -->
<div class="chartSection">
<dees-chart-area
.label=${'Network Traffic'}
.series=${[
{
name: 'Requests/min',
data: this.trafficData,
}
]}
></dees-chart-area>
</div>
<!-- Requests Table -->
<div class="tableSection">
<dees-table
.data=${this.getFilteredRequests()}
.displayFunction=${(req: INetworkRequest) => ({
Time: new Date(req.timestamp).toLocaleTimeString(),
Protocol: html`<span class="protocolBadge ${req.protocol}">${req.protocol.toUpperCase()}</span>`,
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"
></dees-table>
</div>
</div>
`;
}
private async showRequestDetails(request: INetworkRequest) {
const { DeesModal } = await import('@design.estate/dees-catalog');
await DeesModal.createAndShow({
heading: 'Request Details',
content: html`
<div style="padding: 20px;">
<dees-dataview-codebox
.heading=${'Request Information'}
progLang="json"
.codeToDisplay=${JSON.stringify({
id: request.id,
timestamp: new Date(request.timestamp).toISOString(),
protocol: request.protocol,
method: request.method,
url: request.url,
hostname: request.hostname,
port: request.port,
statusCode: request.statusCode,
duration: `${request.duration}ms`,
bytesIn: request.bytesIn,
bytesOut: request.bytesOut,
remoteIp: request.remoteIp,
route: request.route,
}, null, 2)}
></dees-dataview-codebox>
</div>
`,
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<string, string[]> = {
'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`<span class="statusBadge warning">N/A</span>`;
}
const statusClass = statusCode >= 200 && statusCode < 300 ? 'success' :
statusCode >= 400 ? 'error' : 'warning';
return html`<span class="statusBadge ${statusClass}">${statusCode}</span>`;
}
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<string, string> = {
'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`
<dees-statsgrid
.tiles=${tiles}
.minTileWidth=${200}
.gridActions=${[
{
name: 'Export Data',
iconName: 'fileExport',
action: async () => {
// TODO: Export network data
// TODO: Implement toast notification when DeesToast.show is available
console.log('Export feature coming soon');
},
},
]}
></dees-statsgrid>
`;
}
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();
}
}

View File

@ -9,7 +9,9 @@ import {
state, state,
css, css,
cssManager, cssManager,
type TemplateResult,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import { type IStatsTile } from '@design.estate/dees-catalog';
@customElement('ops-view-overview') @customElement('ops-view-overview')
export class OpsViewOverview extends DeesElement { export class OpsViewOverview extends DeesElement {
@ -38,39 +40,13 @@ export class OpsViewOverview extends DeesElement {
cssManager.defaultStyles, cssManager.defaultStyles,
shared.viewHostCss, shared.viewHostCss,
css` css`
.statsGrid { h2 {
display: grid; margin: 32px 0 16px 0;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); font-size: 24px;
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;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
} }
.statValue {
font-size: 32px;
font-weight: 700;
color: #2196F3;
margin-bottom: 8px;
}
.statLabel {
font-size: 14px;
color: #666;
}
.chartGrid { .chartGrid {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
@ -92,6 +68,10 @@ export class OpsViewOverview extends DeesElement {
color: #c00; color: #c00;
margin: 16px 0; margin: 16px 0;
} }
dees-statsgrid {
margin-bottom: 32px;
}
`, `,
]; ];
@ -109,79 +89,11 @@ export class OpsViewOverview extends DeesElement {
Error loading statistics: ${this.statsState.error} Error loading statistics: ${this.statsState.error}
</div> </div>
` : html` ` : html`
<div class="statsGrid"> ${this.renderServerStats()}
${this.statsState.serverStats ? html`
<div class="statCard">
<h3>Server Status</h3>
<div class="statValue">${this.statsState.serverStats.uptime ? 'Online' : 'Offline'}</div>
<div class="statLabel">Uptime: ${this.formatUptime(this.statsState.serverStats.uptime)}</div>
</div>
<div class="statCard"> ${this.renderEmailStats()}
<h3>Connections</h3>
<div class="statValue">${this.statsState.serverStats.activeConnections}</div>
<div class="statLabel">Active connections</div>
</div>
<div class="statCard"> ${this.renderDnsStats()}
<h3>Memory Usage</h3>
<div class="statValue">${this.formatBytes(this.statsState.serverStats.memoryUsage.heapUsed)}</div>
<div class="statLabel">of ${this.formatBytes(this.statsState.serverStats.memoryUsage.heapTotal)}</div>
</div>
<div class="statCard">
<h3>CPU Usage</h3>
<div class="statValue">${Math.round((this.statsState.serverStats.cpuUsage.user + this.statsState.serverStats.cpuUsage.system) / 2)}%</div>
<div class="statLabel">Average load</div>
</div>
` : ''}
</div>
${this.statsState.emailStats ? html`
<h2>Email Statistics</h2>
<div class="statsGrid">
<div class="statCard">
<h3>Emails Sent</h3>
<div class="statValue">${this.statsState.emailStats.sent}</div>
<div class="statLabel">Total sent</div>
</div>
<div class="statCard">
<h3>Emails Received</h3>
<div class="statValue">${this.statsState.emailStats.received}</div>
<div class="statLabel">Total received</div>
</div>
<div class="statCard">
<h3>Failed Deliveries</h3>
<div class="statValue">${this.statsState.emailStats.failed}</div>
<div class="statLabel">Delivery failures</div>
</div>
<div class="statCard">
<h3>Queued</h3>
<div class="statValue">${this.statsState.emailStats.queued}</div>
<div class="statLabel">In queue</div>
</div>
</div>
` : ''}
${this.statsState.dnsStats ? html`
<h2>DNS Statistics</h2>
<div class="statsGrid">
<div class="statCard">
<h3>DNS Queries</h3>
<div class="statValue">${this.statsState.dnsStats.totalQueries}</div>
<div class="statLabel">Total queries handled</div>
</div>
<div class="statCard">
<h3>Cache Hit Rate</h3>
<div class="statValue">${Math.round(this.statsState.dnsStats.cacheHitRate * 100)}%</div>
<div class="statLabel">Cache efficiency</div>
</div>
</div>
` : ''}
<div class="chartGrid"> <div class="chartGrid">
<dees-chart-area .label=${'Email Traffic (24h)'} .data=${[]}></dees-chart-area> <dees-chart-area .label=${'Email Traffic (24h)'} .data=${[]}></dees-chart-area>
@ -222,4 +134,171 @@ export class OpsViewOverview extends DeesElement {
return `${size.toFixed(1)} ${units[unitIndex]}`; 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`
<dees-statsgrid
.tiles=${tiles}
.gridActions=${[
{
name: 'Refresh',
iconName: 'arrowsRotate',
action: async () => {
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
},
},
]}
></dees-statsgrid>
`;
}
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`
<h2>Email Statistics</h2>
<dees-statsgrid .tiles=${tiles}></dees-statsgrid>
`;
}
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`
<h2>DNS Statistics</h2>
<dees-statsgrid .tiles=${tiles}></dees-statsgrid>
`;
}
} }

View File

@ -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`
<ops-sectionheading>Statistics</ops-sectionheading>
<div class="controls">
<div class="refreshButton">
<dees-button
@click=${() => appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null)}
.disabled=${this.statsState.isLoading}
>
${this.statsState.isLoading ? html`<dees-spinner size="small"></dees-spinner>` : 'Refresh'}
</dees-button>
<dees-button
@click=${() => appstate.uiStatePart.dispatchAction(appstate.toggleAutoRefreshAction, null)}
.type=${this.uiState.autoRefresh ? 'highlighted' : 'normal'}
>
Auto-refresh: ${this.uiState.autoRefresh ? 'ON' : 'OFF'}
</dees-button>
</div>
<div class="lastUpdated">
${this.statsState.lastUpdated ? html`
Last updated: ${new Date(this.statsState.lastUpdated).toLocaleTimeString()}
` : ''}
</div>
</div>
${this.statsState.serverStats ? html`
<div class="statsSection">
<h2 class="sectionTitle">Server Metrics</h2>
<div class="metricsGrid">
<div class="metricCard">
<div class="metricLabel">Uptime</div>
<div class="metricValue">${this.formatUptime(this.statsState.serverStats.uptime)}</div>
</div>
<div class="metricCard">
<div class="metricLabel">CPU Usage</div>
<div class="metricValue">${Math.round((this.statsState.serverStats.cpuUsage.user + this.statsState.serverStats.cpuUsage.system) / 2)}<span class="metricUnit">%</span></div>
</div>
<div class="metricCard">
<div class="metricLabel">Memory Used</div>
<div class="metricValue">${this.formatBytes(this.statsState.serverStats.memoryUsage.heapUsed)}</div>
</div>
<div class="metricCard">
<div class="metricLabel">Active Connections</div>
<div class="metricValue">${this.statsState.serverStats.activeConnections}</div>
</div>
</div>
<div class="chartContainer">
<dees-chart-area
.label=${'Server Performance (Last 24 Hours)'}
.data=${[]}
></dees-chart-area>
</div>
</div>
` : ''}
${this.statsState.emailStats ? html`
<div class="statsSection">
<h2 class="sectionTitle">Email Statistics</h2>
<dees-table
.heading1=${'Email Metrics'}
.heading2=${'Current statistics for email processing'}
.data=${[
{ metric: 'Total Sent', value: this.statsState.emailStats.sent, unit: 'emails' },
{ metric: 'Total Received', value: this.statsState.emailStats.received, unit: 'emails' },
{ metric: 'Failed Deliveries', value: this.statsState.emailStats.failed, unit: 'emails' },
{ metric: 'Currently Queued', value: this.statsState.emailStats.queued, unit: 'emails' },
{ metric: 'Average Delivery Time', value: this.statsState.emailStats.averageDeliveryTime, unit: 'ms' },
{ metric: 'Delivery Rate', value: `${Math.round(this.statsState.emailStats.deliveryRate * 100)}`, unit: '%' },
]}
.displayFunction=${(item) => ({
Metric: item.metric,
Value: `${item.value} ${item.unit}`,
})}
></dees-table>
</div>
` : ''}
${this.statsState.dnsStats ? html`
<div class="statsSection">
<h2 class="sectionTitle">DNS Statistics</h2>
<div class="metricsGrid">
<div class="metricCard">
<div class="metricLabel">Total Queries</div>
<div class="metricValue">${this.formatNumber(this.statsState.dnsStats.totalQueries)}</div>
</div>
<div class="metricCard">
<div class="metricLabel">Cache Hit Rate</div>
<div class="metricValue">${Math.round(this.statsState.dnsStats.cacheHitRate * 100)}<span class="metricUnit">%</span></div>
</div>
<div class="metricCard">
<div class="metricLabel">Average Response Time</div>
<div class="metricValue">${this.statsState.dnsStats.averageResponseTime}<span class="metricUnit">ms</span></div>
</div>
<div class="metricCard">
<div class="metricLabel">Domains Configured</div>
<div class="metricValue">${this.statsState.dnsStats.activeDomains}</div>
</div>
</div>
</div>
` : ''}
${this.statsState.securityMetrics ? html`
<div class="statsSection">
<h2 class="sectionTitle">Security Metrics</h2>
<dees-table
.heading1=${'Security Events'}
.heading2=${'Recent security-related activities'}
.data=${[
{ metric: 'Blocked IPs', value: this.statsState.securityMetrics.blockedIPs.length, severity: 'high' },
{ metric: 'Failed Authentications', value: this.statsState.securityMetrics.authenticationFailures, severity: 'medium' },
{ metric: 'Spam Detected', value: this.statsState.securityMetrics.spamDetected, severity: 'low' },
{ metric: 'Spam Detected', value: this.statsState.securityMetrics.spamDetected, severity: 'medium' },
{ metric: 'Malware Detected', value: this.statsState.securityMetrics.malwareDetected, severity: 'high' },
{ metric: 'Phishing Detected', value: this.statsState.securityMetrics.phishingDetected, severity: 'high' },
]}
.displayFunction=${(item) => ({
'Security Metric': item.metric,
'Count': item.value,
'Severity': item.severity,
})}
></dees-table>
</div>
` : ''}
`;
}
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();
}
}