Compare commits

...

14 Commits

Author SHA1 Message Date
79765d6729 v7.4.0
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-21 14:02:48 +00:00
ffc93eb9d3 feat(opsserver): add real-time log push to ops dashboard and recent DNS query tracking 2026-02-21 14:02:48 +00:00
1337a4905a v7.3.0
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-20 15:37:15 +00:00
c7418d9e1a feat(dcrouter): Wire DNS server query events to MetricsManager for time-series tracking and bump @push.rocks/smartdns to ^7.9.0 2026-02-20 15:37:15 +00:00
2a94ffd4c9 v7.2.0
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-20 12:55:20 +00:00
b2fe6caf33 feat(logs): replace custom logs list with dees-chart-log component and push logs to chart, add log mapping and lifecycle sync, and bump smartlog dependency 2026-02-20 12:55:20 +00:00
822bbc1957 v7.1.0
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-19 17:23:43 +00:00
eacddc7ce1 feat(ops/monitoring): add in-memory log buffer, metrics time-series and ops UI integration 2026-02-19 17:23:43 +00:00
dc6ce341bd v7.0.1
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-19 14:36:11 +00:00
1aadc93f92 fix(monitoring): Use smartMetrics cpuPercentage for cpuUsage.user and update smartmetrics and smartproxy dependencies 2026-02-19 14:36:11 +00:00
8fdcd479d6 v7.0.0
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-19 10:16:23 +00:00
d24dde8eff BREAKING CHANGE(deps): bump dependencies: @serve.zone/remoteingress to ^4.0.0 (breaking), @push.rocks/smartproxy to ^25.7.6, @types/node to ^25.3.0 2026-02-19 10:16:23 +00:00
40a34073e9 v6.13.2
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-19 08:33:41 +00:00
9ac297c197 fix(runtime): prevent memory leaks and improve shutdown/stream handling across services 2026-02-19 08:33:41 +00:00
19 changed files with 887 additions and 353 deletions

View File

@@ -1,5 +1,68 @@
# Changelog # Changelog
## 2026-02-21 - 7.4.0 - feat(opsserver)
add real-time log push to ops dashboard and recent DNS query tracking
- Export baseLogger and add a log destination that pushes log entries to connected ops_dashboard TypedSocket clients (ts/opsserver/handlers/logs.handler.ts, ts/logger.ts).
- Introduce a new TypedRequest (pushLogEntry) interface for server→client log pushes (ts_interfaces/requests/logs.ts) and wire client handling in the web UI (ts_web/appstate.ts, ts_web/plugins.ts).
- Add TypedSocket client connection lifecycle to the web app, stream pushed log entries into app state and update log views incrementally (ts_web/appstate.ts, ts_web/elements/ops-view-logs.ts).
- MetricsManager now records recent DNS queries (timestamp, domain, type, answered, responseTimeMs) and exposes them via stats endpoints for display in the UI (ts/monitoring/classes.metricsmanager.ts, ts/opsserver/handlers/stats.handler.ts, ts_interfaces/data/stats.ts).
- UI overview now displays DNS query entries and uses answered flag to set log level (ts_web/elements/ops-view-overview.ts).
- Add import/export for typedsocket in web plugins to enable real-time push (ts_web/plugins.ts).
- Bump dependency @push.rocks/smartproxy patch version ^25.7.8 → ^25.7.9 (package.json).
## 2026-02-20 - 7.3.0 - feat(dcrouter)
Wire DNS server 'query' events to MetricsManager for time-series tracking and bump @push.rocks/smartdns to ^7.9.0
- Add dnsServer 'query' event listener that iterates event.questions and calls metricsManager.trackDnsQuery(question.type, question.name, false, event.responseTimeMs).
- Listener is guarded by a metricsManager existence check to avoid runtime errors when metrics are not configured.
- Bump dependency @push.rocks/smartdns from ^7.8.1 to ^7.9.0 in package.json.
## 2026-02-20 - 7.2.0 - feat(logs)
replace custom logs list with dees-chart-log component and push logs to chart, add log mapping and lifecycle sync, and bump smartlog dependency
- Replaced the legacy in-component log list and styling with a dees-chart-log element to render application logs.
- Added updated() lifecycle handler to push new logs to the chart and new helper methods pushLogsToChart() and getMappedLogEntries() to map log entries to the chart's expected format.
- Removed the streaming toggle, getActiveFilters(), legacy CSS for the log list, and the old per-entry rendering markup.
- Added explicit typing for dropdown @selectedOption handlers (e: any).
- Bumped dependency @push.rocks/smartlog from ^3.2.0 to ^3.2.1 in package.json.
## 2026-02-19 - 7.1.0 - feat(ops/monitoring)
add in-memory log buffer, metrics time-series and ops UI integration
- bump @push.rocks/smartlog to ^3.2.0
- introduce SmartlogDestinationBuffer (logBuffer) and wire it into the base logger to provide an in-memory log store for the Ops UI
- implement minute-resolution time-series buckets in MetricsManager with increment/prune helpers and new APIs getEmailTimeSeries and getDnsTimeSeries
- sync security counters from SecurityLogger and expose recent security events via StatsHandler
- wire email delivery lifecycle events and bounce processing to MetricsManager for tracking sent/received/failed metrics
- LogsHandler now queries the in-memory log buffer, maps smartlog levels/categories, supports search/level/time-range filtering and pagination
- UI updates: ops-view-overview, ops-view-logs and ops-view-security consume time-series and recent events to render charts, tables and filters
## 2026-02-19 - 7.0.1 - fix(monitoring)
Use smartMetrics cpuPercentage for cpuUsage.user and update smartmetrics and smartproxy dependencies
- Switch cpuUsage.user from parseFloat(smartMetricsData.cpuUsageText) to smartMetricsData.cpuPercentage to align with smartmetrics v3 API
- Bump @push.rocks/smartmetrics from ^2.0.10 to ^3.0.1
- Bump @push.rocks/smartproxy from ^25.7.6 to ^25.7.8
## 2026-02-19 - 7.0.0 - BREAKING CHANGE(deps)
bump dependencies: @serve.zone/remoteingress to ^4.0.0 (breaking), @push.rocks/smartproxy to ^25.7.6, @types/node to ^25.3.0
- Updated @serve.zone/remoteingress from ^3.3.0 to ^4.0.0 — major breaking change; may require code changes to adapt to new API.
- Updated @push.rocks/smartproxy from ^25.7.3 to ^25.7.6 — patch update (non-breaking).
- Updated @types/node from ^25.2.3 to ^25.3.0 — patch update (non-breaking).
- Current package version is 6.13.2; recommend bumping to 7.0.0 due to the breaking dependency upgrade.
## 2026-02-19 - 6.13.2 - fix(runtime)
prevent memory leaks and improve shutdown/stream handling across services
- Add CertProvisionScheduler.clear() to reset in-memory backoff cache and call it during DcRouter shutdown
- Stop any existing SmartAcme instance before creating a new one (await stop and log errors) to avoid duplicate running instances
- Null out many DcRouter service references and clear certificateStatusMap on shutdown to allow GC of stopped services
- Cap emailMetrics.recipients map size and trim to ~80% of MAX_TOP_DOMAINS to prevent unbounded growth
- Await virtualStream.sendData in logs follow handler and clear the interval if the stream errors/closes to avoid interval leaks
- Limit normalizedMacCache size and evict oldest entries when it exceeds 10000 to prevent unbounded cache growth
## 2026-02-18 - 6.13.1 - fix(dcrouter) ## 2026-02-18 - 6.13.1 - fix(dcrouter)
enable PROXY protocol v1 handling for SmartProxy when remoteIngress is enabled to preserve client IPs enable PROXY protocol v1 handling for SmartProxy when remoteIngress is enabled to preserve client IPs

View File

