Compare commits

...

24 Commits

Author SHA1 Message Date
a77ec6884a v12.8.0
Some checks failed
Docker (tags) / security (push) Failing after 2s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-03 19:08:46 +00:00
6112e4e884 feat(certificates): add force renew option for domain certificate reprovisioning 2026-04-03 19:08:46 +00:00
4a6913d4bb v12.7.0
Some checks failed
Docker (tags) / security (push) Failing after 3s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-03 14:11:17 +00:00
f6a9e344e5 feat(opsserver): add RADIUS and VPN metrics to combined ops stats and overview dashboards, and stream live log buffer entries in follow mode 2026-04-03 14:11:17 +00:00
b3296c6522 v12.6.6
Some checks failed
Docker (tags) / security (push) Failing after 2s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-03 13:53:20 +00:00
10a2b922d3 fix(deps): bump @design.estate/dees-catalog to ^3.52.3 2026-04-03 13:53:20 +00:00
ee5cdde225 v12.6.5
Some checks failed
Docker (tags) / security (push) Failing after 3s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-03 13:40:52 +00:00
d2e9efccd0 fix(deps): bump @design.estate/dees-catalog to ^3.52.2 2026-04-03 13:40:51 +00:00
a07901a28a v12.6.4
Some checks failed
Docker (tags) / security (push) Failing after 2s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-03 13:02:07 +00:00
a3954d6eb5 fix(deps): bump @design.estate/dees-catalog to ^3.52.0 2026-04-03 13:02:07 +00:00
9685fcd89d v12.6.3
Some checks failed
Docker (tags) / security (push) Failing after 3s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-03 12:39:02 +00:00
74c23ce5ff fix(deps): bump @types/node and @design.estate/dees-catalog patch versions 2026-04-03 12:39:02 +00:00
746fbb15e6 v12.6.2
Some checks failed
Docker (tags) / security (push) Failing after 2s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-03 10:48:26 +00:00
415065b246 fix(deps): bump @design.estate/dees-catalog to ^3.51.1 2026-04-03 10:48:26 +00:00
30aeef7bbd v12.6.1
Some checks failed
Docker (tags) / security (push) Failing after 2s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-03 10:15:10 +00:00
dba1c70fa7 fix(repo): no changes to commit 2026-04-03 10:15:10 +00:00
f9cfb3d36b v12.6.0
Some checks failed
Docker (tags) / security (push) Failing after 3s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-03 10:14:52 +00:00
43b92b784d feat(certificates): add confirmation before force renewing valid certificates from the certificate actions menu 2026-04-03 10:14:52 +00:00
b62a322c54 v12.5.2
Some checks failed
Docker (tags) / security (push) Failing after 2s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-03 08:19:02 +00:00
a3a64e9a02 fix(repo): no changes to commit 2026-04-03 08:19:02 +00:00
491e51f40b v12.5.1
Some checks failed
Docker (tags) / security (push) Failing after 3s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-03 08:18:28 +00:00
b46247d9cb fix(ops-view-network): centralize traffic chart timing constants for consistent rolling window updates 2026-04-03 08:18:28 +00:00
9c0e46ff4e v12.5.0
Some checks failed
Docker (tags) / security (push) Failing after 3s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-02 22:55:57 +00:00
f62bc4a526 feat(ops-view-routes): add priority support and list-based domain editing for routes 2026-04-02 22:55:57 +00:00
17 changed files with 459 additions and 158 deletions

View File

@@ -1,5 +1,74 @@
# Changelog
## 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
- Adds a priority field to route create and edit forms so route matching order can be configured.
- Replaces comma-separated domain text input with a list-based domain editor and updates form handling to persist domains as arrays.
## 2026-04-02 - 12.4.0 - feat(routes)
add route edit and delete actions to the ops routes view

View File

