Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d2c63cf170 | |||
| 09d66e4528 | |||
| 3078fa9d7b | |||
| 57fbb128e6 | |||
| d73266eeb8 | |||
| 2dbdf2d2b1 | |||
| 383e0adc23 | |||
| d7789f5a44 | |||
| 2638990667 | |||
| c33ecdc26f | |||
| b033d80927 | |||
| cf5d616769 | |||
| 8e722f5ab6 | |||
| 2b75709161 | |||
| c5e2c262b7 | |||
| d10896196d | |||
| 8be1e87bdc | |||
| 96cefe984a | |||
| ca112c3e42 | |||
| 85b6c4fa51 | |||
| ee550e6f25 | |||
| 108a8bb51d | |||
| 3c5b26d1c1 | |||
| 01fbc3db95 | |||
| 8dd9770339 | |||
| 77842647fd | |||
| a309145829 |
100
changelog.md
100
changelog.md
@@ -1,5 +1,105 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-02-14 - 5.4.5 - fix(dcrouter)
|
||||
bump patch for release pipeline consistency - no code changes
|
||||
|
||||
- current version: 5.4.4 (from package.json)
|
||||
- git diff: no changes detected
|
||||
- recommend patch bump to trigger release artifacts if required
|
||||
|
||||
## 2026-02-14 - 5.4.4 - fix(deps)
|
||||
bump @push.rocks/smartproxy to ^25.2.0
|
||||
|
||||
- Updated @push.rocks/smartproxy from ^25.1.0 to ^25.2.0 (patch, non-breaking).
|
||||
- Current package version is 5.4.3; recommend a patch release to 5.4.4.
|
||||
|
||||
## 2026-02-14 - 5.4.3 - fix(dependencies)
|
||||
bump @push.rocks/smartproxy to ^25.1.0
|
||||
|
||||
- Updated @push.rocks/smartproxy from ^25.0.0 to ^25.1.0 in package.json
|
||||
|
||||
## 2026-02-13 - 5.4.2 - fix(dcrouter)
|
||||
improve domain pattern matching to support routing-glob and wildcard patterns and use matching logic when resolving routes
|
||||
|
||||
- Support routing-glob patterns beginning with '*' (e.g. *example.com) to match base domain, wildcard form, and subdomains
|
||||
- Treat standard wildcard patterns ('*.example.com') as matching both the base domain (example.com) and its subdomains
|
||||
- Use isDomainMatch when resolving routes instead of exact array includes to allow pattern matching
|
||||
- Normalize domain and pattern to lowercase and simplify equality checks
|
||||
|
||||
## 2026-02-13 - 5.4.1 - fix(network,dcrouter)
|
||||
Always register SmartProxy certificate event handlers and include total bytes + improved connection metrics in network stats/UI
|
||||
|
||||
- Always register SmartProxy 'certificate-issued', 'certificate-renewed', and 'certificate-failed' handlers (previously only registered when acmeConfig was present) so certificate events are processed regardless of provisioning path.
|
||||
- Add totalBytes (in/out) to network stats and propagate it through ts_interfaces and app state so total data transferred is available to the UI.
|
||||
- Combine metricsManager.getNetworkStats with collectServerStats to compute activeConnections and adjust connectionDetails/TopEndpoints handling.
|
||||
- Update ops UI to display totalBytes in throughput cards and remove a redundant network-specific auto-refresh fetch.
|
||||
- Type and state updates: ts_interfaces/data/stats.ts and ts_web/appstate.ts updated with totalBytes and initialization/default mapping adjusted.
|
||||
|
||||
## 2026-02-13 - 5.4.0 - feat(certificates)
|
||||
include certificate source/issuer and Rust-side status checks; pass eventComms into certProvisionFunction and record expiry information
|
||||
|
||||
- bump @push.rocks/smartproxy dependency to ^25.0.0
|
||||
- add optional 'source' field to certificate status and propagate event.source when certificates are issued, renewed, or failed
|
||||
- change smartProxy.certProvisionFunction signature to accept eventComms; use it to log attempts, set source and expiryDate, and fall back to http-01 on DNS-01 failure
|
||||
- make buildCertificateOverview async and query smartProxy.getCertificateStatus for a route when event-based status is unknown
|
||||
- improve logging to include certificate source and more contextual messages
|
||||
|
||||
## 2026-02-13 - 5.3.0 - feat(certificates)
|
||||
add certificate overview and reprovisioning in ops UI and API; track SmartProxy certificate events
|
||||
|
||||
- Add CertificateHandler with typedrequest endpoints: getCertificateOverview and reprovisionCertificate
|
||||
- Introduce ICertificateInfo and request/response interfaces for certificate operations
|
||||
- Frontend: add certificate state part, actions (fetchCertificateOverview, reprovisionCertificate), router view, and ops-view-certificates component
|
||||
- DcRouter: add certificateStatusMap, listen to SmartProxy certificate-issued/renewed/failed events, and add findRouteNameForDomain helper
|
||||
- Bump dependency @push.rocks/smartproxy to ^24.0.0
|
||||
|
||||
## 2026-02-13 - 5.2.0 - feat(monitoring)
|
||||
add throughput metrics and expose them in ops UI
|
||||
|
||||
- MetricsManager now reports bytesInPerSecond and bytesOutPerSecond as part of throughput
|
||||
- Extended IServerStats with requestsPerSecond and throughput {bytesIn, bytesOut, bytesInPerSecond, bytesOutPerSecond}
|
||||
- Stats handler updated to include requestsPerSecond and throughput; fallback stats initialize throughput fields to zero
|
||||
- Web UI ops overview displays Throughput In/Out (bits/s) and total bytes with new formatting helper
|
||||
- Bumped dependency @push.rocks/smartproxy to ^23.1.6
|
||||
|
||||
## 2026-02-13 - 5.1.0 - feat(acme)
|
||||
Integrate SmartAcme DNS-01 handling and add certificate provisioning for SmartProxy
|
||||
|
||||
- Add smartAcme property and lifecycle management (start/stop) in DcRouter
|
||||
- Create SmartAcme instance when DNS challenge handlers are present and wire certProvisionFunction to SmartProxy to return certificates for domains
|
||||
- Fall back to http-01 provisioning on SmartAcme errors for a domain
|
||||
- Stop SmartAcme during shutdown sequence to clean up resources
|
||||
- Bump dependency @push.rocks/smartproxy to ^23.1.5
|
||||
|
||||
## 2026-02-13 - 5.0.7 - fix(deps)
|
||||
bump @push.rocks/smartdns to ^7.8.1 and @push.rocks/smartmta to ^5.2.2
|
||||
|
||||
- package.json: updated @push.rocks/smartdns from ^7.8.0 to ^7.8.1 (patch)
|
||||
- package.json: updated @push.rocks/smartmta from ^5.2.1 to ^5.2.2 (patch)
|
||||
|
||||
## 2026-02-12 - 5.0.6 - fix(deps)
|
||||
bump @push.rocks/smartproxy to ^23.1.4
|
||||
|
||||
- package.json: @push.rocks/smartproxy ^23.1.2 → ^23.1.4
|
||||
- Dependency-only version bump, no source code changes
|
||||
|
||||
## 2026-02-12 - 5.0.5 - fix(dcrouter)
|
||||
remove legacy handling of emailConfig.routes that added domain-based routes
|
||||
|
||||
- Removed loop that added domain-based email routes from emailConfig.routes into emailRoutes
|
||||
- Previously created match.domains by extracting the recipient domain (split on '@') and defaulted forward target port to 25
|
||||
- Removed creation of TLS passthrough configuration for those forwarded routes
|
||||
- This prevents duplicate or incorrect domain-based routes being appended during email route construction
|
||||
|
||||
## 2026-02-12 - 5.0.4 - fix(cache)
|
||||
use user-writable ~/.serve.zone/dcrouter for TsmDB and centralize data path logic
|
||||
|
||||
- Default TsmDB storage changed from /etc/dcrouter/tsmdb to ~/.serve.zone/dcrouter/tsmdb
|
||||
- Introduced dcrouterHomeDir, dataDir, and defaultTsmDbPath in ts/paths.ts
|
||||
- CacheDb now defaults to defaultTsmDbPath when no storagePath is provided
|
||||
- DcRouter initialization updated to use paths.defaultTsmDbPath; README and readme.hints updated to document the new defaults
|
||||
- Avoids /etc permission issues and prevents starting a real MongoDB process in tests by using a user-writable default path
|
||||
|
||||
## 2026-02-12 - 5.0.3 - fix(packaging)
|
||||
add files whitelist to package.json and remove Playwright-generated screenshots
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@serve.zone/dcrouter",
|
||||
"private": false,
|
||||
"version": "5.0.3",
|
||||
"version": "5.4.5",
|
||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -38,18 +38,18 @@
|
||||
"@push.rocks/qenv": "^6.1.3",
|
||||
"@push.rocks/smartacme": "^8.0.0",
|
||||
"@push.rocks/smartdata": "^7.0.15",
|
||||
"@push.rocks/smartdns": "^7.8.0",
|
||||
"@push.rocks/smartdns": "^7.8.1",
|
||||
"@push.rocks/smartfile": "^13.1.2",
|
||||
"@push.rocks/smartguard": "^3.1.0",
|
||||
"@push.rocks/smartjwt": "^2.2.1",
|
||||
"@push.rocks/smartlog": "^3.1.10",
|
||||
"@push.rocks/smartmetrics": "^2.0.10",
|
||||
"@push.rocks/smartmongo": "^5.1.0",
|
||||
"@push.rocks/smartmta": "^5.2.1",
|
||||
"@push.rocks/smartmta": "^5.2.2",
|
||||
"@push.rocks/smartnetwork": "^4.4.0",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartproxy": "^23.1.2",
|
||||
"@push.rocks/smartproxy": "^25.2.0",
|
||||
"@push.rocks/smartradius": "^1.1.1",
|
||||
"@push.rocks/smartrequest": "^5.0.1",
|
||||
"@push.rocks/smartrx": "^3.0.10",
|
||||
|
||||
98
pnpm-lock.yaml
generated
98
pnpm-lock.yaml
generated
@@ -37,13 +37,13 @@ importers:
|
||||
version: 6.1.3
|
||||
'@push.rocks/smartacme':
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.0(socks@2.8.7)
|
||||
version: 8.0.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)
|
||||
'@push.rocks/smartdata':
|
||||
specifier: ^7.0.15
|
||||
version: 7.0.15(socks@2.8.7)
|
||||
'@push.rocks/smartdns':
|
||||
specifier: ^7.8.0
|
||||
version: 7.8.0
|
||||
specifier: ^7.8.1
|
||||
version: 7.8.1
|
||||
'@push.rocks/smartfile':
|
||||
specifier: ^13.1.2
|
||||
version: 13.1.2
|
||||
@@ -63,8 +63,8 @@ importers:
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0(socks@2.8.7)
|
||||
'@push.rocks/smartmta':
|
||||
specifier: ^5.2.1
|
||||
version: 5.2.1
|
||||
specifier: ^5.2.2
|
||||
version: 5.2.2
|
||||
'@push.rocks/smartnetwork':
|
||||
specifier: ^4.4.0
|
||||
version: 4.4.0
|
||||
@@ -75,8 +75,8 @@ importers:
|
||||
specifier: ^4.2.3
|
||||
version: 4.2.3
|
||||
'@push.rocks/smartproxy':
|
||||
specifier: ^23.1.2
|
||||
version: 23.1.2(socks@2.8.7)
|
||||
specifier: ^25.2.0
|
||||
version: 25.2.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)
|
||||
'@push.rocks/smartradius':
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
@@ -116,7 +116,7 @@ importers:
|
||||
version: 2.0.1
|
||||
'@git.zone/tstest':
|
||||
specifier: ^3.1.8
|
||||
version: 3.1.8(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)
|
||||
version: 3.1.8(socks@2.8.7)(typescript@5.9.3)
|
||||
'@git.zone/tswatch':
|
||||
specifier: ^3.1.0
|
||||
version: 3.1.0(@tiptap/pm@2.27.2)
|
||||
@@ -904,8 +904,8 @@ packages:
|
||||
'@push.rocks/smartdns@6.2.2':
|
||||
resolution: {integrity: sha512-MhJcHujbyIuwIIFdnXb2OScGtRjNsliLUS8GoAurFsKtcCOaA0ytfP+PNzkukyBufjb1nMiJF3rjhswXdHakAQ==}
|
||||
|
||||
'@push.rocks/smartdns@7.8.0':
|
||||
resolution: {integrity: sha512-5FX74AAgQSqWPZkpTsI/BbUKBQpZKSvs+UdX9IZpwcuPldI+K7D1WeE02mMAGd1Ncd/sYAMor5CTlhnG6L+QhQ==}
|
||||
'@push.rocks/smartdns@7.8.1':
|
||||
resolution: {integrity: sha512-qEizM9dFzhq4XGICDC8Im7JLjwdokHdDZ6wLufBInaEOupq+8XOa9bC6EGlBQVsCXFUyrKzsFk6eBa9BSZMKPw==}
|
||||
|
||||
'@push.rocks/smartenv@5.0.13':
|
||||
resolution: {integrity: sha512-ACXmUcHZHl2CF2jnVuRw9saRRrZvJblCRs2d+K5aLR1DfkYFX3eA21kcMlKeLisI3aGNbIj9vz/rowN5qkRkfA==}
|
||||
@@ -1000,8 +1000,8 @@ packages:
|
||||
'@push.rocks/smartmongo@5.1.0':
|
||||
resolution: {integrity: sha512-2tpKf8K+SMdLHOEpafgKPIN+ypWTLwHc33hCUDNMQ1KaL7vokkavA44+fHxQydOGPMtDi22tSMFeVMCcUSzs4w==}
|
||||
|
||||
'@push.rocks/smartmta@5.2.1':
|
||||
resolution: {integrity: sha512-ITgu1kIJxWgiU6q3YDxAp1HoMmC8ECJhEAFbDtUDRIBcg8Flvbmgasjnqew67nFcXq2fKYh3rGECloS62MBQgw==}
|
||||
'@push.rocks/smartmta@5.2.2':
|
||||
resolution: {integrity: sha512-0xKUi2BMM0HFYIPdNeNJZFitAiJ9CNbLlOJ8TenT+xInp7DKcSQ7ABER1rJKinPtvDjRDSiSqiF2iQR+O7299g==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
cpu: [x64, arm64]
|
||||
os: [darwin, linux, win32]
|
||||
@@ -1040,8 +1040,8 @@ packages:
|
||||
'@push.rocks/smartpromise@4.2.3':
|
||||
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
||||
|
||||
'@push.rocks/smartproxy@23.1.2':
|
||||
resolution: {integrity: sha512-4uOSPp4ymIBLhn0xocmY+6wPWlEBIB//vaOIPM9wTyoyhWdhMSV2J1V7NcXGNAGiZG9OO4zB1yW3pbs/4Wc2NA==}
|
||||
'@push.rocks/smartproxy@25.2.0':
|
||||
resolution: {integrity: sha512-cwqtfSI3QziyZOYXZuL4/jq1KHXQRVwGvimHcqhJDsl4cac9y7fM4gKHU4B3m2/2qaih1scP9FPGwlCCVFXR7Q==}
|
||||
|
||||
'@push.rocks/smartpuppeteer@2.0.5':
|
||||
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
||||
@@ -1061,8 +1061,8 @@ packages:
|
||||
'@push.rocks/smartrouter@1.3.3':
|
||||
resolution: {integrity: sha512-1+xZEnWlhzqLWAaJ1zFNhQ0zgbfCWQl1DBT72LygLxTs+P0K8AwJKgqo/IX6CT55kGCFnPAZIYSbVJlGsgrB0w==}
|
||||
|
||||
'@push.rocks/smartrust@1.2.0':
|
||||
resolution: {integrity: sha512-JlaALselIHoP6C3ceQbrvz424G21cND/QsH/KI3E/JrO4XphJiGZwM6f4yJWrijdPYR/YYMoaIiYN7ybZp0C4w==}
|
||||
'@push.rocks/smartrust@1.2.1':
|
||||
resolution: {integrity: sha512-ANwXXibUwoHNWF1hhXhXVVrfzYlhgHYRa2205Jkd/s/wXzcWHftYZthilJj+52B7nkzSB76umfxKfK5eBYY2Ug==}
|
||||
|
||||
'@push.rocks/smartrx@3.0.10':
|
||||
resolution: {integrity: sha512-USjIYcsSfzn14cwOsxgq/bBmWDTTzy3ouWAnW5NdMyRRzEbmeNrvmy6TRqNeDlJ2PsYNTt1rr/zGUqvIy72ITg==}
|
||||
@@ -2041,6 +2041,10 @@ packages:
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
balanced-match@4.0.2:
|
||||
resolution: {integrity: sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
bare-events@2.8.2:
|
||||
resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==}
|
||||
peerDependencies:
|
||||
@@ -2109,6 +2113,10 @@ packages:
|
||||
brace-expansion@2.0.2:
|
||||
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
|
||||
|
||||
brace-expansion@5.0.2:
|
||||
resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
broadcast-channel@7.3.0:
|
||||
resolution: {integrity: sha512-UHPhLBQKfQ8OmMFMpmPfO5dRakyA1vsfiDGWTYNvChYol65tbuhivPEGgZZiuetorvExdvxaWiBy/ym1Ty08yA==}
|
||||
|
||||
@@ -3282,6 +3290,10 @@ packages:
|
||||
resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
minimatch@10.2.0:
|
||||
resolution: {integrity: sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
|
||||
@@ -4232,7 +4244,7 @@ packages:
|
||||
hasBin: true
|
||||
|
||||
wordwrap@1.0.0:
|
||||
resolution: {integrity: sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=}
|
||||
resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
|
||||
|
||||
wrap-ansi@6.2.0:
|
||||
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
||||
@@ -5296,7 +5308,7 @@ snapshots:
|
||||
'@push.rocks/smartshell': 3.3.0
|
||||
tsx: 4.21.0
|
||||
|
||||
'@git.zone/tstest@3.1.8(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)':
|
||||
'@git.zone/tstest@3.1.8(socks@2.8.7)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
|
||||
'@git.zone/tsbundle': 2.8.3
|
||||
@@ -5327,7 +5339,6 @@ snapshots:
|
||||
- '@aws-sdk/credential-providers'
|
||||
- '@mongodb-js/zstd'
|
||||
- '@nuxt/kit'
|
||||
- '@push.rocks/smartserve'
|
||||
- '@swc/helpers'
|
||||
- aws-crt
|
||||
- bare-abort-controller
|
||||
@@ -5821,7 +5832,7 @@ snapshots:
|
||||
'@push.rocks/smartlog': 3.1.10
|
||||
'@push.rocks/smartpath': 6.0.0
|
||||
|
||||
'@push.rocks/smartacme@8.0.0(socks@2.8.7)':
|
||||
'@push.rocks/smartacme@8.0.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)':
|
||||
dependencies:
|
||||
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
|
||||
'@apiclient.xyz/cloudflare': 6.4.3
|
||||
@@ -5843,7 +5854,9 @@ snapshots:
|
||||
- '@aws-sdk/credential-providers'
|
||||
- '@mongodb-js/zstd'
|
||||
- '@nuxt/kit'
|
||||
- '@push.rocks/smartserve'
|
||||
- bare-abort-controller
|
||||
- bufferutil
|
||||
- encoding
|
||||
- gcp-metadata
|
||||
- kerberos
|
||||
@@ -5853,6 +5866,7 @@ snapshots:
|
||||
- snappy
|
||||
- socks
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
- vue
|
||||
|
||||
'@push.rocks/smartarchive@4.2.4':
|
||||
@@ -6041,18 +6055,15 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@push.rocks/smartdns@7.8.0':
|
||||
'@push.rocks/smartdns@7.8.1':
|
||||
dependencies:
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartenv': 5.0.13
|
||||
'@push.rocks/smartenv': 6.0.0
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrequest': 2.1.0
|
||||
'@push.rocks/smartrust': 1.2.0
|
||||
'@push.rocks/smartrust': 1.2.1
|
||||
'@tsclass/tsclass': 9.3.0
|
||||
'@types/dns-packet': 5.6.5
|
||||
acme-client: 5.4.0
|
||||
dns-packet: 5.6.1
|
||||
minimatch: 10.1.2
|
||||
minimatch: 10.2.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -6222,7 +6233,7 @@ snapshots:
|
||||
|
||||
'@push.rocks/smartmail@2.2.0':
|
||||
dependencies:
|
||||
'@push.rocks/smartdns': 7.8.0
|
||||
'@push.rocks/smartdns': 7.8.1
|
||||
'@push.rocks/smartfile': 13.1.2
|
||||
'@push.rocks/smartmustache': 3.0.2
|
||||
'@push.rocks/smartpath': 6.0.0
|
||||
@@ -6323,14 +6334,14 @@ snapshots:
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
'@push.rocks/smartmta@5.2.1':
|
||||
'@push.rocks/smartmta@5.2.2':
|
||||
dependencies:
|
||||
'@push.rocks/smartfile': 13.1.2
|
||||
'@push.rocks/smartfs': 1.3.1
|
||||
'@push.rocks/smartlog': 3.1.10
|
||||
'@push.rocks/smartmail': 2.2.0
|
||||
'@push.rocks/smartpath': 6.0.0
|
||||
'@push.rocks/smartrust': 1.2.0
|
||||
'@push.rocks/smartrust': 1.2.1
|
||||
'@tsclass/tsclass': 9.3.0
|
||||
lru-cache: 11.2.6
|
||||
mailparser: 3.9.3
|
||||
@@ -6344,7 +6355,7 @@ snapshots:
|
||||
|
||||
'@push.rocks/smartnetwork@4.4.0':
|
||||
dependencies:
|
||||
'@push.rocks/smartdns': 7.8.0
|
||||
'@push.rocks/smartdns': 7.8.1
|
||||
'@push.rocks/smartping': 1.0.8
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartstring': 4.1.0
|
||||
@@ -6430,10 +6441,10 @@ snapshots:
|
||||
|
||||
'@push.rocks/smartpromise@4.2.3': {}
|
||||
|
||||
'@push.rocks/smartproxy@23.1.2(socks@2.8.7)':
|
||||
'@push.rocks/smartproxy@25.2.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)':
|
||||
dependencies:
|
||||
'@push.rocks/lik': 6.2.2
|
||||
'@push.rocks/smartacme': 8.0.0(socks@2.8.7)
|
||||
'@push.rocks/smartacme': 8.0.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)
|
||||
'@push.rocks/smartcrypto': 2.0.4
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartfile': 13.1.2
|
||||
@@ -6441,20 +6452,21 @@ snapshots:
|
||||
'@push.rocks/smartnetwork': 4.4.0
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrequest': 5.0.1
|
||||
'@push.rocks/smartrust': 1.2.0
|
||||
'@push.rocks/smartrust': 1.2.1
|
||||
'@push.rocks/smartrx': 3.0.10
|
||||
'@push.rocks/smartstring': 4.1.0
|
||||
'@push.rocks/taskbuffer': 4.2.0
|
||||
'@tsclass/tsclass': 9.3.0
|
||||
'@types/minimatch': 6.0.0
|
||||
'@types/ws': 8.18.1
|
||||
minimatch: 10.1.2
|
||||
minimatch: 10.2.0
|
||||
pretty-ms: 9.3.0
|
||||
ws: 8.19.0
|
||||
transitivePeerDependencies:
|
||||
- '@aws-sdk/credential-providers'
|
||||
- '@mongodb-js/zstd'
|
||||
- '@nuxt/kit'
|
||||
- '@push.rocks/smartserve'
|
||||
- bare-abort-controller
|
||||
- bufferutil
|
||||
- encoding
|
||||
@@ -6520,7 +6532,7 @@ snapshots:
|
||||
'@push.rocks/smartrx': 3.0.10
|
||||
path-to-regexp: 8.3.0
|
||||
|
||||
'@push.rocks/smartrust@1.2.0':
|
||||
'@push.rocks/smartrust@1.2.1':
|
||||
dependencies:
|
||||
'@push.rocks/smartpath': 6.0.0
|
||||
|
||||
@@ -7560,7 +7572,7 @@ snapshots:
|
||||
|
||||
'@types/minimatch@6.0.0':
|
||||
dependencies:
|
||||
minimatch: 10.1.2
|
||||
minimatch: 10.2.0
|
||||
|
||||
'@types/ms@2.1.0': {}
|
||||
|
||||
@@ -7758,6 +7770,10 @@ snapshots:
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
balanced-match@4.0.2:
|
||||
dependencies:
|
||||
jackspeak: 4.2.3
|
||||
|
||||
bare-events@2.8.2: {}
|
||||
|
||||
bare-fs@4.5.3:
|
||||
@@ -7830,6 +7846,10 @@ snapshots:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
|
||||
brace-expansion@5.0.2:
|
||||
dependencies:
|
||||
balanced-match: 4.0.2
|
||||
|
||||
broadcast-channel@7.3.0:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
@@ -9299,6 +9319,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@isaacs/brace-expansion': 5.0.1
|
||||
|
||||
minimatch@10.2.0:
|
||||
dependencies:
|
||||
brace-expansion: 5.0.2
|
||||
|
||||
minimatch@3.1.2:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.12
|
||||
|
||||
@@ -46,7 +46,7 @@ Source at `../../push.rocks/smartmta`, release with `gitzone commit -ypbrt`
|
||||
### SmartProxy v23.1.2 Route Validation
|
||||
- SmartProxy 23.1.2 enforces stricter route validation
|
||||
- Forward actions MUST use `targets` (array) instead of `target` (singular)
|
||||
- Test configurations that call `DcRouter.start()` need `cacheConfig: { enabled: false }` to avoid `/etc/dcrouter` permission errors
|
||||
- Test configurations that call `DcRouter.start()` need `cacheConfig: { enabled: false }` to avoid starting a real MongoDB process in tests
|
||||
|
||||
```typescript
|
||||
// WRONG - will fail validation
|
||||
@@ -693,7 +693,7 @@ The configuration UI has been converted from an editable interface to a read-onl
|
||||
## Smartdata Cache System (2026-02-03)
|
||||
|
||||
### Overview
|
||||
DcRouter now uses smartdata + LocalTsmDb for persistent caching. Data is stored at `/etc/dcrouter/tsmdb`.
|
||||
DcRouter now uses smartdata + LocalTsmDb for persistent caching. Data is stored at `~/.serve.zone/dcrouter/tsmdb`.
|
||||
|
||||
### Technology Stack
|
||||
| Layer | Package | Purpose |
|
||||
@@ -747,7 +747,7 @@ await email.delete();
|
||||
const dcRouter = new DcRouter({
|
||||
cacheConfig: {
|
||||
enabled: true,
|
||||
storagePath: '/etc/dcrouter/tsmdb',
|
||||
storagePath: '~/.serve.zone/dcrouter/tsmdb',
|
||||
dbName: 'dcrouter',
|
||||
cleanupIntervalHours: 1,
|
||||
ttlConfig: {
|
||||
|
||||
@@ -219,7 +219,7 @@ const router = new DcRouter({
|
||||
storage: { fsPath: '/var/lib/dcrouter/data' },
|
||||
|
||||
// Cache database
|
||||
cacheConfig: { enabled: true, storagePath: '/etc/dcrouter/tsmdb' },
|
||||
cacheConfig: { enabled: true, storagePath: '~/.serve.zone/dcrouter/tsmdb' },
|
||||
|
||||
// TLS & ACME
|
||||
tls: { contactEmail: 'admin@example.com' },
|
||||
@@ -388,7 +388,7 @@ interface IDcRouterOptions {
|
||||
};
|
||||
cacheConfig?: {
|
||||
enabled?: boolean; // default: true
|
||||
storagePath?: string; // default: '/etc/dcrouter/tsmdb'
|
||||
storagePath?: string; // default: '~/.serve.zone/dcrouter/tsmdb'
|
||||
dbName?: string; // default: 'dcrouter'
|
||||
cleanupIntervalHours?: number; // default: 1
|
||||
ttlConfig?: {
|
||||
@@ -734,7 +734,7 @@ An embedded MongoDB-compatible database (via smartdata + LocalTsmDb) for persist
|
||||
```typescript
|
||||
cacheConfig: {
|
||||
enabled: true,
|
||||
storagePath: '/etc/dcrouter/tsmdb',
|
||||
storagePath: '~/.serve.zone/dcrouter/tsmdb',
|
||||
dbName: 'dcrouter',
|
||||
cleanupIntervalHours: 1,
|
||||
ttlConfig: {
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '5.0.3',
|
||||
version: '5.4.5',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
5
ts/cache/classes.cachedb.ts
vendored
5
ts/cache/classes.cachedb.ts
vendored
@@ -1,11 +1,12 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { logger } from '../logger.js';
|
||||
import { defaultTsmDbPath } from '../paths.js';
|
||||
|
||||
/**
|
||||
* Configuration options for CacheDb
|
||||
*/
|
||||
export interface ICacheDbOptions {
|
||||
/** Base storage path for TsmDB data (default: /etc/dcrouter/tsmdb) */
|
||||
/** Base storage path for TsmDB data (default: ~/.serve.zone/dcrouter/tsmdb) */
|
||||
storagePath?: string;
|
||||
/** Database name (default: dcrouter) */
|
||||
dbName?: string;
|
||||
@@ -29,7 +30,7 @@ export class CacheDb {
|
||||
|
||||
constructor(options: ICacheDbOptions = {}) {
|
||||
this.options = {
|
||||
storagePath: options.storagePath || '/etc/dcrouter/tsmdb',
|
||||
storagePath: options.storagePath || defaultTsmDbPath,
|
||||
dbName: options.dbName || 'dcrouter',
|
||||
debug: options.debug || false,
|
||||
};
|
||||
|
||||
@@ -122,7 +122,7 @@ export interface IDcRouterOptions {
|
||||
cacheConfig?: {
|
||||
/** Enable cache database (default: true) */
|
||||
enabled?: boolean;
|
||||
/** Storage path for TsmDB data (default: /etc/dcrouter/tsmdb) */
|
||||
/** Storage path for TsmDB data (default: ~/.serve.zone/dcrouter/tsmdb) */
|
||||
storagePath?: string;
|
||||
/** Database name (default: dcrouter) */
|
||||
dbName?: string;
|
||||
@@ -171,6 +171,7 @@ export class DcRouter {
|
||||
|
||||
// Core services
|
||||
public smartProxy?: plugins.smartproxy.SmartProxy;
|
||||
public smartAcme?: plugins.smartacme.SmartAcme;
|
||||
public dnsServer?: plugins.smartdns.dnsServerMod.DnsServer;
|
||||
public emailServer?: UnifiedEmailServer;
|
||||
public radiusServer?: RadiusServer;
|
||||
@@ -182,6 +183,16 @@ export class DcRouter {
|
||||
public cacheDb?: CacheDb;
|
||||
public cacheCleaner?: CacheCleaner;
|
||||
|
||||
// Certificate status tracking from SmartProxy events
|
||||
public certificateStatusMap = new Map<string, {
|
||||
status: 'valid' | 'failed';
|
||||
domain: string;
|
||||
expiryDate?: string;
|
||||
issuedAt?: string;
|
||||
source?: string;
|
||||
error?: string;
|
||||
}>();
|
||||
|
||||
// TypedRouter for API endpoints
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
|
||||
@@ -349,7 +360,7 @@ export class DcRouter {
|
||||
|
||||
// Initialize CacheDb singleton
|
||||
this.cacheDb = CacheDb.getInstance({
|
||||
storagePath: cacheConfig.storagePath || '/etc/dcrouter/tsmdb',
|
||||
storagePath: cacheConfig.storagePath || paths.defaultTsmDbPath,
|
||||
dbName: cacheConfig.dbName || 'dcrouter',
|
||||
debug: false,
|
||||
});
|
||||
@@ -429,12 +440,39 @@ export class DcRouter {
|
||||
acme: acmeConfig
|
||||
};
|
||||
|
||||
// If we have DNS challenge handlers, enhance the config
|
||||
// If we have DNS challenge handlers, create SmartAcme and wire to certProvisionFunction
|
||||
if (challengeHandlers.length > 0) {
|
||||
// We'll need to pass this to SmartProxy somehow
|
||||
// For now, we'll set it as a property
|
||||
(smartProxyConfig as any).acmeChallengeHandlers = challengeHandlers;
|
||||
(smartProxyConfig as any).acmeChallengePriority = ['dns-01', 'http-01'];
|
||||
this.smartAcme = new plugins.smartacme.SmartAcme({
|
||||
accountEmail: acmeConfig?.accountEmail || this.options.tls?.contactEmail || 'admin@example.com',
|
||||
certManager: new plugins.smartacme.certmanagers.MemoryCertManager(),
|
||||
environment: 'production',
|
||||
challengeHandlers: challengeHandlers,
|
||||
challengePriority: ['dns-01'],
|
||||
});
|
||||
await this.smartAcme.start();
|
||||
|
||||
smartProxyConfig.certProvisionFunction = async (domain, eventComms) => {
|
||||
try {
|
||||
eventComms.log(`Attempting DNS-01 via SmartAcme for ${domain}`);
|
||||
eventComms.setSource('smartacme-dns-01');
|
||||
const cert = await this.smartAcme.getCertificateForDomain(domain);
|
||||
if (cert.validUntil) {
|
||||
eventComms.setExpiryDate(new Date(cert.validUntil));
|
||||
}
|
||||
return {
|
||||
id: cert.id,
|
||||
domainName: cert.domainName,
|
||||
created: cert.created,
|
||||
validUntil: cert.validUntil,
|
||||
privateKey: cert.privateKey,
|
||||
publicKey: cert.publicKey,
|
||||
csr: cert.csr,
|
||||
};
|
||||
} catch (err) {
|
||||
eventComms.warn(`SmartAcme DNS-01 failed for ${domain}: ${err.message}, falling back to http-01`);
|
||||
return 'http01';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Create SmartProxy instance
|
||||
@@ -453,19 +491,41 @@ export class DcRouter {
|
||||
console.error('[DcRouter] Error stack:', err.stack);
|
||||
});
|
||||
|
||||
if (acmeConfig) {
|
||||
this.smartProxy.on('certificate-issued', (event) => {
|
||||
console.log(`[DcRouter] Certificate issued for ${event.domain}, expires ${event.expiryDate}`);
|
||||
});
|
||||
// Always listen for certificate events — emitted by both ACME and certProvisionFunction paths
|
||||
this.smartProxy.on('certificate-issued', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
|
||||
console.log(`[DcRouter] Certificate issued for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
|
||||
const routeName = this.findRouteNameForDomain(event.domain);
|
||||
if (routeName) {
|
||||
this.certificateStatusMap.set(routeName, {
|
||||
status: 'valid', domain: event.domain,
|
||||
expiryDate: event.expiryDate, issuedAt: new Date().toISOString(),
|
||||
source: event.source,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.smartProxy.on('certificate-renewed', (event) => {
|
||||
console.log(`[DcRouter] Certificate renewed for ${event.domain}, expires ${event.expiryDate}`);
|
||||
});
|
||||
this.smartProxy.on('certificate-renewed', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
|
||||
console.log(`[DcRouter] Certificate renewed for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
|
||||
const routeName = this.findRouteNameForDomain(event.domain);
|
||||
if (routeName) {
|
||||
this.certificateStatusMap.set(routeName, {
|
||||
status: 'valid', domain: event.domain,
|
||||
expiryDate: event.expiryDate, issuedAt: new Date().toISOString(),
|
||||
source: event.source,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.smartProxy.on('certificate-failed', (event) => {
|
||||
console.error(`[DcRouter] Certificate failed for ${event.domain}:`, event.error);
|
||||
});
|
||||
}
|
||||
this.smartProxy.on('certificate-failed', (event: plugins.smartproxy.ICertificateFailedEvent) => {
|
||||
console.error(`[DcRouter] Certificate failed for ${event.domain} (${event.source}):`, event.error);
|
||||
const routeName = this.findRouteNameForDomain(event.domain);
|
||||
if (routeName) {
|
||||
this.certificateStatusMap.set(routeName, {
|
||||
status: 'failed', domain: event.domain, error: event.error,
|
||||
source: event.source,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Start SmartProxy
|
||||
console.log('[DcRouter] Starting SmartProxy...');
|
||||
@@ -568,29 +628,6 @@ export class DcRouter {
|
||||
emailRoutes.push(routeConfig);
|
||||
}
|
||||
|
||||
// Add email domain-based routes if configured
|
||||
if (emailConfig.routes) {
|
||||
for (const route of emailConfig.routes) {
|
||||
emailRoutes.push({
|
||||
name: route.name,
|
||||
match: {
|
||||
ports: emailConfig.ports,
|
||||
domains: route.match.recipients ? [route.match.recipients.toString().split('@')[1]] : []
|
||||
},
|
||||
action: {
|
||||
type: 'forward',
|
||||
targets: route.action.type === 'forward' && route.action.forward ? [{
|
||||
host: route.action.forward.host,
|
||||
port: route.action.forward.port || 25
|
||||
}] : undefined,
|
||||
tls: {
|
||||
mode: 'passthrough'
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return emailRoutes;
|
||||
}
|
||||
|
||||
@@ -637,27 +674,45 @@ export class DcRouter {
|
||||
* @returns Whether the domain matches the pattern
|
||||
*/
|
||||
private isDomainMatch(domain: string, pattern: string): boolean {
|
||||
// Normalize inputs
|
||||
domain = domain.toLowerCase();
|
||||
pattern = pattern.toLowerCase();
|
||||
|
||||
// Check for exact match
|
||||
if (domain === pattern) {
|
||||
return true;
|
||||
if (domain === pattern) return true;
|
||||
|
||||
// Routing-glob: *example.com matches example.com, sub.example.com, *.example.com
|
||||
if (pattern.startsWith('*') && !pattern.startsWith('*.')) {
|
||||
const baseDomain = pattern.slice(1); // *nevermind.cloud → nevermind.cloud
|
||||
if (domain === baseDomain || domain === `*.${baseDomain}`) return true;
|
||||
if (domain.endsWith(baseDomain) && domain.length > baseDomain.length) return true;
|
||||
}
|
||||
|
||||
// Check for wildcard match (*.example.com)
|
||||
// Standard wildcard: *.example.com matches sub.example.com and example.com
|
||||
if (pattern.startsWith('*.')) {
|
||||
const patternSuffix = pattern.slice(2); // Remove the "*." prefix
|
||||
|
||||
// Check if domain ends with the pattern suffix and has at least one character before it
|
||||
return domain.endsWith(patternSuffix) && domain.length > patternSuffix.length;
|
||||
const suffix = pattern.slice(2);
|
||||
if (domain === suffix) return true;
|
||||
return domain.endsWith(suffix) && domain.length > suffix.length;
|
||||
}
|
||||
|
||||
// No match
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the route name that matches a given domain
|
||||
*/
|
||||
private findRouteNameForDomain(domain: string): string | undefined {
|
||||
if (!this.smartProxy) return undefined;
|
||||
for (const route of this.smartProxy.routeManager.getRoutes()) {
|
||||
if (!route.match.domains || !route.name) continue;
|
||||
const routeDomains = Array.isArray(route.match.domains)
|
||||
? route.match.domains
|
||||
: [route.match.domains];
|
||||
for (const pattern of routeDomains) {
|
||||
if (this.isDomainMatch(domain, pattern)) return route.name;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
console.log('Stopping DcRouter services...');
|
||||
|
||||
@@ -675,6 +730,9 @@ export class DcRouter {
|
||||
// Stop unified email server if running
|
||||
this.emailServer ? this.emailServer.stop().catch(err => console.error('Error stopping email server:', err)) : Promise.resolve(),
|
||||
|
||||
// Stop SmartAcme if running
|
||||
this.smartAcme ? this.smartAcme.stop().catch(err => console.error('Error stopping SmartAcme:', err)) : Promise.resolve(),
|
||||
|
||||
// Stop HTTP SmartProxy if running
|
||||
this.smartProxy ? this.smartProxy.stop().catch(err => console.error('Error stopping SmartProxy:', err)) : Promise.resolve(),
|
||||
|
||||
|
||||
@@ -147,8 +147,10 @@ export class MetricsManager {
|
||||
requestsPerSecond: proxyMetrics ? proxyMetrics.requests.perSecond() : 0,
|
||||
throughput: proxyMetrics ? {
|
||||
bytesIn: proxyMetrics.totals.bytesIn(),
|
||||
bytesOut: proxyMetrics.totals.bytesOut()
|
||||
} : { bytesIn: 0, bytesOut: 0 },
|
||||
bytesOut: proxyMetrics.totals.bytesOut(),
|
||||
bytesInPerSecond: proxyMetrics.throughput.instant().in,
|
||||
bytesOutPerSecond: proxyMetrics.throughput.instant().out,
|
||||
} : { bytesIn: 0, bytesOut: 0, bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -487,8 +489,12 @@ export class MetricsManager {
|
||||
return {
|
||||
connectionsByIP: new Map<string, number>(),
|
||||
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
||||
topIPs: [],
|
||||
topIPs: [] as Array<{ ip: string; count: number }>,
|
||||
totalDataTransferred: { bytesIn: 0, bytesOut: 0 },
|
||||
throughputHistory: [] as Array<{ timestamp: number; in: number; out: number }>,
|
||||
throughputByIP: new Map<string, { in: number; out: number }>(),
|
||||
requestsPerSecond: 0,
|
||||
requestsTotal: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -511,11 +517,25 @@ export class MetricsManager {
|
||||
bytesOut: proxyMetrics.totals.bytesOut()
|
||||
};
|
||||
|
||||
// Get throughput history from Rust engine (up to 300 seconds)
|
||||
const throughputHistory = proxyMetrics.throughput.history(300);
|
||||
|
||||
// Get per-IP throughput
|
||||
const throughputByIP = proxyMetrics.throughput.byIP();
|
||||
|
||||
// Get HTTP request rates
|
||||
const requestsPerSecond = proxyMetrics.requests.perSecond();
|
||||
const requestsTotal = proxyMetrics.requests.total();
|
||||
|
||||
return {
|
||||
connectionsByIP,
|
||||
throughputRate,
|
||||
topIPs,
|
||||
totalDataTransferred,
|
||||
throughputHistory,
|
||||
throughputByIP,
|
||||
requestsPerSecond,
|
||||
requestsTotal,
|
||||
};
|
||||
}, 200); // Use 200ms cache for more frequent updates
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ export class OpsServer {
|
||||
private statsHandler: handlers.StatsHandler;
|
||||
private radiusHandler: handlers.RadiusHandler;
|
||||
private emailOpsHandler: handlers.EmailOpsHandler;
|
||||
private certificateHandler: handlers.CertificateHandler;
|
||||
|
||||
constructor(dcRouterRefArg: DcRouter) {
|
||||
this.dcRouterRef = dcRouterRefArg;
|
||||
@@ -57,6 +58,7 @@ export class OpsServer {
|
||||
this.statsHandler = new handlers.StatsHandler(this);
|
||||
this.radiusHandler = new handlers.RadiusHandler(this);
|
||||
this.emailOpsHandler = new handlers.EmailOpsHandler(this);
|
||||
this.certificateHandler = new handlers.CertificateHandler(this);
|
||||
|
||||
console.log('✅ OpsServer TypedRequest handlers initialized');
|
||||
}
|
||||
|
||||
186
ts/opsserver/handlers/certificate.handler.ts
Normal file
186
ts/opsserver/handlers/certificate.handler.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { OpsServer } from '../classes.opsserver.js';
|
||||
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||
|
||||
export class CertificateHandler {
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
|
||||
constructor(private opsServerRef: OpsServer) {
|
||||
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||
this.registerHandlers();
|
||||
}
|
||||
|
||||
private registerHandlers(): void {
|
||||
// Get Certificate Overview
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCertificateOverview>(
|
||||
'getCertificateOverview',
|
||||
async (dataArg) => {
|
||||
const certificates = await this.buildCertificateOverview();
|
||||
const summary = this.buildSummary(certificates);
|
||||
return { certificates, summary };
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Reprovision Certificate
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificate>(
|
||||
'reprovisionCertificate',
|
||||
async (dataArg) => {
|
||||
return this.reprovisionCertificate(dataArg.routeName);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private async buildCertificateOverview(): Promise<interfaces.requests.ICertificateInfo[]> {
|
||||
const dcRouter = this.opsServerRef.dcRouterRef;
|
||||
const smartProxy = dcRouter.smartProxy;
|
||||
if (!smartProxy) return [];
|
||||
|
||||
const routes = smartProxy.routeManager.getRoutes();
|
||||
const certificates: interfaces.requests.ICertificateInfo[] = [];
|
||||
|
||||
for (const route of routes) {
|
||||
if (!route.name) continue;
|
||||
|
||||
const tls = route.action?.tls;
|
||||
if (!tls) continue;
|
||||
|
||||
// Skip passthrough routes - they don't manage certificates
|
||||
if (tls.mode === 'passthrough') continue;
|
||||
|
||||
const routeDomains = route.match.domains
|
||||
? (Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains])
|
||||
: [];
|
||||
|
||||
// Determine source
|
||||
let source: interfaces.requests.TCertificateSource = 'none';
|
||||
if (tls.certificate === 'auto') {
|
||||
// Check if a certProvisionFunction is configured
|
||||
if ((smartProxy.settings as any).certProvisionFunction) {
|
||||
source = 'provision-function';
|
||||
} else {
|
||||
source = 'acme';
|
||||
}
|
||||
} else if (tls.certificate && typeof tls.certificate === 'object') {
|
||||
source = 'static';
|
||||
}
|
||||
|
||||
// Start with unknown status
|
||||
let status: interfaces.requests.TCertificateStatus = 'unknown';
|
||||
let expiryDate: string | undefined;
|
||||
let issuedAt: string | undefined;
|
||||
let issuer: string | undefined;
|
||||
let error: string | undefined;
|
||||
|
||||
// Check event-based status from DcRouter's certificateStatusMap
|
||||
const eventStatus = dcRouter.certificateStatusMap.get(route.name);
|
||||
if (eventStatus) {
|
||||
status = eventStatus.status;
|
||||
expiryDate = eventStatus.expiryDate;
|
||||
issuedAt = eventStatus.issuedAt;
|
||||
error = eventStatus.error;
|
||||
if (eventStatus.source) {
|
||||
issuer = eventStatus.source;
|
||||
}
|
||||
}
|
||||
|
||||
// Try Rust-side certificate status if no event data
|
||||
if (status === 'unknown') {
|
||||
try {
|
||||
const rustStatus = await smartProxy.getCertificateStatus(route.name);
|
||||
if (rustStatus) {
|
||||
if (rustStatus.expiryDate) expiryDate = rustStatus.expiryDate;
|
||||
if (rustStatus.issuer) issuer = rustStatus.issuer;
|
||||
if (rustStatus.issuedAt) issuedAt = rustStatus.issuedAt;
|
||||
if (rustStatus.status === 'valid' || rustStatus.status === 'expired') {
|
||||
status = rustStatus.status;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Rust bridge may not support this command yet — ignore
|
||||
}
|
||||
}
|
||||
|
||||
// Compute status from expiry date if we have one and status is still valid/unknown
|
||||
if (expiryDate && (status === 'valid' || status === 'unknown')) {
|
||||
const expiry = new Date(expiryDate);
|
||||
const now = new Date();
|
||||
const daysUntilExpiry = (expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24);
|
||||
|
||||
if (daysUntilExpiry < 0) {
|
||||
status = 'expired';
|
||||
} else if (daysUntilExpiry < 30) {
|
||||
status = 'expiring';
|
||||
} else {
|
||||
status = 'valid';
|
||||
}
|
||||
}
|
||||
|
||||
// Static certs with no other info default to 'valid'
|
||||
if (source === 'static' && status === 'unknown') {
|
||||
status = 'valid';
|
||||
}
|
||||
|
||||
const canReprovision = source === 'acme' || source === 'provision-function';
|
||||
|
||||
certificates.push({
|
||||
routeName: route.name,
|
||||
domains: routeDomains,
|
||||
status,
|
||||
source,
|
||||
tlsMode: tls.mode as 'terminate' | 'terminate-and-reencrypt' | 'passthrough',
|
||||
expiryDate,
|
||||
issuer,
|
||||
issuedAt,
|
||||
error,
|
||||
canReprovision,
|
||||
});
|
||||
}
|
||||
|
||||
return certificates;
|
||||
}
|
||||
|
||||
private buildSummary(certificates: interfaces.requests.ICertificateInfo[]): {
|
||||
total: number;
|
||||
valid: number;
|
||||
expiring: number;
|
||||
expired: number;
|
||||
failed: number;
|
||||
unknown: number;
|
||||
} {
|
||||
const summary = { total: 0, valid: 0, expiring: 0, expired: 0, failed: 0, unknown: 0 };
|
||||
summary.total = certificates.length;
|
||||
for (const cert of certificates) {
|
||||
switch (cert.status) {
|
||||
case 'valid': summary.valid++; break;
|
||||
case 'expiring': summary.expiring++; break;
|
||||
case 'expired': summary.expired++; break;
|
||||
case 'failed': summary.failed++; break;
|
||||
case 'provisioning': // count as unknown
|
||||
case 'unknown': summary.unknown++; break;
|
||||
}
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
private async reprovisionCertificate(routeName: string): Promise<{ success: boolean; message?: string }> {
|
||||
const dcRouter = this.opsServerRef.dcRouterRef;
|
||||
const smartProxy = dcRouter.smartProxy;
|
||||
|
||||
if (!smartProxy) {
|
||||
return { success: false, message: 'SmartProxy is not running' };
|
||||
}
|
||||
|
||||
try {
|
||||
await smartProxy.provisionCertificate(routeName);
|
||||
// Clear event-based status so it gets refreshed
|
||||
dcRouter.certificateStatusMap.delete(routeName);
|
||||
return { success: true, message: `Certificate reprovisioning triggered for route '${routeName}'` };
|
||||
} catch (err) {
|
||||
return { success: false, message: err.message || 'Failed to reprovision certificate' };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,3 +5,4 @@ export * from './security.handler.js';
|
||||
export * from './stats.handler.js';
|
||||
export * from './radius.handler.js';
|
||||
export * from './email-ops.handler.js';
|
||||
export * from './certificate.handler.js';
|
||||
@@ -85,11 +85,23 @@ export class SecurityHandler {
|
||||
if (this.opsServerRef.dcRouterRef.metricsManager) {
|
||||
const networkStats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats();
|
||||
|
||||
// Convert per-IP throughput Map to serializable array
|
||||
const throughputByIP: Array<{ ip: string; in: number; out: number }> = [];
|
||||
if (networkStats.throughputByIP) {
|
||||
for (const [ip, tp] of networkStats.throughputByIP) {
|
||||
throughputByIP.push({ ip, in: tp.in, out: tp.out });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
connectionsByIP: Array.from(networkStats.connectionsByIP.entries()).map(([ip, count]) => ({ ip, count })),
|
||||
throughputRate: networkStats.throughputRate,
|
||||
topIPs: networkStats.topIPs,
|
||||
totalDataTransferred: networkStats.totalDataTransferred,
|
||||
throughputHistory: networkStats.throughputHistory || [],
|
||||
throughputByIP,
|
||||
requestsPerSecond: networkStats.requestsPerSecond || 0,
|
||||
requestsTotal: networkStats.requestsTotal || 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,6 +111,10 @@ export class SecurityHandler {
|
||||
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
||||
topIPs: [],
|
||||
totalDataTransferred: { bytesIn: 0, bytesOut: 0 },
|
||||
throughputHistory: [],
|
||||
throughputByIP: [],
|
||||
requestsPerSecond: 0,
|
||||
requestsTotal: 0,
|
||||
};
|
||||
}
|
||||
)
|
||||
|
||||
@@ -27,6 +27,8 @@ export class StatsHandler {
|
||||
cpuUsage: stats.cpuUsage,
|
||||
activeConnections: stats.activeConnections,
|
||||
totalConnections: stats.totalConnections,
|
||||
requestsPerSecond: stats.requestsPerSecond,
|
||||
throughput: stats.throughput,
|
||||
},
|
||||
history: dataArg.includeHistory ? stats.history : undefined,
|
||||
};
|
||||
@@ -191,6 +193,8 @@ export class StatsHandler {
|
||||
cpuUsage: stats.cpuUsage,
|
||||
activeConnections: stats.activeConnections,
|
||||
totalConnections: stats.totalConnections,
|
||||
requestsPerSecond: stats.requestsPerSecond,
|
||||
throughput: stats.throughput,
|
||||
};
|
||||
})
|
||||
);
|
||||
@@ -247,36 +251,39 @@ export class StatsHandler {
|
||||
|
||||
if (sections.network && this.opsServerRef.dcRouterRef.metricsManager) {
|
||||
promises.push(
|
||||
this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats().then(stats => {
|
||||
const connectionDetails: interfaces.data.IConnectionDetails[] = [];
|
||||
stats.connectionsByIP.forEach((count, ip) => {
|
||||
connectionDetails.push({
|
||||
remoteAddress: ip,
|
||||
protocol: 'https' as any,
|
||||
state: 'established' as any,
|
||||
startTime: Date.now(),
|
||||
bytesIn: 0,
|
||||
bytesOut: 0,
|
||||
});
|
||||
});
|
||||
(async () => {
|
||||
const stats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats();
|
||||
const serverStats = await this.collectServerStats();
|
||||
|
||||
// Build per-IP bandwidth lookup from throughputByIP
|
||||
const ipBandwidth = new Map<string, { in: number; out: number }>();
|
||||
if (stats.throughputByIP) {
|
||||
for (const [ip, tp] of stats.throughputByIP) {
|
||||
ipBandwidth.set(ip, { in: tp.in, out: tp.out });
|
||||
}
|
||||
}
|
||||
|
||||
metrics.network = {
|
||||
totalBandwidth: {
|
||||
in: stats.throughputRate.bytesInPerSecond,
|
||||
out: stats.throughputRate.bytesOutPerSecond,
|
||||
},
|
||||
activeConnections: stats.connectionsByIP.size,
|
||||
connectionDetails: connectionDetails.slice(0, 50), // Limit to 50 connections
|
||||
totalBytes: {
|
||||
in: stats.totalDataTransferred.bytesIn,
|
||||
out: stats.totalDataTransferred.bytesOut,
|
||||
},
|
||||
activeConnections: serverStats.activeConnections,
|
||||
connectionDetails: [],
|
||||
topEndpoints: stats.topIPs.map(ip => ({
|
||||
endpoint: ip.ip,
|
||||
requests: ip.count,
|
||||
bandwidth: {
|
||||
in: 0,
|
||||
out: 0,
|
||||
},
|
||||
bandwidth: ipBandwidth.get(ip.ip) || { in: 0, out: 0 },
|
||||
})),
|
||||
throughputHistory: stats.throughputHistory || [],
|
||||
requestsPerSecond: stats.requestsPerSecond || 0,
|
||||
requestsTotal: stats.requestsTotal || 0,
|
||||
};
|
||||
})
|
||||
})()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -301,6 +308,7 @@ export class StatsHandler {
|
||||
requestsPerSecond: number;
|
||||
activeConnections: number;
|
||||
totalConnections: number;
|
||||
throughput: interfaces.data.IServerStats['throughput'];
|
||||
history: Array<{
|
||||
timestamp: number;
|
||||
value: number;
|
||||
@@ -316,6 +324,7 @@ export class StatsHandler {
|
||||
requestsPerSecond: serverStats.requestsPerSecond,
|
||||
activeConnections: serverStats.activeConnections,
|
||||
totalConnections: serverStats.totalConnections,
|
||||
throughput: serverStats.throughput,
|
||||
history: [], // TODO: Implement history tracking
|
||||
};
|
||||
}
|
||||
@@ -340,6 +349,7 @@ export class StatsHandler {
|
||||
requestsPerSecond: 0,
|
||||
activeConnections: 0,
|
||||
totalConnections: 0,
|
||||
throughput: { bytesIn: 0, bytesOut: 0, bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
||||
history: [],
|
||||
};
|
||||
}
|
||||
|
||||
12
ts/paths.ts
12
ts/paths.ts
@@ -8,11 +8,17 @@ export const packageDir = plugins.path.join(
|
||||
);
|
||||
export const distServe = plugins.path.join(packageDir, './dist_serve');
|
||||
|
||||
// Configure data directory with environment variable or default to .nogit/data
|
||||
const DEFAULT_DATA_PATH = '.nogit/data';
|
||||
// Default base for all dcrouter data (always user-writable)
|
||||
export const dcrouterHomeDir = plugins.path.join(plugins.os.homedir(), '.serve.zone', 'dcrouter');
|
||||
|
||||
// Configure data directory with environment variable or default to ~/.serve.zone/dcrouter/data
|
||||
const DEFAULT_DATA_PATH = plugins.path.join(dcrouterHomeDir, 'data');
|
||||
export const dataDir = process.env.DATA_DIR
|
||||
? process.env.DATA_DIR
|
||||
: plugins.path.join(baseDir, DEFAULT_DATA_PATH);
|
||||
: DEFAULT_DATA_PATH;
|
||||
|
||||
// Default TsmDB path for CacheDb
|
||||
export const defaultTsmDbPath = plugins.path.join(dcrouterHomeDir, 'tsmdb');
|
||||
|
||||
// MTA directories
|
||||
export const keysDir = plugins.path.join(dataDir, 'keys');
|
||||
|
||||
@@ -17,6 +17,13 @@ export interface IServerStats {
|
||||
};
|
||||
activeConnections: number;
|
||||
totalConnections: number;
|
||||
requestsPerSecond: number;
|
||||
throughput: {
|
||||
bytesIn: number;
|
||||
bytesOut: number;
|
||||
bytesInPerSecond: number;
|
||||
bytesOutPerSecond: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IEmailStats {
|
||||
@@ -109,6 +116,10 @@ export interface INetworkMetrics {
|
||||
in: number;
|
||||
out: number;
|
||||
};
|
||||
totalBytes?: {
|
||||
in: number;
|
||||
out: number;
|
||||
};
|
||||
activeConnections: number;
|
||||
connectionDetails: IConnectionDetails[];
|
||||
topEndpoints: Array<{
|
||||
@@ -119,6 +130,9 @@ export interface INetworkMetrics {
|
||||
out: number;
|
||||
};
|
||||
}>;
|
||||
throughputHistory?: Array<{ timestamp: number; in: number; out: number }>;
|
||||
requestsPerSecond?: number;
|
||||
requestsTotal?: number;
|
||||
}
|
||||
|
||||
export interface IConnectionDetails {
|
||||
|
||||
54
ts_interfaces/requests/certificate.ts
Normal file
54
ts_interfaces/requests/certificate.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as authInterfaces from '../data/auth.js';
|
||||
|
||||
export type TCertificateStatus = 'valid' | 'expiring' | 'expired' | 'provisioning' | 'failed' | 'unknown';
|
||||
export type TCertificateSource = 'acme' | 'provision-function' | 'static' | 'none';
|
||||
|
||||
export interface ICertificateInfo {
|
||||
routeName: string;
|
||||
domains: string[];
|
||||
status: TCertificateStatus;
|
||||
source: TCertificateSource;
|
||||
tlsMode: 'terminate' | 'terminate-and-reencrypt' | 'passthrough';
|
||||
expiryDate?: string; // ISO string
|
||||
issuer?: string;
|
||||
issuedAt?: string; // ISO string
|
||||
error?: string; // if status === 'failed'
|
||||
canReprovision: boolean; // true for acme/provision-function routes
|
||||
}
|
||||
|
||||
export interface IReq_GetCertificateOverview extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_GetCertificateOverview
|
||||
> {
|
||||
method: 'getCertificateOverview';
|
||||
request: {
|
||||
identity?: authInterfaces.IIdentity;
|
||||
};
|
||||
response: {
|
||||
certificates: ICertificateInfo[];
|
||||
summary: {
|
||||
total: number;
|
||||
valid: number;
|
||||
expiring: number;
|
||||
expired: number;
|
||||
failed: number;
|
||||
unknown: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_ReprovisionCertificate extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_ReprovisionCertificate
|
||||
> {
|
||||
method: 'reprovisionCertificate';
|
||||
request: {
|
||||
identity?: authInterfaces.IIdentity;
|
||||
routeName: string;
|
||||
};
|
||||
response: {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
};
|
||||
}
|
||||
@@ -5,3 +5,4 @@ export * from './stats.js';
|
||||
export * from './combined.stats.js';
|
||||
export * from './radius.js';
|
||||
export * from './email-ops.js';
|
||||
export * from './certificate.js';
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '5.0.3',
|
||||
version: '5.4.5',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -47,12 +47,25 @@ export interface INetworkState {
|
||||
connections: interfaces.data.IConnectionInfo[];
|
||||
connectionsByIP: { [ip: string]: number };
|
||||
throughputRate: { bytesInPerSecond: number; bytesOutPerSecond: number };
|
||||
totalBytes: { in: number; out: number };
|
||||
topIPs: Array<{ ip: string; count: number }>;
|
||||
throughputByIP: Array<{ ip: string; in: number; out: number }>;
|
||||
throughputHistory: Array<{ timestamp: number; in: number; out: number }>;
|
||||
requestsPerSecond: number;
|
||||
requestsTotal: number;
|
||||
lastUpdated: number;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface ICertificateState {
|
||||
certificates: interfaces.requests.ICertificateInfo[];
|
||||
summary: { total: number; valid: number; expiring: number; expired: number; failed: number; unknown: number };
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
lastUpdated: number;
|
||||
}
|
||||
|
||||
export interface IEmailOpsState {
|
||||
currentView: 'queued' | 'sent' | 'failed' | 'received' | 'security';
|
||||
queuedEmails: interfaces.requests.IEmailQueueItem[];
|
||||
@@ -103,7 +116,7 @@ export const configStatePart = await appState.getStatePart<IConfigState>(
|
||||
// Determine initial view from URL path
|
||||
const getInitialView = (): string => {
|
||||
const path = typeof window !== 'undefined' ? window.location.pathname : '/';
|
||||
const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security'];
|
||||
const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates'];
|
||||
const segments = path.split('/').filter(Boolean);
|
||||
const view = segments[0];
|
||||
return validViews.includes(view) ? view : 'overview';
|
||||
@@ -136,7 +149,12 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
|
||||
connections: [],
|
||||
connectionsByIP: {},
|
||||
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
||||
totalBytes: { in: 0, out: 0 },
|
||||
topIPs: [],
|
||||
throughputByIP: [],
|
||||
throughputHistory: [],
|
||||
requestsPerSecond: 0,
|
||||
requestsTotal: 0,
|
||||
lastUpdated: 0,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
@@ -162,6 +180,18 @@ export const emailOpsStatePart = await appState.getStatePart<IEmailOpsState>(
|
||||
'soft'
|
||||
);
|
||||
|
||||
export const certificateStatePart = await appState.getStatePart<ICertificateState>(
|
||||
'certificates',
|
||||
{
|
||||
certificates: [],
|
||||
summary: { total: 0, valid: 0, expiring: 0, expired: 0, failed: 0, unknown: 0 },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
lastUpdated: 0,
|
||||
},
|
||||
'soft'
|
||||
);
|
||||
|
||||
// Actions for state management
|
||||
interface IActionContext {
|
||||
identity: interfaces.data.IIdentity | null;
|
||||
@@ -341,6 +371,13 @@ export const setActiveViewAction = uiStatePart.createAction<string>(async (state
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// If switching to certificates view, ensure we fetch certificate data
|
||||
if (viewName === 'certificates' && currentState.activeView !== 'certificates') {
|
||||
setTimeout(() => {
|
||||
certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
return {
|
||||
...currentState,
|
||||
activeView: viewName,
|
||||
@@ -394,7 +431,14 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
||||
connections: connectionsResponse.connections,
|
||||
connectionsByIP,
|
||||
throughputRate: networkStatsResponse.throughputRate || { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
||||
totalBytes: networkStatsResponse.totalDataTransferred
|
||||
? { in: networkStatsResponse.totalDataTransferred.bytesIn, out: networkStatsResponse.totalDataTransferred.bytesOut }
|
||||
: { in: 0, out: 0 },
|
||||
topIPs: networkStatsResponse.topIPs || [],
|
||||
throughputByIP: networkStatsResponse.throughputByIP || [],
|
||||
throughputHistory: networkStatsResponse.throughputHistory || [],
|
||||
requestsPerSecond: networkStatsResponse.requestsPerSecond || 0,
|
||||
requestsTotal: networkStatsResponse.requestsTotal || 0,
|
||||
lastUpdated: Date.now(),
|
||||
isLoading: false,
|
||||
error: null,
|
||||
@@ -641,6 +685,66 @@ export const removeFromSuppressionListAction = emailOpsStatePart.createAction<st
|
||||
}
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// Certificate Actions
|
||||
// ============================================================================
|
||||
|
||||
export const fetchCertificateOverviewAction = certificateStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetCertificateOverview
|
||||
>('/typedrequest', 'getCertificateOverview');
|
||||
|
||||
const response = await request.fire({
|
||||
identity: context.identity,
|
||||
});
|
||||
|
||||
return {
|
||||
certificates: response.certificates,
|
||||
summary: response.summary,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
lastUpdated: Date.now(),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
...currentState,
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch certificate overview',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const reprovisionCertificateAction = certificateStatePart.createAction<string>(
|
||||
async (statePartArg, routeName) => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_ReprovisionCertificate
|
||||
>('/typedrequest', 'reprovisionCertificate');
|
||||
|
||||
await request.fire({
|
||||
identity: context.identity,
|
||||
routeName,
|
||||
});
|
||||
|
||||
// Re-fetch overview after reprovisioning
|
||||
await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
|
||||
return statePartArg.getState();
|
||||
} catch (error) {
|
||||
return {
|
||||
...currentState,
|
||||
error: error instanceof Error ? error.message : 'Failed to reprovision certificate',
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Combined refresh action for efficient polling
|
||||
async function dispatchCombinedRefreshAction() {
|
||||
const context = getActionContext();
|
||||
@@ -703,7 +807,12 @@ async function dispatchCombinedRefreshAction() {
|
||||
bytesInPerSecond: network.totalBandwidth.in,
|
||||
bytesOutPerSecond: network.totalBandwidth.out
|
||||
},
|
||||
totalBytes: network.totalBytes || { in: 0, out: 0 },
|
||||
topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })),
|
||||
throughputByIP: network.topEndpoints.map(e => ({ ip: e.endpoint, in: e.bandwidth?.in || 0, out: e.bandwidth?.out || 0 })),
|
||||
throughputHistory: network.throughputHistory || [],
|
||||
requestsPerSecond: network.requestsPerSecond || 0,
|
||||
requestsTotal: network.requestsTotal || 0,
|
||||
lastUpdated: Date.now(),
|
||||
isLoading: false,
|
||||
error: null,
|
||||
@@ -718,13 +827,27 @@ async function dispatchCombinedRefreshAction() {
|
||||
bytesInPerSecond: network.totalBandwidth.in,
|
||||
bytesOutPerSecond: network.totalBandwidth.out
|
||||
},
|
||||
totalBytes: network.totalBytes || { in: 0, out: 0 },
|
||||
topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })),
|
||||
throughputByIP: network.topEndpoints.map(e => ({ ip: e.endpoint, in: e.bandwidth?.in || 0, out: e.bandwidth?.out || 0 })),
|
||||
throughputHistory: network.throughputHistory || [],
|
||||
requestsPerSecond: network.requestsPerSecond || 0,
|
||||
requestsTotal: network.requestsTotal || 0,
|
||||
lastUpdated: Date.now(),
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh certificate data if on certificates view
|
||||
if (currentView === 'certificates') {
|
||||
try {
|
||||
await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
|
||||
} catch (error) {
|
||||
console.error('Certificate refresh failed:', error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Combined refresh failed:', error);
|
||||
}
|
||||
@@ -749,13 +872,6 @@ let currentRefreshRate = 1000; // Track current refresh rate to avoid unnecessar
|
||||
refreshInterval = setInterval(() => {
|
||||
// Use combined refresh action for efficiency
|
||||
dispatchCombinedRefreshAction();
|
||||
|
||||
// If network view is active, also ensure we have fresh network data
|
||||
const currentView = uiStatePart.getState().activeView;
|
||||
if (currentView === 'network') {
|
||||
// Network view needs more frequent updates, fetch directly
|
||||
networkStatePart.dispatchAction(fetchNetworkStatsAction, null);
|
||||
}
|
||||
}, uiState.refreshInterval);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -5,4 +5,5 @@ export * from './ops-view-emails.js';
|
||||
export * from './ops-view-logs.js';
|
||||
export * from './ops-view-config.js';
|
||||
export * from './ops-view-security.js';
|
||||
export * from './ops-view-certificates.js';
|
||||
export * from './shared/index.js';
|
||||
@@ -19,6 +19,7 @@ import { OpsViewEmails } from './ops-view-emails.js';
|
||||
import { OpsViewLogs } from './ops-view-logs.js';
|
||||
import { OpsViewConfig } from './ops-view-config.js';
|
||||
import { OpsViewSecurity } from './ops-view-security.js';
|
||||
import { OpsViewCertificates } from './ops-view-certificates.js';
|
||||
|
||||
@customElement('ops-dashboard')
|
||||
export class OpsDashboard extends DeesElement {
|
||||
@@ -61,6 +62,10 @@ export class OpsDashboard extends DeesElement {
|
||||
name: 'Security',
|
||||
element: OpsViewSecurity,
|
||||
},
|
||||
{
|
||||
name: 'Certificates',
|
||||
element: OpsViewCertificates,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
355
ts_web/elements/ops-view-certificates.ts
Normal file
355
ts_web/elements/ops-view-certificates.ts
Normal file
@@ -0,0 +1,355 @@
|
||||
import {
|
||||
DeesElement,
|
||||
html,
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
css,
|
||||
state,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
import * as appstate from '../appstate.js';
|
||||
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||
import { viewHostCss } from './shared/css.js';
|
||||
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'ops-view-certificates': OpsViewCertificates;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('ops-view-certificates')
|
||||
export class OpsViewCertificates extends DeesElement {
|
||||
@state()
|
||||
accessor certState: appstate.ICertificateState = appstate.certificateStatePart.getState();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const sub = appstate.certificateStatePart.state.subscribe((newState) => {
|
||||
this.certState = newState;
|
||||
});
|
||||
this.rxSubscriptions.push(sub);
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
await super.connectedCallback();
|
||||
await appstate.certificateStatePart.dispatchAction(appstate.fetchCertificateOverviewAction, null);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
viewHostCss,
|
||||
css`
|
||||
.certificatesContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.statusBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 3px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.statusBadge.valid {
|
||||
background: ${cssManager.bdTheme('#dcfce7', '#14532d')};
|
||||
color: ${cssManager.bdTheme('#166534', '#4ade80')};
|
||||
}
|
||||
|
||||
.statusBadge.expiring {
|
||||
background: ${cssManager.bdTheme('#fff7ed', '#431407')};
|
||||
color: ${cssManager.bdTheme('#9a3412', '#fb923c')};
|
||||
}
|
||||
|
||||
.statusBadge.expired,
|
||||
.statusBadge.failed {
|
||||
background: ${cssManager.bdTheme('#fef2f2', '#450a0a')};
|
||||
color: ${cssManager.bdTheme('#991b1b', '#f87171')};
|
||||
}
|
||||
|
||||
.statusBadge.provisioning {
|
||||
background: ${cssManager.bdTheme('#eff6ff', '#172554')};
|
||||
color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};
|
||||
}
|
||||
|
||||
.statusBadge.unknown {
|
||||
background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
|
||||
color: ${cssManager.bdTheme('#4b5563', '#9ca3af')};
|
||||
}
|
||||
|
||||
.sourceBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
|
||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
||||
}
|
||||
|
||||
.domainPills {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.domainPill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
background: ${cssManager.bdTheme('#e0e7ff', '#1e1b4b')};
|
||||
color: ${cssManager.bdTheme('#3730a3', '#a5b4fc')};
|
||||
}
|
||||
|
||||
.moreCount {
|
||||
font-size: 11px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.errorText {
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#991b1b', '#f87171')};
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.expiryInfo {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.expiryInfo .daysLeft {
|
||||
font-size: 11px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
}
|
||||
|
||||
.expiryInfo .daysLeft.warn {
|
||||
color: ${cssManager.bdTheme('#9a3412', '#fb923c')};
|
||||
}
|
||||
|
||||
.expiryInfo .daysLeft.danger {
|
||||
color: ${cssManager.bdTheme('#991b1b', '#f87171')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
const { summary } = this.certState;
|
||||
|
||||
return html`
|
||||
<ops-sectionheading>Certificates</ops-sectionheading>
|
||||
|
||||
<div class="certificatesContainer">
|
||||
${this.renderStatsTiles(summary)}
|
||||
${this.renderCertificateTable()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStatsTiles(summary: appstate.ICertificateState['summary']): TemplateResult {
|
||||
const tiles: IStatsTile[] = [
|
||||
{
|
||||
id: 'total',
|
||||
title: 'Total Certificates',
|
||||
value: summary.total,
|
||||
type: 'number',
|
||||
icon: 'shieldHalved',
|
||||
color: '#3b82f6',
|
||||
},
|
||||
{
|
||||
id: 'valid',
|
||||
title: 'Valid',
|
||||
value: summary.valid,
|
||||
type: 'number',
|
||||
icon: 'check',
|
||||
color: '#22c55e',
|
||||
},
|
||||
{
|
||||
id: 'expiring',
|
||||
title: 'Expiring Soon',
|
||||
value: summary.expiring,
|
||||
type: 'number',
|
||||
icon: 'clock',
|
||||
color: '#f59e0b',
|
||||
},
|
||||
{
|
||||
id: 'problems',
|
||||
title: 'Failed / Expired',
|
||||
value: summary.failed + summary.expired,
|
||||
type: 'number',
|
||||
icon: 'triangleExclamation',
|
||||
color: '#ef4444',
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<dees-statsgrid
|
||||
.tiles=${tiles}
|
||||
.minTileWidth=${200}
|
||||
.gridActions=${[
|
||||
{
|
||||
name: 'Refresh',
|
||||
iconName: 'arrowsRotate',
|
||||
action: async () => {
|
||||
await appstate.certificateStatePart.dispatchAction(
|
||||
appstate.fetchCertificateOverviewAction,
|
||||
null
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
></dees-statsgrid>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderCertificateTable(): TemplateResult {
|
||||
return html`
|
||||
<dees-table
|
||||
.data=${this.certState.certificates}
|
||||
.displayFunction=${(cert: interfaces.requests.ICertificateInfo) => ({
|
||||
Route: cert.routeName,
|
||||
Domains: this.renderDomainPills(cert.domains),
|
||||
Status: this.renderStatusBadge(cert.status),
|
||||
Source: this.renderSourceBadge(cert.source),
|
||||
Expires: this.renderExpiry(cert.expiryDate),
|
||||
Error: cert.error
|
||||
? html`<span class="errorText" title="${cert.error}">${cert.error}</span>`
|
||||
: '',
|
||||
})}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'Reprovision',
|
||||
iconName: 'arrowsRotate',
|
||||
type: ['inRow'],
|
||||
actionFunc: async (actionData: { item: interfaces.requests.ICertificateInfo }) => {
|
||||
const cert = actionData.item;
|
||||
if (!cert.canReprovision) {
|
||||
const { DeesToast } = await import('@design.estate/dees-catalog');
|
||||
DeesToast.show({
|
||||
message: 'This certificate source does not support reprovisioning.',
|
||||
type: 'warning',
|
||||
duration: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
await appstate.certificateStatePart.dispatchAction(
|
||||
appstate.reprovisionCertificateAction,
|
||||
cert.routeName,
|
||||
);
|
||||
const { DeesToast } = await import('@design.estate/dees-catalog');
|
||||
DeesToast.show({
|
||||
message: `Reprovisioning triggered for ${cert.routeName}`,
|
||||
type: 'success',
|
||||
duration: 3000,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'View Details',
|
||||
iconName: 'magnifyingGlass',
|
||||
type: ['doubleClick', 'contextmenu'],
|
||||
actionFunc: async (actionData: { item: interfaces.requests.ICertificateInfo }) => {
|
||||
const cert = actionData.item;
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
await DeesModal.createAndShow({
|
||||
heading: `Certificate: ${cert.routeName}`,
|
||||
content: html`
|
||||
<div style="padding: 20px;">
|
||||
<dees-dataview-codebox
|
||||
.heading=${'Certificate Details'}
|
||||
progLang="json"
|
||||
.codeToDisplay=${JSON.stringify(cert, null, 2)}
|
||||
></dees-dataview-codebox>
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Copy Route Name',
|
||||
iconName: 'copy',
|
||||
action: async () => {
|
||||
await navigator.clipboard.writeText(cert.routeName);
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
]}
|
||||
heading1="Certificate Status"
|
||||
heading2="TLS certificates across all routes"
|
||||
searchable
|
||||
.pagination=${true}
|
||||
.paginationSize=${50}
|
||||
dataName="certificate"
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderDomainPills(domains: string[]): TemplateResult {
|
||||
const maxShow = 3;
|
||||
const visible = domains.slice(0, maxShow);
|
||||
const remaining = domains.length - maxShow;
|
||||
|
||||
return html`
|
||||
<span class="domainPills">
|
||||
${visible.map((d) => html`<span class="domainPill">${d}</span>`)}
|
||||
${remaining > 0 ? html`<span class="moreCount">+${remaining} more</span>` : ''}
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStatusBadge(status: interfaces.requests.TCertificateStatus): TemplateResult {
|
||||
return html`<span class="statusBadge ${status}">${status}</span>`;
|
||||
}
|
||||
|
||||
private renderSourceBadge(source: interfaces.requests.TCertificateSource): TemplateResult {
|
||||
const labels: Record<string, string> = {
|
||||
acme: 'ACME',
|
||||
'provision-function': 'Custom',
|
||||
static: 'Static',
|
||||
none: 'None',
|
||||
};
|
||||
return html`<span class="sourceBadge">${labels[source] || source}</span>`;
|
||||
}
|
||||
|
||||
private renderExpiry(expiryDate?: string): TemplateResult {
|
||||
if (!expiryDate) {
|
||||
return html`<span style="color: ${cssManager.bdTheme('#9ca3af', '#4b5563')}">--</span>`;
|
||||
}
|
||||
|
||||
const expiry = new Date(expiryDate);
|
||||
const now = new Date();
|
||||
const daysLeft = Math.ceil((expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
const dateStr = expiry.toLocaleDateString();
|
||||
let daysClass = '';
|
||||
let daysText = '';
|
||||
|
||||
if (daysLeft < 0) {
|
||||
daysClass = 'danger';
|
||||
daysText = `(expired)`;
|
||||
} else if (daysLeft < 30) {
|
||||
daysClass = 'warn';
|
||||
daysText = `(${daysLeft}d left)`;
|
||||
} else {
|
||||
daysText = `(${daysLeft}d left)`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<span class="expiryInfo">
|
||||
${dateStr} <span class="daysLeft ${daysClass}">${daysText}</span>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -52,8 +52,7 @@ export class OpsViewNetwork extends DeesElement {
|
||||
private requestCountHistory = new Map<number, number>(); // Track requests per time bucket
|
||||
private trafficUpdateTimer: any = null;
|
||||
private requestsPerSecHistory: number[] = []; // Track requests/sec over time for trend
|
||||
|
||||
// Removed byte tracking - now using real-time data from SmartProxy
|
||||
private historyLoaded = false; // Whether server-side throughput history has been loaded
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -111,6 +110,54 @@ export class OpsViewNetwork extends DeesElement {
|
||||
this.lastTrafficUpdateTime = now;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load server-side throughput history into the chart.
|
||||
* Called once when history data first arrives from the Rust engine.
|
||||
* This pre-populates the chart so users see historical data immediately
|
||||
* instead of starting from all zeros.
|
||||
*/
|
||||
private loadThroughputHistory() {
|
||||
const history = this.networkState.throughputHistory;
|
||||
if (!history || history.length === 0) return;
|
||||
|
||||
this.historyLoaded = true;
|
||||
|
||||
// Convert history points to chart data format (bytes/sec → Mbit/s)
|
||||
const historyIn = history.map(p => ({
|
||||
x: new Date(p.timestamp).toISOString(),
|
||||
y: Math.round((p.in * 8) / 1000000 * 10) / 10,
|
||||
}));
|
||||
const historyOut = history.map(p => ({
|
||||
x: new Date(p.timestamp).toISOString(),
|
||||
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);
|
||||
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) {
|
||||
const now = Date.now();
|
||||
const range = 5 * 60 * 1000;
|
||||
const bucketSize = range / 60;
|
||||
const padCount = 60 - 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(),
|
||||
y: 0,
|
||||
}));
|
||||
const padOut = padIn.map(p => ({ ...p }));
|
||||
|
||||
this.trafficDataIn = [...padIn, ...this.trafficDataIn];
|
||||
this.trafficDataOut = [...padOut, ...this.trafficDataOut];
|
||||
}
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
viewHostCss,
|
||||
@@ -352,21 +399,6 @@ export class OpsViewNetwork extends DeesElement {
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
private calculateRequestsPerSecond(): number {
|
||||
// Calculate from actual request data in the last minute
|
||||
const oneMinuteAgo = Date.now() - 60000;
|
||||
const recentRequests = this.networkRequests.filter(req => req.timestamp >= oneMinuteAgo);
|
||||
const reqPerSec = Math.round(recentRequests.length / 60);
|
||||
|
||||
// Track history for trend (keep last 20 values)
|
||||
this.requestsPerSecHistory.push(reqPerSec);
|
||||
if (this.requestsPerSecHistory.length > 20) {
|
||||
this.requestsPerSecHistory.shift();
|
||||
}
|
||||
|
||||
return reqPerSec;
|
||||
}
|
||||
|
||||
private calculateThroughput(): { in: number; out: number } {
|
||||
// Use real throughput data from network state
|
||||
return {
|
||||
@@ -376,16 +408,17 @@ export class OpsViewNetwork extends DeesElement {
|
||||
}
|
||||
|
||||
private renderNetworkStats(): TemplateResult {
|
||||
const reqPerSec = this.calculateRequestsPerSecond();
|
||||
// Use server-side requests/sec from SmartProxy's Rust engine
|
||||
const reqPerSec = this.networkState.requestsPerSecond || 0;
|
||||
const throughput = this.calculateThroughput();
|
||||
const activeConnections = this.statsState.serverStats?.activeConnections || 0;
|
||||
|
||||
// Throughput data is now available in the stats tiles
|
||||
|
||||
// Use request count history for the requests/sec trend
|
||||
// Track requests/sec history for the trend sparkline
|
||||
this.requestsPerSecHistory.push(reqPerSec);
|
||||
if (this.requestsPerSecHistory.length > 20) {
|
||||
this.requestsPerSecHistory.shift();
|
||||
}
|
||||
const trendData = [...this.requestsPerSecHistory];
|
||||
|
||||
// If we don't have enough data, pad with zeros
|
||||
while (trendData.length < 20) {
|
||||
trendData.unshift(0);
|
||||
}
|
||||
@@ -398,7 +431,7 @@ export class OpsViewNetwork extends DeesElement {
|
||||
type: 'number',
|
||||
icon: 'plug',
|
||||
color: activeConnections > 100 ? '#f59e0b' : '#22c55e',
|
||||
description: `Total: ${this.statsState.serverStats?.totalConnections || 0}`,
|
||||
description: `Total: ${this.networkState.requestsTotal || this.statsState.serverStats?.totalConnections || 0}`,
|
||||
actions: [
|
||||
{
|
||||
name: 'View Details',
|
||||
@@ -416,7 +449,7 @@ export class OpsViewNetwork extends DeesElement {
|
||||
icon: 'chartLine',
|
||||
color: '#3b82f6',
|
||||
trendData: trendData,
|
||||
description: `Average over last minute`,
|
||||
description: `Total: ${this.formatNumber(this.networkState.requestsTotal || 0)} requests`,
|
||||
},
|
||||
{
|
||||
id: 'throughputIn',
|
||||
@@ -426,6 +459,7 @@ export class OpsViewNetwork extends DeesElement {
|
||||
type: 'number',
|
||||
icon: 'download',
|
||||
color: '#22c55e',
|
||||
description: `Total: ${this.formatBytes(this.networkState.totalBytes?.in || 0)}`,
|
||||
},
|
||||
{
|
||||
id: 'throughputOut',
|
||||
@@ -435,6 +469,7 @@ export class OpsViewNetwork extends DeesElement {
|
||||
type: 'number',
|
||||
icon: 'upload',
|
||||
color: '#8b5cf6',
|
||||
description: `Total: ${this.formatBytes(this.networkState.totalBytes?.out || 0)}`,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -461,19 +496,32 @@ export class OpsViewNetwork extends DeesElement {
|
||||
return html``;
|
||||
}
|
||||
|
||||
// Build per-IP bandwidth lookup
|
||||
const bandwidthByIP = new Map<string, { in: number; out: number }>();
|
||||
if (this.networkState.throughputByIP) {
|
||||
for (const entry of this.networkState.throughputByIP) {
|
||||
bandwidthByIP.set(entry.ip, { in: entry.in, out: entry.out });
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate total connections across all top IPs
|
||||
const totalConnections = this.networkState.topIPs.reduce((sum, ipData) => sum + ipData.count, 0);
|
||||
|
||||
return html`
|
||||
<dees-table
|
||||
.data=${this.networkState.topIPs}
|
||||
.displayFunction=${(ipData: { ip: string; count: number }) => ({
|
||||
'IP Address': ipData.ip,
|
||||
'Connections': ipData.count,
|
||||
'Percentage': totalConnections > 0 ? ((ipData.count / totalConnections) * 100).toFixed(1) + '%' : '0%',
|
||||
})}
|
||||
.displayFunction=${(ipData: { ip: string; count: number }) => {
|
||||
const bw = bandwidthByIP.get(ipData.ip);
|
||||
return {
|
||||
'IP Address': ipData.ip,
|
||||
'Connections': ipData.count,
|
||||
'Bandwidth In': bw ? this.formatBitsPerSecond(bw.in) : '0 bit/s',
|
||||
'Bandwidth Out': bw ? this.formatBitsPerSecond(bw.out) : '0 bit/s',
|
||||
'Share': totalConnections > 0 ? ((ipData.count / totalConnections) * 100).toFixed(1) + '%' : '0%',
|
||||
};
|
||||
}}
|
||||
heading1="Top Connected IPs"
|
||||
heading2="IPs with most active connections"
|
||||
heading2="IPs with most active connections and bandwidth"
|
||||
.pagination=${false}
|
||||
dataName="ip"
|
||||
></dees-table>
|
||||
@@ -513,13 +561,10 @@ export class OpsViewNetwork extends DeesElement {
|
||||
}
|
||||
}
|
||||
|
||||
// Generate traffic data based on request history
|
||||
this.updateTrafficData();
|
||||
}
|
||||
|
||||
private updateTrafficData() {
|
||||
// This method is called when network data updates
|
||||
// The actual chart updates are handled by the timer calling addTrafficDataPoint()
|
||||
// Load server-side throughput history into chart (once)
|
||||
if (!this.historyLoaded && this.networkState.throughputHistory && this.networkState.throughputHistory.length > 0) {
|
||||
this.loadThroughputHistory();
|
||||
}
|
||||
}
|
||||
|
||||
private startTrafficUpdateTimer() {
|
||||
|
||||
@@ -135,6 +135,20 @@ export class OpsViewOverview extends DeesElement {
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
private formatBitsPerSecond(bytesPerSecond: number): string {
|
||||
const bitsPerSecond = bytesPerSecond * 8;
|
||||
const units = ['bit/s', 'kbit/s', 'Mbit/s', 'Gbit/s'];
|
||||
let size = bitsPerSecond;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1000 && unitIndex < units.length - 1) {
|
||||
size /= 1000;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
private renderServerStats(): TemplateResult {
|
||||
if (!this.statsState.serverStats) return html``;
|
||||
|
||||
@@ -162,6 +176,24 @@ export class OpsViewOverview extends DeesElement {
|
||||
color: '#3b82f6',
|
||||
description: `Total: ${this.statsState.serverStats.totalConnections}`,
|
||||
},
|
||||
{
|
||||
id: 'throughputIn',
|
||||
title: 'Throughput In',
|
||||
value: this.formatBitsPerSecond(this.statsState.serverStats.throughput?.bytesInPerSecond || 0),
|
||||
type: 'text',
|
||||
icon: 'download',
|
||||
color: '#22c55e',
|
||||
description: `Total: ${this.formatBytes(this.statsState.serverStats.throughput?.bytesIn || 0)}`,
|
||||
},
|
||||
{
|
||||
id: 'throughputOut',
|
||||
title: 'Throughput Out',
|
||||
value: this.formatBitsPerSecond(this.statsState.serverStats.throughput?.bytesOutPerSecond || 0),
|
||||
type: 'text',
|
||||
icon: 'upload',
|
||||
color: '#8b5cf6',
|
||||
description: `Total: ${this.formatBytes(this.statsState.serverStats.throughput?.bytesOut || 0)}`,
|
||||
},
|
||||
{
|
||||
id: 'cpu',
|
||||
title: 'CPU Usage',
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as appstate from './appstate.js';
|
||||
|
||||
const SmartRouter = plugins.domtools.plugins.smartrouter.SmartRouter;
|
||||
|
||||
export const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security'] as const;
|
||||
export const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates'] as const;
|
||||
export const validEmailFolders = ['queued', 'sent', 'failed', 'security'] as const;
|
||||
|
||||
export type TValidView = typeof validViews[number];
|
||||
|
||||
Reference in New Issue
Block a user