Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b835e2d0eb | |||
| 6c3d8714a2 | |||
| 94f53f0259 | |||
| 1004f8579f | |||
| a77ec6884a | |||
| 6112e4e884 | |||
| 4a6913d4bb | |||
| f6a9e344e5 | |||
| b3296c6522 | |||
| 10a2b922d3 | |||
| ee5cdde225 | |||
| d2e9efccd0 | |||
| a07901a28a | |||
| a3954d6eb5 | |||
| 9685fcd89d | |||
| 74c23ce5ff | |||
| 746fbb15e6 | |||
| 415065b246 | |||
| 30aeef7bbd | |||
| dba1c70fa7 | |||
| f9cfb3d36b | |||
| 43b92b784d | |||
| b62a322c54 | |||
| a3a64e9a02 | |||
| 491e51f40b | |||
| b46247d9cb |
78
changelog.md
78
changelog.md
@@ -1,5 +1,83 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-04-04 - 12.9.0 - feat(monitoring)
|
||||
add frontend and backend protocol distribution metrics to network stats
|
||||
|
||||
- Expose frontend and backend protocol distribution data in monitoring metrics, stats responses, and shared interfaces.
|
||||
- Render protocol distribution donut charts in the ops network view using the new stats fields.
|
||||
- Preserve existing stored certificate IDs when updating certificate records by domain.
|
||||
- Bump @design.estate/dees-catalog to ^3.55.5 for the new chart component support.
|
||||
|
||||
## 2026-04-04 - 12.8.1 - fix(ops-view-routes)
|
||||
correct route form dropdown selection handling for security profiles and network targets
|
||||
|
||||
- Update route edit and create forms to use selectedOption for dropdowns backed by the newer dees-catalog version
|
||||
- Normalize submitted dropdown values to extract option keys before storing securityProfileRef and networkTargetRef
|
||||
- Refresh documentation to reflect expanded stats coverage for network, RADIUS, and VPN metrics
|
||||
|
||||
## 2026-04-03 - 12.8.0 - feat(certificates)
|
||||
add force renew option for domain certificate reprovisioning
|
||||
|
||||
- pass an optional forceRenew flag through certificate reprovision requests from the UI to the ops handler
|
||||
- use smartacme forceRenew support and return renewal-specific success messages
|
||||
- update the SmartAcme dependency to version ^9.4.0
|
||||
|
||||
## 2026-04-03 - 12.7.0 - feat(opsserver)
|
||||
add RADIUS and VPN metrics to combined ops stats and overview dashboards, and stream live log buffer entries in follow mode
|
||||
|
||||
- Expose RADIUS and VPN sections in the combined stats API and shared TypeScript interfaces
|
||||
- Populate frontend app state and overview tiles with RADIUS authentication, session, traffic, and VPN client metrics
|
||||
- Replace simulated follow-mode log events with real log buffer tailing and timestamp-based incremental streaming
|
||||
- Use commit metadata for reported server version instead of a hardcoded value
|
||||
|
||||
## 2026-04-03 - 12.6.6 - fix(deps)
|
||||
bump @design.estate/dees-catalog to ^3.52.3
|
||||
|
||||
- Updates @design.estate/dees-catalog from ^3.52.2 to ^3.52.3 in package.json
|
||||
|
||||
## 2026-04-03 - 12.6.5 - fix(deps)
|
||||
bump @design.estate/dees-catalog to ^3.52.2
|
||||
|
||||
- Updates the @design.estate/dees-catalog dependency from ^3.52.0 to ^3.52.2 in package.json.
|
||||
|
||||
## 2026-04-03 - 12.6.4 - fix(deps)
|
||||
bump @design.estate/dees-catalog to ^3.52.0
|
||||
|
||||
- Updates the @design.estate/dees-catalog dependency from ^3.51.2 to ^3.52.0 in package.json.
|
||||
|
||||
## 2026-04-03 - 12.6.3 - fix(deps)
|
||||
bump @types/node and @design.estate/dees-catalog patch versions
|
||||
|
||||
- updates @types/node from ^25.5.1 to ^25.5.2
|
||||
- updates @design.estate/dees-catalog from ^3.51.1 to ^3.51.2
|
||||
|
||||
## 2026-04-03 - 12.6.2 - fix(deps)
|
||||
bump @design.estate/dees-catalog to ^3.51.1
|
||||
|
||||
- Updates @design.estate/dees-catalog from ^3.51.0 to ^3.51.1 in package.json
|
||||
|
||||
## 2026-04-03 - 12.6.1 - fix(repo)
|
||||
no changes to commit
|
||||
|
||||
|
||||
## 2026-04-03 - 12.6.0 - feat(certificates)
|
||||
add confirmation before force renewing valid certificates from the certificate actions menu
|
||||
|
||||
- Expose the Reprovision action in the certificate context menu
|
||||
- Prompt for confirmation when reprovisioning a certificate that is still valid
|
||||
- Update dees-catalog and @types/node dependencies
|
||||
|
||||
## 2026-04-03 - 12.5.2 - fix(repo)
|
||||
no changes to commit
|
||||
|
||||
|
||||
## 2026-04-03 - 12.5.1 - fix(ops-view-network)
|
||||
centralize traffic chart timing constants for consistent rolling window updates
|
||||
|
||||
- Defines shared constants for the chart window, update interval, and maximum buffered data points
|
||||
- Replaces hardcoded traffic history sizes and timer intervals with derived values across initialization, history loading, and live updates
|
||||
- Keeps the chart rolling window configuration aligned with the in-memory traffic buffer
|
||||
|
||||
## 2026-04-02 - 12.5.0 - feat(ops-view-routes)
|
||||
add priority support and list-based domain editing for routes
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@serve.zone/dcrouter",
|
||||
"private": false,
|
||||
"version": "12.5.0",
|
||||
"version": "12.9.0",
|
||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -27,7 +27,7 @@
|
||||
"@git.zone/tsrun": "^2.0.2",
|
||||
"@git.zone/tstest": "^3.6.3",
|
||||
"@git.zone/tswatch": "^3.3.2",
|
||||
"@types/node": "^25.5.0"
|
||||
"@types/node": "^25.5.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@api.global/typedrequest": "^3.3.0",
|
||||
@@ -35,12 +35,12 @@
|
||||
"@api.global/typedserver": "^8.4.6",
|
||||
"@api.global/typedsocket": "^4.1.2",
|
||||
"@apiclient.xyz/cloudflare": "^7.1.0",
|
||||
"@design.estate/dees-catalog": "^3.50.2",
|
||||
"@design.estate/dees-catalog": "^3.55.5",
|
||||
"@design.estate/dees-element": "^2.2.4",
|
||||
"@push.rocks/lik": "^6.4.0",
|
||||
"@push.rocks/projectinfo": "^5.1.0",
|
||||
"@push.rocks/qenv": "^6.1.3",
|
||||
"@push.rocks/smartacme": "^9.3.1",
|
||||
"@push.rocks/smartacme": "^9.4.0",
|
||||
"@push.rocks/smartdata": "^7.1.3",
|
||||
"@push.rocks/smartdb": "^2.1.1",
|
||||
"@push.rocks/smartdns": "^7.9.0",
|
||||
|
||||
164
pnpm-lock.yaml
generated
164
pnpm-lock.yaml
generated
@@ -24,8 +24,8 @@ importers:
|
||||
specifier: ^7.1.0
|
||||
version: 7.1.0
|
||||
'@design.estate/dees-catalog':
|
||||
specifier: ^3.50.2
|
||||
version: 3.50.2(@tiptap/pm@2.27.2)
|
||||
specifier: ^3.55.5
|
||||
version: 3.55.5(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-element':
|
||||
specifier: ^2.2.4
|
||||
version: 2.2.4
|
||||
@@ -39,8 +39,8 @@ importers:
|
||||
specifier: ^6.1.3
|
||||
version: 6.1.3
|
||||
'@push.rocks/smartacme':
|
||||
specifier: ^9.3.1
|
||||
version: 9.3.1(socks@2.8.7)
|
||||
specifier: ^9.4.0
|
||||
version: 9.4.0(socks@2.8.7)
|
||||
'@push.rocks/smartdata':
|
||||
specifier: ^7.1.3
|
||||
version: 7.1.3(socks@2.8.7)
|
||||
@@ -142,8 +142,8 @@ importers:
|
||||
specifier: ^3.3.2
|
||||
version: 3.3.2(@tiptap/pm@2.27.2)
|
||||
'@types/node':
|
||||
specifier: ^25.5.0
|
||||
version: 25.5.0
|
||||
specifier: ^25.5.2
|
||||
version: 25.5.2
|
||||
|
||||
packages:
|
||||
|
||||
@@ -350,8 +350,8 @@ packages:
|
||||
'@configvault.io/interfaces@1.0.17':
|
||||
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
|
||||
|
||||
'@design.estate/dees-catalog@3.50.2':
|
||||
resolution: {integrity: sha512-oxB1kB3IxEwHgf+DjytTBilkDVVb8hryq465OhhzgBiJiHaNLPyBASAQaNTVp6eaORQGzyCmy/ac/GdQglZiIg==}
|
||||
'@design.estate/dees-catalog@3.55.5':
|
||||
resolution: {integrity: sha512-NAMUkTVqdZZmwI/g1xKOxOYM9QUd9FHODh6MYkP6LhLjD0NOGh3bITCnNN9Z3x8/mI7vQQOlSe9tyTtxCP1itQ==}
|
||||
|
||||
'@design.estate/dees-comms@1.0.30':
|
||||
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
|
||||
@@ -1108,8 +1108,8 @@ packages:
|
||||
'@push.rocks/qenv@6.1.3':
|
||||
resolution: {integrity: sha512-+z2hsAU/7CIgpYLFqvda8cn9rUBMHqLdQLjsFfRn5jPoD7dJ5rFlpkbhfM4Ws8mHMniwWaxGKo+q/YBhtzRBLg==}
|
||||
|
||||
'@push.rocks/smartacme@9.3.1':
|
||||
resolution: {integrity: sha512-Cl1DVQ+rfpaYkk6VVm/KYVeUYzWfXzSfTXybHfCZ5SuiACuTVHZ6jK8TouELaV1RgrdYnIp0MrbiY2Kqi8ayAw==}
|
||||
'@push.rocks/smartacme@9.4.0':
|
||||
resolution: {integrity: sha512-mSqsI859mHI9fCZxLfayzPf/WvukDFzVHOh02vXq3ujxbb5M+ArMnXe0MmC2egR9GeXmQTm3DTENaETX5ffMtw==}
|
||||
|
||||
'@push.rocks/smartarchive@4.2.4':
|
||||
resolution: {integrity: sha512-uiqVAXPxmr8G5rv3uZvZFMOCt8l7cZC3nzvsy4YQqKf/VkPhKIEX+b7LkAeNlxPSYUiBQUkNRoawg9+5BaMcHg==}
|
||||
@@ -2050,11 +2050,11 @@ packages:
|
||||
'@types/node@18.19.130':
|
||||
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
|
||||
|
||||
'@types/node@22.19.15':
|
||||
resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
|
||||
'@types/node@22.19.17':
|
||||
resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==}
|
||||
|
||||
'@types/node@25.5.0':
|
||||
resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==}
|
||||
'@types/node@25.5.2':
|
||||
resolution: {integrity: sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==}
|
||||
|
||||
'@types/qrcode@1.5.6':
|
||||
resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==}
|
||||
@@ -2151,9 +2151,6 @@ packages:
|
||||
any-base@1.1.0:
|
||||
resolution: {integrity: sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==}
|
||||
|
||||
apexcharts@5.10.4:
|
||||
resolution: {integrity: sha512-gt0VUqZ2+mr25ScbUcKZgJr96jKYm4vjOcxEWCEh/E5F4dWqhyo3dBhPRvNNnkKiWxkMd2cBwj3ZYH3rK39fkA==}
|
||||
|
||||
argparse@1.0.10:
|
||||
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
|
||||
|
||||
@@ -2519,6 +2516,9 @@ packages:
|
||||
ecdsa-sig-formatter@1.0.11:
|
||||
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
||||
|
||||
echarts@5.6.0:
|
||||
resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==}
|
||||
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
@@ -2628,6 +2628,9 @@ packages:
|
||||
resolution: {integrity: sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
fancy-canvas@2.1.0:
|
||||
resolution: {integrity: sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ==}
|
||||
|
||||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
@@ -2866,8 +2869,8 @@ packages:
|
||||
humanize-ms@1.2.1:
|
||||
resolution: {integrity: sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=}
|
||||
|
||||
ibantools@4.5.1:
|
||||
resolution: {integrity: sha512-DfKQpLlFq9yEUIEnFuCJzss3XavD7iHZTU5PyqXiAJ+rmaMp+NFP3hboumHKuK8nZjuOJg93WemTzcQ5b9jOZA==}
|
||||
ibantools@4.5.2:
|
||||
resolution: {integrity: sha512-is+8TgZcKS/AMv/z9nW1zz0bhjhoyjpA1p0nc3A6GkW/InOdcQiUZpkufADzh/aO/LY/TOD/P3oPWncNRn5QMA==}
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||
@@ -3025,6 +3028,9 @@ packages:
|
||||
libqp@2.1.1:
|
||||
resolution: {integrity: sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow==}
|
||||
|
||||
lightweight-charts@5.1.0:
|
||||
resolution: {integrity: sha512-jEAYR4ODYeyNZcWUigsoLTl52rbPmgXnvd5FLIv/ZoA/2sSDw63YKnef8n4yhzum7W926yHeFwlm7ididKb7YQ==}
|
||||
|
||||
lines-and-columns@1.2.4:
|
||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||
|
||||
@@ -3694,11 +3700,11 @@ packages:
|
||||
prosemirror-state: ^1.4.2
|
||||
prosemirror-view: ^1.33.8
|
||||
|
||||
prosemirror-transform@1.11.0:
|
||||
resolution: {integrity: sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==}
|
||||
prosemirror-transform@1.12.0:
|
||||
resolution: {integrity: sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==}
|
||||
|
||||
prosemirror-view@1.41.7:
|
||||
resolution: {integrity: sha512-jUwKNCEIGiqdvhlS91/2QAg21e4dfU5bH2iwmSDQeosXJgKF7smG0YSplOWK0cjSNgIqXe7VXqo7EIfUFJdt3w==}
|
||||
prosemirror-view@1.41.8:
|
||||
resolution: {integrity: sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==}
|
||||
|
||||
proto-list@1.2.4:
|
||||
resolution: {integrity: sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=}
|
||||
@@ -4065,6 +4071,9 @@ packages:
|
||||
tslib@1.14.1:
|
||||
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
|
||||
|
||||
tslib@2.3.0:
|
||||
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
@@ -4312,6 +4321,9 @@ packages:
|
||||
zod@3.25.76:
|
||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||
|
||||
zrender@5.6.1:
|
||||
resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==}
|
||||
|
||||
zwitch@2.0.4:
|
||||
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
||||
|
||||
@@ -4339,7 +4351,7 @@ snapshots:
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@api.global/typedsocket': 4.1.2(@push.rocks/smartserve@2.0.3)
|
||||
'@cloudflare/workers-types': 4.20260317.1
|
||||
'@design.estate/dees-catalog': 3.50.2(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-catalog': 3.55.5(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-comms': 1.0.30
|
||||
'@push.rocks/lik': 6.4.0
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
@@ -4868,7 +4880,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
|
||||
'@design.estate/dees-catalog@3.50.2(@tiptap/pm@2.27.2)':
|
||||
'@design.estate/dees-catalog@3.55.5(@tiptap/pm@2.27.2)':
|
||||
dependencies:
|
||||
'@design.estate/dees-domtools': 2.5.4
|
||||
'@design.estate/dees-element': 2.2.4
|
||||
@@ -4888,9 +4900,10 @@ snapshots:
|
||||
'@tiptap/extension-underline': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))
|
||||
'@tiptap/starter-kit': 2.27.2
|
||||
'@tsclass/tsclass': 9.5.0
|
||||
apexcharts: 5.10.4
|
||||
echarts: 5.6.0
|
||||
highlight.js: 11.11.1
|
||||
ibantools: 4.5.1
|
||||
ibantools: 4.5.2
|
||||
lightweight-charts: 5.1.0
|
||||
lucide: 0.577.0
|
||||
monaco-editor: 0.55.1
|
||||
pdfjs-dist: 4.10.38
|
||||
@@ -5369,7 +5382,7 @@ snapshots:
|
||||
'@inquirer/figures': 1.0.15
|
||||
'@inquirer/type': 2.0.0
|
||||
'@types/mute-stream': 0.0.4
|
||||
'@types/node': 22.19.15
|
||||
'@types/node': 22.19.17
|
||||
'@types/wrap-ansi': 3.0.0
|
||||
ansi-escapes: 4.3.2
|
||||
cli-width: 4.1.0
|
||||
@@ -5957,7 +5970,7 @@ snapshots:
|
||||
'@push.rocks/smartlog': 3.2.1
|
||||
'@push.rocks/smartpath': 6.0.0
|
||||
|
||||
'@push.rocks/smartacme@9.3.1(socks@2.8.7)':
|
||||
'@push.rocks/smartacme@9.4.0(socks@2.8.7)':
|
||||
dependencies:
|
||||
'@apiclient.xyz/cloudflare': 7.1.0
|
||||
'@peculiar/x509': 2.0.0
|
||||
@@ -6906,7 +6919,7 @@ snapshots:
|
||||
|
||||
'@serve.zone/catalog@2.11.0(@tiptap/pm@2.27.2)':
|
||||
dependencies:
|
||||
'@design.estate/dees-catalog': 3.50.2(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-catalog': 3.55.5(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-domtools': 2.5.4
|
||||
'@design.estate/dees-element': 2.2.4
|
||||
'@design.estate/dees-wcctools': 3.8.0
|
||||
@@ -7398,9 +7411,9 @@ snapshots:
|
||||
prosemirror-schema-list: 1.5.1
|
||||
prosemirror-state: 1.4.4
|
||||
prosemirror-tables: 1.8.5
|
||||
prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.7)
|
||||
prosemirror-transform: 1.11.0
|
||||
prosemirror-view: 1.41.7
|
||||
prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8)
|
||||
prosemirror-transform: 1.12.0
|
||||
prosemirror-view: 1.41.8
|
||||
|
||||
'@tiptap/starter-kit@2.27.2':
|
||||
dependencies:
|
||||
@@ -7454,7 +7467,7 @@ snapshots:
|
||||
|
||||
'@types/clean-css@4.2.11':
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.5.2
|
||||
source-map: 0.6.1
|
||||
|
||||
'@types/debug@4.1.13':
|
||||
@@ -7464,7 +7477,7 @@ snapshots:
|
||||
'@types/fs-extra@11.0.4':
|
||||
dependencies:
|
||||
'@types/jsonfile': 6.1.4
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.5.2
|
||||
|
||||
'@types/hast@3.0.4':
|
||||
dependencies:
|
||||
@@ -7484,12 +7497,12 @@ snapshots:
|
||||
|
||||
'@types/jsonfile@6.1.4':
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.5.2
|
||||
|
||||
'@types/jsonwebtoken@9.0.10':
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.5.2
|
||||
|
||||
'@types/linkify-it@5.0.0': {}
|
||||
|
||||
@@ -7510,16 +7523,16 @@ snapshots:
|
||||
|
||||
'@types/mute-stream@0.0.4':
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.5.2
|
||||
|
||||
'@types/node-fetch@2.6.13':
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.5.2
|
||||
form-data: 4.0.5
|
||||
|
||||
'@types/node-forge@1.3.14':
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.5.2
|
||||
|
||||
'@types/node@16.9.1': {}
|
||||
|
||||
@@ -7527,17 +7540,17 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 5.26.5
|
||||
|
||||
'@types/node@22.19.15':
|
||||
'@types/node@22.19.17':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/node@25.5.0':
|
||||
'@types/node@25.5.2':
|
||||
dependencies:
|
||||
undici-types: 7.18.2
|
||||
|
||||
'@types/qrcode@1.5.6':
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.5.2
|
||||
|
||||
'@types/randomatic@3.1.5': {}
|
||||
|
||||
@@ -7547,11 +7560,11 @@ snapshots:
|
||||
|
||||
'@types/tar-stream@3.1.4':
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.5.2
|
||||
|
||||
'@types/through2@2.0.41':
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.5.2
|
||||
|
||||
'@types/trusted-types@2.0.7': {}
|
||||
|
||||
@@ -7581,11 +7594,11 @@ snapshots:
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.5.2
|
||||
|
||||
'@types/yauzl@2.10.3':
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.5.2
|
||||
optional: true
|
||||
|
||||
'@ungap/structured-clone@1.3.0': {}
|
||||
@@ -7630,8 +7643,6 @@ snapshots:
|
||||
|
||||
any-base@1.1.0: {}
|
||||
|
||||
apexcharts@5.10.4: {}
|
||||
|
||||
argparse@1.0.10:
|
||||
dependencies:
|
||||
sprintf-js: 1.0.3
|
||||
@@ -7977,6 +7988,11 @@ snapshots:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
echarts@5.6.0:
|
||||
dependencies:
|
||||
tslib: 2.3.0
|
||||
zrender: 5.6.1
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
encoding-japanese@2.2.0: {}
|
||||
@@ -8093,6 +8109,8 @@ snapshots:
|
||||
|
||||
fake-indexeddb@6.2.5: {}
|
||||
|
||||
fancy-canvas@2.1.0: {}
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
fast-fifo@1.3.2: {}
|
||||
@@ -8407,7 +8425,7 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
ibantools@4.5.1: {}
|
||||
ibantools@4.5.2: {}
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
dependencies:
|
||||
@@ -8589,6 +8607,10 @@ snapshots:
|
||||
|
||||
libqp@2.1.1: {}
|
||||
|
||||
lightweight-charts@5.1.0:
|
||||
dependencies:
|
||||
fancy-canvas: 2.1.0
|
||||
|
||||
lines-and-columns@1.2.4: {}
|
||||
|
||||
linkify-it@5.0.0:
|
||||
@@ -9341,7 +9363,7 @@ snapshots:
|
||||
|
||||
prosemirror-changeset@2.4.0:
|
||||
dependencies:
|
||||
prosemirror-transform: 1.11.0
|
||||
prosemirror-transform: 1.12.0
|
||||
|
||||
prosemirror-collab@1.3.1:
|
||||
dependencies:
|
||||
@@ -9351,32 +9373,32 @@ snapshots:
|
||||
dependencies:
|
||||
prosemirror-model: 1.25.4
|
||||
prosemirror-state: 1.4.4
|
||||
prosemirror-transform: 1.11.0
|
||||
prosemirror-transform: 1.12.0
|
||||
|
||||
prosemirror-dropcursor@1.8.2:
|
||||
dependencies:
|
||||
prosemirror-state: 1.4.4
|
||||
prosemirror-transform: 1.11.0
|
||||
prosemirror-view: 1.41.7
|
||||
prosemirror-transform: 1.12.0
|
||||
prosemirror-view: 1.41.8
|
||||
|
||||
prosemirror-gapcursor@1.4.1:
|
||||
dependencies:
|
||||
prosemirror-keymap: 1.2.3
|
||||
prosemirror-model: 1.25.4
|
||||
prosemirror-state: 1.4.4
|
||||
prosemirror-view: 1.41.7
|
||||
prosemirror-view: 1.41.8
|
||||
|
||||
prosemirror-history@1.5.0:
|
||||
dependencies:
|
||||
prosemirror-state: 1.4.4
|
||||
prosemirror-transform: 1.11.0
|
||||
prosemirror-view: 1.41.7
|
||||
prosemirror-transform: 1.12.0
|
||||
prosemirror-view: 1.41.8
|
||||
rope-sequence: 1.3.4
|
||||
|
||||
prosemirror-inputrules@1.5.1:
|
||||
dependencies:
|
||||
prosemirror-state: 1.4.4
|
||||
prosemirror-transform: 1.11.0
|
||||
prosemirror-transform: 1.12.0
|
||||
|
||||
prosemirror-keymap@1.2.3:
|
||||
dependencies:
|
||||
@@ -9408,39 +9430,39 @@ snapshots:
|
||||
dependencies:
|
||||
prosemirror-model: 1.25.4
|
||||
prosemirror-state: 1.4.4
|
||||
prosemirror-transform: 1.11.0
|
||||
prosemirror-transform: 1.12.0
|
||||
|
||||
prosemirror-state@1.4.4:
|
||||
dependencies:
|
||||
prosemirror-model: 1.25.4
|
||||
prosemirror-transform: 1.11.0
|
||||
prosemirror-view: 1.41.7
|
||||
prosemirror-transform: 1.12.0
|
||||
prosemirror-view: 1.41.8
|
||||
|
||||
prosemirror-tables@1.8.5:
|
||||
dependencies:
|
||||
prosemirror-keymap: 1.2.3
|
||||
prosemirror-model: 1.25.4
|
||||
prosemirror-state: 1.4.4
|
||||
prosemirror-transform: 1.11.0
|
||||
prosemirror-view: 1.41.7
|
||||
prosemirror-transform: 1.12.0
|
||||
prosemirror-view: 1.41.8
|
||||
|
||||
prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.7):
|
||||
prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8):
|
||||
dependencies:
|
||||
'@remirror/core-constants': 3.0.0
|
||||
escape-string-regexp: 4.0.0
|
||||
prosemirror-model: 1.25.4
|
||||
prosemirror-state: 1.4.4
|
||||
prosemirror-view: 1.41.7
|
||||
prosemirror-view: 1.41.8
|
||||
|
||||
prosemirror-transform@1.11.0:
|
||||
prosemirror-transform@1.12.0:
|
||||
dependencies:
|
||||
prosemirror-model: 1.25.4
|
||||
|
||||
prosemirror-view@1.41.7:
|
||||
prosemirror-view@1.41.8:
|
||||
dependencies:
|
||||
prosemirror-model: 1.25.4
|
||||
prosemirror-state: 1.4.4
|
||||
prosemirror-transform: 1.11.0
|
||||
prosemirror-transform: 1.12.0
|
||||
|
||||
proto-list@1.2.4: {}
|
||||
|
||||
@@ -9935,6 +9957,8 @@ snapshots:
|
||||
|
||||
tslib@1.14.1: {}
|
||||
|
||||
tslib@2.3.0: {}
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
tsx@4.21.0:
|
||||
@@ -10162,4 +10186,8 @@ snapshots:
|
||||
|
||||
zod@3.25.76: {}
|
||||
|
||||
zrender@5.6.1:
|
||||
dependencies:
|
||||
tslib: 2.3.0
|
||||
|
||||
zwitch@2.0.4: {}
|
||||
|
||||
@@ -25,7 +25,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
||||
- [Remote Ingress](#remote-ingress)
|
||||
- [VPN Access Control](#vpn-access-control)
|
||||
- [Certificate Management](#certificate-management)
|
||||
- [Storage & Caching](#storage--caching)
|
||||
- [Storage & Database](#storage--database)
|
||||
- [Security Features](#security-features)
|
||||
- [OpsServer Dashboard](#opsserver-dashboard)
|
||||
- [API Client](#api-client)
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '12.5.0',
|
||||
version: '12.9.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -29,9 +29,9 @@ export class StorageBackedCertManager implements plugins.smartacme.ICertManager
|
||||
let doc = await AcmeCertDoc.findByDomain(cert.domainName);
|
||||
if (!doc) {
|
||||
doc = new AcmeCertDoc();
|
||||
doc.id = cert.id;
|
||||
doc.domainName = cert.domainName;
|
||||
}
|
||||
doc.id = cert.id;
|
||||
doc.created = cert.created;
|
||||
doc.privateKey = cert.privateKey;
|
||||
doc.publicKey = cert.publicKey;
|
||||
|
||||
@@ -591,6 +591,11 @@ export class MetricsManager {
|
||||
const requestsPerSecond = proxyMetrics.requests.perSecond();
|
||||
const requestsTotal = proxyMetrics.requests.total();
|
||||
|
||||
// Get frontend/backend protocol distribution (available in SmartProxy >= next release)
|
||||
const conn = proxyMetrics.connections as any;
|
||||
const frontendProtocols = conn.frontendProtocols?.() ?? null;
|
||||
const backendProtocols = conn.backendProtocols?.() ?? null;
|
||||
|
||||
// Collect backend protocol data
|
||||
const backendMetrics = proxyMetrics.backends.byBackend();
|
||||
const protocolCache = proxyMetrics.backends.detectedProtocols();
|
||||
@@ -705,6 +710,8 @@ export class MetricsManager {
|
||||
requestsPerSecond,
|
||||
requestsTotal,
|
||||
backends,
|
||||
frontendProtocols,
|
||||
backendProtocols,
|
||||
};
|
||||
}, 1000); // 1s cache — matches typical dashboard poll interval
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export class CertificateHandler {
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificateDomain>(
|
||||
'reprovisionCertificateDomain',
|
||||
async (dataArg) => {
|
||||
return this.reprovisionCertificateDomain(dataArg.domain);
|
||||
return this.reprovisionCertificateDomain(dataArg.domain, dataArg.forceRenew);
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -318,7 +318,7 @@ export class CertificateHandler {
|
||||
/**
|
||||
* Domain-based reprovisioning — clears backoff first, then triggers provision
|
||||
*/
|
||||
private async reprovisionCertificateDomain(domain: string): Promise<{ success: boolean; message?: string }> {
|
||||
private async reprovisionCertificateDomain(domain: string, forceRenew?: boolean): Promise<{ success: boolean; message?: string }> {
|
||||
const dcRouter = this.opsServerRef.dcRouterRef;
|
||||
const smartProxy = dcRouter.smartProxy;
|
||||
|
||||
@@ -337,8 +337,8 @@ export class CertificateHandler {
|
||||
// Try to provision via SmartAcme directly
|
||||
if (dcRouter.smartAcme) {
|
||||
try {
|
||||
await dcRouter.smartAcme.getCertificateForDomain(domain);
|
||||
return { success: true, message: `Certificate reprovisioning triggered for domain '${domain}'` };
|
||||
await dcRouter.smartAcme.getCertificateForDomain(domain, { forceRenew: forceRenew ?? false });
|
||||
return { success: true, message: forceRenew ? `Certificate force-renewed for domain '${domain}'` : `Certificate reprovisioning triggered for domain '${domain}'` };
|
||||
} catch (err: unknown) {
|
||||
return { success: false, message: (err as Error).message || `Failed to reprovision certificate for ${domain}` };
|
||||
}
|
||||
|
||||
@@ -255,7 +255,7 @@ export class LogsHandler {
|
||||
} {
|
||||
let intervalId: NodeJS.Timeout | null = null;
|
||||
let stopped = false;
|
||||
let logIndex = 0;
|
||||
let lastTimestamp = Date.now();
|
||||
|
||||
const stop = () => {
|
||||
stopped = true;
|
||||
@@ -284,53 +284,65 @@ export class LogsHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// For follow mode, simulate real-time log streaming
|
||||
// For follow mode, tail real log entries from the in-memory buffer
|
||||
intervalId = setInterval(async () => {
|
||||
if (stopped) {
|
||||
// Guard: clear interval if stop() was called between ticks
|
||||
clearInterval(intervalId!);
|
||||
intervalId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const categories: Array<'smtp' | 'dns' | 'security' | 'system' | 'email'> = ['smtp', 'dns', 'security', 'system', 'email'];
|
||||
const levels: Array<'debug' | 'info' | 'warn' | 'error'> = ['info', 'warn', 'error', 'debug'];
|
||||
// Fetch new entries since last poll
|
||||
const rawEntries = logBuffer.getEntries({
|
||||
since: lastTimestamp,
|
||||
limit: 50,
|
||||
});
|
||||
|
||||
const mockCategory = categories[Math.floor(Math.random() * categories.length)];
|
||||
const mockLevel = levels[Math.floor(Math.random() * levels.length)];
|
||||
if (rawEntries.length === 0) return;
|
||||
|
||||
// Filter by requested criteria
|
||||
if (levelFilter && !levelFilter.includes(mockLevel)) return;
|
||||
if (categoryFilter && !categoryFilter.includes(mockCategory)) return;
|
||||
for (const raw of rawEntries) {
|
||||
const mappedLevel = LogsHandler.mapLogLevel(raw.level);
|
||||
const mappedCategory = LogsHandler.deriveCategory(
|
||||
(raw as any).context?.zone,
|
||||
raw.message,
|
||||
);
|
||||
|
||||
const logEntry = {
|
||||
timestamp: Date.now(),
|
||||
level: mockLevel,
|
||||
category: mockCategory,
|
||||
message: `Real-time log ${logIndex++} from ${mockCategory}`,
|
||||
metadata: {
|
||||
requestId: plugins.uuid.v4(),
|
||||
},
|
||||
};
|
||||
// Apply filters
|
||||
if (levelFilter && !levelFilter.includes(mappedLevel)) continue;
|
||||
if (categoryFilter && !categoryFilter.includes(mappedCategory)) continue;
|
||||
|
||||
const logData = JSON.stringify(logEntry);
|
||||
const encoder = new TextEncoder();
|
||||
try {
|
||||
// Use a timeout to detect hung streams (sendData can hang if the
|
||||
// VirtualStream's keepAlive loop has ended)
|
||||
let timeoutHandle: ReturnType<typeof setTimeout>;
|
||||
await Promise.race([
|
||||
virtualStream.sendData(encoder.encode(logData)).then((result) => {
|
||||
clearTimeout(timeoutHandle);
|
||||
return result;
|
||||
}),
|
||||
new Promise<never>((_, reject) => {
|
||||
timeoutHandle = setTimeout(() => reject(new Error('stream send timeout')), 10_000);
|
||||
}),
|
||||
]);
|
||||
} catch {
|
||||
// Stream closed, errored, or timed out — clean up
|
||||
stop();
|
||||
const logEntry = {
|
||||
timestamp: raw.timestamp || Date.now(),
|
||||
level: mappedLevel,
|
||||
category: mappedCategory,
|
||||
message: raw.message,
|
||||
metadata: (raw as any).data,
|
||||
};
|
||||
|
||||
const logData = JSON.stringify(logEntry);
|
||||
const encoder = new TextEncoder();
|
||||
try {
|
||||
let timeoutHandle: ReturnType<typeof setTimeout>;
|
||||
await Promise.race([
|
||||
virtualStream.sendData(encoder.encode(logData)).then((result) => {
|
||||
clearTimeout(timeoutHandle);
|
||||
return result;
|
||||
}),
|
||||
new Promise<never>((_, reject) => {
|
||||
timeoutHandle = setTimeout(() => reject(new Error('stream send timeout')), 10_000);
|
||||
}),
|
||||
]);
|
||||
} catch {
|
||||
// Stream closed, errored, or timed out — clean up
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Advance the watermark past all entries we just processed
|
||||
const newest = rawEntries[rawEntries.length - 1];
|
||||
if (newest.timestamp && newest.timestamp >= lastTimestamp) {
|
||||
lastTimestamp = newest.timestamp + 1;
|
||||
}
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { OpsServer } from '../classes.opsserver.js';
|
||||
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||
import { MetricsManager } from '../../monitoring/index.js';
|
||||
import { SecurityLogger } from '../../security/classes.securitylogger.js';
|
||||
import { commitinfo } from '../../00_commitinfo_data.js';
|
||||
|
||||
export class StatsHandler {
|
||||
constructor(private opsServerRef: OpsServer) {
|
||||
@@ -158,7 +159,7 @@ export class StatsHandler {
|
||||
};
|
||||
return acc;
|
||||
}, {} as any),
|
||||
version: '2.12.0', // TODO: Get from package.json
|
||||
version: commitinfo.version,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -310,11 +311,53 @@ export class StatsHandler {
|
||||
requestsPerSecond: stats.requestsPerSecond || 0,
|
||||
requestsTotal: stats.requestsTotal || 0,
|
||||
backends: stats.backends || [],
|
||||
frontendProtocols: stats.frontendProtocols || null,
|
||||
backendProtocols: stats.backendProtocols || null,
|
||||
};
|
||||
})()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (sections.radius) {
|
||||
promises.push(
|
||||
(async () => {
|
||||
const radiusServer = this.opsServerRef.dcRouterRef.radiusServer;
|
||||
if (!radiusServer) return;
|
||||
const stats = radiusServer.getStats();
|
||||
const accountingStats = radiusServer.getAccountingManager().getStats();
|
||||
metrics.radius = {
|
||||
running: stats.running,
|
||||
uptime: stats.uptime,
|
||||
authRequests: stats.authRequests,
|
||||
authAccepts: stats.authAccepts,
|
||||
authRejects: stats.authRejects,
|
||||
accountingRequests: stats.accountingRequests,
|
||||
activeSessions: stats.activeSessions,
|
||||
totalInputBytes: accountingStats.totalInputBytes,
|
||||
totalOutputBytes: accountingStats.totalOutputBytes,
|
||||
};
|
||||
})()
|
||||
);
|
||||
}
|
||||
|
||||
if (sections.vpn) {
|
||||
promises.push(
|
||||
(async () => {
|
||||
const vpnManager = this.opsServerRef.dcRouterRef.vpnManager;
|
||||
const vpnConfig = this.opsServerRef.dcRouterRef.options.vpnConfig;
|
||||
if (!vpnManager) return;
|
||||
const connected = await vpnManager.getConnectedClients();
|
||||
metrics.vpn = {
|
||||
running: vpnManager.running,
|
||||
subnet: vpnManager.getSubnet(),
|
||||
registeredClients: vpnManager.listClients().length,
|
||||
connectedClients: connected.length,
|
||||
wgListenPort: vpnConfig?.wgListenPort ?? 51820,
|
||||
};
|
||||
})()
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
return {
|
||||
|
||||
@@ -166,6 +166,21 @@ export interface INetworkMetrics {
|
||||
requestsPerSecond?: number;
|
||||
requestsTotal?: number;
|
||||
backends?: IBackendInfo[];
|
||||
frontendProtocols?: IProtocolDistribution | null;
|
||||
backendProtocols?: IProtocolDistribution | null;
|
||||
}
|
||||
|
||||
export interface IProtocolDistribution {
|
||||
h1Active: number;
|
||||
h1Total: number;
|
||||
h2Active: number;
|
||||
h2Total: number;
|
||||
h3Active: number;
|
||||
h3Total: number;
|
||||
wsActive: number;
|
||||
wsTotal: number;
|
||||
otherActive: number;
|
||||
otherTotal: number;
|
||||
}
|
||||
|
||||
export interface IConnectionDetails {
|
||||
@@ -197,4 +212,24 @@ export interface IBackendInfo {
|
||||
h3ConsecutiveFailures: number | null;
|
||||
h3Port: number | null;
|
||||
cacheAgeSecs: number | null;
|
||||
}
|
||||
|
||||
export interface IRadiusStats {
|
||||
running: boolean;
|
||||
uptime: number;
|
||||
authRequests: number;
|
||||
authAccepts: number;
|
||||
authRejects: number;
|
||||
accountingRequests: number;
|
||||
activeSessions: number;
|
||||
totalInputBytes: number;
|
||||
totalOutputBytes: number;
|
||||
}
|
||||
|
||||
export interface IVpnStats {
|
||||
running: boolean;
|
||||
subnet: string;
|
||||
registeredClients: number;
|
||||
connectedClients: number;
|
||||
wgListenPort: number;
|
||||
}
|
||||
@@ -80,6 +80,8 @@ interface IIdentity {
|
||||
| `IQueueStatus` | Queue name, size, processing/failed/retrying counts |
|
||||
| `IHealthStatus` | Healthy flag, uptime, per-service status map |
|
||||
| `INetworkMetrics` | Bandwidth, connection counts, top endpoints |
|
||||
| `IRadiusStats` | Running, uptime, auth requests/accepts/rejects, sessions, data transfer |
|
||||
| `IVpnStats` | Running, subnet, registered/connected clients, WireGuard port |
|
||||
| `ILogEntry` | Timestamp, level, category, message, metadata |
|
||||
|
||||
#### Route Management Interfaces
|
||||
@@ -135,7 +137,8 @@ TypedRequest interfaces for the OpsServer API, organized by domain:
|
||||
| `IReq_GetActiveConnections` | `getActiveConnections` | Active connection list |
|
||||
| `IReq_GetQueueStatus` | `getQueueStatus` | Email queue status |
|
||||
| `IReq_GetHealthStatus` | `getHealthStatus` | System health check |
|
||||
| `IReq_GetCombinedMetrics` | `getCombinedMetrics` | All metrics in one request |
|
||||
| `IReq_GetNetworkStats` | `getNetworkStats` | Network throughput and connection analytics |
|
||||
| `IReq_GetCombinedMetrics` | `getCombinedMetrics` | All metrics in one request (server, email, DNS, security, network, RADIUS, VPN) |
|
||||
|
||||
#### ⚙️ Configuration
|
||||
| Interface | Method | Description |
|
||||
|
||||
@@ -68,6 +68,7 @@ export interface IReq_ReprovisionCertificateDomain extends plugins.typedrequestI
|
||||
request: {
|
||||
identity: authInterfaces.IIdentity;
|
||||
domain: string;
|
||||
forceRenew?: boolean;
|
||||
};
|
||||
response: {
|
||||
success: boolean;
|
||||
|
||||
@@ -10,6 +10,8 @@ export interface IReq_GetCombinedMetrics {
|
||||
dns?: boolean;
|
||||
security?: boolean;
|
||||
network?: boolean;
|
||||
radius?: boolean;
|
||||
vpn?: boolean;
|
||||
};
|
||||
};
|
||||
response: {
|
||||
@@ -19,6 +21,8 @@ export interface IReq_GetCombinedMetrics {
|
||||
dns?: data.IDnsStats;
|
||||
security?: data.ISecurityMetrics;
|
||||
network?: data.INetworkMetrics;
|
||||
radius?: data.IRadiusStats;
|
||||
vpn?: data.IVpnStats;
|
||||
};
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '12.5.0',
|
||||
version: '12.9.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ export interface IStatsState {
|
||||
emailStats: interfaces.data.IEmailStats | null;
|
||||
dnsStats: interfaces.data.IDnsStats | null;
|
||||
securityMetrics: interfaces.data.ISecurityMetrics | null;
|
||||
radiusStats: interfaces.data.IRadiusStats | null;
|
||||
vpnStats: interfaces.data.IVpnStats | null;
|
||||
lastUpdated: number;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
@@ -54,6 +56,8 @@ export interface INetworkState {
|
||||
requestsPerSecond: number;
|
||||
requestsTotal: number;
|
||||
backends: interfaces.data.IBackendInfo[];
|
||||
frontendProtocols: interfaces.data.IProtocolDistribution | null;
|
||||
backendProtocols: interfaces.data.IProtocolDistribution | null;
|
||||
lastUpdated: number;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
@@ -91,6 +95,8 @@ export const statsStatePart = await appState.getStatePart<IStatsState>(
|
||||
emailStats: null,
|
||||
dnsStats: null,
|
||||
securityMetrics: null,
|
||||
radiusStats: null,
|
||||
vpnStats: null,
|
||||
lastUpdated: 0,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
@@ -150,6 +156,8 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
|
||||
requestsPerSecond: 0,
|
||||
requestsTotal: 0,
|
||||
backends: [],
|
||||
frontendProtocols: null,
|
||||
backendProtocols: null,
|
||||
lastUpdated: 0,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
@@ -319,6 +327,8 @@ export const fetchAllStatsAction = statsStatePart.createAction(async (statePartA
|
||||
dns: true,
|
||||
security: true,
|
||||
network: false, // Network is fetched separately for the network view
|
||||
radius: true,
|
||||
vpn: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -328,6 +338,8 @@ export const fetchAllStatsAction = statsStatePart.createAction(async (statePartA
|
||||
emailStats: combinedResponse.metrics.email || currentState.emailStats,
|
||||
dnsStats: combinedResponse.metrics.dns || currentState.dnsStats,
|
||||
securityMetrics: combinedResponse.metrics.security || currentState.securityMetrics,
|
||||
radiusStats: combinedResponse.metrics.radius || currentState.radiusStats,
|
||||
vpnStats: combinedResponse.metrics.vpn || currentState.vpnStats,
|
||||
lastUpdated: Date.now(),
|
||||
isLoading: false,
|
||||
error: null,
|
||||
@@ -515,6 +527,8 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
||||
requestsPerSecond: networkStatsResponse.requestsPerSecond || 0,
|
||||
requestsTotal: networkStatsResponse.requestsTotal || 0,
|
||||
backends: networkStatsResponse.backends || [],
|
||||
frontendProtocols: networkStatsResponse.frontendProtocols || null,
|
||||
backendProtocols: networkStatsResponse.backendProtocols || null,
|
||||
lastUpdated: Date.now(),
|
||||
isLoading: false,
|
||||
error: null,
|
||||
@@ -597,8 +611,8 @@ export const fetchCertificateOverviewAction = certificateStatePart.createAction(
|
||||
}
|
||||
});
|
||||
|
||||
export const reprovisionCertificateAction = certificateStatePart.createAction<string>(
|
||||
async (statePartArg, domain, actionContext): Promise<ICertificateState> => {
|
||||
export const reprovisionCertificateAction = certificateStatePart.createAction<{ domain: string; forceRenew?: boolean }>(
|
||||
async (statePartArg, dataArg, actionContext): Promise<ICertificateState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState()!;
|
||||
|
||||
@@ -609,7 +623,8 @@ export const reprovisionCertificateAction = certificateStatePart.createAction<st
|
||||
|
||||
await request.fire({
|
||||
identity: context.identity!,
|
||||
domain,
|
||||
domain: dataArg.domain,
|
||||
forceRenew: dataArg.forceRenew,
|
||||
});
|
||||
|
||||
// Re-fetch overview after reprovisioning
|
||||
@@ -1781,6 +1796,8 @@ async function dispatchCombinedRefreshActionInner() {
|
||||
dns: true,
|
||||
security: true,
|
||||
network: currentView === 'network', // Only fetch network if on network view
|
||||
radius: true,
|
||||
vpn: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1792,6 +1809,8 @@ async function dispatchCombinedRefreshActionInner() {
|
||||
emailStats: combinedResponse.metrics.email || currentStatsState.emailStats,
|
||||
dnsStats: combinedResponse.metrics.dns || currentStatsState.dnsStats,
|
||||
securityMetrics: combinedResponse.metrics.security || currentStatsState.securityMetrics,
|
||||
radiusStats: combinedResponse.metrics.radius || currentStatsState.radiusStats,
|
||||
vpnStats: combinedResponse.metrics.vpn || currentStatsState.vpnStats,
|
||||
lastUpdated: Date.now(),
|
||||
isLoading: false,
|
||||
error: null,
|
||||
@@ -1832,6 +1851,8 @@ async function dispatchCombinedRefreshActionInner() {
|
||||
requestsPerSecond: network.requestsPerSecond || 0,
|
||||
requestsTotal: network.requestsTotal || 0,
|
||||
backends: network.backends || [],
|
||||
frontendProtocols: network.frontendProtocols || null,
|
||||
backendProtocols: network.backendProtocols || null,
|
||||
lastUpdated: Date.now(),
|
||||
isLoading: false,
|
||||
error: null,
|
||||
@@ -1853,6 +1874,8 @@ async function dispatchCombinedRefreshActionInner() {
|
||||
requestsPerSecond: network.requestsPerSecond || 0,
|
||||
requestsTotal: network.requestsTotal || 0,
|
||||
backends: network.backends || [],
|
||||
frontendProtocols: network.frontendProtocols || null,
|
||||
backendProtocols: network.backendProtocols || null,
|
||||
lastUpdated: Date.now(),
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
@@ -299,7 +299,7 @@ export class OpsViewCertificates extends DeesElement {
|
||||
{
|
||||
name: 'Reprovision',
|
||||
iconName: 'lucide:RefreshCw',
|
||||
type: ['inRow'],
|
||||
type: ['inRow', 'contextmenu'],
|
||||
actionFunc: async (actionData: { item: interfaces.requests.ICertificateInfo }) => {
|
||||
const cert = actionData.item;
|
||||
if (!cert.canReprovision) {
|
||||
@@ -311,16 +311,41 @@ export class OpsViewCertificates extends DeesElement {
|
||||
});
|
||||
return;
|
||||
}
|
||||
await appstate.certificateStatePart.dispatchAction(
|
||||
appstate.reprovisionCertificateAction,
|
||||
cert.domain,
|
||||
);
|
||||
const { DeesToast } = await import('@design.estate/dees-catalog');
|
||||
DeesToast.show({
|
||||
message: `Reprovisioning triggered for ${cert.domain}`,
|
||||
type: 'success',
|
||||
duration: 3000,
|
||||
});
|
||||
|
||||
const doReprovision = async (forceRenew = false) => {
|
||||
await appstate.certificateStatePart.dispatchAction(
|
||||
appstate.reprovisionCertificateAction,
|
||||
{ domain: cert.domain, forceRenew },
|
||||
);
|
||||
const { DeesToast } = await import('@design.estate/dees-catalog');
|
||||
DeesToast.show({
|
||||
message: forceRenew
|
||||
? `Force renewal triggered for ${cert.domain}`
|
||||
: `Reprovisioning triggered for ${cert.domain}`,
|
||||
type: 'success',
|
||||
duration: 3000,
|
||||
});
|
||||
};
|
||||
|
||||
if (cert.status === 'valid') {
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
DeesModal.createAndShow({
|
||||
heading: 'Certificate Still Valid',
|
||||
content: html`<p style="margin: 0; line-height: 1.5;">The certificate for <strong>${cert.domain}</strong> is still valid${cert.expiryDate ? ` until ${new Date(cert.expiryDate).toLocaleDateString()}` : ''}. Do you want to force renew it now?</p>`,
|
||||
menuOptions: [
|
||||
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||
{
|
||||
name: 'Force Renew',
|
||||
action: async (modalArg: any) => {
|
||||
await modalArg.destroy();
|
||||
await doReprovision(true);
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
await doReprovision();
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -28,6 +28,13 @@ interface INetworkRequest {
|
||||
|
||||
@customElement('ops-view-network')
|
||||
export class OpsViewNetwork extends DeesElement {
|
||||
/** How far back the traffic chart shows */
|
||||
private static readonly CHART_WINDOW_MS = 5 * 60 * 1000; // 5 minutes
|
||||
/** How often a new data point is added */
|
||||
private static readonly UPDATE_INTERVAL_MS = 1000; // 1 second
|
||||
/** Derived: max data points the buffer holds */
|
||||
private static readonly MAX_DATA_POINTS = OpsViewNetwork.CHART_WINDOW_MS / OpsViewNetwork.UPDATE_INTERVAL_MS;
|
||||
|
||||
@state()
|
||||
accessor statsState = appstate.statsStatePart.getState()!;
|
||||
|
||||
@@ -46,7 +53,7 @@ export class OpsViewNetwork extends DeesElement {
|
||||
|
||||
// Track if we need to update the chart to avoid unnecessary re-renders
|
||||
private lastChartUpdate = 0;
|
||||
private chartUpdateThreshold = 1000; // Minimum ms between chart updates
|
||||
private chartUpdateThreshold = OpsViewNetwork.UPDATE_INTERVAL_MS; // Minimum ms between chart updates
|
||||
|
||||
private trafficUpdateTimer: any = null;
|
||||
private requestsPerSecHistory: number[] = []; // Track requests/sec over time for trend
|
||||
@@ -104,13 +111,11 @@ export class OpsViewNetwork extends DeesElement {
|
||||
|
||||
private initializeTrafficData() {
|
||||
const now = Date.now();
|
||||
// Fixed 5 minute time range
|
||||
const range = 5 * 60 * 1000; // 5 minutes
|
||||
const bucketSize = range / 60; // 60 data points
|
||||
const { MAX_DATA_POINTS, UPDATE_INTERVAL_MS } = OpsViewNetwork;
|
||||
|
||||
// Initialize with empty data points for both in and out
|
||||
const emptyData = Array.from({ length: 60 }, (_, i) => {
|
||||
const time = now - ((59 - i) * bucketSize);
|
||||
const emptyData = Array.from({ length: MAX_DATA_POINTS }, (_, i) => {
|
||||
const time = now - ((MAX_DATA_POINTS - 1 - i) * UPDATE_INTERVAL_MS);
|
||||
return {
|
||||
x: new Date(time).toISOString(),
|
||||
y: 0,
|
||||
@@ -143,23 +148,23 @@ export class OpsViewNetwork extends DeesElement {
|
||||
y: Math.round((p.out * 8) / 1000000 * 10) / 10,
|
||||
}));
|
||||
|
||||
// Use history as the chart data, keeping the most recent 60 points (5 min window)
|
||||
const sliceStart = Math.max(0, historyIn.length - 60);
|
||||
const { MAX_DATA_POINTS, UPDATE_INTERVAL_MS } = OpsViewNetwork;
|
||||
|
||||
// Use history as the chart data, keeping the most recent points within the window
|
||||
const sliceStart = Math.max(0, historyIn.length - MAX_DATA_POINTS);
|
||||
this.trafficDataIn = historyIn.slice(sliceStart);
|
||||
this.trafficDataOut = historyOut.slice(sliceStart);
|
||||
|
||||
// If fewer than 60 points, pad the front with zeros
|
||||
if (this.trafficDataIn.length < 60) {
|
||||
// If fewer than MAX_DATA_POINTS, pad the front with zeros
|
||||
if (this.trafficDataIn.length < MAX_DATA_POINTS) {
|
||||
const now = Date.now();
|
||||
const range = 5 * 60 * 1000;
|
||||
const bucketSize = range / 60;
|
||||
const padCount = 60 - this.trafficDataIn.length;
|
||||
const padCount = MAX_DATA_POINTS - this.trafficDataIn.length;
|
||||
const firstTimestamp = this.trafficDataIn.length > 0
|
||||
? new Date(this.trafficDataIn[0].x).getTime()
|
||||
: now;
|
||||
|
||||
const padIn = Array.from({ length: padCount }, (_, i) => ({
|
||||
x: new Date(firstTimestamp - ((padCount - i) * bucketSize)).toISOString(),
|
||||
x: new Date(firstTimestamp - ((padCount - i) * UPDATE_INTERVAL_MS)).toISOString(),
|
||||
y: 0,
|
||||
}));
|
||||
const padOut = padIn.map(p => ({ ...p }));
|
||||
@@ -269,6 +274,12 @@ export class OpsViewNetwork extends DeesElement {
|
||||
background: ${cssManager.bdTheme('#fff3e0', '#3a2a1a')};
|
||||
color: ${cssManager.bdTheme('#f57c00', '#ff9933')};
|
||||
}
|
||||
|
||||
.protocolChartGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -296,10 +307,13 @@ export class OpsViewNetwork extends DeesElement {
|
||||
}
|
||||
]}
|
||||
.realtimeMode=${true}
|
||||
.rollingWindow=${300000}
|
||||
.rollingWindow=${OpsViewNetwork.CHART_WINDOW_MS}
|
||||
.yAxisFormatter=${(val: number) => `${val} Mbit/s`}
|
||||
></dees-chart-area>
|
||||
|
||||
<!-- Protocol Distribution Charts -->
|
||||
${this.renderProtocolCharts()}
|
||||
|
||||
<!-- Top IPs Section -->
|
||||
${this.renderTopIPs()}
|
||||
|
||||
@@ -522,7 +536,54 @@ export class OpsViewNetwork extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
private renderProtocolCharts(): TemplateResult {
|
||||
const fp = this.networkState.frontendProtocols;
|
||||
const bp = this.networkState.backendProtocols;
|
||||
|
||||
const protoColors: Record<string, string> = {
|
||||
'HTTP/1.1': '#1976d2',
|
||||
'HTTP/2': '#388e3c',
|
||||
'HTTP/3': '#7b1fa2',
|
||||
'WebSocket': '#f57c00',
|
||||
'Other': '#757575',
|
||||
};
|
||||
|
||||
const buildDonutData = (dist: interfaces.data.IProtocolDistribution | null) => {
|
||||
if (!dist) return [];
|
||||
const items: Array<{ name: string; value: number; color: string }> = [];
|
||||
if (dist.h1Active > 0) items.push({ name: 'HTTP/1.1', value: dist.h1Active, color: protoColors['HTTP/1.1'] });
|
||||
if (dist.h2Active > 0) items.push({ name: 'HTTP/2', value: dist.h2Active, color: protoColors['HTTP/2'] });
|
||||
if (dist.h3Active > 0) items.push({ name: 'HTTP/3', value: dist.h3Active, color: protoColors['HTTP/3'] });
|
||||
if (dist.wsActive > 0) items.push({ name: 'WebSocket', value: dist.wsActive, color: protoColors['WebSocket'] });
|
||||
if (dist.otherActive > 0) items.push({ name: 'Other', value: dist.otherActive, color: protoColors['Other'] });
|
||||
return items;
|
||||
};
|
||||
|
||||
const frontendData = buildDonutData(fp);
|
||||
const backendData = buildDonutData(bp);
|
||||
|
||||
return html`
|
||||
<div class="protocolChartGrid">
|
||||
<dees-chart-donut
|
||||
.label=${'Frontend Protocols'}
|
||||
.data=${frontendData.length > 0 ? frontendData : [{ name: 'No Traffic', value: 1, color: '#757575' }]}
|
||||
.showLegend=${true}
|
||||
.showLabels=${true}
|
||||
.innerRadiusPercent=${'55%'}
|
||||
.valueFormatter=${(val: number) => `${val} active`}
|
||||
></dees-chart-donut>
|
||||
<dees-chart-donut
|
||||
.label=${'Backend Protocols'}
|
||||
.data=${backendData.length > 0 ? backendData : [{ name: 'No Traffic', value: 1, color: '#757575' }]}
|
||||
.showLegend=${true}
|
||||
.showLabels=${true}
|
||||
.innerRadiusPercent=${'55%'}
|
||||
.valueFormatter=${(val: number) => `${val} active`}
|
||||
></dees-chart-donut>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderTopIPs(): TemplateResult {
|
||||
if (this.networkState.topIPs.length === 0) {
|
||||
return html``;
|
||||
@@ -709,9 +770,8 @@ export class OpsViewNetwork extends DeesElement {
|
||||
private startTrafficUpdateTimer() {
|
||||
this.stopTrafficUpdateTimer(); // Clear any existing timer
|
||||
this.trafficUpdateTimer = setInterval(() => {
|
||||
// Add a new data point every second
|
||||
this.addTrafficDataPoint();
|
||||
}, 1000); // Update every second
|
||||
}, OpsViewNetwork.UPDATE_INTERVAL_MS);
|
||||
}
|
||||
|
||||
private addTrafficDataPoint() {
|
||||
@@ -742,7 +802,7 @@ export class OpsViewNetwork extends DeesElement {
|
||||
};
|
||||
|
||||
// In-place mutation then reassign for Lit reactivity (avoids 4 intermediate arrays)
|
||||
if (this.trafficDataIn.length >= 60) {
|
||||
if (this.trafficDataIn.length >= OpsViewNetwork.MAX_DATA_POINTS) {
|
||||
this.trafficDataIn.shift();
|
||||
this.trafficDataOut.shift();
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ export class OpsViewOverview extends DeesElement {
|
||||
emailStats: null,
|
||||
dnsStats: null,
|
||||
securityMetrics: null,
|
||||
radiusStats: null,
|
||||
vpnStats: null,
|
||||
lastUpdated: 0,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
@@ -117,6 +119,10 @@ export class OpsViewOverview extends DeesElement {
|
||||
|
||||
${this.renderDnsStats()}
|
||||
|
||||
${this.renderRadiusStats()}
|
||||
|
||||
${this.renderVpnStats()}
|
||||
|
||||
<div class="chartGrid">
|
||||
<dees-chart-area
|
||||
.label=${'Email Traffic (24h)'}
|
||||
@@ -378,6 +384,97 @@ export class OpsViewOverview extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderRadiusStats(): TemplateResult {
|
||||
if (!this.statsState.radiusStats) return html``;
|
||||
|
||||
const stats = this.statsState.radiusStats;
|
||||
const authTotal = stats.authRequests || 0;
|
||||
const acceptRate = authTotal > 0 ? ((stats.authAccepts / authTotal) * 100).toFixed(1) : '0.0';
|
||||
|
||||
const tiles: IStatsTile[] = [
|
||||
{
|
||||
id: 'radiusStatus',
|
||||
title: 'RADIUS Status',
|
||||
value: stats.running ? 'Running' : 'Stopped',
|
||||
type: 'text',
|
||||
icon: 'lucide:ShieldCheck',
|
||||
color: stats.running ? '#22c55e' : '#ef4444',
|
||||
description: stats.running ? `Uptime: ${this.formatUptime(stats.uptime / 1000)}` : undefined,
|
||||
},
|
||||
{
|
||||
id: 'authRequests',
|
||||
title: 'Auth Requests',
|
||||
value: stats.authRequests,
|
||||
type: 'number',
|
||||
icon: 'lucide:KeyRound',
|
||||
color: '#3b82f6',
|
||||
description: `Accept rate: ${acceptRate}% (${stats.authAccepts} / ${stats.authRejects} rejected)`,
|
||||
},
|
||||
{
|
||||
id: 'activeSessions',
|
||||
title: 'Active Sessions',
|
||||
value: stats.activeSessions,
|
||||
type: 'number',
|
||||
icon: 'lucide:Users',
|
||||
color: '#8b5cf6',
|
||||
},
|
||||
{
|
||||
id: 'radiusTraffic',
|
||||
title: 'Data Transfer',
|
||||
value: this.formatBytes(stats.totalInputBytes + stats.totalOutputBytes),
|
||||
type: 'text',
|
||||
icon: 'lucide:ArrowLeftRight',
|
||||
color: '#f59e0b',
|
||||
description: `In: ${this.formatBytes(stats.totalInputBytes)} / Out: ${this.formatBytes(stats.totalOutputBytes)}`,
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<h2>RADIUS Statistics</h2>
|
||||
<dees-statsgrid .tiles=${tiles}></dees-statsgrid>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderVpnStats(): TemplateResult {
|
||||
if (!this.statsState.vpnStats) return html``;
|
||||
|
||||
const stats = this.statsState.vpnStats;
|
||||
|
||||
const tiles: IStatsTile[] = [
|
||||
{
|
||||
id: 'vpnStatus',
|
||||
title: 'VPN Status',
|
||||
value: stats.running ? 'Running' : 'Stopped',
|
||||
type: 'text',
|
||||
icon: 'lucide:Shield',
|
||||
color: stats.running ? '#22c55e' : '#ef4444',
|
||||
description: `Subnet: ${stats.subnet}`,
|
||||
},
|
||||
{
|
||||
id: 'connectedClients',
|
||||
title: 'Connected Clients',
|
||||
value: stats.connectedClients,
|
||||
type: 'number',
|
||||
icon: 'lucide:Wifi',
|
||||
color: '#3b82f6',
|
||||
description: `${stats.registeredClients} registered`,
|
||||
},
|
||||
{
|
||||
id: 'wgPort',
|
||||
title: 'WireGuard Port',
|
||||
value: stats.wgListenPort,
|
||||
type: 'number',
|
||||
icon: 'lucide:Network',
|
||||
color: '#8b5cf6',
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<h2>VPN Statistics</h2>
|
||||
<dees-statsgrid .tiles=${tiles}></dees-statsgrid>
|
||||
`;
|
||||
}
|
||||
|
||||
// --- Chart data helpers ---
|
||||
|
||||
private getRecentEventEntries(): Array<{ timestamp: string; level: 'debug' | 'info' | 'warn' | 'error' | 'success'; message: string; source?: string }> {
|
||||
|
||||
@@ -431,8 +431,8 @@ export class OpsViewRoutes extends DeesElement {
|
||||
<dees-input-text .key=${'ports'} .label=${'Ports (comma-separated)'} .value=${currentPorts} .required=${true}></dees-input-text>
|
||||
<dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'Add domain...'} .value=${currentDomains}></dees-input-list>
|
||||
<dees-input-text .key=${'priority'} .label=${'Priority (higher = matched first)'} .value=${route.priority != null ? String(route.priority) : ''}></dees-input-text>
|
||||
<dees-input-dropdown .key=${'securityProfileRef'} .label=${'Security Profile'} .options=${profileOptions} .selectedKey=${merged.metadata?.securityProfileRef || ''}></dees-input-dropdown>
|
||||
<dees-input-dropdown .key=${'networkTargetRef'} .label=${'Network Target'} .options=${targetOptions} .selectedKey=${merged.metadata?.networkTargetRef || ''}></dees-input-dropdown>
|
||||
<dees-input-dropdown .key=${'securityProfileRef'} .label=${'Security Profile'} .options=${profileOptions} .selectedOption=${profileOptions.find((o) => o.key === (merged.metadata?.securityProfileRef || '')) || null}></dees-input-dropdown>
|
||||
<dees-input-dropdown .key=${'networkTargetRef'} .label=${'Network Target'} .options=${targetOptions} .selectedOption=${targetOptions.find((o) => o.key === (merged.metadata?.networkTargetRef || '')) || null}></dees-input-dropdown>
|
||||
<dees-input-text .key=${'targetHost'} .label=${'Target Host (if no target selected)'} .value=${currentTargetHost}></dees-input-text>
|
||||
<dees-input-text .key=${'targetPort'} .label=${'Target Port (if no target selected)'} .value=${currentTargetPort}></dees-input-text>
|
||||
</dees-form>
|
||||
@@ -477,11 +477,15 @@ export class OpsViewRoutes extends DeesElement {
|
||||
};
|
||||
|
||||
const metadata: any = {};
|
||||
if (formData.securityProfileRef) {
|
||||
metadata.securityProfileRef = formData.securityProfileRef;
|
||||
const profileRefValue = formData.securityProfileRef as any;
|
||||
const profileKey = typeof profileRefValue === 'string' ? profileRefValue : profileRefValue?.key;
|
||||
if (profileKey) {
|
||||
metadata.securityProfileRef = profileKey;
|
||||
}
|
||||
if (formData.networkTargetRef) {
|
||||
metadata.networkTargetRef = formData.networkTargetRef;
|
||||
const targetRefValue = formData.networkTargetRef as any;
|
||||
const targetKey = typeof targetRefValue === 'string' ? targetRefValue : targetRefValue?.key;
|
||||
if (targetKey) {
|
||||
metadata.networkTargetRef = targetKey;
|
||||
}
|
||||
|
||||
await appstate.routeManagementStatePart.dispatchAction(
|
||||
@@ -528,8 +532,8 @@ export class OpsViewRoutes extends DeesElement {
|
||||
<dees-input-text .key=${'ports'} .label=${'Ports (comma-separated)'} .required=${true}></dees-input-text>
|
||||
<dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'Add domain...'}></dees-input-list>
|
||||
<dees-input-text .key=${'priority'} .label=${'Priority (higher = matched first)'}></dees-input-text>
|
||||
<dees-input-dropdown .key=${'securityProfileRef'} .label=${'Security Profile'} .options=${profileOptions} .selectedKey=${''}></dees-input-dropdown>
|
||||
<dees-input-dropdown .key=${'networkTargetRef'} .label=${'Network Target'} .options=${targetOptions} .selectedKey=${''}></dees-input-dropdown>
|
||||
<dees-input-dropdown .key=${'securityProfileRef'} .label=${'Security Profile'} .options=${profileOptions}></dees-input-dropdown>
|
||||
<dees-input-dropdown .key=${'networkTargetRef'} .label=${'Network Target'} .options=${targetOptions}></dees-input-dropdown>
|
||||
<dees-input-text .key=${'targetHost'} .label=${'Target Host (if no target selected)'} .value=${'localhost'}></dees-input-text>
|
||||
<dees-input-text .key=${'targetPort'} .label=${'Target Port (if no target selected)'}></dees-input-text>
|
||||
</dees-form>
|
||||
@@ -575,11 +579,15 @@ export class OpsViewRoutes extends DeesElement {
|
||||
|
||||
// Build metadata if profile/target selected
|
||||
const metadata: any = {};
|
||||
if (formData.securityProfileRef) {
|
||||
metadata.securityProfileRef = formData.securityProfileRef;
|
||||
const profileRefValue = formData.securityProfileRef as any;
|
||||
const profileKey = typeof profileRefValue === 'string' ? profileRefValue : profileRefValue?.key;
|
||||
if (profileKey) {
|
||||
metadata.securityProfileRef = profileKey;
|
||||
}
|
||||
if (formData.networkTargetRef) {
|
||||
metadata.networkTargetRef = formData.networkTargetRef;
|
||||
const targetRefValue = formData.networkTargetRef as any;
|
||||
const targetKey = typeof targetRefValue === 'string' ? targetRefValue : targetRefValue?.key;
|
||||
if (targetKey) {
|
||||
metadata.networkTargetRef = targetKey;
|
||||
}
|
||||
|
||||
await appstate.routeManagementStatePart.dispatchAction(
|
||||
|
||||
@@ -20,6 +20,8 @@ export class OpsViewSecurity extends DeesElement {
|
||||
emailStats: null,
|
||||
dnsStats: null,
|
||||
securityMetrics: null,
|
||||
radiusStats: null,
|
||||
vpnStats: null,
|
||||
lastUpdated: 0,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
@@ -131,7 +131,7 @@ The app uses `@push.rocks/smartstate` v2.3+ with multiple state parts, scheduled
|
||||
| State Part | Mode | Description |
|
||||
|-----------|------|-------------|
|
||||
| `loginStatePart` | Persistent (IndexedDB) | JWT identity and login status |
|
||||
| `statsStatePart` | Soft (memory) | Server, email, DNS, security metrics |
|
||||
| `statsStatePart` | Soft (memory) | Server, email, DNS, security, RADIUS, VPN metrics |
|
||||
| `configStatePart` | Soft | Current system configuration |
|
||||
| `uiStatePart` | Soft | Active view, sidebar, auto-refresh, theme |
|
||||
| `logStatePart` | Soft | Recent logs, streaming status, filters |
|
||||
|
||||
Reference in New Issue
Block a user