@@ -1,7 +1,7 @@
{
"name": "@serve.zone/dcrouter",
"private": false,
"version": "12.4.0",
"version": "12.8.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.52.3",
"@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",

135
pnpm-lock.yaml generated
View File

@@ -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.52.3
version: 3.52.3(@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.52.3':
resolution: {integrity: sha512-4/vybRZQtdkpa3IOZQ/EbL6gGjIMO+430db43RRfdw+HPXird7Jl+oIXz6pDh+rGah2H2+Srb/c+eve46xAhtQ==}
'@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==}
@@ -2628,6 +2625,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==}
@@ -3025,6 +3025,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 +3697,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=}
@@ -4339,7 +4342,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.52.3(@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 +4871,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.52.3(@tiptap/pm@2.27.2)':
dependencies:
'@design.estate/dees-domtools': 2.5.4
'@design.estate/dees-element': 2.2.4
@@ -4888,9 +4891,9 @@ 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
highlight.js: 11.11.1
ibantools: 4.5.1
lightweight-charts: 5.1.0
lucide: 0.577.0
monaco-editor: 0.55.1
pdfjs-dist: 4.10.38
@@ -5369,7 +5372,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 +5960,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 +6909,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.52.3(@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 +7401,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 +7457,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 +7467,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 +7487,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 +7513,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 +7530,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 +7550,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 +7584,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 +7633,6 @@ snapshots:
any-base@1.1.0: {}
apexcharts@5.10.4: {}
argparse@1.0.10:
dependencies:
sprintf-js: 1.0.3
@@ -8093,6 +8094,8 @@ snapshots:
fake-indexeddb@6.2.5: {}
fancy-canvas@2.1.0: {}
fast-deep-equal@3.1.3: {}
fast-fifo@1.3.2: {}
@@ -8589,6 +8592,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 +9348,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 +9358,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 +9415,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: {}

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '12.4.0',
version: '12.8.0',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}

View File

@@ -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}` };
}

View File

@@ -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);
};

View File

@@ -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,
},
};
}
@@ -314,7 +315,47 @@ export class StatsHandler {
})()
);
}
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 {

View File

@@ -197,4 +197,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;
}

View File

@@ -68,6 +68,7 @@ export interface IReq_ReprovisionCertificateDomain extends plugins.typedrequestI
request: {
identity: authInterfaces.IIdentity;
domain: string;
forceRenew?: boolean;
};
response: {
success: boolean;

View File

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

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '12.4.0',
version: '12.8.0',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}

View File

@@ -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;
@@ -91,6 +93,8 @@ export const statsStatePart = await appState.getStatePart<IStatsState>(
emailStats: null,
dnsStats: null,
securityMetrics: null,
radiusStats: null,
vpnStats: null,
lastUpdated: 0,
isLoading: false,
error: null,
@@ -319,6 +323,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 +334,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,
@@ -597,8 +605,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 +617,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 +1790,8 @@ async function dispatchCombinedRefreshActionInner() {
dns: true,
security: true,
network: currentView === 'network', // Only fetch network if on network view
radius: true,
vpn: true,
},
});
@@ -1792,6 +1803,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,

View File

@@ -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();
}
},
},
{

View File

@@ -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 }));
@@ -296,7 +301,7 @@ export class OpsViewNetwork extends DeesElement {
}
]}
.realtimeMode=${true}
.rollingWindow=${300000}
.rollingWindow=${OpsViewNetwork.CHART_WINDOW_MS}
.yAxisFormatter=${(val: number) => `${val} Mbit/s`}
></dees-chart-area>
@@ -709,9 +714,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 +746,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();
}

View File

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

View File

@@ -414,9 +414,9 @@ export class OpsViewRoutes extends DeesElement {
const currentPorts = Array.isArray(route.match.ports)
? route.match.ports.map((p: any) => typeof p === 'number' ? String(p) : `${p.from}-${p.to}`).join(', ')
: String(route.match.ports);
const currentDomains = route.match.domains
? (Array.isArray(route.match.domains) ? route.match.domains.join(', ') : route.match.domains)
: '';
const currentDomains: string[] = route.match.domains
? (Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains])
: [];
const firstTarget = route.action.targets?.[0];
const currentTargetHost = firstTarget
? (Array.isArray(firstTarget.host) ? firstTarget.host[0] : firstTarget.host)
@@ -429,7 +429,8 @@ export class OpsViewRoutes extends DeesElement {
<dees-form>
<dees-input-text .key=${'name'} .label=${'Route Name'} .value=${route.name || ''} .required=${true}></dees-input-text>
<dees-input-text .key=${'ports'} .label=${'Ports (comma-separated)'} .value=${currentPorts} .required=${true}></dees-input-text>
<dees-input-text .key=${'domains'} .label=${'Domains (comma-separated, optional)'} .value=${currentDomains}></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-text .key=${'targetHost'} .label=${'Target Host (if no target selected)'} .value=${currentTargetHost}></dees-input-text>
@@ -452,15 +453,16 @@ export class OpsViewRoutes extends DeesElement {
if (!formData.name || !formData.ports) return;
const ports = formData.ports.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p));
const domains = formData.domains
? formData.domains.split(',').map((d: string) => d.trim()).filter(Boolean)
: undefined;
const domains: string[] = Array.isArray(formData.domains)
? formData.domains.filter(Boolean)
: [];
const priority = formData.priority ? parseInt(formData.priority, 10) : undefined;
const updatedRoute: any = {
name: formData.name,
match: {
ports,
...(domains && domains.length > 0 ? { domains } : {}),
...(domains.length > 0 ? { domains } : {}),
},
action: {
type: 'forward',
@@ -471,6 +473,7 @@ export class OpsViewRoutes extends DeesElement {
},
],
},
...(priority != null && !isNaN(priority) ? { priority } : {}),
};
const metadata: any = {};
@@ -523,7 +526,8 @@ export class OpsViewRoutes extends DeesElement {
<dees-form>
<dees-input-text .key=${'name'} .label=${'Route Name'} .required=${true}></dees-input-text>
<dees-input-text .key=${'ports'} .label=${'Ports (comma-separated)'} .required=${true}></dees-input-text>
<dees-input-text .key=${'domains'} .label=${'Domains (comma-separated, optional)'}></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-text .key=${'targetHost'} .label=${'Target Host (if no target selected)'} .value=${'localhost'}></dees-input-text>
@@ -546,15 +550,16 @@ export class OpsViewRoutes extends DeesElement {
if (!formData.name || !formData.ports) return;
const ports = formData.ports.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p));
const domains = formData.domains
? formData.domains.split(',').map((d: string) => d.trim()).filter(Boolean)
: undefined;
const domains: string[] = Array.isArray(formData.domains)
? formData.domains.filter(Boolean)
: [];
const priority = formData.priority ? parseInt(formData.priority, 10) : undefined;
const route: any = {
name: formData.name,
match: {
ports,
...(domains && domains.length > 0 ? { domains } : {}),
...(domains.length > 0 ? { domains } : {}),
},
action: {
type: 'forward',
@@ -565,6 +570,7 @@ export class OpsViewRoutes extends DeesElement {
},
],
},
...(priority != null && !isNaN(priority) ? { priority } : {}),
};
// Build metadata if profile/target selected

View File

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