@@ -1,7 +1,7 @@
{ {
"name": "@serve.zone/dcrouter", "name": "@serve.zone/dcrouter",
"private": false, "private": false,
"version": "6.13.1", "version": "7.4.0",
"description": "A multifaceted routing service handling mail and SMS delivery functions.", "description": "A multifaceted routing service handling mail and SMS delivery functions.",
"type": "module", "type": "module",
"exports": { "exports": {
@@ -24,7 +24,7 @@
"@git.zone/tsrun": "^2.0.1", "@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^3.1.8", "@git.zone/tstest": "^3.1.8",
"@git.zone/tswatch": "^3.1.0", "@git.zone/tswatch": "^3.1.0",
"@types/node": "^25.2.3" "@types/node": "^25.3.0"
}, },
"dependencies": { "dependencies": {
"@api.global/typedrequest": "^3.2.6", "@api.global/typedrequest": "^3.2.6",
@@ -38,25 +38,25 @@
"@push.rocks/qenv": "^6.1.3", "@push.rocks/qenv": "^6.1.3",
"@push.rocks/smartacme": "^9.1.3", "@push.rocks/smartacme": "^9.1.3",
"@push.rocks/smartdata": "^7.0.15", "@push.rocks/smartdata": "^7.0.15",
"@push.rocks/smartdns": "^7.8.1", "@push.rocks/smartdns": "^7.9.0",
"@push.rocks/smartfile": "^13.1.2", "@push.rocks/smartfile": "^13.1.2",
"@push.rocks/smartguard": "^3.1.0", "@push.rocks/smartguard": "^3.1.0",
"@push.rocks/smartjwt": "^2.2.1", "@push.rocks/smartjwt": "^2.2.1",
"@push.rocks/smartlog": "^3.1.11", "@push.rocks/smartlog": "^3.2.1",
"@push.rocks/smartmetrics": "^2.0.10", "@push.rocks/smartmetrics": "^3.0.1",
"@push.rocks/smartmongo": "^5.1.0", "@push.rocks/smartmongo": "^5.1.0",
"@push.rocks/smartmta": "^5.2.2", "@push.rocks/smartmta": "^5.2.2",
"@push.rocks/smartnetwork": "^4.4.0", "@push.rocks/smartnetwork": "^4.4.0",
"@push.rocks/smartpath": "^6.0.0", "@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3", "@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartproxy": "^25.7.3", "@push.rocks/smartproxy": "^25.7.9",
"@push.rocks/smartradius": "^1.1.1", "@push.rocks/smartradius": "^1.1.1",
"@push.rocks/smartrequest": "^5.0.1", "@push.rocks/smartrequest": "^5.0.1",
"@push.rocks/smartrx": "^3.0.10", "@push.rocks/smartrx": "^3.0.10",
"@push.rocks/smartstate": "^2.0.30", "@push.rocks/smartstate": "^2.0.30",
"@push.rocks/smartunique": "^3.0.9", "@push.rocks/smartunique": "^3.0.9",
"@serve.zone/interfaces": "^5.3.0", "@serve.zone/interfaces": "^5.3.0",
"@serve.zone/remoteingress": "^3.3.0", "@serve.zone/remoteingress": "^4.0.0",
"@tsclass/tsclass": "^9.3.0", "@tsclass/tsclass": "^9.3.0",
"lru-cache": "^11.2.6", "lru-cache": "^11.2.6",
"uuid": "^13.0.0" "uuid": "^13.0.0"

234
pnpm-lock.yaml generated
View File

@@ -42,8 +42,8 @@ importers:
specifier: ^7.0.15 specifier: ^7.0.15
version: 7.0.15(socks@2.8.7) version: 7.0.15(socks@2.8.7)
'@push.rocks/smartdns': '@push.rocks/smartdns':
specifier: ^7.8.1 specifier: ^7.9.0
version: 7.8.1 version: 7.9.0
'@push.rocks/smartfile': '@push.rocks/smartfile':
specifier: ^13.1.2 specifier: ^13.1.2
version: 13.1.2 version: 13.1.2
@@ -54,11 +54,11 @@ importers:
specifier: ^2.2.1 specifier: ^2.2.1
version: 2.2.1 version: 2.2.1
'@push.rocks/smartlog': '@push.rocks/smartlog':
specifier: ^3.1.11 specifier: ^3.2.1
version: 3.1.11 version: 3.2.1
'@push.rocks/smartmetrics': '@push.rocks/smartmetrics':
specifier: ^2.0.10 specifier: ^3.0.1
version: 2.0.10 version: 3.0.1
'@push.rocks/smartmongo': '@push.rocks/smartmongo':
specifier: ^5.1.0 specifier: ^5.1.0
version: 5.1.0(socks@2.8.7) version: 5.1.0(socks@2.8.7)
@@ -75,8 +75,8 @@ importers:
specifier: ^4.2.3 specifier: ^4.2.3
version: 4.2.3 version: 4.2.3
'@push.rocks/smartproxy': '@push.rocks/smartproxy':
specifier: ^25.7.3 specifier: ^25.7.9
version: 25.7.3 version: 25.7.9
'@push.rocks/smartradius': '@push.rocks/smartradius':
specifier: ^1.1.1 specifier: ^1.1.1
version: 1.1.1 version: 1.1.1
@@ -96,8 +96,8 @@ importers:
specifier: ^5.3.0 specifier: ^5.3.0
version: 5.3.0 version: 5.3.0
'@serve.zone/remoteingress': '@serve.zone/remoteingress':
specifier: ^3.3.0 specifier: ^4.0.0
version: 3.3.0 version: 4.0.0
'@tsclass/tsclass': '@tsclass/tsclass':
specifier: ^9.3.0 specifier: ^9.3.0
version: 9.3.0 version: 9.3.0
@@ -124,8 +124,8 @@ importers:
specifier: ^3.1.0 specifier: ^3.1.0
version: 3.1.0(@tiptap/pm@2.27.2) version: 3.1.0(@tiptap/pm@2.27.2)
'@types/node': '@types/node':
specifier: ^25.2.3 specifier: ^25.3.0
version: 25.2.3 version: 25.3.0
packages: packages:
@@ -757,10 +757,6 @@ packages:
'@napi-rs/wasm-runtime@1.1.1': '@napi-rs/wasm-runtime@1.1.1':
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
'@opentelemetry/api@1.9.0':
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
engines: {node: '>=8.0.0'}
'@oxc-project/types@0.99.0': '@oxc-project/types@0.99.0':
resolution: {integrity: sha512-LLDEhXB7g1m5J+woRSgfKsFPS3LhR9xRhTeIoEBm5WrkwMxn6eZ0Ld0c0K5eHB57ChZX6I3uSmmLjZ8pcjlRcw==} resolution: {integrity: sha512-LLDEhXB7g1m5J+woRSgfKsFPS3LhR9xRhTeIoEBm5WrkwMxn6eZ0Ld0c0K5eHB57ChZX6I3uSmmLjZ8pcjlRcw==}
@@ -898,8 +894,8 @@ packages:
'@push.rocks/smartdelay@3.0.5': '@push.rocks/smartdelay@3.0.5':
resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==} resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==}
'@push.rocks/smartdns@7.8.1': '@push.rocks/smartdns@7.9.0':
resolution: {integrity: sha512-qEizM9dFzhq4XGICDC8Im7JLjwdokHdDZ6wLufBInaEOupq+8XOa9bC6EGlBQVsCXFUyrKzsFk6eBa9BSZMKPw==} resolution: {integrity: sha512-1nDUfyXQo6j9HTUfcjE+BLeAv9QZ7WtAsM1V28zIoFdUpjNg/5g382L024H73PHsxh6lSYNhYYmFvWqzFQhXKg==}
'@push.rocks/smartenv@5.0.13': '@push.rocks/smartenv@5.0.13':
resolution: {integrity: sha512-ACXmUcHZHl2CF2jnVuRw9saRRrZvJblCRs2d+K5aLR1DfkYFX3eA21kcMlKeLisI3aGNbIj9vz/rowN5qkRkfA==} resolution: {integrity: sha512-ACXmUcHZHl2CF2jnVuRw9saRRrZvJblCRs2d+K5aLR1DfkYFX3eA21kcMlKeLisI3aGNbIj9vz/rowN5qkRkfA==}
@@ -964,8 +960,8 @@ packages:
'@push.rocks/smartlog-interfaces@3.0.2': '@push.rocks/smartlog-interfaces@3.0.2':
resolution: {integrity: sha512-8hGRTJehbsFSJxLhCQkA018mZtXVPxPTblbg9VaE/EqISRzUw+eosJ2EJV7M4Qu0eiTJZjnWnNLn8CkD77ziWw==} resolution: {integrity: sha512-8hGRTJehbsFSJxLhCQkA018mZtXVPxPTblbg9VaE/EqISRzUw+eosJ2EJV7M4Qu0eiTJZjnWnNLn8CkD77ziWw==}
'@push.rocks/smartlog@3.1.11': '@push.rocks/smartlog@3.2.1':
resolution: {integrity: sha512-zyLH8pQD2UD7l76wJBESEWXU1FSTBLOuRI0/DN139EYyMkwMq1+pdQKptTkJhhVL/OIj56oMg9SpJb4bJB7uKg==} resolution: {integrity: sha512-x9/P59pfzY6HOGYmYrhqmoRl/pliTVx44g2Vbb8dIr/0zA39cAJHlPze1+UGncn37XKGmutK2iLSsJLEsexD0A==}
'@push.rocks/smartmail@2.2.0': '@push.rocks/smartmail@2.2.0':
resolution: {integrity: sha512-28K4HAcda7ODUUpFCgbS/uA+eqwVRcmLJERIdM9AvLHXaHAPLHH97HmwPPcAu9Sp3z05Um0inmDF51X6yVVkcw==} resolution: {integrity: sha512-28K4HAcda7ODUUpFCgbS/uA+eqwVRcmLJERIdM9AvLHXaHAPLHH97HmwPPcAu9Sp3z05Um0inmDF51X6yVVkcw==}
@@ -979,8 +975,8 @@ packages:
'@push.rocks/smartmatch@2.0.0': '@push.rocks/smartmatch@2.0.0':
resolution: {integrity: sha512-MBzP++1yNIBeox71X6VxpIgZ8m4bXnJpZJ4nWVH6IWpmO38MXTu4X0QF8tQnyT4LFcwvc9iiWaD15cstHa7Mmw==} resolution: {integrity: sha512-MBzP++1yNIBeox71X6VxpIgZ8m4bXnJpZJ4nWVH6IWpmO38MXTu4X0QF8tQnyT4LFcwvc9iiWaD15cstHa7Mmw==}
'@push.rocks/smartmetrics@2.0.10': '@push.rocks/smartmetrics@3.0.1':
resolution: {integrity: sha512-Fr4ZzJWFqTR67ThmPsj+uUfPoWZ+p87n7wItpXVjlQ2mf4pboWnsfmrrC1gqIXca1Dwa2i7L+GPoJ3hOZ+Qhfw==} resolution: {integrity: sha512-wdc9v8F0dQXwraAVCZjGvt2lnGpbVYHWzER9OFR+u+ultMq7aLjUevxSpkS8BkhBMLTuNMCCWIqyAzaCqHzFgQ==}
'@push.rocks/smartmime@1.0.6': '@push.rocks/smartmime@1.0.6':
resolution: {integrity: sha512-PHd+I4UcsnOATNg8wjDsSAmmJ4CwQFrQCNzd0HSJMs4ZpiK3Ya91almd6GLpDPU370U4HFh4FaPF4eEAI6vkJQ==} resolution: {integrity: sha512-PHd+I4UcsnOATNg8wjDsSAmmJ4CwQFrQCNzd0HSJMs4ZpiK3Ya91almd6GLpDPU370U4HFh4FaPF4eEAI6vkJQ==}
@@ -1034,8 +1030,8 @@ packages:
'@push.rocks/smartpromise@4.2.3': '@push.rocks/smartpromise@4.2.3':
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==} resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
'@push.rocks/smartproxy@25.7.3': '@push.rocks/smartproxy@25.7.9':
resolution: {integrity: sha512-9b5dwsLAhuDqnJptGBum4qBHlZwZPqPG3CJKxAwE3uFKjCmcE8qGDwodI0CjrQ7KW2PJ1BMq/Lk4ghs3Da6PWw==} resolution: {integrity: sha512-5esFvD72TEyveaEQbDYRgD7C5hDfWMSBvurNx3KPi02CBKG1gnhx/WWT7RHDS3KRF5fEQh9YxvI9aMkOwjc7sQ==}
'@push.rocks/smartpuppeteer@2.0.5': '@push.rocks/smartpuppeteer@2.0.5':
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==} resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
@@ -1131,9 +1127,6 @@ packages:
'@push.rocks/webrequest@3.0.37': '@push.rocks/webrequest@3.0.37':
resolution: {integrity: sha512-fLN7kP6GeHFxE4UH4r9C9pjcQb0QkJxHeAMwXvbOqB9hh0MFNKhtGU7GoaTn8SVRGRMPc9UqZVNwo6u5l8Wn0A==} resolution: {integrity: sha512-fLN7kP6GeHFxE4UH4r9C9pjcQb0QkJxHeAMwXvbOqB9hh0MFNKhtGU7GoaTn8SVRGRMPc9UqZVNwo6u5l8Wn0A==}
'@push.rocks/webrequest@4.0.1':
resolution: {integrity: sha512-I60XZZLVf8W5I7YdmUVVu4G92teE3rg3/aKaV00BRg8vJ3VXx3wc59Qj4em7zxQ5o0HvL8m1Aezw3RFMDPyVgA==}
'@push.rocks/webrequest@4.0.2': '@push.rocks/webrequest@4.0.2':
resolution: {integrity: sha512-rowzty+Q2papFBcnNYPcy+8CQJukSn/FGfQG8ap0bUgQUsx882u8kEyLM0Q+GlGHS5OiZ+Z0z5TZqLKlk3XHxA==} resolution: {integrity: sha512-rowzty+Q2papFBcnNYPcy+8CQJukSn/FGfQG8ap0bUgQUsx882u8kEyLM0Q+GlGHS5OiZ+Z0z5TZqLKlk3XHxA==}
@@ -1340,8 +1333,8 @@ packages:
'@serve.zone/interfaces@5.3.0': '@serve.zone/interfaces@5.3.0':
resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==} resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==}
'@serve.zone/remoteingress@3.3.0': '@serve.zone/remoteingress@4.0.0':
resolution: {integrity: sha512-nmw0F+Otrg78Xai9G3qLcP3NP4VkGPGm/6IGJmrXEgx3Z+ewh5Rhs1/rtN0mJFNXP77LZz1HuEBgR8aWbSHFQw==} resolution: {integrity: sha512-kcC9pnQdS5Mw0ypmwT3GIYwrjVQJOjWw5TJ+j46t6Y98S4Mb7wEAAPsU+YRXozpkqUxWUDBeDK1fQHLvoF9PmQ==}
'@sindresorhus/is@5.6.0': '@sindresorhus/is@5.6.0':
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
@@ -1834,11 +1827,8 @@ packages:
'@types/node@22.19.11': '@types/node@22.19.11':
resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==} resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==}
'@types/node@25.2.3': '@types/node@25.3.0':
resolution: {integrity: sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==} resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==}
'@types/pidusage@2.0.5':
resolution: {integrity: sha512-MIiyZI4/MK9UGUXWt0jJcCZhVw7YdhBuTOuqP/BjuLDLZ2PmmViMIQgZiWxtaMicQfAz/kMrZ5T7PKxFSkTeUA==}
'@types/ping@0.4.4': '@types/ping@0.4.4':
resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==} resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==}
@@ -2059,9 +2049,6 @@ packages:
resolution: {integrity: sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==} resolution: {integrity: sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
bintrees@1.0.2:
resolution: {integrity: sha1-SfiW1uhYpKSZ34XDj7OZua/4QPg=}
body-parser@2.2.2: body-parser@2.2.2:
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -3234,8 +3221,8 @@ packages:
resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==} resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
minimatch@10.2.0: minimatch@10.2.1:
resolution: {integrity: sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==} resolution: {integrity: sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
minimatch@3.1.2: minimatch@3.1.2:
@@ -3548,15 +3535,6 @@ packages:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'} engines: {node: '>=12'}
pidtree@0.6.0:
resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==}
engines: {node: '>=0.10'}
hasBin: true
pidusage@4.0.1:
resolution: {integrity: sha512-yCH2dtLHfEBnzlHUJymR/Z1nN2ePG3m392Mv8TFlTP1B0xkpMQNHAnfkY0n2tAi6ceKO6YWhxYfZ96V4vVkh/g==}
engines: {node: '>=18'}
ping@0.4.4: ping@0.4.4:
resolution: {integrity: sha512-56ZMC0j7SCsMMLdOoUg12VZCfj/+ZO+yfOSjaNCRrmZZr6GLbN2X/Ui56T15dI8NhiHckaw5X2pvyfAomanwqQ==} resolution: {integrity: sha512-56ZMC0j7SCsMMLdOoUg12VZCfj/+ZO+yfOSjaNCRrmZZr6GLbN2X/Ui56T15dI8NhiHckaw5X2pvyfAomanwqQ==}
engines: {node: '>=4.0.0'} engines: {node: '>=4.0.0'}
@@ -3576,10 +3554,6 @@ packages:
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
prom-client@15.1.3:
resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==}
engines: {node: ^16 || ^18 || >=20}
property-information@7.1.0: property-information@7.1.0:
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
@@ -3978,9 +3952,6 @@ packages:
tar-stream@3.1.7: tar-stream@3.1.7:
resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==}
tdigest@0.1.2:
resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==}
text-decoder@1.2.3: text-decoder@1.2.3:
resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==}
@@ -4089,8 +4060,8 @@ packages:
undici-types@6.21.0: undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
undici-types@7.16.0: undici-types@7.18.2:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
unified@11.0.5: unified@11.0.5:
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
@@ -4325,7 +4296,7 @@ snapshots:
'@push.rocks/smartfeed': 1.4.0 '@push.rocks/smartfeed': 1.4.0
'@push.rocks/smartfile': 11.2.7 '@push.rocks/smartfile': 11.2.7
'@push.rocks/smartjson': 5.2.0 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartlog': 3.1.11 '@push.rocks/smartlog': 3.2.1
'@push.rocks/smartlog-destination-devtools': 1.0.12 '@push.rocks/smartlog-destination-devtools': 1.0.12
'@push.rocks/smartlog-interfaces': 3.0.2 '@push.rocks/smartlog-interfaces': 3.0.2
'@push.rocks/smartmanifest': 2.0.2 '@push.rocks/smartmanifest': 2.0.2
@@ -4374,7 +4345,7 @@ snapshots:
'@push.rocks/smartfile': 13.1.2 '@push.rocks/smartfile': 13.1.2
'@push.rocks/smartfs': 1.3.1 '@push.rocks/smartfs': 1.3.1
'@push.rocks/smartjson': 5.2.0 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartlog': 3.1.11 '@push.rocks/smartlog': 3.2.1
'@push.rocks/smartlog-destination-devtools': 1.0.12 '@push.rocks/smartlog-destination-devtools': 1.0.12
'@push.rocks/smartlog-interfaces': 3.0.2 '@push.rocks/smartlog-interfaces': 3.0.2
'@push.rocks/smartmanifest': 2.0.2 '@push.rocks/smartmanifest': 2.0.2
@@ -4441,7 +4412,7 @@ snapshots:
'@apiclient.xyz/cloudflare@7.1.0': '@apiclient.xyz/cloudflare@7.1.0':
dependencies: dependencies:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.1.11 '@push.rocks/smartlog': 3.2.1
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrequest': 5.0.1 '@push.rocks/smartrequest': 5.0.1
'@push.rocks/smartstring': 4.1.0 '@push.rocks/smartstring': 4.1.0
@@ -5175,7 +5146,7 @@ snapshots:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartfile': 13.1.2 '@push.rocks/smartfile': 13.1.2
'@push.rocks/smartfs': 1.3.1 '@push.rocks/smartfs': 1.3.1
'@push.rocks/smartlog': 3.1.11 '@push.rocks/smartlog': 3.2.1
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
typescript: 5.9.3 typescript: 5.9.3
@@ -5196,7 +5167,7 @@ snapshots:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartfs': 1.3.1 '@push.rocks/smartfs': 1.3.1
'@push.rocks/smartinteract': 2.0.16 '@push.rocks/smartinteract': 2.0.16
'@push.rocks/smartlog': 3.1.11 '@push.rocks/smartlog': 3.2.1
'@push.rocks/smartlog-destination-local': 9.0.2 '@push.rocks/smartlog-destination-local': 9.0.2
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
@@ -5222,7 +5193,7 @@ snapshots:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartfile': 13.1.2 '@push.rocks/smartfile': 13.1.2
'@push.rocks/smartfs': 1.3.1 '@push.rocks/smartfs': 1.3.1
'@push.rocks/smartlog': 3.1.11 '@push.rocks/smartlog': 3.2.1
'@push.rocks/smartnpm': 2.0.6 '@push.rocks/smartnpm': 2.0.6
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartrequest': 5.0.1 '@push.rocks/smartrequest': 5.0.1
@@ -5257,7 +5228,7 @@ snapshots:
'@push.rocks/smartexpect': 2.5.0 '@push.rocks/smartexpect': 2.5.0
'@push.rocks/smartfile': 11.2.7 '@push.rocks/smartfile': 11.2.7
'@push.rocks/smartjson': 5.2.0 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartlog': 3.1.11 '@push.rocks/smartlog': 3.2.1
'@push.rocks/smartmongo': 2.2.0(socks@2.8.7) '@push.rocks/smartmongo': 2.2.0(socks@2.8.7)
'@push.rocks/smartnetwork': 4.4.0 '@push.rocks/smartnetwork': 4.4.0
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
@@ -5303,7 +5274,7 @@ snapshots:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartfs': 1.3.1 '@push.rocks/smartfs': 1.3.1
'@push.rocks/smartinteract': 2.0.16 '@push.rocks/smartinteract': 2.0.16
'@push.rocks/smartlog': 3.1.11 '@push.rocks/smartlog': 3.2.1
'@push.rocks/smartlog-destination-local': 9.0.2 '@push.rocks/smartlog-destination-local': 9.0.2
'@push.rocks/smartshell': 3.3.0 '@push.rocks/smartshell': 3.3.0
'@push.rocks/smartwatch': 6.3.0 '@push.rocks/smartwatch': 6.3.0
@@ -5534,8 +5505,6 @@ snapshots:
'@tybys/wasm-util': 0.10.1 '@tybys/wasm-util': 0.10.1
optional: true optional: true
'@opentelemetry/api@1.9.0': {}
'@oxc-project/types@0.99.0': {} '@oxc-project/types@0.99.0': {}
'@pdf-lib/standard-fonts@1.0.0': '@pdf-lib/standard-fonts@1.0.0':
@@ -5738,7 +5707,7 @@ snapshots:
'@push.rocks/qenv': 6.1.3 '@push.rocks/qenv': 6.1.3
'@push.rocks/smartfile': 11.2.7 '@push.rocks/smartfile': 11.2.7
'@push.rocks/smartjson': 5.2.0 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartlog': 3.1.11 '@push.rocks/smartlog': 3.2.1
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
@@ -5762,7 +5731,7 @@ snapshots:
'@api.global/typedrequest': 3.2.6 '@api.global/typedrequest': 3.2.6
'@configvault.io/interfaces': 1.0.17 '@configvault.io/interfaces': 1.0.17
'@push.rocks/smartfile': 11.2.7 '@push.rocks/smartfile': 11.2.7
'@push.rocks/smartlog': 3.1.11 '@push.rocks/smartlog': 3.2.1
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartacme@9.1.3(socks@2.8.7)': '@push.rocks/smartacme@9.1.3(socks@2.8.7)':
@@ -5772,8 +5741,8 @@ snapshots:
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartdata': 7.0.15(socks@2.8.7) '@push.rocks/smartdata': 7.0.15(socks@2.8.7)
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartdns': 7.8.1 '@push.rocks/smartdns': 7.9.0
'@push.rocks/smartlog': 3.1.11 '@push.rocks/smartlog': 3.2.1
'@push.rocks/smartnetwork': 4.4.0 '@push.rocks/smartnetwork': 4.4.0
'@push.rocks/smartstring': 4.1.0 '@push.rocks/smartstring': 4.1.0
'@push.rocks/smarttime': 4.2.3 '@push.rocks/smarttime': 4.2.3
@@ -5883,7 +5852,7 @@ snapshots:
'@push.rocks/smartcli@4.0.20': '@push.rocks/smartcli@4.0.20':
dependencies: dependencies:
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartlog': 3.1.11 '@push.rocks/smartlog': 3.2.1
'@push.rocks/smartobject': 1.0.12 '@push.rocks/smartobject': 1.0.12
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
@@ -5908,7 +5877,7 @@ snapshots:
dependencies: dependencies:
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.1.11 '@push.rocks/smartlog': 3.2.1
'@push.rocks/smartmongo': 2.2.0(socks@2.8.7) '@push.rocks/smartmongo': 2.2.0(socks@2.8.7)
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
@@ -5937,7 +5906,7 @@ snapshots:
dependencies: dependencies:
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.1.11 '@push.rocks/smartlog': 3.2.1
'@push.rocks/smartmongo': 2.2.0(socks@2.8.7) '@push.rocks/smartmongo': 2.2.0(socks@2.8.7)
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
@@ -5966,7 +5935,7 @@ snapshots:
dependencies: dependencies:
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartdns@7.8.1': '@push.rocks/smartdns@7.9.0':
dependencies: dependencies:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartenv': 6.0.0 '@push.rocks/smartenv': 6.0.0
@@ -5974,7 +5943,7 @@ snapshots:
'@push.rocks/smartrust': 1.2.1 '@push.rocks/smartrust': 1.2.1
'@tsclass/tsclass': 9.3.0 '@tsclass/tsclass': 9.3.0
acme-client: 5.4.0 acme-client: 5.4.0
minimatch: 10.2.0 minimatch: 10.2.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -6129,7 +6098,7 @@ snapshots:
'@api.global/typedrequest-interfaces': 2.0.2 '@api.global/typedrequest-interfaces': 2.0.2
'@tsclass/tsclass': 4.4.4 '@tsclass/tsclass': 4.4.4
'@push.rocks/smartlog@3.1.11': '@push.rocks/smartlog@3.2.1':
dependencies: dependencies:
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@push.rocks/consolecolor': 2.0.3 '@push.rocks/consolecolor': 2.0.3
@@ -6138,13 +6107,13 @@ snapshots:
'@push.rocks/smartfile': 11.2.7 '@push.rocks/smartfile': 11.2.7
'@push.rocks/smarthash': 3.2.6 '@push.rocks/smarthash': 3.2.6
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smarttime': 4.1.1 '@push.rocks/smarttime': 4.2.3
'@push.rocks/webrequest': 4.0.1 '@push.rocks/webrequest': 4.0.2
'@tsclass/tsclass': 9.3.0 '@tsclass/tsclass': 9.3.0
'@push.rocks/smartmail@2.2.0': '@push.rocks/smartmail@2.2.0':
dependencies: dependencies:
'@push.rocks/smartdns': 7.8.1 '@push.rocks/smartdns': 7.9.0
'@push.rocks/smartfile': 13.1.2 '@push.rocks/smartfile': 13.1.2
'@push.rocks/smartmustache': 3.0.2 '@push.rocks/smartmustache': 3.0.2
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
@@ -6173,14 +6142,10 @@ snapshots:
dependencies: dependencies:
matcher: 5.0.0 matcher: 5.0.0
'@push.rocks/smartmetrics@2.0.10': '@push.rocks/smartmetrics@3.0.1':
dependencies: dependencies:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.1.11 '@push.rocks/smartlog': 3.2.1
'@types/pidusage': 2.0.5
pidtree: 0.6.0
pidusage: 4.0.1
prom-client: 15.1.3
'@push.rocks/smartmime@1.0.6': '@push.rocks/smartmime@1.0.6':
dependencies: dependencies:
@@ -6249,7 +6214,7 @@ snapshots:
dependencies: dependencies:
'@push.rocks/smartfile': 13.1.2 '@push.rocks/smartfile': 13.1.2
'@push.rocks/smartfs': 1.3.1 '@push.rocks/smartfs': 1.3.1
'@push.rocks/smartlog': 3.1.11 '@push.rocks/smartlog': 3.2.1
'@push.rocks/smartmail': 2.2.0 '@push.rocks/smartmail': 2.2.0
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartrust': 1.2.1 '@push.rocks/smartrust': 1.2.1
@@ -6266,7 +6231,7 @@ snapshots:
'@push.rocks/smartnetwork@4.4.0': '@push.rocks/smartnetwork@4.4.0':
dependencies: dependencies:
'@push.rocks/smartdns': 7.8.1 '@push.rocks/smartdns': 7.9.0
'@push.rocks/smartping': 1.0.8 '@push.rocks/smartping': 1.0.8
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartstring': 4.1.0 '@push.rocks/smartstring': 4.1.0
@@ -6352,13 +6317,13 @@ snapshots:
'@push.rocks/smartpromise@4.2.3': {} '@push.rocks/smartpromise@4.2.3': {}
'@push.rocks/smartproxy@25.7.3': '@push.rocks/smartproxy@25.7.9':
dependencies: dependencies:
'@push.rocks/smartcrypto': 2.0.4 '@push.rocks/smartcrypto': 2.0.4
'@push.rocks/smartlog': 3.1.11 '@push.rocks/smartlog': 3.2.1
'@push.rocks/smartrust': 1.2.1 '@push.rocks/smartrust': 1.2.1
'@tsclass/tsclass': 9.3.0 '@tsclass/tsclass': 9.3.0
minimatch: 10.2.0 minimatch: 10.2.1
'@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)': '@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)':
dependencies: dependencies:
@@ -6436,7 +6401,7 @@ snapshots:
'@cfworker/json-schema': 4.1.1 '@cfworker/json-schema': 4.1.1
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartenv': 6.0.0 '@push.rocks/smartenv': 6.0.0
'@push.rocks/smartlog': 3.1.11 '@push.rocks/smartlog': 3.2.1
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
ws: 8.19.0 ws: 8.19.0
transitivePeerDependencies: transitivePeerDependencies:
@@ -6471,7 +6436,7 @@ snapshots:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartenv': 5.0.13 '@push.rocks/smartenv': 5.0.13
'@push.rocks/smartjson': 5.2.0 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartlog': 3.1.11 '@push.rocks/smartlog': 3.2.1
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
'@push.rocks/smarttime': 4.1.1 '@push.rocks/smarttime': 4.1.1
@@ -6586,7 +6551,7 @@ snapshots:
'@design.estate/dees-element': 2.1.6 '@design.estate/dees-element': 2.1.6
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.1.11 '@push.rocks/smartlog': 3.2.1
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
'@push.rocks/smarttime': 4.1.1 '@push.rocks/smarttime': 4.1.1
@@ -6602,7 +6567,7 @@ snapshots:
'@design.estate/dees-element': 2.1.6 '@design.estate/dees-element': 2.1.6
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.1.11 '@push.rocks/smartlog': 3.2.1
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
'@push.rocks/smarttime': 4.1.1 '@push.rocks/smarttime': 4.1.1
@@ -6618,7 +6583,7 @@ snapshots:
'@design.estate/dees-element': 2.1.6 '@design.estate/dees-element': 2.1.6
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.1.11 '@push.rocks/smartlog': 3.2.1
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
'@push.rocks/smarttime': 4.2.3 '@push.rocks/smarttime': 4.2.3
@@ -6637,14 +6602,6 @@ snapshots:
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/webstore': 2.0.20 '@push.rocks/webstore': 2.0.20
'@push.rocks/webrequest@4.0.1':
dependencies:
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartenv': 5.0.13
'@push.rocks/smartjson': 5.2.0
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/webstore': 2.0.20
'@push.rocks/webrequest@4.0.2': '@push.rocks/webrequest@4.0.2':
dependencies: dependencies:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
@@ -6830,7 +6787,7 @@ snapshots:
'@push.rocks/smartlog-interfaces': 3.0.2 '@push.rocks/smartlog-interfaces': 3.0.2
'@tsclass/tsclass': 9.3.0 '@tsclass/tsclass': 9.3.0
'@serve.zone/remoteingress@3.3.0': '@serve.zone/remoteingress@4.0.0':
dependencies: dependencies:
'@push.rocks/qenv': 6.1.3 '@push.rocks/qenv': 6.1.3
'@push.rocks/smartrust': 1.2.1 '@push.rocks/smartrust': 1.2.1
@@ -7362,22 +7319,22 @@ snapshots:
'@types/body-parser@1.19.6': '@types/body-parser@1.19.6':
dependencies: dependencies:
'@types/connect': 3.4.38 '@types/connect': 3.4.38
'@types/node': 25.2.3 '@types/node': 25.3.0
'@types/buffer-json@2.0.3': {} '@types/buffer-json@2.0.3': {}
'@types/clean-css@4.2.11': '@types/clean-css@4.2.11':
dependencies: dependencies:
'@types/node': 25.2.3 '@types/node': 25.3.0
source-map: 0.6.1 source-map: 0.6.1
'@types/connect@3.4.38': '@types/connect@3.4.38':
dependencies: dependencies:
'@types/node': 25.2.3 '@types/node': 25.3.0
'@types/cors@2.8.19': '@types/cors@2.8.19':
dependencies: dependencies:
'@types/node': 25.2.3 '@types/node': 25.3.0
'@types/debug@4.1.12': '@types/debug@4.1.12':
dependencies: dependencies:
@@ -7385,7 +7342,7 @@ snapshots:
'@types/express-serve-static-core@5.1.1': '@types/express-serve-static-core@5.1.1':
dependencies: dependencies:
'@types/node': 25.2.3 '@types/node': 25.3.0
'@types/qs': 6.14.0 '@types/qs': 6.14.0
'@types/range-parser': 1.2.7 '@types/range-parser': 1.2.7
'@types/send': 1.2.1 '@types/send': 1.2.1
@@ -7398,17 +7355,17 @@ snapshots:
'@types/from2@2.3.6': '@types/from2@2.3.6':
dependencies: dependencies:
'@types/node': 25.2.3 '@types/node': 25.3.0
'@types/fs-extra@11.0.4': '@types/fs-extra@11.0.4':
dependencies: dependencies:
'@types/jsonfile': 6.1.4 '@types/jsonfile': 6.1.4
'@types/node': 25.2.3 '@types/node': 25.3.0
'@types/glob@8.1.0': '@types/glob@8.1.0':
dependencies: dependencies:
'@types/minimatch': 5.1.2 '@types/minimatch': 5.1.2
'@types/node': 25.2.3 '@types/node': 25.3.0
'@types/hast@3.0.4': '@types/hast@3.0.4':
dependencies: dependencies:
@@ -7430,12 +7387,12 @@ snapshots:
'@types/jsonfile@6.1.4': '@types/jsonfile@6.1.4':
dependencies: dependencies:
'@types/node': 25.2.3 '@types/node': 25.3.0
'@types/jsonwebtoken@9.0.10': '@types/jsonwebtoken@9.0.10':
dependencies: dependencies:
'@types/ms': 2.1.0 '@types/ms': 2.1.0
'@types/node': 25.2.3 '@types/node': 25.3.0
'@types/linkify-it@5.0.0': {} '@types/linkify-it@5.0.0': {}
@@ -7458,16 +7415,16 @@ snapshots:
'@types/mute-stream@0.0.4': '@types/mute-stream@0.0.4':
dependencies: dependencies:
'@types/node': 25.2.3 '@types/node': 25.3.0
'@types/node-fetch@2.6.13': '@types/node-fetch@2.6.13':
dependencies: dependencies:
'@types/node': 25.2.3 '@types/node': 25.3.0
form-data: 4.0.5 form-data: 4.0.5
'@types/node-forge@1.3.14': '@types/node-forge@1.3.14':
dependencies: dependencies:
'@types/node': 25.2.3 '@types/node': 25.3.0
'@types/node@18.19.130': '@types/node@18.19.130':
dependencies: dependencies:
@@ -7477,11 +7434,9 @@ snapshots:
dependencies: dependencies:
undici-types: 6.21.0 undici-types: 6.21.0
'@types/node@25.2.3': '@types/node@25.3.0':
dependencies: dependencies:
undici-types: 7.16.0 undici-types: 7.18.2
'@types/pidusage@2.0.5': {}
'@types/ping@0.4.4': {} '@types/ping@0.4.4': {}
@@ -7497,22 +7452,22 @@ snapshots:
'@types/send@1.2.1': '@types/send@1.2.1':
dependencies: dependencies:
'@types/node': 25.2.3 '@types/node': 25.3.0
'@types/serve-static@2.2.0': '@types/serve-static@2.2.0':
dependencies: dependencies:
'@types/http-errors': 2.0.5 '@types/http-errors': 2.0.5
'@types/node': 25.2.3 '@types/node': 25.3.0
'@types/symbol-tree@3.2.5': {} '@types/symbol-tree@3.2.5': {}
'@types/tar-stream@3.1.4': '@types/tar-stream@3.1.4':
dependencies: dependencies:
'@types/node': 25.2.3 '@types/node': 25.3.0
'@types/through2@2.0.41': '@types/through2@2.0.41':
dependencies: dependencies:
'@types/node': 25.2.3 '@types/node': 25.3.0
'@types/trusted-types@2.0.7': {} '@types/trusted-types@2.0.7': {}
@@ -7542,11 +7497,11 @@ snapshots:
'@types/ws@8.18.1': '@types/ws@8.18.1':
dependencies: dependencies:
'@types/node': 25.2.3 '@types/node': 25.3.0
'@types/yauzl@2.10.3': '@types/yauzl@2.10.3':
dependencies: dependencies:
'@types/node': 25.2.3 '@types/node': 25.3.0
optional: true optional: true
'@ungap/structured-clone@1.3.0': {} '@ungap/structured-clone@1.3.0': {}
@@ -7692,8 +7647,6 @@ snapshots:
basic-ftp@5.1.0: {} basic-ftp@5.1.0: {}
bintrees@1.0.2: {}
body-parser@2.2.2: body-parser@2.2.2:
dependencies: dependencies:
bytes: 3.1.2 bytes: 3.1.2
@@ -8019,7 +7972,7 @@ snapshots:
engine.io@6.6.4: engine.io@6.6.4:
dependencies: dependencies:
'@types/cors': 2.8.19 '@types/cors': 2.8.19
'@types/node': 25.2.3 '@types/node': 25.3.0
accepts: 1.3.8 accepts: 1.3.8
base64id: 2.0.0 base64id: 2.0.0
cookie: 0.7.2 cookie: 0.7.2
@@ -9163,7 +9116,7 @@ snapshots:
dependencies: dependencies:
'@isaacs/brace-expansion': 5.0.1 '@isaacs/brace-expansion': 5.0.1
minimatch@10.2.0: minimatch@10.2.1:
dependencies: dependencies:
brace-expansion: 5.0.2 brace-expansion: 5.0.2
@@ -9439,12 +9392,6 @@ snapshots:
picomatch@4.0.3: {} picomatch@4.0.3: {}
pidtree@0.6.0: {}
pidusage@4.0.1:
dependencies:
safe-buffer: 5.2.1
ping@0.4.4: {} ping@0.4.4: {}
pkg-dir@4.2.0: pkg-dir@4.2.0:
@@ -9459,11 +9406,6 @@ snapshots:
progress@2.0.3: {} progress@2.0.3: {}
prom-client@15.1.3:
dependencies:
'@opentelemetry/api': 1.9.0
tdigest: 0.1.2
property-information@7.1.0: {} property-information@7.1.0: {}
prosemirror-changeset@2.4.0: prosemirror-changeset@2.4.0:
@@ -10050,10 +9992,6 @@ snapshots:
- bare-abort-controller - bare-abort-controller
- react-native-b4a - react-native-b4a
tdigest@0.1.2:
dependencies:
bintrees: 1.0.2
text-decoder@1.2.3: text-decoder@1.2.3:
dependencies: dependencies:
b4a: 1.7.3 b4a: 1.7.3
@@ -10152,7 +10090,7 @@ snapshots:
undici-types@6.21.0: {} undici-types@6.21.0: {}
undici-types@7.16.0: {} undici-types@7.18.2: {}
unified@11.0.5: unified@11.0.5:
dependencies: dependencies:

