Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a6913d4bb | |||
| f6a9e344e5 | |||
| b3296c6522 | |||
| 10a2b922d3 | |||
| ee5cdde225 | |||
| d2e9efccd0 | |||
| a07901a28a | |||
| a3954d6eb5 | |||
| 9685fcd89d | |||
| 74c23ce5ff | |||
| 746fbb15e6 | |||
| 415065b246 | |||
| 30aeef7bbd | |||
| dba1c70fa7 | |||
| f9cfb3d36b | |||
| 43b92b784d | |||
| b62a322c54 | |||
| a3a64e9a02 | |||
| 491e51f40b | |||
| b46247d9cb | |||
| 9c0e46ff4e | |||
| f62bc4a526 | |||
| 8f23600ec1 | |||
| 141f185fbf | |||
| 6f4a5f19e7 | |||
| 9d8354e58f |
78
changelog.md
78
changelog.md
@@ -1,5 +1,83 @@
|
||||
# Changelog
|
||||
|
||||
## 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
|
||||
|
||||
- introduces an update route action in web app state and refreshes merged routes after changes
|
||||
- adds edit and delete handlers with modal-based confirmation and route form inputs for programmatic routes
|
||||
- enables realtime chart window configuration in network and overview dashboards
|
||||
- bumps @serve.zone/catalog to ^2.11.0
|
||||
|
||||
## 2026-04-02 - 12.3.0 - feat(docs,ops-dashboard)
|
||||
document unified database and reusable security profile and network target management
|
||||
|
||||
- Update project and interface documentation to replace separate storage/cache configuration with a unified database model
|
||||
- Document new security profile and network target APIs, data models, and dashboard capabilities
|
||||
- Add a global dashboard warning when the database is disabled so unavailable management features are clearly indicated
|
||||
- Bump @design.estate/dees-catalog and @serve.zone/catalog to support the updated dashboard experience
|
||||
|
||||
## 2026-04-02 - 12.2.6 - fix(ops-ui)
|
||||
improve operations table actions and modal form handling for profiles and network targets
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@serve.zone/dcrouter",
|
||||
"private": false,
|
||||
"version": "12.2.6",
|
||||
"version": "12.7.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,7 +35,7 @@
|
||||
"@api.global/typedserver": "^8.4.6",
|
||||
"@api.global/typedsocket": "^4.1.2",
|
||||
"@apiclient.xyz/cloudflare": "^7.1.0",
|
||||
"@design.estate/dees-catalog": "^3.49.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",
|
||||
@@ -61,7 +61,7 @@
|
||||
"@push.rocks/smartunique": "^3.0.9",
|
||||
"@push.rocks/smartvpn": "1.19.1",
|
||||
"@push.rocks/taskbuffer": "^8.0.2",
|
||||
"@serve.zone/catalog": "^2.9.1",
|
||||
"@serve.zone/catalog": "^2.11.0",
|
||||
"@serve.zone/interfaces": "^5.3.0",
|
||||
"@serve.zone/remoteingress": "^4.15.3",
|
||||
"@tsclass/tsclass": "^9.5.0",
|
||||
|
||||
139
pnpm-lock.yaml
generated
139
pnpm-lock.yaml
generated
@@ -24,8 +24,8 @@ importers:
|
||||
specifier: ^7.1.0
|
||||
version: 7.1.0
|
||||
'@design.estate/dees-catalog':
|
||||
specifier: ^3.49.2
|
||||
version: 3.49.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
|
||||
@@ -102,8 +102,8 @@ importers:
|
||||
specifier: ^8.0.2
|
||||
version: 8.0.2
|
||||
'@serve.zone/catalog':
|
||||
specifier: ^2.9.1
|
||||
version: 2.9.1(@tiptap/pm@2.27.2)
|
||||
specifier: ^2.11.0
|
||||
version: 2.11.0(@tiptap/pm@2.27.2)
|
||||
'@serve.zone/interfaces':
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0
|
||||
@@ -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.49.2':
|
||||
resolution: {integrity: sha512-ChVf5IW/w1WSsfuI3BA1SX2QJFjZljnAvnyPDXnbzTXuOdTgs054p66JwlDca9KM8yBlndwibgAYJfD6/4sONw==}
|
||||
'@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==}
|
||||
@@ -1583,8 +1583,8 @@ packages:
|
||||
'@selderee/plugin-htmlparser2@0.11.0':
|
||||
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
||||
|
||||
'@serve.zone/catalog@2.9.1':
|
||||
resolution: {integrity: sha512-W+4x5O834DiEtcqfVNFrP6qYlj/6c9UTR9oEl2Wthf0R2SN0zC6Hbs16US2kP+mmQBKAIDMQYvTUI9oaXqvcog==}
|
||||
'@serve.zone/catalog@2.11.0':
|
||||
resolution: {integrity: sha512-4DFDewp1PFRhw5P+yQAoAw+i6gG2lfR3h+uPgbNxB5jCfW14eNDXi3nuwTMBQWRHL9jv8o0BokASjV9A0+q66g==}
|
||||
|
||||
'@serve.zone/interfaces@5.3.0':
|
||||
resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==}
|
||||
@@ -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=}
|
||||
@@ -4260,13 +4263,11 @@ packages:
|
||||
|
||||
xterm-addon-fit@0.8.0:
|
||||
resolution: {integrity: sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==}
|
||||
deprecated: This package is now deprecated. Move to @xterm/addon-fit instead.
|
||||
peerDependencies:
|
||||
xterm: ^5.0.0
|
||||
|
||||
xterm@5.3.0:
|
||||
resolution: {integrity: sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==}
|
||||
deprecated: This package is now deprecated. Move to @xterm/xterm instead.
|
||||
|
||||
y18n@4.0.3:
|
||||
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
|
||||
@@ -4341,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.49.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
|
||||
@@ -4870,7 +4871,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
|
||||
'@design.estate/dees-catalog@3.49.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
|
||||
@@ -4890,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
|
||||
@@ -4978,7 +4979,7 @@ snapshots:
|
||||
|
||||
'@design.estate/dees-wcctools@3.8.0':
|
||||
dependencies:
|
||||
'@design.estate/dees-domtools': 2.5.3
|
||||
'@design.estate/dees-domtools': 2.5.4
|
||||
'@design.estate/dees-element': 2.2.4
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
lit: 3.3.2
|
||||
@@ -5371,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
|
||||
@@ -6906,9 +6907,9 @@ snapshots:
|
||||
domhandler: 5.0.3
|
||||
selderee: 0.11.0
|
||||
|
||||
'@serve.zone/catalog@2.9.1(@tiptap/pm@2.27.2)':
|
||||
'@serve.zone/catalog@2.11.0(@tiptap/pm@2.27.2)':
|
||||
dependencies:
|
||||
'@design.estate/dees-catalog': 3.49.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
|
||||
@@ -7400,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:
|
||||
@@ -7456,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':
|
||||
@@ -7466,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:
|
||||
@@ -7486,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': {}
|
||||
|
||||
@@ -7512,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': {}
|
||||
|
||||
@@ -7529,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': {}
|
||||
|
||||
@@ -7549,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': {}
|
||||
|
||||
@@ -7583,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': {}
|
||||
@@ -7632,8 +7633,6 @@ snapshots:
|
||||
|
||||
any-base@1.1.0: {}
|
||||
|
||||
apexcharts@5.10.4: {}
|
||||
|
||||
argparse@1.0.10:
|
||||
dependencies:
|
||||
sprintf-js: 1.0.3
|
||||
@@ -8095,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: {}
|
||||
@@ -8591,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:
|
||||
@@ -9343,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:
|
||||
@@ -9353,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:
|
||||
@@ -9410,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: {}
|
||||
|
||||
|
||||
140
readme.md
140
readme.md
@@ -93,10 +93,11 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
||||
- **Socket-handler mode** — direct socket passing eliminates internal port hops
|
||||
- **Real-time metrics** via SmartMetrics (CPU, memory, connections, throughput)
|
||||
|
||||
### 💾 Persistent Storage & Caching
|
||||
- **Multiple storage backends**: filesystem, custom functions, or in-memory
|
||||
- **Embedded cache database** via smartdata + smartdb (MongoDB-compatible)
|
||||
### 💾 Unified Database
|
||||
- **Two deployment modes**: embedded LocalSmartDb (zero-config) or external MongoDB
|
||||
- **15 document classes** covering routes, certs, VPN, RADIUS, security profiles, network targets, and caches
|
||||
- **Automatic TTL-based cleanup** for cached emails and IP reputation data
|
||||
- **Reusable references** — security profiles and network targets that propagate changes to all referencing routes
|
||||
|
||||
### 🖥️ OpsServer Dashboard
|
||||
- **Web-based management interface** with real-time monitoring
|
||||
@@ -104,7 +105,9 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
||||
- **Live views** for connections, email queues, DNS queries, RADIUS sessions, certificates, remote ingress edges, VPN clients, and security events
|
||||
- **Domain-centric certificate overview** with backoff status and one-click reprovisioning
|
||||
- **Remote ingress management** with connection token generation and one-click copy
|
||||
- **Read-only configuration display** — DcRouter is configured through code
|
||||
- **Security profiles & network targets** — reusable security configurations and host:port targets with propagation to referencing routes
|
||||
- **Global warning banners** when database is disabled (management features unavailable)
|
||||
- **Read-only configuration display** for system overview
|
||||
- **Smart tab visibility handling** — auto-pauses all polling, WebSocket connections, and chart updates when the browser tab is hidden, preventing resource waste and tab freezing
|
||||
|
||||
### 🔧 Programmatic API Client
|
||||
@@ -269,11 +272,8 @@ const router = new DcRouter({
|
||||
],
|
||||
},
|
||||
|
||||
// Persistent storage
|
||||
storage: { fsPath: '/var/lib/dcrouter/data' },
|
||||
|
||||
// Cache database
|
||||
cacheConfig: { enabled: true, storagePath: '~/.serve.zone/dcrouter/tsmdb' },
|
||||
// Unified database (embedded LocalSmartDb or external MongoDB)
|
||||
dbConfig: { enabled: true },
|
||||
|
||||
// TLS & ACME
|
||||
tls: { contactEmail: 'admin@example.com' },
|
||||
@@ -311,8 +311,7 @@ graph TB
|
||||
CM[Certificate Manager<br/><i>smartacme v9</i>]
|
||||
OS[OpsServer Dashboard]
|
||||
MM[Metrics Manager]
|
||||
SM[Storage Manager]
|
||||
CD[Cache Database]
|
||||
DB2[DcRouterDb<br/><i>smartdata + smartdb</i>]
|
||||
end
|
||||
|
||||
subgraph "Backend Services"
|
||||
@@ -339,8 +338,7 @@ graph TB
|
||||
DC --> CM
|
||||
DC --> OS
|
||||
DC --> MM
|
||||
DC --> SM
|
||||
DC --> CD
|
||||
DC --> DB2
|
||||
|
||||
SP --> WEB
|
||||
SP --> API
|
||||
@@ -365,8 +363,7 @@ graph TB
|
||||
| **RemoteIngress** | `@serve.zone/remoteingress` | Distributed edge tunneling with Rust data plane and TS management |
|
||||
| **OpsServer** | `@api.global/typedserver` | Web dashboard + TypedRequest API for monitoring and management |
|
||||
| **MetricsManager** | `@push.rocks/smartmetrics` | Real-time metrics collection (CPU, memory, email, DNS, security) |
|
||||
| **StorageManager** | built-in | Pluggable key-value storage (filesystem, custom, or in-memory) |
|
||||
| **CacheDb** | `@push.rocks/smartdb` | Embedded MongoDB-compatible database (LocalSmartDb) for persistent caching |
|
||||
| **DcRouterDb** | `@push.rocks/smartdata` + `@push.rocks/smartdb` | Unified database — embedded LocalSmartDb or external MongoDB for all persistence |
|
||||
|
||||
### How It Works
|
||||
|
||||
@@ -509,24 +506,16 @@ interface IDcRouterOptions {
|
||||
};
|
||||
dnsChallenge?: { cloudflareApiKey?: string };
|
||||
|
||||
// ── Storage & Caching ─────────────────────────────────────────
|
||||
storage?: {
|
||||
fsPath?: string;
|
||||
readFunction?: (key: string) => Promise<string>;
|
||||
writeFunction?: (key: string, value: string) => Promise<void>;
|
||||
};
|
||||
cacheConfig?: {
|
||||
// ── Database ────────────────────────────────────────────────────
|
||||
/** Unified database for all persistence (routes, certs, VPN, RADIUS, etc.) */
|
||||
dbConfig?: {
|
||||
enabled?: boolean; // default: true
|
||||
mongoDbUrl?: string; // External MongoDB URL (omit for embedded LocalSmartDb)
|
||||
storagePath?: string; // default: '~/.serve.zone/dcrouter/tsmdb'
|
||||
dbName?: string; // default: 'dcrouter'
|
||||
cleanupIntervalHours?: number; // default: 1
|
||||
ttlConfig?: {
|
||||
emails?: number; // default: 30 days
|
||||
ipReputation?: number; // default: 1 day
|
||||
bounces?: number; // default: 30 days
|
||||
dkimKeys?: number; // default: 90 days
|
||||
suppression?: number; // default: 30 days
|
||||
};
|
||||
seedOnEmpty?: boolean; // Seed default profiles/targets if DB is empty
|
||||
seedData?: object; // Custom seed data
|
||||
};
|
||||
}
|
||||
```
|
||||
@@ -1213,49 +1202,55 @@ The OpsServer includes a **Certificates** view showing:
|
||||
- One-click reprovisioning per domain
|
||||
- Certificate import and export
|
||||
|
||||
## Storage & Caching
|
||||
## Storage & Database
|
||||
|
||||
### StorageManager
|
||||
DcRouter uses a **unified database** (`DcRouterDb`) powered by [`@push.rocks/smartdata`](https://code.foss.global/push.rocks/smartdata) + [`@push.rocks/smartdb`](https://code.foss.global/push.rocks/smartdb) for all persistence. It supports two modes:
|
||||
|
||||
Provides a unified key-value interface with three backends:
|
||||
### Embedded LocalSmartDb (Default)
|
||||
|
||||
Zero-config, file-based MongoDB-compatible database — no external services needed:
|
||||
|
||||
```typescript
|
||||
// Filesystem backend
|
||||
storage: { fsPath: '/var/lib/dcrouter/data' }
|
||||
|
||||
// Custom backend (Redis, S3, etc.)
|
||||
storage: {
|
||||
readFunction: async (key) => await redis.get(key),
|
||||
writeFunction: async (key, value) => await redis.set(key, value)
|
||||
}
|
||||
|
||||
// In-memory (development only — data lost on restart)
|
||||
// Simply omit the storage config
|
||||
dbConfig: { enabled: true }
|
||||
// Data stored at ~/.serve.zone/dcrouter/tsmdb by default
|
||||
```
|
||||
|
||||
Used for: TLS certificates, DKIM keys, email routes, bounce/suppression lists, IP reputation data, domain configs, cert backoff state, remote ingress edge registrations.
|
||||
### External MongoDB
|
||||
|
||||
### Cache Database
|
||||
|
||||
An embedded MongoDB-compatible database (via smartdata + smartdb) for persistent caching with automatic TTL cleanup:
|
||||
Connect to an existing MongoDB instance:
|
||||
|
||||
```typescript
|
||||
cacheConfig: {
|
||||
dbConfig: {
|
||||
enabled: true,
|
||||
storagePath: '~/.serve.zone/dcrouter/tsmdb',
|
||||
mongoDbUrl: 'mongodb://localhost:27017',
|
||||
dbName: 'dcrouter',
|
||||
cleanupIntervalHours: 1,
|
||||
ttlConfig: {
|
||||
emails: 30, // days
|
||||
ipReputation: 1, // days
|
||||
bounces: 30, // days
|
||||
dkimKeys: 90, // days
|
||||
suppression: 30 // days
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Cached document types: `CachedEmail`, `CachedIPReputation`.
|
||||
### Disabling the Database
|
||||
|
||||
For static, constructor-only deployments where no runtime management is needed:
|
||||
|
||||
```typescript
|
||||
dbConfig: { enabled: false }
|
||||
// Routes come exclusively from constructor config — no CRUD, no persistence
|
||||
// OpsServer still runs but management features are disabled
|
||||
```
|
||||
|
||||
### What's Stored
|
||||
|
||||
DcRouterDb persists all runtime state across 15 document classes:
|
||||
|
||||
| Category | Documents | Purpose |
|
||||
|----------|-----------|---------|
|
||||
| **Routes** | `StoredRouteDoc`, `RouteOverrideDoc` | Programmatic routes and hardcoded route overrides |
|
||||
| **Certificates** | `ProxyCertDoc`, `AcmeCertDoc`, `CertBackoffDoc` | TLS certs, ACME state, per-domain backoff |
|
||||
| **Auth** | `ApiTokenDoc` | API token storage |
|
||||
| **Remote Ingress** | `RemoteIngressEdgeDoc` | Edge node registrations |
|
||||
| **VPN** | `VpnServerKeysDoc`, `VpnClientDoc` | Server keys and client registrations |
|
||||
| **RADIUS** | `VlanMappingsDoc`, `AccountingSessionDoc` | VLAN mappings and accounting sessions |
|
||||
| **References** | `SecurityProfileDoc`, `NetworkTargetDoc` | Reusable security profiles and network targets |
|
||||
| **Cache** | `CachedEmailDoc`, `CachedIpReputationDoc` | TTL-based caches with automatic cleanup |
|
||||
|
||||
## Security Features
|
||||
|
||||
@@ -1324,6 +1319,8 @@ The OpsServer provides a web-based management interface served on port 3000 by d
|
||||
| 🔐 **Certificates** | Domain-centric certificate overview, status, backoff info, reprovisioning, import/export |
|
||||
| 🌍 **RemoteIngress** | Edge node management, connection status, token generation, enable/disable |
|
||||
| 🔐 **VPN** | VPN client management, server status, create/toggle/export/rotate/delete clients |
|
||||
| 🛡️ **Security Profiles** | Reusable security configurations (IP allow/block lists, rate limits) |
|
||||
| 🎯 **Network Targets** | Reusable host:port destinations for route references |
|
||||
| 📡 **RADIUS** | NAS client management, VLAN mappings, session monitoring, accounting |
|
||||
| 📜 **Logs** | Real-time log viewer with level filtering and search |
|
||||
| ⚙️ **Configuration** | Read-only view of current system configuration |
|
||||
@@ -1410,6 +1407,22 @@ All management is done via TypedRequest over HTTP POST to `/typedrequest`:
|
||||
'setVlanMapping' // Add/update VLAN mapping
|
||||
'removeVlanMapping' // Remove VLAN mapping
|
||||
'testVlanAssignment' // Test what VLAN a MAC gets
|
||||
|
||||
// Security Profiles
|
||||
'getSecurityProfiles' // List all security profiles
|
||||
'getSecurityProfile' // Get a single profile by ID
|
||||
'createSecurityProfile' // Create a reusable security profile
|
||||
'updateSecurityProfile' // Update a profile (propagates to referencing routes)
|
||||
'deleteSecurityProfile' // Delete a profile (with optional force)
|
||||
'getSecurityProfileUsage' // Get routes referencing a profile
|
||||
|
||||
// Network Targets
|
||||
'getNetworkTargets' // List all network targets
|
||||
'getNetworkTarget' // Get a single target by ID
|
||||
'createNetworkTarget' // Create a reusable host:port target
|
||||
'updateNetworkTarget' // Update a target (propagates to referencing routes)
|
||||
'deleteNetworkTarget' // Delete a target (with optional force)
|
||||
'getNetworkTargetUsage' // Get routes referencing a target
|
||||
```
|
||||
|
||||
## API Client
|
||||
@@ -1518,12 +1531,12 @@ const router = new DcRouter(options: IDcRouterOptions);
|
||||
| `remoteIngressManager` | `RemoteIngressManager` | Edge registration CRUD manager |
|
||||
| `tunnelManager` | `TunnelManager` | Tunnel lifecycle and status manager |
|
||||
| `vpnManager` | `VpnManager` | VPN server lifecycle and client CRUD manager |
|
||||
| `storageManager` | `StorageManager` | Storage backend |
|
||||
| `opsServer` | `OpsServer` | OpsServer/dashboard instance |
|
||||
| `metricsManager` | `MetricsManager` | Metrics collector |
|
||||
| `cacheDb` | `CacheDb` | Cache database instance |
|
||||
| `certProvisionScheduler` | `CertProvisionScheduler` | Per-domain backoff scheduler for cert provisioning |
|
||||
| `certificateStatusMap` | `Map<string, ...>` | Domain-keyed certificate status from SmartProxy events |
|
||||
| `dcRouterDb` | `DcRouterDb` | Unified database instance (smartdata + smartdb) |
|
||||
| `routeConfigManager` | `RouteConfigManager` | Programmatic route CRUD manager |
|
||||
| `apiTokenManager` | `ApiTokenManager` | API token management |
|
||||
| `referenceResolver` | `ReferenceResolver` | Security profile and network target resolver |
|
||||
|
||||
### Re-exported Types
|
||||
|
||||
@@ -1589,7 +1602,8 @@ tstest test/test.opsserver-api.ts --verbose --timeout 60
|
||||
| `test.jwt-auth.ts` | JWT login, verification, logout, invalid credentials | 8 |
|
||||
| `test.opsserver-api.ts` | Health, statistics, configuration, log APIs | 8 |
|
||||
| `test.protected-endpoint.ts` | Admin auth, identity verification, public endpoints | 8 |
|
||||
| `test.storagemanager.ts` | Memory, filesystem, custom backends, concurrency | 8 |
|
||||
| `test.reference-resolver.ts` | Security profiles, network targets, route resolution | 20 |
|
||||
| `test.security-profiles-api.ts` | Profile/target API endpoints, auth enforcement | 13 |
|
||||
|
||||
## Docker / OCI Container Deployment
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '12.2.6',
|
||||
version: '12.7.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -90,6 +90,13 @@ interface IIdentity {
|
||||
| `IApiTokenInfo` | Token info: id, name, scopes, createdAt, expiresAt, enabled |
|
||||
| `TApiTokenScope` | Token scopes: `routes:read`, `routes:write`, `config:read`, `tokens:read`, `tokens:manage` |
|
||||
|
||||
#### Security & Reference Interfaces
|
||||
| Interface | Description |
|
||||
|-----------|-------------|
|
||||
| `ISecurityProfile` | Reusable security config: id, name, description, security (ipAllowList, ipBlockList, maxConnections, rateLimit, etc.), extendsProfiles |
|
||||
| `INetworkTarget` | Reusable host:port destination: id, name, description, host (string or string[]), port |
|
||||
| `IRouteMetadata` | Route-to-reference links: securityProfileRef, networkTargetRef, snapshot names, lastResolvedAt |
|
||||
|
||||
#### Remote Ingress Interfaces
|
||||
| Interface | Description |
|
||||
|-----------|-------------|
|
||||
@@ -241,6 +248,26 @@ interface ICertificateInfo {
|
||||
| `IReq_GetRadiusStatistics` | `getRadiusStatistics` | RADIUS stats |
|
||||
| `IReq_GetRadiusAccountingSummary` | `getRadiusAccountingSummary` | Accounting summary |
|
||||
|
||||
#### 🛡️ Security Profiles
|
||||
| Interface | Method | Description |
|
||||
|-----------|--------|-------------|
|
||||
| `IReq_GetSecurityProfiles` | `getSecurityProfiles` | List all security profiles |
|
||||
| `IReq_GetSecurityProfile` | `getSecurityProfile` | Get a single profile by ID |
|
||||
| `IReq_CreateSecurityProfile` | `createSecurityProfile` | Create a reusable security profile |
|
||||
| `IReq_UpdateSecurityProfile` | `updateSecurityProfile` | Update a profile (propagates to routes) |
|
||||
| `IReq_DeleteSecurityProfile` | `deleteSecurityProfile` | Delete a profile (with optional force) |
|
||||
| `IReq_GetSecurityProfileUsage` | `getSecurityProfileUsage` | Get routes referencing a profile |
|
||||
|
||||
#### 🎯 Network Targets
|
||||
| Interface | Method | Description |
|
||||
|-----------|--------|-------------|
|
||||
| `IReq_GetNetworkTargets` | `getNetworkTargets` | List all network targets |
|
||||
| `IReq_GetNetworkTarget` | `getNetworkTarget` | Get a single target by ID |
|
||||
| `IReq_CreateNetworkTarget` | `createNetworkTarget` | Create a reusable host:port target |
|
||||
| `IReq_UpdateNetworkTarget` | `updateNetworkTarget` | Update a target (propagates to routes) |
|
||||
| `IReq_DeleteNetworkTarget` | `deleteNetworkTarget` | Delete a target (with optional force) |
|
||||
| `IReq_GetNetworkTargetUsage` | `getNetworkTargetUsage` | Get routes referencing a target |
|
||||
|
||||
## Example: Full API Integration
|
||||
|
||||
> 💡 **Tip:** For a higher-level, object-oriented API, use [`@serve.zone/dcrouter-apiclient`](https://www.npmjs.com/package/@serve.zone/dcrouter-apiclient) which wraps these interfaces with resource classes and builder patterns.
|
||||
|
||||
@@ -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.2.6',
|
||||
version: '12.7.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;
|
||||
@@ -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,
|
||||
@@ -1441,6 +1449,37 @@ export const createRouteAction = routeManagementStatePart.createAction<{
|
||||
}
|
||||
});
|
||||
|
||||
export const updateRouteAction = routeManagementStatePart.createAction<{
|
||||
id: string;
|
||||
route?: any;
|
||||
enabled?: boolean;
|
||||
metadata?: any;
|
||||
}>(async (statePartArg, dataArg, actionContext): Promise<IRouteManagementState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState()!;
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_UpdateRoute
|
||||
>('/typedrequest', 'updateRoute');
|
||||
|
||||
await request.fire({
|
||||
identity: context.identity!,
|
||||
id: dataArg.id,
|
||||
route: dataArg.route,
|
||||
enabled: dataArg.enabled,
|
||||
metadata: dataArg.metadata,
|
||||
});
|
||||
|
||||
return await actionContext!.dispatch(fetchMergedRoutesAction, null);
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
...currentState,
|
||||
error: error instanceof Error ? error.message : 'Failed to update route',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const deleteRouteAction = routeManagementStatePart.createAction<string>(
|
||||
async (statePartArg, routeId, actionContext): Promise<IRouteManagementState> => {
|
||||
const context = getActionContext();
|
||||
@@ -1750,6 +1789,8 @@ async function dispatchCombinedRefreshActionInner() {
|
||||
dns: true,
|
||||
security: true,
|
||||
network: currentView === 'network', // Only fetch network if on network view
|
||||
radius: true,
|
||||
vpn: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1761,6 +1802,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,
|
||||
|
||||
@@ -2,7 +2,6 @@ import * as plugins from '../plugins.js';
|
||||
import * as appstate from '../appstate.js';
|
||||
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||
import { appRouter } from '../router.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
css,
|
||||
@@ -43,6 +42,12 @@ export class OpsDashboard extends DeesElement {
|
||||
theme: 'light',
|
||||
};
|
||||
|
||||
@state() accessor configState: appstate.IConfigState = {
|
||||
config: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
// Store viewTabs as a property to maintain object references
|
||||
private viewTabs = [
|
||||
{
|
||||
@@ -112,6 +117,20 @@ export class OpsDashboard extends DeesElement {
|
||||
},
|
||||
];
|
||||
|
||||
private get globalMessages() {
|
||||
const messages: Array<{ id: string; type: string; message: string; dismissible?: boolean }> = [];
|
||||
const config = this.configState.config;
|
||||
if (config && !config.cache.enabled) {
|
||||
messages.push({
|
||||
id: 'db-disabled',
|
||||
type: 'warning',
|
||||
message: 'Database is disabled. Creating and editing routes, profiles, targets, and API tokens is not available.',
|
||||
dismissible: false,
|
||||
});
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current view tab based on the UI state's activeView.
|
||||
* Used to pass the correct selectedView to dees-simple-appdash on initial render.
|
||||
@@ -137,6 +156,14 @@ export class OpsDashboard extends DeesElement {
|
||||
});
|
||||
this.rxSubscriptions.push(loginSubscription);
|
||||
|
||||
// Subscribe to config state (for global warnings)
|
||||
const configSubscription = appstate.configStatePart
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((configState) => {
|
||||
this.configState = configState;
|
||||
});
|
||||
this.rxSubscriptions.push(configSubscription);
|
||||
|
||||
// Subscribe to UI state
|
||||
const uiSubscription = appstate.uiStatePart
|
||||
.select((stateArg) => stateArg)
|
||||
@@ -205,6 +232,7 @@ export class OpsDashboard extends DeesElement {
|
||||
name="DCRouter OpsServer"
|
||||
.viewTabs=${this.viewTabs}
|
||||
.selectedView=${this.currentViewTab}
|
||||
.globalMessages=${this.globalMessages}
|
||||
>
|
||||
</dees-simple-appdash>
|
||||
</dees-simple-login>
|
||||
|
||||
@@ -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,39 @@ 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 () => {
|
||||
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,
|
||||
});
|
||||
};
|
||||
|
||||
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();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
} 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 }));
|
||||
@@ -287,27 +292,17 @@ export class OpsViewNetwork extends DeesElement {
|
||||
{
|
||||
name: 'Inbound',
|
||||
data: this.trafficDataIn,
|
||||
color: '#22c55e', // Green for download
|
||||
color: '#22c55e',
|
||||
},
|
||||
{
|
||||
name: 'Outbound',
|
||||
data: this.trafficDataOut,
|
||||
color: '#8b5cf6', // Purple for upload
|
||||
color: '#8b5cf6',
|
||||
}
|
||||
]}
|
||||
.stacked=${false}
|
||||
.realtimeMode=${true}
|
||||
.rollingWindow=${OpsViewNetwork.CHART_WINDOW_MS}
|
||||
.yAxisFormatter=${(val: number) => `${val} Mbit/s`}
|
||||
.tooltipFormatter=${(point: any) => {
|
||||
const mbps = point.y || 0;
|
||||
const seriesName = point.series?.name || 'Throughput';
|
||||
const timestamp = new Date(point.x).toLocaleTimeString();
|
||||
return `
|
||||
<div style="padding: 8px;">
|
||||
<div style="font-weight: bold; margin-bottom: 4px;">${timestamp}</div>
|
||||
<div>${seriesName}: ${mbps.toFixed(2)} Mbit/s</div>
|
||||
</div>
|
||||
`;
|
||||
}}
|
||||
></dees-chart-area>
|
||||
|
||||
<!-- Top IPs Section -->
|
||||
@@ -719,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() {
|
||||
@@ -752,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();
|
||||
}
|
||||
|
||||
@@ -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,15 +119,23 @@ export class OpsViewOverview extends DeesElement {
|
||||
|
||||
${this.renderDnsStats()}
|
||||
|
||||
${this.renderRadiusStats()}
|
||||
|
||||
${this.renderVpnStats()}
|
||||
|
||||
<div class="chartGrid">
|
||||
<dees-chart-area
|
||||
.label=${'Email Traffic (24h)'}
|
||||
.series=${this.getEmailTrafficSeries()}
|
||||
.realtimeMode=${true}
|
||||
.rollingWindow=${86400000}
|
||||
.yAxisFormatter=${(val: number) => `${val}`}
|
||||
></dees-chart-area>
|
||||
<dees-chart-area
|
||||
.label=${'DNS Queries (24h)'}
|
||||
.series=${this.getDnsQuerySeries()}
|
||||
.realtimeMode=${true}
|
||||
.rollingWindow=${86400000}
|
||||
.yAxisFormatter=${(val: number) => `${val}`}
|
||||
></dees-chart-area>
|
||||
<dees-chart-log
|
||||
@@ -374,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 }> {
|
||||
|
||||
@@ -204,7 +204,10 @@ export class OpsViewRoutes extends DeesElement {
|
||||
? html`
|
||||
<sz-route-list-view
|
||||
.routes=${szRoutes}
|
||||
.showActionsFilter=${(route: any) => route.tags?.includes('programmatic') ?? false}
|
||||
@route-click=${(e: CustomEvent) => this.handleRouteClick(e)}
|
||||
@route-edit=${(e: CustomEvent) => this.handleRouteEdit(e)}
|
||||
@route-delete=${(e: CustomEvent) => this.handleRouteDelete(e)}
|
||||
></sz-route-list-view>
|
||||
`
|
||||
: html`
|
||||
@@ -337,6 +340,165 @@ export class OpsViewRoutes extends DeesElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async handleRouteEdit(e: CustomEvent) {
|
||||
const clickedRoute = e.detail;
|
||||
if (!clickedRoute) return;
|
||||
|
||||
const merged = this.routeState.mergedRoutes.find(
|
||||
(mr) => mr.route.name === clickedRoute.name,
|
||||
);
|
||||
if (!merged || !merged.storedRouteId) return;
|
||||
|
||||
this.showEditRouteDialog(merged);
|
||||
}
|
||||
|
||||
private async handleRouteDelete(e: CustomEvent) {
|
||||
const clickedRoute = e.detail;
|
||||
if (!clickedRoute) return;
|
||||
|
||||
const merged = this.routeState.mergedRoutes.find(
|
||||
(mr) => mr.route.name === clickedRoute.name,
|
||||
);
|
||||
if (!merged || !merged.storedRouteId) return;
|
||||
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
await DeesModal.createAndShow({
|
||||
heading: `Delete Route: ${merged.route.name}`,
|
||||
content: html`
|
||||
<div style="color: #ccc; padding: 8px 0;">
|
||||
<p>Are you sure you want to delete this route? This action cannot be undone.</p>
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Cancel',
|
||||
iconName: 'lucide:x',
|
||||
action: async (modalArg: any) => await modalArg.destroy(),
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
iconName: 'lucide:trash-2',
|
||||
action: async (modalArg: any) => {
|
||||
await appstate.routeManagementStatePart.dispatchAction(
|
||||
appstate.deleteRouteAction,
|
||||
merged.storedRouteId!,
|
||||
);
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private async showEditRouteDialog(merged: interfaces.data.IMergedRoute) {
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
const profiles = this.profilesTargetsState.profiles;
|
||||
const targets = this.profilesTargetsState.targets;
|
||||
|
||||
const profileOptions = [
|
||||
{ key: '', option: '(none — inline security)' },
|
||||
...profiles.map((p) => ({
|
||||
key: p.id,
|
||||
option: `${p.name}${p.description ? ' — ' + p.description : ''}`,
|
||||
})),
|
||||
];
|
||||
const targetOptions = [
|
||||
{ key: '', option: '(none — inline target)' },
|
||||
...targets.map((t) => ({
|
||||
key: t.id,
|
||||
option: `${t.name} (${Array.isArray(t.host) ? t.host.join(',') : t.host}:${t.port})`,
|
||||
})),
|
||||
];
|
||||
|
||||
const route = merged.route;
|
||||
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: 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)
|
||||
: '';
|
||||
const currentTargetPort = firstTarget?.port != null ? String(firstTarget.port) : '';
|
||||
|
||||
await DeesModal.createAndShow({
|
||||
heading: `Edit Route: ${route.name}`,
|
||||
content: html`
|
||||
<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-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>
|
||||
<dees-input-text .key=${'targetPort'} .label=${'Target Port (if no target selected)'} .value=${currentTargetPort}></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Cancel',
|
||||
iconName: 'lucide:x',
|
||||
action: async (modalArg: any) => await modalArg.destroy(),
|
||||
},
|
||||
{
|
||||
name: 'Save',
|
||||
iconName: 'lucide:check',
|
||||
action: async (modalArg: any) => {
|
||||
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
||||
if (!form) return;
|
||||
const formData = await form.collectFormData();
|
||||
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: 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.length > 0 ? { domains } : {}),
|
||||
},
|
||||
action: {
|
||||
type: 'forward',
|
||||
targets: [
|
||||
{
|
||||
host: formData.targetHost || 'localhost',
|
||||
port: parseInt(formData.targetPort, 10) || 443,
|
||||
},
|
||||
],
|
||||
},
|
||||
...(priority != null && !isNaN(priority) ? { priority } : {}),
|
||||
};
|
||||
|
||||
const metadata: any = {};
|
||||
if (formData.securityProfileRef) {
|
||||
metadata.securityProfileRef = formData.securityProfileRef;
|
||||
}
|
||||
if (formData.networkTargetRef) {
|
||||
metadata.networkTargetRef = formData.networkTargetRef;
|
||||
}
|
||||
|
||||
await appstate.routeManagementStatePart.dispatchAction(
|
||||
appstate.updateRouteAction,
|
||||
{
|
||||
id: merged.storedRouteId!,
|
||||
route: updatedRoute,
|
||||
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
||||
},
|
||||
);
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private async showCreateRouteDialog() {
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
const profiles = this.profilesTargetsState.profiles;
|
||||
@@ -364,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>
|
||||
@@ -387,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',
|
||||
@@ -406,6 +570,7 @@ export class OpsViewRoutes extends DeesElement {
|
||||
},
|
||||
],
|
||||
},
|
||||
...(priority != null && !isNaN(priority) ? { priority } : {}),
|
||||
};
|
||||
|
||||
// Build metadata if profile/target selected
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -68,6 +68,12 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
||||
- API token creation, revocation, and scope management
|
||||
- Routes tab and API Tokens tab in unified view
|
||||
|
||||
### 🛡️ Security Profiles & Network Targets
|
||||
- Create, edit, and delete reusable security profiles (IP allow/block lists, rate limits, max connections)
|
||||
- Create, edit, and delete reusable network targets (host:port destinations)
|
||||
- In-row and context menu actions for quick editing
|
||||
- Changes propagate automatically to all referencing routes
|
||||
|
||||
### ⚙️ Configuration
|
||||
- Read-only display of current system configuration
|
||||
- Status badges for boolean values (enabled/disabled)
|
||||
|
||||
Reference in New Issue
Block a user