View File

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

View File

@@ -106,6 +106,13 @@ export class CertProvisionScheduler {
} }
} }
/**
* Clear all in-memory backoff cache entries
*/
public clear(): void {
this.backoffCache.clear();
}
/** /**
* Get backoff info for UI display * Get backoff info for UI display
*/ */

View File

@@ -534,6 +534,12 @@ export class DcRouter {
// If we have DNS challenge handlers, create SmartAcme and wire to certProvisionFunction // If we have DNS challenge handlers, create SmartAcme and wire to certProvisionFunction
if (challengeHandlers.length > 0) { if (challengeHandlers.length > 0) {
// Stop old SmartAcme if it exists (e.g., during updateSmartProxyConfig)
if (this.smartAcme) {
await this.smartAcme.stop().catch(err =>
console.error('[DcRouter] Error stopping old SmartAcme:', err)
);
}
this.smartAcme = new plugins.smartacme.SmartAcme({ this.smartAcme = new plugins.smartacme.SmartAcme({
accountEmail: acmeConfig?.accountEmail || this.options.tls?.contactEmail || 'admin@example.com', accountEmail: acmeConfig?.accountEmail || this.options.tls?.contactEmail || 'admin@example.com',
certManager: new StorageBackedCertManager(this.storageManager), certManager: new StorageBackedCertManager(this.storageManager),
@@ -944,6 +950,25 @@ export class DcRouter {
await this.cacheDb.stop().catch(err => console.error('Error stopping CacheDb:', err)); await this.cacheDb.stop().catch(err => console.error('Error stopping CacheDb:', err));
} }
// Clear backoff cache in cert scheduler
if (this.certProvisionScheduler) {
this.certProvisionScheduler.clear();
}
// Allow GC of stopped services by nulling references
this.smartProxy = undefined;
this.emailServer = undefined;
this.dnsServer = undefined;
this.metricsManager = undefined;
this.cacheCleaner = undefined;
this.cacheDb = undefined;
this.tunnelManager = undefined;
this.radiusServer = undefined;
this.smartAcme = undefined;
this.certProvisionScheduler = undefined;
this.remoteIngressManager = undefined;
this.certificateStatusMap.clear();
console.log('All DcRouter services stopped'); console.log('All DcRouter services stopped');
} catch (error) { } catch (error) {
console.error('Error during DcRouter shutdown:', error); console.error('Error during DcRouter shutdown:', error);
@@ -1030,7 +1055,25 @@ export class DcRouter {
// Start the server // Start the server
await this.emailServer.start(); await this.emailServer.start();
// Wire delivery events to MetricsManager for time-series tracking
if (this.metricsManager && this.emailServer.deliverySystem) {
this.emailServer.deliverySystem.on('deliveryStart', (item: any) => {
this.metricsManager.trackEmailReceived(item?.from);
});
this.emailServer.deliverySystem.on('deliverySuccess', (item: any) => {
this.metricsManager.trackEmailSent(item?.to);
});
this.emailServer.deliverySystem.on('deliveryFailed', (item: any, error: any) => {
this.metricsManager.trackEmailFailed(item?.to, error?.message);
});
}
if (this.metricsManager && this.emailServer) {
this.emailServer.on('bounceProcessed', () => {
this.metricsManager.trackEmailBounced();
});
}
logger.log('info', `Email server started on ports: ${emailConfig.ports.join(', ')}`); logger.log('info', `Email server started on ports: ${emailConfig.ports.join(', ')}`);
} }
@@ -1213,6 +1256,21 @@ export class DcRouter {
// Start the DNS server (UDP only) // Start the DNS server (UDP only)
await this.dnsServer.start(); await this.dnsServer.start();
logger.log('info', `DNS server started on UDP ${vmIpAddress}:53`); logger.log('info', `DNS server started on UDP ${vmIpAddress}:53`);
// Wire DNS query events to MetricsManager for time-series tracking
if (this.metricsManager && this.dnsServer) {
this.dnsServer.on('query', (event: plugins.smartdns.dnsServerMod.IDnsQueryCompletedEvent) => {
for (const question of event.questions) {
this.metricsManager.trackDnsQuery(
question.type,
question.name,
false,
event.responseTimeMs,
event.answered,
);
}
});
}
// Validate DNS configuration // Validate DNS configuration
await this.validateDnsConfiguration(); await this.validateDnsConfiguration();

View File

@@ -1,5 +1,6 @@
import * as plugins from './plugins.js'; import * as plugins from './plugins.js';
import { randomUUID } from 'node:crypto'; import { randomUUID } from 'node:crypto';
import { SmartlogDestinationBuffer } from '@push.rocks/smartlog/destination-buffer';
// Map NODE_ENV to valid TEnvironment // Map NODE_ENV to valid TEnvironment
const nodeEnv = process.env.NODE_ENV || 'production'; const nodeEnv = process.env.NODE_ENV || 'production';
@@ -10,8 +11,11 @@ const envMap: Record<string, 'local' | 'test' | 'staging' | 'production'> = {
'production': 'production' 'production': 'production'
}; };
// Default Smartlog instance // In-memory log buffer for the OpsServer UI
const baseLogger = new plugins.smartlog.Smartlog({ export const logBuffer = new SmartlogDestinationBuffer({ maxEntries: 2000 });
// Default Smartlog instance (exported so OpsServer can add push destinations)
export const baseLogger = new plugins.smartlog.Smartlog({
logContext: { logContext: {
environment: envMap[nodeEnv] || 'production', environment: envMap[nodeEnv] || 'production',
runtime: 'node', runtime: 'node',
@@ -19,6 +23,9 @@ const baseLogger = new plugins.smartlog.Smartlog({
} }
}); });
// Wire the buffer destination so all logs are captured
baseLogger.addLogDestination(logBuffer);
// Extended logger compatible with the original enhanced logger API // Extended logger compatible with the original enhanced logger API
class StandardLogger { class StandardLogger {
private defaultContext: Record<string, any> = {}; private defaultContext: Record<string, any> = {};

View File

@@ -1,6 +1,7 @@
import * as plugins from '../plugins.js'; import * as plugins from '../plugins.js';
import { DcRouter } from '../classes.dcrouter.js'; import { DcRouter } from '../classes.dcrouter.js';
import { MetricsCache } from './classes.metricscache.js'; import { MetricsCache } from './classes.metricscache.js';
import { SecurityLogger, SecurityEventType } from '../security/classes.securitylogger.js';
export class MetricsManager { export class MetricsManager {
private logger: plugins.smartlog.Smartlog; private logger: plugins.smartlog.Smartlog;
@@ -35,8 +36,13 @@ export class MetricsManager {
lastResetDate: new Date().toDateString(), lastResetDate: new Date().toDateString(),
queryTimestamps: [] as number[], // Track query timestamps for rate calculation queryTimestamps: [] as number[], // Track query timestamps for rate calculation
responseTimes: [] as number[], // Track response times in ms responseTimes: [] as number[], // Track response times in ms
recentQueries: [] as Array<{ timestamp: number; domain: string; type: string; answered: boolean; responseTimeMs: number }>,
}; };
// Per-minute time-series buckets for charts
private emailMinuteBuckets = new Map<number, { sent: number; received: number; failed: number }>();
private dnsMinuteBuckets = new Map<number, { queries: number }>();
// Track security-specific metrics // Track security-specific metrics
private securityMetrics = { private securityMetrics = {
blockedIPs: 0, blockedIPs: 0,
@@ -90,6 +96,7 @@ export class MetricsManager {
this.dnsMetrics.topDomains.clear(); this.dnsMetrics.topDomains.clear();
this.dnsMetrics.queryTimestamps = []; this.dnsMetrics.queryTimestamps = [];
this.dnsMetrics.responseTimes = []; this.dnsMetrics.responseTimes = [];
this.dnsMetrics.recentQueries = [];
this.dnsMetrics.lastResetDate = currentDate; this.dnsMetrics.lastResetDate = currentDate;
} }
@@ -139,8 +146,8 @@ export class MetricsManager {
actualUsagePercentage: smartMetricsData.memoryPercentage, actualUsagePercentage: smartMetricsData.memoryPercentage,
}, },
cpuUsage: { cpuUsage: {
user: parseFloat(smartMetricsData.cpuUsageText || '0'), user: smartMetricsData.cpuPercentage,
system: 0, // SmartMetrics doesn't separate user/system system: 0,
}, },
activeConnections: proxyStats ? proxyStats.activeConnections : 0, activeConnections: proxyStats ? proxyStats.activeConnections : 0,
totalConnections: proxyMetrics ? proxyMetrics.totals.connections() : 0, totalConnections: proxyMetrics ? proxyMetrics.totals.connections() : 0,
@@ -223,24 +230,50 @@ export class MetricsManager {
queryTypes: this.dnsMetrics.queryTypes, queryTypes: this.dnsMetrics.queryTypes,
averageResponseTime: Math.round(avgResponseTime), averageResponseTime: Math.round(avgResponseTime),
activeDomains: this.dnsMetrics.topDomains.size, activeDomains: this.dnsMetrics.topDomains.size,
recentQueries: this.dnsMetrics.recentQueries.slice(),
}; };
}); });
} }
/**
* Sync security metrics from the SecurityLogger singleton (last 24h).
* Called before returning security stats so counters reflect real events.
*/
private syncFromSecurityLogger(): void {
try {
const securityLogger = SecurityLogger.getInstance();
const summary = securityLogger.getEventsSummary(86400000); // last 24h
this.securityMetrics.spamDetected = summary.byType[SecurityEventType.SPAM] || 0;
this.securityMetrics.malwareDetected = summary.byType[SecurityEventType.MALWARE] || 0;
this.securityMetrics.phishingDetected = summary.byType[SecurityEventType.DMARC] || 0; // phishing via DMARC
this.securityMetrics.authFailures =
summary.byType[SecurityEventType.AUTHENTICATION] || 0;
this.securityMetrics.blockedIPs =
(summary.byType[SecurityEventType.IP_REPUTATION] || 0) +
(summary.byType[SecurityEventType.REJECTED_CONNECTION] || 0);
} catch {
// SecurityLogger may not be initialized yet — ignore
}
}
// Get security metrics // Get security metrics
public async getSecurityStats() { public async getSecurityStats() {
return this.metricsCache.get('securityStats', () => { return this.metricsCache.get('securityStats', () => {
// Sync counters from the real SecurityLogger events
this.syncFromSecurityLogger();
// Get recent incidents (last 20) // Get recent incidents (last 20)
const recentIncidents = this.securityMetrics.incidents.slice(-20); const recentIncidents = this.securityMetrics.incidents.slice(-20);
return { return {
blockedIPs: this.securityMetrics.blockedIPs, blockedIPs: this.securityMetrics.blockedIPs,
authFailures: this.securityMetrics.authFailures, authFailures: this.securityMetrics.authFailures,
spamDetected: this.securityMetrics.spamDetected, spamDetected: this.securityMetrics.spamDetected,
malwareDetected: this.securityMetrics.malwareDetected, malwareDetected: this.securityMetrics.malwareDetected,
phishingDetected: this.securityMetrics.phishingDetected, phishingDetected: this.securityMetrics.phishingDetected,
totalThreatsBlocked: this.securityMetrics.spamDetected + totalThreatsBlocked: this.securityMetrics.spamDetected +
this.securityMetrics.malwareDetected + this.securityMetrics.malwareDetected +
this.securityMetrics.phishingDetected, this.securityMetrics.phishingDetected,
recentIncidents, recentIncidents,
}; };
@@ -275,10 +308,19 @@ export class MetricsManager {
// Email event tracking methods // Email event tracking methods
public trackEmailSent(recipient?: string, deliveryTimeMs?: number): void { public trackEmailSent(recipient?: string, deliveryTimeMs?: number): void {
this.emailMetrics.sentToday++; this.emailMetrics.sentToday++;
this.incrementEmailBucket('sent');
if (recipient) { if (recipient) {
const count = this.emailMetrics.recipients.get(recipient) || 0; const count = this.emailMetrics.recipients.get(recipient) || 0;
this.emailMetrics.recipients.set(recipient, count + 1); this.emailMetrics.recipients.set(recipient, count + 1);
// Cap recipients map to prevent unbounded growth within a day
if (this.emailMetrics.recipients.size > this.MAX_TOP_DOMAINS) {
const sorted = Array.from(this.emailMetrics.recipients.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, Math.floor(this.MAX_TOP_DOMAINS * 0.8));
this.emailMetrics.recipients = new Map(sorted);
}
} }
if (deliveryTimeMs) { if (deliveryTimeMs) {
@@ -303,6 +345,7 @@ export class MetricsManager {
public trackEmailReceived(sender?: string): void { public trackEmailReceived(sender?: string): void {
this.emailMetrics.receivedToday++; this.emailMetrics.receivedToday++;
this.incrementEmailBucket('received');
this.emailMetrics.recentActivity.push({ this.emailMetrics.recentActivity.push({
timestamp: Date.now(), timestamp: Date.now(),
@@ -318,6 +361,7 @@ export class MetricsManager {
public trackEmailFailed(recipient?: string, reason?: string): void { public trackEmailFailed(recipient?: string, reason?: string): void {
this.emailMetrics.failedToday++; this.emailMetrics.failedToday++;
this.incrementEmailBucket('failed');
this.emailMetrics.recentActivity.push({ this.emailMetrics.recentActivity.push({
timestamp: Date.now(), timestamp: Date.now(),
@@ -351,8 +395,21 @@ export class MetricsManager {
} }
// DNS event tracking methods // DNS event tracking methods
public trackDnsQuery(queryType: string, domain: string, cacheHit: boolean, responseTimeMs?: number): void { public trackDnsQuery(queryType: string, domain: string, cacheHit: boolean, responseTimeMs?: number, answered?: boolean): void {
this.dnsMetrics.totalQueries++; this.dnsMetrics.totalQueries++;
this.incrementDnsBucket();
// Store recent query entry
this.dnsMetrics.recentQueries.push({
timestamp: Date.now(),
domain,
type: queryType,
answered: answered ?? true,
responseTimeMs: responseTimeMs ?? 0,
});
if (this.dnsMetrics.recentQueries.length > 100) {
this.dnsMetrics.recentQueries.shift();
}
if (cacheHit) { if (cacheHit) {
this.dnsMetrics.cacheHits++; this.dnsMetrics.cacheHits++;
@@ -539,4 +596,90 @@ export class MetricsManager {
}; };
}, 200); // Use 200ms cache for more frequent updates }, 200); // Use 200ms cache for more frequent updates
} }
// --- Time-series helpers ---
private static minuteKey(ts: number = Date.now()): number {
return Math.floor(ts / 60000) * 60000;
}
private incrementEmailBucket(field: 'sent' | 'received' | 'failed'): void {
const key = MetricsManager.minuteKey();
let bucket = this.emailMinuteBuckets.get(key);
if (!bucket) {
bucket = { sent: 0, received: 0, failed: 0 };
this.emailMinuteBuckets.set(key, bucket);
}
bucket[field]++;
}
private incrementDnsBucket(): void {
const key = MetricsManager.minuteKey();
let bucket = this.dnsMinuteBuckets.get(key);
if (!bucket) {
bucket = { queries: 0 };
this.dnsMinuteBuckets.set(key, bucket);
}
bucket.queries++;
}
private pruneOldBuckets(): void {
const cutoff = Date.now() - 86400000; // 24h
for (const key of this.emailMinuteBuckets.keys()) {
if (key < cutoff) this.emailMinuteBuckets.delete(key);
}
for (const key of this.dnsMinuteBuckets.keys()) {
if (key < cutoff) this.dnsMinuteBuckets.delete(key);
}
}
/**
* Get email time-series data for the last N hours, aggregated per minute.
*/
public getEmailTimeSeries(hours: number = 24): {
sent: Array<{ timestamp: number; value: number }>;
received: Array<{ timestamp: number; value: number }>;
failed: Array<{ timestamp: number; value: number }>;
} {
this.pruneOldBuckets();
const cutoff = Date.now() - hours * 3600000;
const sent: Array<{ timestamp: number; value: number }> = [];
const received: Array<{ timestamp: number; value: number }> = [];
const failed: Array<{ timestamp: number; value: number }> = [];
const sortedKeys = Array.from(this.emailMinuteBuckets.keys())
.filter((k) => k >= cutoff)
.sort((a, b) => a - b);
for (const key of sortedKeys) {
const bucket = this.emailMinuteBuckets.get(key)!;
sent.push({ timestamp: key, value: bucket.sent });
received.push({ timestamp: key, value: bucket.received });
failed.push({ timestamp: key, value: bucket.failed });
}
return { sent, received, failed };
}
/**
* Get DNS time-series data for the last N hours, aggregated per minute.
*/
public getDnsTimeSeries(hours: number = 24): {
queries: Array<{ timestamp: number; value: number }>;
} {
this.pruneOldBuckets();
const cutoff = Date.now() - hours * 3600000;
const queries: Array<{ timestamp: number; value: number }> = [];
const sortedKeys = Array.from(this.dnsMinuteBuckets.keys())
.filter((k) => k >= cutoff)
.sort((a, b) => a - b);
for (const key of sortedKeys) {
const bucket = this.dnsMinuteBuckets.get(key)!;
queries.push({ timestamp: key, value: bucket.queries });
}
return { queries };
}
} }

View File

@@ -1,14 +1,16 @@
import * as plugins from '../../plugins.js'; import * as plugins from '../../plugins.js';
import type { OpsServer } from '../classes.opsserver.js'; import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
import { logBuffer, baseLogger } from '../../logger.js';
export class LogsHandler { export class LogsHandler {
public typedrouter = new plugins.typedrequest.TypedRouter(); public typedrouter = new plugins.typedrequest.TypedRouter();
constructor(private opsServerRef: OpsServer) { constructor(private opsServerRef: OpsServer) {
// Add this handler's router to the parent // Add this handler's router to the parent
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter); this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers(); this.registerHandlers();
this.setupLogPushDestination();
} }
private registerHandlers(): void { private registerHandlers(): void {
@@ -64,6 +66,32 @@ export class LogsHandler {
); );
} }
private static mapLogLevel(smartlogLevel: string): 'debug' | 'info' | 'warn' | 'error' {
switch (smartlogLevel) {
case 'silly':
case 'debug':
return 'debug';
case 'warn':
return 'warn';
case 'error':
return 'error';
default:
return 'info';
}
}
private static deriveCategory(
zone?: string,
message?: string
): 'smtp' | 'dns' | 'security' | 'system' | 'email' {
const msg = (message || '').toLowerCase();
if (msg.includes('[security:') || msg.includes('security')) return 'security';
if (zone === 'email' || msg.includes('email') || msg.includes('smtp') || msg.includes('mta')) return 'email';
if (zone === 'dns' || msg.includes('dns')) return 'dns';
if (msg.includes('smtp')) return 'smtp';
return 'system';
}
private async getRecentLogs( private async getRecentLogs(
level?: 'error' | 'warn' | 'info' | 'debug', level?: 'error' | 'warn' | 'info' | 'debug',
category?: 'smtp' | 'dns' | 'security' | 'system' | 'email', category?: 'smtp' | 'dns' | 'security' | 'system' | 'email',
@@ -78,44 +106,110 @@ export class LogsHandler {
message: string; message: string;
metadata?: any; metadata?: any;
}>> { }>> {
// TODO: Implement actual log retrieval from storage or logger // Compute a timestamp cutoff from timeRange
// For now, return mock data let since: number | undefined;
const mockLogs: Array<{ if (timeRange) {
const rangeMs: Record<string, number> = {
'1h': 3600000,
'6h': 21600000,
'24h': 86400000,
'7d': 604800000,
'30d': 2592000000,
};
since = Date.now() - (rangeMs[timeRange] || 86400000);
}
// Map the UI level to smartlog levels for filtering
const smartlogLevels: string[] | undefined = level
? level === 'debug'
? ['debug', 'silly']
: level === 'info'
? ['info', 'ok', 'success', 'note', 'lifecycle']
: [level]
: undefined;
// Fetch a larger batch from buffer, then apply category filter client-side
const rawEntries = logBuffer.getEntries({
level: smartlogLevels as any,
search,
since,
limit: limit * 3, // over-fetch to compensate for category filtering
offset: 0,
});
// Map ILogPackage → UI log format and apply category filter
const mapped: Array<{
timestamp: number; timestamp: number;
level: 'debug' | 'info' | 'warn' | 'error'; level: 'debug' | 'info' | 'warn' | 'error';
category: 'smtp' | 'dns' | 'security' | 'system' | 'email'; category: 'smtp' | 'dns' | 'security' | 'system' | 'email';
message: string; message: string;
metadata?: any; metadata?: any;
}> = []; }> = [];
const categories: Array<'smtp' | 'dns' | 'security' | 'system' | 'email'> = ['smtp', 'dns', 'security', 'system', 'email']; for (const pkg of rawEntries) {
const levels: Array<'debug' | 'info' | 'warn' | 'error'> = ['info', 'warn', 'error', 'debug']; const uiLevel = LogsHandler.mapLogLevel(pkg.level);
const now = Date.now(); const uiCategory = LogsHandler.deriveCategory(pkg.context?.zone, pkg.message);
// Generate some mock log entries if (category && uiCategory !== category) continue;
for (let i = 0; i < 50; i++) {
const mockCategory = categories[Math.floor(Math.random() * categories.length)]; mapped.push({
const mockLevel = levels[Math.floor(Math.random() * levels.length)]; timestamp: pkg.timestamp,
level: uiLevel,
// Filter by requested criteria category: uiCategory,
if (level && mockLevel !== level) continue; message: pkg.message,
if (category && mockCategory !== category) continue; metadata: pkg.data,
mockLogs.push({
timestamp: now - (i * 60000), // 1 minute apart
level: mockLevel,
category: mockCategory,
message: `Sample log message ${i} from ${mockCategory}`,
metadata: {
requestId: plugins.uuid.v4(),
},
}); });
if (mapped.length >= limit) break;
} }
// Apply pagination return mapped;
return mockLogs.slice(offset, offset + limit);
} }
/**
* Add a log destination to the base logger that pushes entries
* to all connected ops_dashboard TypedSocket clients.
*/
private setupLogPushDestination(): void {
const opsServerRef = this.opsServerRef;
baseLogger.addLogDestination({
async handleLog(logPackage: any) {
// Access the TypedSocket server instance from OpsServer
const typedsocket = opsServerRef.server?.typedserver?.typedsocket;
if (!typedsocket) return;
let connections: any[];
try {
connections = await typedsocket.findAllTargetConnectionsByTag('role', 'ops_dashboard');
} catch {
return;
}
if (connections.length === 0) return;
const entry: interfaces.data.ILogEntry = {
timestamp: logPackage.timestamp || Date.now(),
level: LogsHandler.mapLogLevel(logPackage.level),
category: LogsHandler.deriveCategory(logPackage.context?.zone, logPackage.message),
message: logPackage.message,
metadata: logPackage.data,
};
for (const conn of connections) {
try {
const push = typedsocket.createTypedRequest<interfaces.requests.IReq_PushLogEntry>(
'pushLogEntry',
conn,
);
push.fire({ entry }).catch(() => {}); // fire-and-forget
} catch {
// connection may have closed
}
}
},
});
}
private setupLogStream( private setupLogStream(
virtualStream: plugins.typedrequest.VirtualStream<Uint8Array>, virtualStream: plugins.typedrequest.VirtualStream<Uint8Array>,
levelFilter?: string[], levelFilter?: string[],
@@ -148,17 +242,17 @@ export class LogsHandler {
} }
// For follow mode, simulate real-time log streaming // For follow mode, simulate real-time log streaming
intervalId = setInterval(() => { intervalId = setInterval(async () => {
const categories: Array<'smtp' | 'dns' | 'security' | 'system' | 'email'> = ['smtp', 'dns', 'security', 'system', 'email']; const categories: Array<'smtp' | 'dns' | 'security' | 'system' | 'email'> = ['smtp', 'dns', 'security', 'system', 'email'];
const levels: Array<'debug' | 'info' | 'warn' | 'error'> = ['info', 'warn', 'error', 'debug']; const levels: Array<'debug' | 'info' | 'warn' | 'error'> = ['info', 'warn', 'error', 'debug'];
const mockCategory = categories[Math.floor(Math.random() * categories.length)]; const mockCategory = categories[Math.floor(Math.random() * categories.length)];
const mockLevel = levels[Math.floor(Math.random() * levels.length)]; const mockLevel = levels[Math.floor(Math.random() * levels.length)];
// Filter by requested criteria // Filter by requested criteria
if (levelFilter && !levelFilter.includes(mockLevel)) return; if (levelFilter && !levelFilter.includes(mockLevel)) return;
if (categoryFilter && !categoryFilter.includes(mockCategory)) return; if (categoryFilter && !categoryFilter.includes(mockCategory)) return;
const logEntry = { const logEntry = {
timestamp: Date.now(), timestamp: Date.now(),
level: mockLevel, level: mockLevel,
@@ -168,10 +262,16 @@ export class LogsHandler {
requestId: plugins.uuid.v4(), requestId: plugins.uuid.v4(),
}, },
}; };
const logData = JSON.stringify(logEntry); const logData = JSON.stringify(logEntry);
const encoder = new TextEncoder(); const encoder = new TextEncoder();
virtualStream.sendData(encoder.encode(logData)); try {
await virtualStream.sendData(encoder.encode(logData));
} catch {
// Stream closed or errored — clean up to prevent interval leak
clearInterval(intervalId!);
intervalId = null;
}
}, 2000); // Send a log every 2 seconds }, 2000); // Send a log every 2 seconds
// TODO: Hook into actual logger events // TODO: Hook into actual logger events

View File

@@ -2,6 +2,7 @@ import * as plugins from '../../plugins.js';
import type { OpsServer } from '../classes.opsserver.js'; import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
import { MetricsManager } from '../../monitoring/index.js'; import { MetricsManager } from '../../monitoring/index.js';
import { SecurityLogger } from '../../security/classes.securitylogger.js';
export class StatsHandler { export class StatsHandler {
public typedrouter = new plugins.typedrequest.TypedRouter(); public typedrouter = new plugins.typedrequest.TypedRouter();
@@ -203,6 +204,11 @@ export class StatsHandler {
if (sections.email) { if (sections.email) {
promises.push( promises.push(
this.collectEmailStats().then(stats => { this.collectEmailStats().then(stats => {
// Get time-series data from MetricsManager
const timeSeries = this.opsServerRef.dcRouterRef.metricsManager
? this.opsServerRef.dcRouterRef.metricsManager.getEmailTimeSeries(24)
: undefined;
metrics.email = { metrics.email = {
sent: stats.sentToday, sent: stats.sentToday,
received: stats.receivedToday, received: stats.receivedToday,
@@ -212,6 +218,7 @@ export class StatsHandler {
averageDeliveryTime: 0, averageDeliveryTime: 0,
deliveryRate: stats.deliveryRate, deliveryRate: stats.deliveryRate,
bounceRate: stats.bounceRate, bounceRate: stats.bounceRate,
timeSeries,
}; };
}) })
); );
@@ -220,6 +227,11 @@ export class StatsHandler {
if (sections.dns) { if (sections.dns) {
promises.push( promises.push(
this.collectDnsStats().then(stats => { this.collectDnsStats().then(stats => {
// Get time-series data from MetricsManager
const timeSeries = this.opsServerRef.dcRouterRef.metricsManager
? this.opsServerRef.dcRouterRef.metricsManager.getDnsTimeSeries(24)
: undefined;
metrics.dns = { metrics.dns = {
totalQueries: stats.totalQueries, totalQueries: stats.totalQueries,
cacheHits: stats.cacheHits, cacheHits: stats.cacheHits,
@@ -228,6 +240,8 @@ export class StatsHandler {
activeDomains: stats.topDomains.length, activeDomains: stats.topDomains.length,
averageResponseTime: 0, averageResponseTime: 0,
queryTypes: stats.queryTypes, queryTypes: stats.queryTypes,
timeSeries,
recentQueries: stats.recentQueries,
}; };
}) })
); );
@@ -236,6 +250,19 @@ export class StatsHandler {
if (sections.security && this.opsServerRef.dcRouterRef.metricsManager) { if (sections.security && this.opsServerRef.dcRouterRef.metricsManager) {
promises.push( promises.push(
this.opsServerRef.dcRouterRef.metricsManager.getSecurityStats().then(stats => { this.opsServerRef.dcRouterRef.metricsManager.getSecurityStats().then(stats => {
// Get recent events from the SecurityLogger singleton
const securityLogger = SecurityLogger.getInstance();
const recentEvents = securityLogger.getRecentEvents(50).map((evt) => ({
timestamp: evt.timestamp,
level: evt.level,
type: evt.type,
message: evt.message,
details: evt.details,
ipAddress: evt.ipAddress,
domain: evt.domain,
success: evt.success,
}));
metrics.security = { metrics.security = {
blockedIPs: stats.blockedIPs, blockedIPs: stats.blockedIPs,
reputationScores: {}, reputationScores: {},
@@ -244,6 +271,7 @@ export class StatsHandler {
phishingDetected: stats.phishingDetected, phishingDetected: stats.phishingDetected,
authenticationFailures: stats.authFailures, authenticationFailures: stats.authFailures,
suspiciousActivities: stats.totalThreatsBlocked, suspiciousActivities: stats.totalThreatsBlocked,
recentEvents,
}; };
}) })
); );
@@ -395,6 +423,7 @@ export class StatsHandler {
count: number; count: number;
}>; }>;
queryTypes: { [key: string]: number }; queryTypes: { [key: string]: number };
recentQueries?: Array<{ timestamp: number; domain: string; type: string; answered: boolean; responseTimeMs: number }>;
domainBreakdown?: { [domain: string]: interfaces.data.IDnsStats }; domainBreakdown?: { [domain: string]: interfaces.data.IDnsStats };
}> { }> {
// Get metrics from MetricsManager if available // Get metrics from MetricsManager if available
@@ -408,9 +437,10 @@ export class StatsHandler {
cacheHitRate: dnsStats.cacheHitRate, cacheHitRate: dnsStats.cacheHitRate,
topDomains: dnsStats.topDomains, topDomains: dnsStats.topDomains,
queryTypes: dnsStats.queryTypes, queryTypes: dnsStats.queryTypes,
recentQueries: dnsStats.recentQueries,
}; };
} }
// Fallback if MetricsManager not available // Fallback if MetricsManager not available
return { return {
queriesPerSecond: 0, queriesPerSecond: 0,

View File

@@ -100,6 +100,14 @@ export class VlanManager {
// Cache the result // Cache the result
this.normalizedMacCache.set(mac, normalized); this.normalizedMacCache.set(mac, normalized);
// Prevent unbounded cache growth
if (this.normalizedMacCache.size > 10000) {
const iterator = this.normalizedMacCache.keys();
for (let i = 0; i < 1000; i++) {
this.normalizedMacCache.delete(iterator.next().value);
}
}
return normalized; return normalized;
} }

View File

@@ -26,6 +26,11 @@ export interface IServerStats {
}; };
} }
export interface ITimeSeriesPoint {
timestamp: number;
value: number;
}
export interface IEmailStats { export interface IEmailStats {
sent: number; sent: number;
received: number; received: number;
@@ -35,6 +40,11 @@ export interface IEmailStats {
averageDeliveryTime: number; averageDeliveryTime: number;
deliveryRate: number; deliveryRate: number;
bounceRate: number; bounceRate: number;
timeSeries?: {
sent: ITimeSeriesPoint[];
received: ITimeSeriesPoint[];
failed: ITimeSeriesPoint[];
};
} }
export interface IDnsStats { export interface IDnsStats {
@@ -47,6 +57,16 @@ export interface IDnsStats {
queryTypes: { queryTypes: {
[key: string]: number; [key: string]: number;
}; };
timeSeries?: {
queries: ITimeSeriesPoint[];
};
recentQueries?: Array<{
timestamp: number;
domain: string;
type: string;
answered: boolean;
responseTimeMs: number;
}>;
} }
export interface IRateLimitInfo { export interface IRateLimitInfo {
@@ -58,6 +78,17 @@ export interface IRateLimitInfo {
blocked: boolean; blocked: boolean;
} }
export interface ISecurityEvent {
timestamp: number;
level: string;
type: string;
message: string;
details?: any;
ipAddress?: string;
domain?: string;
success?: boolean;
}
export interface ISecurityMetrics { export interface ISecurityMetrics {
blockedIPs: string[]; blockedIPs: string[];
reputationScores: { reputationScores: {
@@ -68,6 +99,7 @@ export interface ISecurityMetrics {
phishingDetected: number; phishingDetected: number;
authenticationFailures: number; authenticationFailures: number;
suspiciousActivities: number; suspiciousActivities: number;
recentEvents?: ISecurityEvent[];
} }
export interface ILogEntry { export interface ILogEntry {

View File

@@ -41,4 +41,16 @@ export interface IReq_GetLogStream extends plugins.typedrequestInterfaces.implem
response: { response: {
logStream: plugins.typedrequestInterfaces.IVirtualStream; logStream: plugins.typedrequestInterfaces.IVirtualStream;
}; };
}
// Push Log Entry (server → client via TypedSocket)
export interface IReq_PushLogEntry extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_PushLogEntry
> {
method: 'pushLogEntry';
request: {
entry: statsInterfaces.ILogEntry;
};
response: {};
} }

View File

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

View File

@@ -1075,6 +1075,55 @@ export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
} }
}); });
// ============================================================================
// TypedSocket Client for Real-time Log Streaming
// ============================================================================
let socketClient: plugins.typedsocket.TypedSocket | null = null;
const socketRouter = new plugins.domtools.plugins.typedrequest.TypedRouter();
// Register handler for pushed log entries from the server
socketRouter.addTypedHandler(
new plugins.domtools.plugins.typedrequest.TypedHandler<interfaces.requests.IReq_PushLogEntry>(
'pushLogEntry',
async (dataArg) => {
const current = logStatePart.getState();
const updated = [...current.recentLogs, dataArg.entry];
// Cap at 2000 entries
if (updated.length > 2000) {
updated.splice(0, updated.length - 2000);
}
logStatePart.setState({ ...current, recentLogs: updated });
return {};
}
)
);
async function connectSocket() {
if (socketClient) return;
try {
socketClient = await plugins.typedsocket.TypedSocket.createClient(
socketRouter,
plugins.typedsocket.TypedSocket.useWindowLocationOriginUrl(),
);
await socketClient.setTag('role', 'ops_dashboard');
} catch (err) {
console.error('TypedSocket connection failed:', err);
socketClient = null;
}
}
async function disconnectSocket() {
if (socketClient) {
try {
await socketClient.stop();
} catch {
// ignore disconnect errors
}
socketClient = null;
}
}
// Combined refresh action for efficient polling // Combined refresh action for efficient polling
async function dispatchCombinedRefreshAction() { async function dispatchCombinedRefreshAction() {
const context = getActionContext(); const context = getActionContext();
@@ -1237,9 +1286,21 @@ let currentRefreshRate = 1000; // Track current refresh rate to avoid unnecessar
if (state.isLoggedIn !== previousIsLoggedIn) { if (state.isLoggedIn !== previousIsLoggedIn) {
previousIsLoggedIn = state.isLoggedIn; previousIsLoggedIn = state.isLoggedIn;
startAutoRefresh(); startAutoRefresh();
// Connect/disconnect TypedSocket based on login state
if (state.isLoggedIn) {
connectSocket();
} else {
disconnectSocket();
}
} }
}); });
// Initial start // Initial start
startAutoRefresh(); startAutoRefresh();
// Connect TypedSocket if already logged in (e.g., persistent session)
if (loginStatePart.getState().isLoggedIn) {
connectSocket();
}
})(); })();

View File

@@ -20,6 +20,17 @@ export class OpsViewLogs extends DeesElement {
filters: {}, filters: {},
}; };
@state()
accessor filterLevel: string | undefined;
@state()
accessor filterCategory: string | undefined;
@state()
accessor filterLimit: number = 100;
private lastPushedCount = 0;
constructor() { constructor() {
super(); super();
const subscription = appstate.logStatePart const subscription = appstate.logStatePart
@@ -46,87 +57,20 @@ export class OpsViewLogs extends DeesElement {
align-items: center; align-items: center;
gap: 8px; gap: 8px;
} }
.logContainer {
background: ${cssManager.bdTheme('#f8f9fa', '#1e1e1e')};
border-radius: 8px;
padding: 16px;
max-height: 600px;
overflow-y: auto;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
}
.logEntry {
margin-bottom: 8px;
line-height: 1.5;
}
.logTimestamp {
color: ${cssManager.bdTheme('#7a7a7a', '#7a7a7a')};
margin-right: 8px;
}
.logLevel {
font-weight: bold;
margin-right: 8px;
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
}
.logLevel.debug {
color: ${cssManager.bdTheme('#6a9955', '#6a9955')};
background: ${cssManager.bdTheme('rgba(106, 153, 85, 0.1)', 'rgba(106, 153, 85, 0.1)')};
}
.logLevel.info {
color: ${cssManager.bdTheme('#569cd6', '#569cd6')};
background: ${cssManager.bdTheme('rgba(86, 156, 214, 0.1)', 'rgba(86, 156, 214, 0.1)')};
}
.logLevel.warn {
color: ${cssManager.bdTheme('#ce9178', '#ce9178')};
background: ${cssManager.bdTheme('rgba(206, 145, 120, 0.1)', 'rgba(206, 145, 120, 0.1)')};
}
.logLevel.error {
color: ${cssManager.bdTheme('#f44747', '#f44747')};
background: ${cssManager.bdTheme('rgba(244, 71, 71, 0.1)', 'rgba(244, 71, 71, 0.1)')};
}
.logCategory {
color: ${cssManager.bdTheme('#c586c0', '#c586c0')};
margin-right: 8px;
}
.logMessage {
color: ${cssManager.bdTheme('#333', '#d4d4d4')};
}
.noLogs {
color: ${cssManager.bdTheme('#7a7a7a', '#7a7a7a')};
text-align: center;
padding: 40px;
}
`, `,
]; ];
public render() { public render() {
return html` return html`
<ops-sectionheading>Logs</ops-sectionheading> <ops-sectionheading>Logs</ops-sectionheading>
<div class="controls"> <div class="controls">
<div class="filterGroup"> <div class="filterGroup">
<dees-button <dees-button
@click=${() => this.fetchLogs()} @click=${() => this.fetchLogs()}
> >
Refresh Logs Refresh Logs
</dees-button> </dees-button>
<dees-button
@click=${() => this.toggleStreaming()}
.type=${this.logState.isStreaming ? 'highlighted' : 'normal'}
>
${this.logState.isStreaming ? 'Stop Streaming' : 'Start Streaming'}
</dees-button>
</div> </div>
<div class="filterGroup"> <div class="filterGroup">
@@ -134,7 +78,7 @@ export class OpsViewLogs extends DeesElement {
<dees-input-dropdown <dees-input-dropdown
.options=${['all', 'debug', 'info', 'warn', 'error']} .options=${['all', 'debug', 'info', 'warn', 'error']}
.selectedOption=${'all'} .selectedOption=${'all'}
@selectedOption=${(e) => this.updateFilter('level', e.detail)} @selectedOption=${(e: any) => this.updateFilter('level', e.detail)}
></dees-input-dropdown> ></dees-input-dropdown>
</div> </div>
@@ -143,7 +87,7 @@ export class OpsViewLogs extends DeesElement {
<dees-input-dropdown <dees-input-dropdown
.options=${['all', 'smtp', 'dns', 'security', 'system', 'email']} .options=${['all', 'smtp', 'dns', 'security', 'system', 'email']}
.selectedOption=${'all'} .selectedOption=${'all'}
@selectedOption=${(e) => this.updateFilter('category', e.detail)} @selectedOption=${(e: any) => this.updateFilter('category', e.detail)}
></dees-input-dropdown> ></dees-input-dropdown>
</div> </div>
@@ -152,56 +96,88 @@ export class OpsViewLogs extends DeesElement {
<dees-input-dropdown <dees-input-dropdown
.options=${['50', '100', '200', '500']} .options=${['50', '100', '200', '500']}
.selectedOption=${'100'} .selectedOption=${'100'}
@selectedOption=${(e) => this.updateFilter('limit', e.detail)} @selectedOption=${(e: any) => this.updateFilter('limit', e.detail)}
></dees-input-dropdown> ></dees-input-dropdown>
</div> </div>
</div> </div>
<div class="logContainer"> <dees-chart-log
${this.logState.recentLogs.length > 0 ? .label=${'Application Logs'}
this.logState.recentLogs.map(log => html` .autoScroll=${true}
<div class="logEntry"> .maxEntries=${2000}
<span class="logTimestamp">${new Date(log.timestamp).toLocaleTimeString()}</span> .showMetrics=${true}
<span class="logLevel ${log.level}">${log.level.toUpperCase()}</span> ></dees-chart-log>
<span class="logCategory">[${log.category}]</span>
<span class="logMessage">${log.message}</span>
</div>
`) : html`
<div class="noLogs">No logs to display</div>
`
}
</div>
`; `;
} }
async connectedCallback() {
super.connectedCallback();
this.lastPushedCount = 0;
// Only fetch if state is empty (streaming will handle new entries)
if (this.logState.recentLogs.length === 0) {
this.fetchLogs();
}
}
async updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
if (changedProperties.has('logState')) {
this.pushLogsToChart();
}
}
private async pushLogsToChart() {
const chartLog = this.shadowRoot?.querySelector('dees-chart-log') as any;
if (!chartLog) return;
// Ensure the chart element has finished its own initialization
await chartLog.updateComplete;
const allEntries = this.getMappedLogEntries();
if (this.lastPushedCount === 0 && allEntries.length > 0) {
// Initial load: push all entries
chartLog.updateLog(allEntries);
this.lastPushedCount = allEntries.length;
} else if (allEntries.length > this.lastPushedCount) {
// Incremental: only push new entries
const newEntries = allEntries.slice(this.lastPushedCount);
chartLog.updateLog(newEntries);
this.lastPushedCount = allEntries.length;
}
}
private getMappedLogEntries() {
return this.logState.recentLogs.map((log) => ({
timestamp: new Date(log.timestamp).toISOString(),
level: log.level as 'debug' | 'info' | 'warn' | 'error',
message: log.message,
source: log.category,
}));
}
private async fetchLogs() { private async fetchLogs() {
const filters = this.getActiveFilters();
await appstate.logStatePart.dispatchAction(appstate.fetchRecentLogsAction, { await appstate.logStatePart.dispatchAction(appstate.fetchRecentLogsAction, {
limit: filters.limit || 100, limit: this.filterLimit,
level: filters.level as 'debug' | 'info' | 'warn' | 'error' | undefined, level: this.filterLevel as 'debug' | 'info' | 'warn' | 'error' | undefined,
category: filters.category as 'smtp' | 'dns' | 'security' | 'system' | 'email' | undefined, category: this.filterCategory as 'smtp' | 'dns' | 'security' | 'system' | 'email' | undefined,
}); });
} }
private updateFilter(type: string, value: string) { private updateFilter(type: string, value: string) {
if (value === 'all') { const resolved = value === 'all' ? undefined : value;
value = undefined;
switch (type) {
case 'level':
this.filterLevel = resolved;
break;
case 'category':
this.filterCategory = resolved;
break;
case 'limit':
this.filterLimit = resolved ? parseInt(resolved, 10) : 100;
break;
} }
// Update filters then fetch logs
this.fetchLogs(); this.fetchLogs();
} }
}
private getActiveFilters() {
return {
level: this.logState.filters.level?.[0],
category: this.logState.filters.category?.[0],
limit: 100,
};
}
private toggleStreaming() {
// TODO: Implement log streaming with VirtualStream
console.log('Streaming toggle not yet implemented');
}
}

View File

@@ -26,14 +26,36 @@ export class OpsViewOverview extends DeesElement {
error: null, error: null,
}; };
@state()
accessor logState: appstate.ILogState = {
recentLogs: [],
isStreaming: false,
filters: {},
};
constructor() { constructor() {
super(); super();
const subscription = appstate.statsStatePart const statsSub = appstate.statsStatePart
.select((stateArg) => stateArg) .select((stateArg) => stateArg)
.subscribe((statsState) => { .subscribe((statsState) => {
this.statsState = statsState; this.statsState = statsState;
}); });
this.rxSubscriptions.push(subscription); this.rxSubscriptions.push(statsSub);
const logSub = appstate.logStatePart
.select((stateArg) => stateArg)
.subscribe((logState) => {
this.logState = logState;
});
this.rxSubscriptions.push(logSub);
}
async connectedCallback() {
super.connectedCallback();
// Ensure logs are fetched for the overview charts
if (this.logState.recentLogs.length === 0) {
appstate.logStatePart.dispatchAction(appstate.fetchRecentLogsAction, { limit: 100 });
}
} }
public static styles = [ public static styles = [
@@ -96,10 +118,24 @@ export class OpsViewOverview extends DeesElement {
${this.renderDnsStats()} ${this.renderDnsStats()}
<div class="chartGrid"> <div class="chartGrid">
<dees-chart-area .label=${'Email Traffic (24h)'} .data=${[]}></dees-chart-area> <dees-chart-area
<dees-chart-area .label=${'DNS Queries (24h)'} .data=${[]}></dees-chart-area> .label=${'Email Traffic (24h)'}
<dees-chart-log .label=${'Recent Events'} .data=${[]}></dees-chart-log> .series=${this.getEmailTrafficSeries()}
<dees-chart-log .label=${'Security Alerts'} .data=${[]}></dees-chart-log> .yAxisFormatter=${(val: number) => `${val}`}
></dees-chart-area>
<dees-chart-area
.label=${'DNS Queries (24h)'}
.series=${this.getDnsQuerySeries()}
.yAxisFormatter=${(val: number) => `${val}`}
></dees-chart-area>
<dees-chart-log
.label=${'Recent Events'}
.logEntries=${this.getRecentEventEntries()}
></dees-chart-log>
<dees-chart-log
.label=${'DNS Queries'}
.logEntries=${this.getDnsQueryEntries()}
></dees-chart-log>
</div> </div>
`} `}
`; `;
@@ -337,4 +373,52 @@ export class OpsViewOverview extends DeesElement {
<dees-statsgrid .tiles=${tiles}></dees-statsgrid> <dees-statsgrid .tiles=${tiles}></dees-statsgrid>
`; `;
} }
// --- Chart data helpers ---
private getRecentEventEntries(): Array<{ timestamp: string; level: 'debug' | 'info' | 'warn' | 'error' | 'success'; message: string; source?: string }> {
return this.logState.recentLogs.map((log) => ({
timestamp: new Date(log.timestamp).toISOString(),
level: log.level as 'debug' | 'info' | 'warn' | 'error',
message: log.message,
source: log.category,
}));
}
private getSecurityAlertEntries(): Array<{ timestamp: string; level: 'debug' | 'info' | 'warn' | 'error' | 'success'; message: string; source?: string }> {
const events: any[] = this.statsState.securityMetrics?.recentEvents || [];
return events.map((evt: any) => ({
timestamp: new Date(evt.timestamp).toISOString(),
level: evt.level === 'critical' || evt.level === 'error' ? 'error' as const : evt.level === 'warn' ? 'warn' as const : 'info' as const,
message: evt.message,
source: evt.type,
}));
}
private getDnsQueryEntries(): Array<{ timestamp: string; level: 'debug' | 'info' | 'warn' | 'error' | 'success'; message: string; source?: string }> {
const queries: any[] = (this.statsState.dnsStats as any)?.recentQueries || [];
return queries.map((q: any) => ({
timestamp: new Date(q.timestamp).toISOString(),
level: q.answered ? 'info' as const : 'warn' as const,
message: `${q.type} ${q.domain} (${q.responseTimeMs}ms)`,
source: 'dns',
}));
}
private getEmailTrafficSeries(): Array<{ name: string; color: string; data: Array<{ x: number; y: number }> }> {
const ts = this.statsState.emailStats?.timeSeries;
if (!ts) return [];
return [
{ name: 'Sent', color: '#22c55e', data: (ts.sent || []).map((p: any) => ({ x: p.timestamp, y: p.value })) },
{ name: 'Received', color: '#3b82f6', data: (ts.received || []).map((p: any) => ({ x: p.timestamp, y: p.value })) },
];
}
private getDnsQuerySeries(): Array<{ name: string; color: string; data: Array<{ x: number; y: number }> }> {
const ts = this.statsState.dnsStats?.timeSeries;
if (!ts) return [];
return [
{ name: 'Queries', color: '#8b5cf6', data: (ts.queries || []).map((p: any) => ({ x: p.timestamp, y: p.value })) },
];
}
} }

View File

@@ -249,7 +249,14 @@ export class OpsViewSecurity extends DeesElement {
private renderOverview(metrics: any) { private renderOverview(metrics: any) {
const threatLevel = this.calculateThreatLevel(metrics); const threatLevel = this.calculateThreatLevel(metrics);
const threatScore = this.getThreatScore(metrics); const threatScore = this.getThreatScore(metrics);
// Derive active sessions from recent successful auth events (last hour)
const allEvents: any[] = metrics.recentEvents || [];
const oneHourAgo = Date.now() - 3600000;
const recentAuthSuccesses = allEvents.filter(
(evt: any) => evt.type === 'authentication' && evt.success === true && evt.timestamp >= oneHourAgo
).length;
const tiles: IStatsTile[] = [ const tiles: IStatsTile[] = [
{ {
id: 'threatLevel', id: 'threatLevel',
@@ -271,7 +278,7 @@ export class OpsViewSecurity extends DeesElement {
{ {
id: 'blockedThreats', id: 'blockedThreats',
title: 'Blocked Threats', title: 'Blocked Threats',
value: metrics.blockedIPs.length + metrics.spamDetected, value: (metrics.blockedIPs?.length || 0) + metrics.spamDetected,
type: 'number', type: 'number',
icon: 'lucide:ShieldCheck', icon: 'lucide:ShieldCheck',
color: '#ef4444', color: '#ef4444',
@@ -280,11 +287,11 @@ export class OpsViewSecurity extends DeesElement {
{ {
id: 'activeSessions', id: 'activeSessions',
title: 'Active Sessions', title: 'Active Sessions',
value: 0, value: recentAuthSuccesses,
type: 'number', type: 'number',
icon: 'lucide:Users', icon: 'lucide:Users',
color: '#22c55e', color: '#22c55e',
description: 'Current authenticated sessions', description: 'Authenticated in last hour',
}, },
{ {
id: 'authFailures', id: 'authFailures',
@@ -349,6 +356,11 @@ export class OpsViewSecurity extends DeesElement {
} }
private renderAuthentication(metrics: any) { private renderAuthentication(metrics: any) {
// Derive auth events from recentEvents
const allEvents: any[] = metrics.recentEvents || [];
const authEvents = allEvents.filter((evt: any) => evt.type === 'authentication');
const successfulLogins = authEvents.filter((evt: any) => evt.success === true).length;
const tiles: IStatsTile[] = [ const tiles: IStatsTile[] = [
{ {
id: 'authFailures', id: 'authFailures',
@@ -362,7 +374,7 @@ export class OpsViewSecurity extends DeesElement {
{ {
id: 'successfulLogins', id: 'successfulLogins',
title: 'Successful Logins', title: 'Successful Logins',
value: 0, value: successfulLogins,
type: 'number', type: 'number',
icon: 'lucide:Lock', icon: 'lucide:Lock',
color: '#22c55e', color: '#22c55e',
@@ -370,6 +382,15 @@ export class OpsViewSecurity extends DeesElement {
}, },
]; ];
// Map auth events to login history table data
const loginHistory = authEvents.map((evt: any) => ({
timestamp: evt.timestamp,
username: evt.details?.username || 'unknown',
ipAddress: evt.ipAddress || 'unknown',
success: evt.success ?? false,
reason: evt.success ? '' : evt.message || 'Authentication failed',
}));
return html` return html`
<dees-statsgrid <dees-statsgrid
.tiles=${tiles} .tiles=${tiles}
@@ -380,7 +401,7 @@ export class OpsViewSecurity extends DeesElement {
<dees-table <dees-table
.heading1=${'Login History'} .heading1=${'Login History'}
.heading2=${'Recent authentication attempts'} .heading2=${'Recent authentication attempts'}
.data=${[]} .data=${loginHistory}
.displayFunction=${(item) => ({ .displayFunction=${(item) => ({
'Time': new Date(item.timestamp).toLocaleString(), 'Time': new Date(item.timestamp).toLocaleString(),
'Username': item.username, 'Username': item.username,
@@ -483,48 +504,38 @@ export class OpsViewSecurity extends DeesElement {
private getThreatScore(metrics: any): number { private getThreatScore(metrics: any): number {
// Simple scoring algorithm // Simple scoring algorithm
let score = 100; let score = 100;
score -= metrics.blockedIPs.length * 2; const blockedCount = Array.isArray(metrics.blockedIPs) ? metrics.blockedIPs.length : (metrics.blockedIPs || 0);
score -= metrics.authenticationFailures * 1; score -= blockedCount * 2;
score -= metrics.spamDetected * 0.5; score -= (metrics.authenticationFailures || 0) * 1;
score -= metrics.malwareDetected * 3; score -= (metrics.spamDetected || 0) * 0.5;
score -= metrics.phishingDetected * 3; score -= (metrics.malwareDetected || 0) * 3;
score -= metrics.suspiciousActivities * 2; score -= (metrics.phishingDetected || 0) * 3;
score -= (metrics.suspiciousActivities || 0) * 2;
return Math.max(0, Math.min(100, Math.round(score))); return Math.max(0, Math.min(100, Math.round(score)));
} }
private getSecurityEvents(metrics: any): any[] { private getSecurityEvents(metrics: any): any[] {
// Mock data - in real implementation, this would come from the server const events: any[] = metrics.recentEvents || [];
return [ return events.map((evt: any) => ({
{ timestamp: evt.timestamp,
timestamp: Date.now() - 1000 * 60 * 5, event: evt.message,
event: 'Multiple failed login attempts', severity: evt.level === 'critical' ? 'critical' : evt.level === 'error' ? 'high' : evt.level === 'warn' ? 'warning' : 'info',
severity: 'warning', details: evt.ipAddress ? `IP: ${evt.ipAddress}` : evt.domain ? `Domain: ${evt.domain}` : evt.type,
details: 'IP: 192.168.1.100', }));
},
{
timestamp: Date.now() - 1000 * 60 * 15,
event: 'SPF check failed',
severity: 'medium',
details: 'Domain: example.com',
},
{
timestamp: Date.now() - 1000 * 60 * 30,
event: 'IP blocked due to spam',
severity: 'high',
details: 'IP: 10.0.0.1',
},
];
} }
private async clearBlockedIPs() { private async clearBlockedIPs() {
console.log('Clear blocked IPs'); // SmartProxy manages IP blocking — not yet exposed via API
alert('Clearing blocked IPs is not yet supported from the UI.');
} }
private async unblockIP(ip: string) { private async unblockIP(ip: string) {
console.log('Unblock IP:', ip); // SmartProxy manages IP blocking — not yet exposed via API
alert(`Unblocking IP ${ip} is not yet supported from the UI.`);
} }
private async saveEmailSecuritySettings() { private async saveEmailSecuritySettings() {
console.log('Save email security settings'); // Config is read-only from the UI for now
alert('Email security settings are read-only. Update the dcrouter configuration file to change these settings.');
} }
} }

View File

@@ -2,9 +2,13 @@
import * as deesElement from '@design.estate/dees-element'; import * as deesElement from '@design.estate/dees-element';
import * as deesCatalog from '@design.estate/dees-catalog'; import * as deesCatalog from '@design.estate/dees-catalog';
// TypedSocket for real-time push communication
import * as typedsocket from '@api.global/typedsocket';
export { export {
deesElement, deesElement,
deesCatalog deesCatalog,
typedsocket,
} }
// domtools gives us TypedRequest and other utilities // domtools gives us TypedRequest and other utilities