Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 37d62c51f3 | |||
| ea9427d46b | |||
| bc77321752 | |||
| 65aa546c1c | |||
| 54484518dc | |||
| 6fe1247d4d | |||
| e59d80a3b3 | |||
| 6c4feba711 | |||
| 006a9af20c | |||
| dfb3b0ac37 | |||
| 44c1a3a928 | |||
| 0c4e28455e | |||
| cfc4cf378f | |||
| a09e69a28b | |||
| 82dd19e274 | |||
| c1d8afdbf7 | |||
| 9b7426f1e6 | |||
| 3c9c865841 | |||
| 8421c9fe46 | |||
| 907e3df156 | |||
| aaa0956148 | |||
| 118019fcf5 | |||
| deb80f4fd0 | |||
| 7d28cea937 | |||
| 2bd5e5c7c5 | |||
| 4d6ac81c59 | |||
| 2ebe0de92d | |||
| f5028ffb60 | |||
| 90016d1217 | |||
| 48d3d1218f | |||
| 4759c4f011 | |||
| 0fbd8d1cdd | |||
| 447cf44d68 | |||
| 82ce17a941 | |||
| 15da996e70 | |||
| 582e19e6a6 |
7
.playwright-mcp/console-2026-02-23T10-44-24-024Z.log
Normal file
7
.playwright-mcp/console-2026-02-23T10-44-24-024Z.log
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[ 74ms] TypeError: Cannot read properties of null (reading 'appendChild')
|
||||||
|
at TypedserverStatusPill.show (http://localhost:3000/typedserver/devtools:17607:21)
|
||||||
|
at TypedserverStatusPill.updateStatus (http://localhost:3000/typedserver/devtools:17567:10)
|
||||||
|
at ReloadChecker.checkReload (http://localhost:3000/typedserver/devtools:18137:23)
|
||||||
|
at async ReloadChecker.start (http://localhost:3000/typedserver/devtools:18224:9)
|
||||||
|
[ 587ms] [ERROR] method: >>getMergedRoutes<< got an ERROR: "unauthorized" with data undefined @ http://localhost:3000/bundle.js:13
|
||||||
|
[ 697ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/routes:0
|
||||||
12
.playwright-mcp/console-2026-02-23T11-19-21-255Z.log
Normal file
12
.playwright-mcp/console-2026-02-23T11-19-21-255Z.log
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[ 669ms] [WARNING] Lit is in dev mode. Not recommended for production! See https://lit.dev/msg/dev-mode for more information. @ http://localhost:3000/chunk-3L5NJTXF.js:13541
|
||||||
|
[ 729ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:3000/favicon.ico:0
|
||||||
|
[ 27973ms] [ERROR] WebSocket connection to 'ws://localhost:3000/ws/reload' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/main.js:115
|
||||||
|
[ 27973ms] [ERROR] [ReloadService] WebSocket error: Event @ http://localhost:3000/main.js:141
|
||||||
|
[ 29975ms] [ERROR] WebSocket connection to 'ws://localhost:3000/ws/reload' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/main.js:115
|
||||||
|
[ 29975ms] [ERROR] [ReloadService] WebSocket error: Event @ http://localhost:3000/main.js:141
|
||||||
|
[ 33977ms] [ERROR] WebSocket connection to 'ws://localhost:3000/ws/reload' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/main.js:115
|
||||||
|
[ 33978ms] [ERROR] [ReloadService] WebSocket error: Event @ http://localhost:3000/main.js:141
|
||||||
|
[ 41980ms] [ERROR] WebSocket connection to 'ws://localhost:3000/ws/reload' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/main.js:115
|
||||||
|
[ 41980ms] [ERROR] [ReloadService] WebSocket error: Event @ http://localhost:3000/main.js:141
|
||||||
|
[ 51983ms] [ERROR] WebSocket connection to 'ws://localhost:3000/ws/reload' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/main.js:115
|
||||||
|
[ 51983ms] [ERROR] [ReloadService] WebSocket error: Event @ http://localhost:3000/main.js:141
|
||||||
6
.playwright-mcp/console-2026-02-23T11-20-31-682Z.log
Normal file
6
.playwright-mcp/console-2026-02-23T11-20-31-682Z.log
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[ 55ms] TypeError: Cannot read properties of null (reading 'appendChild')
|
||||||
|
at TypedserverStatusPill.show (http://localhost:3000/typedserver/devtools:17607:21)
|
||||||
|
at TypedserverStatusPill.updateStatus (http://localhost:3000/typedserver/devtools:17567:10)
|
||||||
|
at ReloadChecker.checkReload (http://localhost:3000/typedserver/devtools:18137:23)
|
||||||
|
at async ReloadChecker.start (http://localhost:3000/typedserver/devtools:18224:9)
|
||||||
|
[ 791ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/overview:0
|
||||||
50
.playwright-mcp/console-2026-02-23T11-21-09-382Z.log
Normal file
50
.playwright-mcp/console-2026-02-23T11-21-09-382Z.log
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
[ 272ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for Refresh-cw
|
||||||
|
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||||
|
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||||
|
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||||
|
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||||
|
at N._$EP (http://localhost:3000/bundle.js:1:9078)
|
||||||
|
at async N._$EP (http://localhost:3000/bundle.js:1:9024) @ http://localhost:3000/bundle.js:1203
|
||||||
|
[ 272ms] [WARNING] Lucide icon 'Refresh-cw' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||||
|
[ 274ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for Pause-circle
|
||||||
|
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||||
|
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||||
|
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||||
|
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||||
|
at N._$EP (http://localhost:3000/bundle.js:1:9078)
|
||||||
|
at async N._$EP (http://localhost:3000/bundle.js:1:9024) @ http://localhost:3000/bundle.js:1203
|
||||||
|
[ 274ms] [WARNING] Lucide icon 'Pause-circle' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||||
|
[ 275ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for Refresh-cw
|
||||||
|
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||||
|
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||||
|
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||||
|
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||||
|
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||||
|
[ 275ms] [WARNING] Lucide icon 'Refresh-cw' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||||
|
[ 276ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for Refresh-cw
|
||||||
|
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||||
|
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||||
|
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||||
|
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||||
|
at N._$EP (http://localhost:3000/bundle.js:1:9078)
|
||||||
|
at async N._$EP (http://localhost:3000/bundle.js:1:9024) @ http://localhost:3000/bundle.js:1203
|
||||||
|
[ 276ms] [WARNING] Lucide icon 'Refresh-cw' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||||
|
[ 276ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for Refresh-cw
|
||||||
|
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||||
|
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||||
|
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||||
|
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||||
|
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||||
|
[ 276ms] [WARNING] Lucide icon 'Refresh-cw' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||||
|
[ 297ms] [ERROR] method: >>getMergedRoutes<< got an ERROR: "unauthorized" with data undefined @ http://localhost:3000/bundle.js:13
|
||||||
|
[ 377ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/routes:0
|
||||||
|
[ 78064ms] [ERROR] method: >>getMergedRoutes<< got an ERROR: "unauthorized" with data undefined @ http://localhost:3000/bundle.js:13
|
||||||
|
[ 78237ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/routes:0
|
||||||
|
[ 127969ms] [ERROR] WebSocket connection to 'ws://localhost:3000/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedserver/devtools:16227
|
||||||
|
[ 127969ms] [ERROR] TypedSocket WebSocket error: Event @ http://localhost:3000/typedserver/devtools:16251
|
||||||
|
[ 129695ms] [ERROR] WebSocket connection to 'ws://localhost:3000/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedserver/devtools:16227
|
||||||
|
[ 129695ms] [ERROR] TypedSocket WebSocket error: Event @ http://localhost:3000/typedserver/devtools:16251
|
||||||
|
[ 133309ms] [ERROR] WebSocket connection to 'ws://localhost:3000/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedserver/devtools:16227
|
||||||
|
[ 133309ms] [ERROR] TypedSocket WebSocket error: Event @ http://localhost:3000/typedserver/devtools:16251
|
||||||
|
[ 141762ms] [ERROR] method: >>getMergedRoutes<< got an ERROR: "unauthorized" with data undefined @ http://localhost:3000/bundle.js:13
|
||||||
|
[ 141910ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/routes:0
|
||||||
23
.playwright-mcp/console-2026-02-23T11-23-44-606Z.log
Normal file
23
.playwright-mcp/console-2026-02-23T11-23-44-606Z.log
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
[ 437ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/overview:0
|
||||||
|
[ 38948ms] [WARNING] FontAwesome icon not found: circle-check @ http://localhost:3000/bundle.js:1203
|
||||||
|
[ 52895ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||||
|
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||||
|
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||||
|
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||||
|
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||||
|
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||||
|
[ 52896ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||||
|
[ 52896ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||||
|
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||||
|
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||||
|
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||||
|
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||||
|
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||||
|
[ 52897ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||||
|
[ 99401ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||||
|
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||||
|
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||||
|
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||||
|
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||||
|
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||||
|
[ 99401ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||||
31
.playwright-mcp/console-2026-02-23T12-47-06-007Z.log
Normal file
31
.playwright-mcp/console-2026-02-23T12-47-06-007Z.log
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
[ 75ms] TypeError: Cannot read properties of null (reading 'appendChild')
|
||||||
|
at TypedserverStatusPill.show (http://localhost:3000/typedserver/devtools:17607:21)
|
||||||
|
at TypedserverStatusPill.updateStatus (http://localhost:3000/typedserver/devtools:17567:10)
|
||||||
|
at ReloadChecker.checkReload (http://localhost:3000/typedserver/devtools:18137:23)
|
||||||
|
at async ReloadChecker.start (http://localhost:3000/typedserver/devtools:18224:9)
|
||||||
|
[ 763ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/overview:0
|
||||||
|
[ 22315ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||||
|
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||||
|
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||||
|
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||||
|
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||||
|
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||||
|
[ 22315ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||||
|
[ 22316ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||||
|
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||||
|
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||||
|
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||||
|
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||||
|
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||||
|
[ 22316ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||||
|
[ 22321ms] [ERROR] method: >>listApiTokens<< got an ERROR: "admin access required" with data undefined @ http://localhost:3000/bundle.js:13
|
||||||
|
[ 22322ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||||
|
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||||
|
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||||
|
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||||
|
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||||
|
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||||
|
[ 22322ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||||
|
[ 22322ms] [ERROR] method: >>listApiTokens<< got an ERROR: "admin access required" with data undefined @ http://localhost:3000/bundle.js:13
|
||||||
|
[ 65371ms] [ERROR] method: >>createApiToken<< got an ERROR: "admin access required" with data undefined @ http://localhost:3000/bundle.js:13
|
||||||
|
[ 65371ms] [ERROR] Failed to create token: zs @ http://localhost:3000/bundle.js:38142
|
||||||
25
.playwright-mcp/console-2026-02-23T12-48-31-563Z.log
Normal file
25
.playwright-mcp/console-2026-02-23T12-48-31-563Z.log
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
[ 642ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/overview:0
|
||||||
|
[ 114916ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/overview:0
|
||||||
|
[ 179731ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||||
|
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||||
|
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||||
|
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||||
|
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||||
|
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||||
|
[ 179731ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||||
|
[ 179731ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||||
|
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||||
|
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||||
|
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||||
|
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||||
|
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||||
|
[ 179732ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||||
|
[ 179737ms] [ERROR] method: >>listApiTokens<< got an ERROR: "admin access required" with data undefined @ http://localhost:3000/bundle.js:13
|
||||||
|
[ 179738ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||||
|
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||||
|
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||||
|
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||||
|
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||||
|
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||||
|
[ 179738ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||||
|
[ 179738ms] [ERROR] method: >>listApiTokens<< got an ERROR: "admin access required" with data undefined @ http://localhost:3000/bundle.js:13
|
||||||
1
.playwright-mcp/console-2026-02-23T12-53-33-702Z.log
Normal file
1
.playwright-mcp/console-2026-02-23T12-53-33-702Z.log
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[ 603ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/overview:0
|
||||||
24
.playwright-mcp/console-2026-02-23T12-55-40-311Z.log
Normal file
24
.playwright-mcp/console-2026-02-23T12-55-40-311Z.log
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[ 308ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||||
|
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||||
|
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||||
|
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||||
|
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||||
|
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||||
|
[ 309ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||||
|
[ 309ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||||
|
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||||
|
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||||
|
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||||
|
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||||
|
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||||
|
[ 310ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||||
|
[ 349ms] [ERROR] method: >>listApiTokens<< got an ERROR: "admin access required" with data undefined @ http://localhost:3000/bundle.js:13
|
||||||
|
[ 350ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||||
|
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||||
|
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||||
|
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||||
|
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||||
|
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||||
|
[ 350ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||||
|
[ 351ms] [ERROR] method: >>listApiTokens<< got an ERROR: "admin access required" with data undefined @ http://localhost:3000/bundle.js:13
|
||||||
|
[ 500ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/apitokens:0
|
||||||
30
.playwright-mcp/console-2026-02-23T12-57-47-953Z.log
Normal file
30
.playwright-mcp/console-2026-02-23T12-57-47-953Z.log
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
[ 427ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/overview:0
|
||||||
|
[ 44124ms] [WARNING] FontAwesome icon not found: circle-check @ http://localhost:3000/bundle.js:1203
|
||||||
|
[ 59106ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||||
|
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||||
|
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||||
|
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||||
|
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||||
|
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||||
|
[ 59106ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||||
|
[ 59107ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||||
|
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||||
|
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||||
|
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||||
|
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||||
|
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||||
|
[ 59107ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||||
|
[ 59116ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||||
|
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||||
|
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||||
|
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||||
|
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||||
|
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||||
|
[ 59116ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||||
|
[ 89192ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
||||||
|
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
||||||
|
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
||||||
|
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
||||||
|
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
||||||
|
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
||||||
|
[ 89192ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
||||||
BIN
.playwright-mcp/page-2026-02-23T11-25-39-255Z.png
Normal file
BIN
.playwright-mcp/page-2026-02-23T11-25-39-255Z.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
BIN
.playwright-mcp/page-2026-02-23T11-26-10-952Z.png
Normal file
BIN
.playwright-mcp/page-2026-02-23T11-26-10-952Z.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
BIN
.playwright-mcp/page-2026-02-23T11-26-15-885Z.png
Normal file
BIN
.playwright-mcp/page-2026-02-23T11-26-15-885Z.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
144
changelog.md
144
changelog.md
@@ -1,5 +1,149 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-02-26 - 9.2.0 - feat(remoteingress)
|
||||||
|
expose connected edge IPs and detected public IP; resolve proxy IPs from SmartProxy and improve ops UI
|
||||||
|
|
||||||
|
- Add detectedPublicIp to DC Router and populate it when a configured or auto-discovered public IP is chosen
|
||||||
|
- Use dcRouter.detectedPublicIp as a fallback for system.publicIp in the config handler
|
||||||
|
- Resolve proxy IPs from SmartProxy runtime settings when opts.proxyIps is not provided
|
||||||
|
- TunnelManager: capture peerAddr on edgeConnected and from Rust heartbeats, store per-edge publicIp, and add getConnectedEdgeIps()
|
||||||
|
- Expose connectedEdgeIps in the config API and return it in remoteIngress config
|
||||||
|
- Ops UI: show Connected Edge IPs, annotate 127.0.0.1 proxy IP as 'Remote Ingress' when applicable, and refresh remote ingress data during combined refresh when viewing remoteingress
|
||||||
|
- Bump dependency @serve.zone/remoteingress to ^4.2.0
|
||||||
|
|
||||||
|
## 2026-02-26 - 9.1.10 - fix(deps)
|
||||||
|
bump @push.rocks/smartproxy to ^25.8.5
|
||||||
|
|
||||||
|
- package.json: @push.rocks/smartproxy version updated from ^25.8.4 to ^25.8.5
|
||||||
|
- No other files changed
|
||||||
|
|
||||||
|
## 2026-02-26 - 9.1.9 - fix(deps(smartmta))
|
||||||
|
bump @push.rocks/smartmta to ^5.3.0
|
||||||
|
|
||||||
|
- Updated @push.rocks/smartmta from ^5.2.6 to ^5.3.0 in package.json
|
||||||
|
- Patch release recommended (no source code changes)
|
||||||
|
|
||||||
|
## 2026-02-26 - 9.1.8 - fix(deps)
|
||||||
|
bump @serve.zone/remoteingress to ^4.1.0
|
||||||
|
|
||||||
|
- Updated dependency @serve.zone/remoteingress from ^4.0.1 to ^4.1.0 in package.json
|
||||||
|
- Non-breaking dependency update; recommend patch version bump
|
||||||
|
|
||||||
|
## 2026-02-26 - 9.1.7 - fix(dcrouter)
|
||||||
|
bump @push.rocks/smartproxy to ^25.8.4 and remove custom smartProxy timeout/connection lifetime settings from dcrouter
|
||||||
|
|
||||||
|
- Bumped dependency @push.rocks/smartproxy from ^25.8.3 to ^25.8.4 in package.json
|
||||||
|
- Removed explicit smartProxy options: socketTimeout, inactivityTimeout, keepAliveInactivityMultiplier, extendedKeepAliveLifetime, and maxConnectionLifetime from ts/classes.dcrouter.ts
|
||||||
|
|
||||||
|
## 2026-02-26 - 9.1.6 - fix(cleanup)
|
||||||
|
prevent event listener and log stream leaks, tighten smartProxy connection timeouts, and improve graceful shutdown behavior
|
||||||
|
|
||||||
|
- Tightened smartProxy connection timeouts and lifetimes (5m socketTimeout, 10m inactivityTimeout, keep-alive multiplier, 1h extendedKeepAliveLifetime, 4h maxConnectionLifetime).
|
||||||
|
- Remove event listeners before stopping services to avoid leaks (smartProxy, emailServer, dnsServer, remote ingress hub).
|
||||||
|
- OpsServer.stop now invokes logsHandler.cleanup to tear down active log streams and avoid duplicate push destinations.
|
||||||
|
- LogsHandler rewritten to use a module-level singleton push destination, track active stream stop callbacks, add cleanup(), guard against hung VirtualStream.sendData with a 10s timeout, and ensure intervals are cleared on stop.
|
||||||
|
- updateSmartProxyConfig removes listeners on the old instance before stopping it.
|
||||||
|
- Dependency bumps: @api.global/typedsocket ^4.1.2, @push.rocks/smartdata ^7.1.0, @push.rocks/smartmta ^5.2.6, @push.rocks/smartproxy ^25.8.3.
|
||||||
|
|
||||||
|
## 2026-02-26 - 9.1.5 - fix(remoteingress)
|
||||||
|
Reconcile tunnel manager edge statuses with authoritative Rust hub periodically; update active tunnel counts and heartbeats, add missed edges, remove stale entries, and clear reconcile interval on stop
|
||||||
|
|
||||||
|
- Add reconcile() to sync TS-side edgeStatuses with hub.getStatus and overwrite activeTunnels with the authoritative activeStreams.
|
||||||
|
- Start a periodic reconcile (setInterval every 15s) and store the interval handle on the tunnel manager.
|
||||||
|
- Clear the reconcile interval in stop() to avoid background timers; remove edgeStatuses entries that are no longer connected in Rust.
|
||||||
|
- Bump dependency @serve.zone/remoteingress from ^4.0.0 to ^4.0.1.
|
||||||
|
|
||||||
|
## 2026-02-25 - 9.1.4 - fix(deps)
|
||||||
|
bump @push.rocks/smartproxy to ^25.8.1
|
||||||
|
|
||||||
|
- Updated package.json dependency @push.rocks/smartproxy from ^25.8.0 to ^25.8.1
|
||||||
|
|
||||||
|
## 2026-02-24 - 9.1.3 - fix(deps)
|
||||||
|
bump @api.global/typedserver to ^8.4.0 and @push.rocks/smartproxy to ^25.8.0
|
||||||
|
|
||||||
|
- Updated @api.global/typedserver from ^8.3.1 to ^8.4.0
|
||||||
|
- Updated @push.rocks/smartproxy from ^25.7.9 to ^25.8.0
|
||||||
|
|
||||||
|
## 2026-02-24 - 9.1.2 - fix(deps)
|
||||||
|
bump dependency versions for build and runtime packages
|
||||||
|
|
||||||
|
- @git.zone/tsbundle: ^2.8.3 -> ^2.9.0
|
||||||
|
- @git.zone/tswatch: ^3.1.0 -> ^3.2.0
|
||||||
|
- @api.global/typedserver: ^8.3.0 -> ^8.3.1
|
||||||
|
- @design.estate/dees-catalog: ^3.43.2 -> ^3.43.3
|
||||||
|
|
||||||
|
## 2026-02-23 - 9.1.1 - fix(dcrouter)
|
||||||
|
no changes detected — no files modified, no release necessary
|
||||||
|
|
||||||
|
- Git diff contained no changes
|
||||||
|
- No files added, modified, or deleted
|
||||||
|
|
||||||
|
## 2026-02-23 - 9.1.0 - feat(ops-dashboard)
|
||||||
|
add lucide icons to Ops dashboard view tabs
|
||||||
|
|
||||||
|
- Added iconName property to 10 view tabs in ts_web/elements/ops-dashboard.ts to enable icons in the UI
|
||||||
|
- Icon mappings: Overview -> lucide:layoutDashboard, Configuration -> lucide:settings, Network -> lucide:network, Emails -> lucide:mail, Logs -> lucide:scrollText, Routes -> lucide:route, ApiTokens -> lucide:key, Security -> lucide:shield, Certificates -> lucide:badgeCheck, RemoteIngress -> lucide:globe
|
||||||
|
- Improves visual clarity of dashboard navigation
|
||||||
|
|
||||||
|
## 2026-02-23 - 9.0.0 - BREAKING CHANGE(opsserver)
|
||||||
|
Return structured configuration (IConfigData) from opsserver and update UI to render detailed config sections
|
||||||
|
|
||||||
|
- Introduce IConfigData interface with typed sections: system, smartProxy, email, dns, tls, cache, radius, remoteIngress.
|
||||||
|
- Replace ConfigHandler.getConfiguration implementation to assemble and return IConfigData (changes API response shape for getConfiguration).
|
||||||
|
- Refactor frontend: update appstate types and ops-view-config to render the new config sections, use @serve.zone/catalog IConfigField/IConfigSectionAction, add uptime formatting and remote ingress UI.
|
||||||
|
- Fix ops-view-apitokens form handling to correctly read dees-input-tags values.
|
||||||
|
- Update tests to expect new configuration fields.
|
||||||
|
- Bump dependency @serve.zone/catalog to ^2.5.0.
|
||||||
|
|
||||||
|
## 2026-02-23 - 8.1.0 - feat(route-management)
|
||||||
|
add programmatic route management API with API tokens and admin UI
|
||||||
|
|
||||||
|
- Introduce RouteConfigManager to persist and manage programmatic routes and hardcoded-route overrides
|
||||||
|
- Add ApiTokenManager to create, validate, list, toggle and revoke API tokens (stored hashed)
|
||||||
|
- New OpsServer TypedRequest handlers: RouteManagementHandler (getMergedRoutes, create/update/delete/toggle routes, set/remove overrides) and ApiTokenHandler (create/list/revoke/toggle tokens)
|
||||||
|
- DcRouter integration: initialize routeConfigManager and apiTokenManager, expose getConstructorRoutes and re-apply programmatic routes after SmartProxy restarts
|
||||||
|
- Front-end additions: new 'Routes' and 'ApiTokens' views and UI components (ops-view-routes, ops-view-apitokens), router and appstate actions to fetch/manage routes and tokens
|
||||||
|
- New TS interfaces and request types for route-management and API tokens, plus storage schemas for persisted routes, overrides and tokens
|
||||||
|
- Bump dependency @serve.zone/catalog to ^2.3.0
|
||||||
|
|
||||||
|
## 2026-02-22 - 8.0.0 - BREAKING CHANGE(email-ops)
|
||||||
|
migrate email operations to catalog-compatible email model and simplify UI/router
|
||||||
|
|
||||||
|
- Add @serve.zone/catalog dependency and import (szCatalog) in web plugins
|
||||||
|
- Replace queue-based typedrequest methods with catalog APIs: getQueuedEmails / getSentEmails / getFailedEmails => getAllEmails and getEmailDetail (request/response shapes changed)
|
||||||
|
- Update TypeScript interfaces: IEmailQueueItem/IBounceRecord/ISecurityIncident etc. replaced by IEmail, IEmailDetail, ISmtpLogEntry, IConnectionInfo, IAuthenticationResults (breaking type changes)
|
||||||
|
- Frontend state and actions consolidated: emailOps state now holds emails array; multiple fetch actions removed and replaced by fetchAllEmailsAction and getEmailDetail usage
|
||||||
|
- UI components updated: ops-view-emails switched to list/detail view and now requests email detail via new API; router no longer exposes email folder routes and email-folder navigation removed
|
||||||
|
- Ops server handler refactored to return catalog-style emails and email detail; added status mapping and size formatting helpers
|
||||||
|
|
||||||
|
## 2026-02-21 - 7.4.3 - fix(logging)
|
||||||
|
add adaptive rate-limited DNS query logging, flush pending DNS logs on shutdown, and enhance email delivery logging
|
||||||
|
|
||||||
|
- Introduce adaptive DNS logging: allow up to 2 individual DNS query logs per second, then aggregate further queries and emit a batched summary (dnsLogWindow, dnsBatchCount, dnsBatchTimer) with a 5s flush.
|
||||||
|
- Flush pending DNS batch on stop() and log final DNS batch count during shutdown.
|
||||||
|
- Enhance email observability by logging deliveryStart, deliverySuccess, deliveryFailed and bounceProcessed events alongside existing MetricsManager tracking.
|
||||||
|
- Dependency bump: @design.estate/dees-catalog updated from ^3.43.1 to ^3.43.2.
|
||||||
|
- Non-breaking change; intended as a patch release.
|
||||||
|
|
||||||
|
## 2026-02-21 - 7.4.2 - fix(monitoring,remoteingress,web)
|
||||||
|
Prune old metrics buckets periodically, clear metrics caches on shutdown, simplify edge disconnect handling, and optimize network view data updates
|
||||||
|
|
||||||
|
- Call pruneOldBuckets() each minute to proactively remove stale time-series buckets in MetricsManager
|
||||||
|
- Clear metricsCache, emailMinuteBuckets and dnsMinuteBuckets when MetricsManager stops to avoid stale state on shutdown
|
||||||
|
- On edgeDisconnected remove the edgeStatuses entry instead of mutating an existing record (more explicit cleanup)
|
||||||
|
- Remove unused traffic-timer variables and move requestsPerSec history updates from render() into updateNetworkData() to avoid unnecessary re-renders
|
||||||
|
- Optimize traffic data array updates by shifting in-place then reassigning arrays to preserve Lit reactivity and reduce intermediate allocations
|
||||||
|
|
||||||
|
## 2026-02-21 - 7.4.1 - fix(dcrouter)
|
||||||
|
replace console logging with structured logger, improve metrics logging, add terminal-ready wait in ops UI, bump dees-catalog patch
|
||||||
|
|
||||||
|
- Replace console.log/console.error calls in classes.dcrouter.ts with structured logger.log (info/debug/error) including contextual data and stringified errors
|
||||||
|
- MetricsManager: create a dedicated Smartlog instance (metricsLogger) for SmartMetrics and use shared logger for lifecycle events (start/stop)
|
||||||
|
- SmartProxy/ACME: convert startup/stop/cert events and error logging to structured logs; include generated route and cert metadata where relevant
|
||||||
|
- Shutdown/startup flows: unify service start/stop/error messages through logger to provide consistent, structured output
|
||||||
|
- UI change: ops-view-logs now waits for xterm terminalReady before pushing initial logs to avoid race conditions
|
||||||
|
- Bump dependency @design.estate/dees-catalog from 3.43.0 to 3.43.1
|
||||||
|
|
||||||
## 2026-02-21 - 7.4.0 - feat(opsserver)
|
## 2026-02-21 - 7.4.0 - feat(opsserver)
|
||||||
add real-time log push to ops dashboard and recent DNS query tracking
|
add real-time log push to ops dashboard and recent DNS query tracking
|
||||||
|
|
||||||
|
|||||||
21
package.json
21
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/dcrouter",
|
"name": "@serve.zone/dcrouter",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "7.4.0",
|
"version": "9.2.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": {
|
||||||
@@ -20,24 +20,24 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^4.1.2",
|
"@git.zone/tsbuild": "^4.1.2",
|
||||||
"@git.zone/tsbundle": "^2.8.3",
|
"@git.zone/tsbundle": "^2.9.0",
|
||||||
"@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.2.0",
|
||||||
"@types/node": "^25.3.0"
|
"@types/node": "^25.3.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@api.global/typedrequest": "^3.2.6",
|
"@api.global/typedrequest": "^3.2.6",
|
||||||
"@api.global/typedrequest-interfaces": "^3.0.19",
|
"@api.global/typedrequest-interfaces": "^3.0.19",
|
||||||
"@api.global/typedserver": "^8.3.0",
|
"@api.global/typedserver": "^8.4.0",
|
||||||
"@api.global/typedsocket": "^4.1.0",
|
"@api.global/typedsocket": "^4.1.2",
|
||||||
"@apiclient.xyz/cloudflare": "^7.1.0",
|
"@apiclient.xyz/cloudflare": "^7.1.0",
|
||||||
"@design.estate/dees-catalog": "^3.43.0",
|
"@design.estate/dees-catalog": "^3.43.3",
|
||||||
"@design.estate/dees-element": "^2.1.6",
|
"@design.estate/dees-element": "^2.1.6",
|
||||||
"@push.rocks/projectinfo": "^5.0.2",
|
"@push.rocks/projectinfo": "^5.0.2",
|
||||||
"@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.1.0",
|
||||||
"@push.rocks/smartdns": "^7.9.0",
|
"@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",
|
||||||
@@ -45,18 +45,19 @@
|
|||||||
"@push.rocks/smartlog": "^3.2.1",
|
"@push.rocks/smartlog": "^3.2.1",
|
||||||
"@push.rocks/smartmetrics": "^3.0.1",
|
"@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.3.0",
|
||||||
"@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.9",
|
"@push.rocks/smartproxy": "^25.8.5",
|
||||||
"@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/catalog": "^2.5.0",
|
||||||
"@serve.zone/interfaces": "^5.3.0",
|
"@serve.zone/interfaces": "^5.3.0",
|
||||||
"@serve.zone/remoteingress": "^4.0.0",
|
"@serve.zone/remoteingress": "^4.2.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"
|
||||||
|
|||||||
288
pnpm-lock.yaml
generated
288
pnpm-lock.yaml
generated
@@ -15,17 +15,17 @@ importers:
|
|||||||
specifier: ^3.0.19
|
specifier: ^3.0.19
|
||||||
version: 3.0.19
|
version: 3.0.19
|
||||||
'@api.global/typedserver':
|
'@api.global/typedserver':
|
||||||
specifier: ^8.3.0
|
specifier: ^8.4.0
|
||||||
version: 8.3.0(@tiptap/pm@2.27.2)
|
version: 8.4.0(@tiptap/pm@2.27.2)
|
||||||
'@api.global/typedsocket':
|
'@api.global/typedsocket':
|
||||||
specifier: ^4.1.0
|
specifier: ^4.1.2
|
||||||
version: 4.1.0(@push.rocks/smartserve@2.0.1)
|
version: 4.1.2(@push.rocks/smartserve@2.0.1)
|
||||||
'@apiclient.xyz/cloudflare':
|
'@apiclient.xyz/cloudflare':
|
||||||
specifier: ^7.1.0
|
specifier: ^7.1.0
|
||||||
version: 7.1.0
|
version: 7.1.0
|
||||||
'@design.estate/dees-catalog':
|
'@design.estate/dees-catalog':
|
||||||
specifier: ^3.43.0
|
specifier: ^3.43.3
|
||||||
version: 3.43.0(@tiptap/pm@2.27.2)
|
version: 3.43.3(@tiptap/pm@2.27.2)
|
||||||
'@design.estate/dees-element':
|
'@design.estate/dees-element':
|
||||||
specifier: ^2.1.6
|
specifier: ^2.1.6
|
||||||
version: 2.1.6
|
version: 2.1.6
|
||||||
@@ -39,8 +39,8 @@ importers:
|
|||||||
specifier: ^9.1.3
|
specifier: ^9.1.3
|
||||||
version: 9.1.3(socks@2.8.7)
|
version: 9.1.3(socks@2.8.7)
|
||||||
'@push.rocks/smartdata':
|
'@push.rocks/smartdata':
|
||||||
specifier: ^7.0.15
|
specifier: ^7.1.0
|
||||||
version: 7.0.15(socks@2.8.7)
|
version: 7.1.0(socks@2.8.7)
|
||||||
'@push.rocks/smartdns':
|
'@push.rocks/smartdns':
|
||||||
specifier: ^7.9.0
|
specifier: ^7.9.0
|
||||||
version: 7.9.0
|
version: 7.9.0
|
||||||
@@ -63,8 +63,8 @@ importers:
|
|||||||
specifier: ^5.1.0
|
specifier: ^5.1.0
|
||||||
version: 5.1.0(socks@2.8.7)
|
version: 5.1.0(socks@2.8.7)
|
||||||
'@push.rocks/smartmta':
|
'@push.rocks/smartmta':
|
||||||
specifier: ^5.2.2
|
specifier: ^5.3.0
|
||||||
version: 5.2.2
|
version: 5.3.0
|
||||||
'@push.rocks/smartnetwork':
|
'@push.rocks/smartnetwork':
|
||||||
specifier: ^4.4.0
|
specifier: ^4.4.0
|
||||||
version: 4.4.0
|
version: 4.4.0
|
||||||
@@ -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.9
|
specifier: ^25.8.5
|
||||||
version: 25.7.9
|
version: 25.8.5
|
||||||
'@push.rocks/smartradius':
|
'@push.rocks/smartradius':
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
@@ -92,12 +92,15 @@ importers:
|
|||||||
'@push.rocks/smartunique':
|
'@push.rocks/smartunique':
|
||||||
specifier: ^3.0.9
|
specifier: ^3.0.9
|
||||||
version: 3.0.9
|
version: 3.0.9
|
||||||
|
'@serve.zone/catalog':
|
||||||
|
specifier: ^2.5.0
|
||||||
|
version: 2.5.0(@tiptap/pm@2.27.2)
|
||||||
'@serve.zone/interfaces':
|
'@serve.zone/interfaces':
|
||||||
specifier: ^5.3.0
|
specifier: ^5.3.0
|
||||||
version: 5.3.0
|
version: 5.3.0
|
||||||
'@serve.zone/remoteingress':
|
'@serve.zone/remoteingress':
|
||||||
specifier: ^4.0.0
|
specifier: ^4.2.0
|
||||||
version: 4.0.0
|
version: 4.2.0
|
||||||
'@tsclass/tsclass':
|
'@tsclass/tsclass':
|
||||||
specifier: ^9.3.0
|
specifier: ^9.3.0
|
||||||
version: 9.3.0
|
version: 9.3.0
|
||||||
@@ -112,8 +115,8 @@ importers:
|
|||||||
specifier: ^4.1.2
|
specifier: ^4.1.2
|
||||||
version: 4.1.2
|
version: 4.1.2
|
||||||
'@git.zone/tsbundle':
|
'@git.zone/tsbundle':
|
||||||
specifier: ^2.8.3
|
specifier: ^2.9.0
|
||||||
version: 2.8.3
|
version: 2.9.0
|
||||||
'@git.zone/tsrun':
|
'@git.zone/tsrun':
|
||||||
specifier: ^2.0.1
|
specifier: ^2.0.1
|
||||||
version: 2.0.1
|
version: 2.0.1
|
||||||
@@ -121,8 +124,8 @@ importers:
|
|||||||
specifier: ^3.1.8
|
specifier: ^3.1.8
|
||||||
version: 3.1.8(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)
|
version: 3.1.8(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)
|
||||||
'@git.zone/tswatch':
|
'@git.zone/tswatch':
|
||||||
specifier: ^3.1.0
|
specifier: ^3.2.0
|
||||||
version: 3.1.0(@tiptap/pm@2.27.2)
|
version: 3.2.0(@tiptap/pm@2.27.2)
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^25.3.0
|
specifier: ^25.3.0
|
||||||
version: 25.3.0
|
version: 25.3.0
|
||||||
@@ -141,8 +144,8 @@ packages:
|
|||||||
'@api.global/typedserver@3.0.80':
|
'@api.global/typedserver@3.0.80':
|
||||||
resolution: {integrity: sha512-dcp0oXsjBL+XdFg1wUUP08uJQid5bQ0Yv3V3Y3lnI2QCbat0FU+Tsb0TZRnZ4+P150Vj/ITBqJUgDzFsF34grA==}
|
resolution: {integrity: sha512-dcp0oXsjBL+XdFg1wUUP08uJQid5bQ0Yv3V3Y3lnI2QCbat0FU+Tsb0TZRnZ4+P150Vj/ITBqJUgDzFsF34grA==}
|
||||||
|
|
||||||
'@api.global/typedserver@8.3.0':
|
'@api.global/typedserver@8.4.0':
|
||||||
resolution: {integrity: sha512-Uh2sQkoQXbsKFb/fhSm7P9oCCEnawGY7R5/9VgCLQUuFV30G0FL0oBTKZNqFli0CNNDDs0nQHE+dpdf4VHhlXQ==}
|
resolution: {integrity: sha512-qOa5jUwiuHEoY1ZuLZiuU1unPl5JNd99Vv+hNAwgEIgAZd4TTy/mjdTp7lyviBzkGf2pROeCmAbDJJF8YQFCSA==}
|
||||||
|
|
||||||
'@api.global/typedsocket@3.1.1':
|
'@api.global/typedsocket@3.1.1':
|
||||||
resolution: {integrity: sha512-Wkz3NlhmfdZMKqXXI2c2dMtGGmSmhdOegZiziL+9b2mqPYdc7Gd8AZRdEOKvbSoIvc9G22/5BEadIWHrfq66TA==}
|
resolution: {integrity: sha512-Wkz3NlhmfdZMKqXXI2c2dMtGGmSmhdOegZiziL+9b2mqPYdc7Gd8AZRdEOKvbSoIvc9G22/5BEadIWHrfq66TA==}
|
||||||
@@ -152,8 +155,8 @@ packages:
|
|||||||
'@push.rocks/smartserve':
|
'@push.rocks/smartserve':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@api.global/typedsocket@4.1.0':
|
'@api.global/typedsocket@4.1.2':
|
||||||
resolution: {integrity: sha512-ttmoU5BNHmLAkAF/o+Ta8F5O4F7CUmkFo6LK7NKHQvuYJvodPMYWdhJ6yCINTF4pfCgljkMDUqoVKobm6ea4mQ==}
|
resolution: {integrity: sha512-fZFuJY9ucFCICjF4wi6OvK8drsv6UcwVVsfamOT1HxFj7OBOYw6QHOceQ+cAQ8IrWbX817sf8gzlesl+jlG8JA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@push.rocks/smartserve': '>=1.1.0'
|
'@push.rocks/smartserve': '>=1.1.0'
|
||||||
|
|
||||||
@@ -348,11 +351,14 @@ packages:
|
|||||||
'@cloudflare/workers-types@4.20260210.0':
|
'@cloudflare/workers-types@4.20260210.0':
|
||||||
resolution: {integrity: sha512-zHaF0RZVYUQwNCJCECnNAJdMur72Lk3FMiD6wU78Dx3Bv7DQRcuXNmPNuJmsGnosVZCcWintHlPTQ/4BEiDG5w==}
|
resolution: {integrity: sha512-zHaF0RZVYUQwNCJCECnNAJdMur72Lk3FMiD6wU78Dx3Bv7DQRcuXNmPNuJmsGnosVZCcWintHlPTQ/4BEiDG5w==}
|
||||||
|
|
||||||
|
'@cloudflare/workers-types@4.20260303.0':
|
||||||
|
resolution: {integrity: sha512-soUlr4NJVkh5dR09RwtziTMbBQ+lbdoEesTGw8WUlvmnQ2M4h7CmJzAjC6a7IivUodiiCSjbLcGV/8PyZpvZkA==}
|
||||||
|
|
||||||
'@configvault.io/interfaces@1.0.17':
|
'@configvault.io/interfaces@1.0.17':
|
||||||
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
|
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
|
||||||
|
|
||||||
'@design.estate/dees-catalog@3.43.0':
|
'@design.estate/dees-catalog@3.43.3':
|
||||||
resolution: {integrity: sha512-UFW8oThP9Mc4L0wVVgmuGux868Ct/TwZ1WP8hZCe4e/+5gmxDc+4EArnt5hePHENboe1Soobh9mmrMN6kQZ3xQ==}
|
resolution: {integrity: sha512-GjTePdwqNBL4isMOx4Ibei6pgK55H+DccbtgyNqjHRBz3LL14mo809ebjY2IZOVobswyzuTcNFvhfiqFP4/HLg==}
|
||||||
|
|
||||||
'@design.estate/dees-comms@1.0.30':
|
'@design.estate/dees-comms@1.0.30':
|
||||||
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
|
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
|
||||||
@@ -555,8 +561,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-S518ulKveO76pS6jrAELrnFaCw5nDAIZD9j6QzVmLYDiZuJmlRwPK3/2E8ugQ+b7ffpkwJ9MT685ooEGDcWQ4Q==}
|
resolution: {integrity: sha512-S518ulKveO76pS6jrAELrnFaCw5nDAIZD9j6QzVmLYDiZuJmlRwPK3/2E8ugQ+b7ffpkwJ9MT685ooEGDcWQ4Q==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@git.zone/tsbundle@2.8.3':
|
'@git.zone/tsbundle@2.9.0':
|
||||||
resolution: {integrity: sha512-9q+KbVGKUTDNND+jDiJuk4bPH/mtiA2B0EWtV+/NyvgZfIbpe/ItHemyIvXB4RAqncMdBhzXquCFCvGjAhwVIQ==}
|
resolution: {integrity: sha512-itXX/oiJjrRHUlIGTHUEqSwPuGwsG4Cq8kh7aqFOm8mYzJwtXYE1gBqLJTWZma6gI5n+xAk5qTxTyfikuPgWQA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@git.zone/tspublish@1.11.0':
|
'@git.zone/tspublish@1.11.0':
|
||||||
@@ -571,8 +577,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-nmiLGeOkKMkLDyIk5BUBLx5ExskFbKHKlPdrWCARPVFkU4cAAiuIyJWVfLwISoS0TO/zSInLqArPwIc76yvaNw==}
|
resolution: {integrity: sha512-nmiLGeOkKMkLDyIk5BUBLx5ExskFbKHKlPdrWCARPVFkU4cAAiuIyJWVfLwISoS0TO/zSInLqArPwIc76yvaNw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@git.zone/tswatch@3.1.0':
|
'@git.zone/tswatch@3.2.0':
|
||||||
resolution: {integrity: sha512-R2ZI+j1OKVgd0zTbtGtJjyt7r2kF0Z4nl8neolHuQL+jpr16i2NHVfVK92uIeeZDnJSqo5vf7Syt0XeQ4rz2HA==}
|
resolution: {integrity: sha512-AlV2HPGPy1s91LwQ8x3a0UwHqe5/P2SRSXfHco5yo+D59KjkV7FaHLWwU+Dk03VNgmdkdUusPiB6/Cfkn1tPzw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@happy-dom/global-registrator@15.11.7':
|
'@happy-dom/global-registrator@15.11.7':
|
||||||
@@ -681,74 +687,74 @@ packages:
|
|||||||
'@mongodb-js/saslprep@1.4.6':
|
'@mongodb-js/saslprep@1.4.6':
|
||||||
resolution: {integrity: sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==}
|
resolution: {integrity: sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==}
|
||||||
|
|
||||||
'@napi-rs/canvas-android-arm64@0.1.93':
|
'@napi-rs/canvas-android-arm64@0.1.95':
|
||||||
resolution: {integrity: sha512-xRIoOPFvneR29Dtq5d9p2AJbijDCFeV4jQ+5Ms/xVAXJVb8R0Jlu+pPr/SkhrG+Mouaml4roPSXugTIeRl6CMA==}
|
resolution: {integrity: sha512-SqTh0wsYbetckMXEvHqmR7HKRJujVf1sYv1xdlhkifg6TlCSysz1opa49LlS3+xWuazcQcfRfmhA07HxxxGsAA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [android]
|
os: [android]
|
||||||
|
|
||||||
'@napi-rs/canvas-darwin-arm64@0.1.93':
|
'@napi-rs/canvas-darwin-arm64@0.1.95':
|
||||||
resolution: {integrity: sha512-daNDi76HN5grC6GXDmpxdfP+N2mQPd3sCfg62VyHwUuvbZh32P7R/IUjkzAxtYMtTza+Zvx9hfLJ3J7ENL6WMA==}
|
resolution: {integrity: sha512-F7jT0Syu+B9DGBUBcMk3qCRIxAWiDXmvEjamwbYfbZl7asI1pmXZUnCOoIu49Wt0RNooToYfRDxU9omD6t5Xuw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@napi-rs/canvas-darwin-x64@0.1.93':
|
'@napi-rs/canvas-darwin-x64@0.1.95':
|
||||||
resolution: {integrity: sha512-1YfuNPIQLawsg/gSNdJRk4kQWUy9M/Gy8FGsOI79nhQEJ2PZdqpSPl5UNzf4elfuNXuVbEbmmjP68EQdUunDuQ==}
|
resolution: {integrity: sha512-54eb2Ho15RDjYGXO/harjRznBrAvu+j5nQ85Z4Qd6Qg3slR8/Ja+Yvvy9G4yo7rdX6NR9GPkZeSTf2UcKXwaXw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.93':
|
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.95':
|
||||||
resolution: {integrity: sha512-8kEkOQPZjuyHjupvXExuJZiuiVNecdABGq3DLI7aO1EvQFOOlWMm2d/8Q5qXdV73Tn+nu3m16+kPajsN1oJefQ==}
|
resolution: {integrity: sha512-hYaLCSLx5bmbnclzQc3ado3PgZ66blJWzjXp0wJmdwpr/kH+Mwhj6vuytJIomgksyJoCdIqIa4N6aiqBGJtJ5Q==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.93':
|
'@napi-rs/canvas-linux-arm64-gnu@0.1.95':
|
||||||
resolution: {integrity: sha512-qIKLKkBkYSyWSYAoDThoxf5y1gr4X0g7W8rDU7d2HDeAAcotdVHUwuKkMeNe6+5VNk7/95EIhbslQjSxiCu32g==}
|
resolution: {integrity: sha512-J7VipONahKsmScPZsipHVQBqpbZx4favaD8/enWzzlGcjiwycOoymL7f4tNeqdjK0su19bDOUt6mjp9gsPWYlw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm64-musl@0.1.93':
|
'@napi-rs/canvas-linux-arm64-musl@0.1.95':
|
||||||
resolution: {integrity: sha512-mAwQBGM3qArS9XEO21AK4E1uGvCuUCXjhIZk0dlVvs49MQ6wAAuCkYKNFpSKeSicKrLWwBMfgWX4qZoPh+M00A==}
|
resolution: {integrity: sha512-PXy0UT1J/8MPG8UAkWp6Fd51ZtIZINFzIjGH909JjQrtCuJf3X6nanHYdz1A+Wq9o4aoPAw1YEUpFS1lelsVlg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.93':
|
'@napi-rs/canvas-linux-riscv64-gnu@0.1.95':
|
||||||
resolution: {integrity: sha512-kaIH5MpPzOZfkM+QMsBxGdM9jlJT+N+fwz2IEaju/S+DL65E5TgPOx4QcD5dQ8vsMxlak6uDrudBc4ns5xzZCw==}
|
resolution: {integrity: sha512-2IzCkW2RHRdcgF9W5/plHvYFpc6uikyjMb5SxjqmNxfyDFz9/HB89yhi8YQo0SNqrGRI7yBVDec7Pt+uMyRWsg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-x64-gnu@0.1.93':
|
'@napi-rs/canvas-linux-x64-gnu@0.1.95':
|
||||||
resolution: {integrity: sha512-KtMZJqYWvOSeW5w3VSV2f5iGnwNdKJm4gwgVid4xNy1NFi+NJSyuglA1lX1u4wIPxizyxh8OW5c5Usf6oSOMNQ==}
|
resolution: {integrity: sha512-OV/ol/OtcUr4qDhQg8G7SdViZX8XyQeKpPsVv/j3+7U178FGoU4M+yIocdVo1ih/A8GQ63+LjF4jDoEjaVU8Pw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-x64-musl@0.1.93':
|
'@napi-rs/canvas-linux-x64-musl@0.1.95':
|
||||||
resolution: {integrity: sha512-qRZhOvlDBooRLX6V3/t9X9B+plZK+OrPLgfFixu0A1RO/3VHbubOknfnMnocSDAqk/L6cRyKI83VP2ciR9UO7w==}
|
resolution: {integrity: sha512-Z5KzqBK/XzPz5+SFHKz7yKqClEQ8pOiEDdgk5SlphBLVNb8JFIJkxhtJKSvnJyHh2rjVgiFmvtJzMF0gNwwKyQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-win32-arm64-msvc@0.1.93':
|
'@napi-rs/canvas-win32-arm64-msvc@0.1.95':
|
||||||
resolution: {integrity: sha512-um5XE44vF8bjkQEsH2iRSUP9fDeQGYbn/qjM/v4whXG83qsqapAXlOPOQqSARZB1SiNvPUAuXoRsJLlKFmAEFw==}
|
resolution: {integrity: sha512-aj0YbRpe8qVJ4OzMsK7NfNQePgcf9zkGFzNZ9mSuaxXzhpLHmlF2GivNdCdNOg8WzA/NxV6IU4c5XkXadUMLeA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@napi-rs/canvas-win32-x64-msvc@0.1.93':
|
'@napi-rs/canvas-win32-x64-msvc@0.1.95':
|
||||||
resolution: {integrity: sha512-maHlizZgmKsAPJwjwBZMnsWfq3Ca9QutoteQwKe7YqsmbECoylrLCCOGCDOredstW4BRWqRTfCl6NJaVVeAQvQ==}
|
resolution: {integrity: sha512-GA8leTTCfdjuHi8reICTIxU0081PhXvl3lzIniLUjeLACx9GubUiyzkwFb+oyeKLS5IAGZFLKnzAf4wm2epRlA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@napi-rs/canvas@0.1.93':
|
'@napi-rs/canvas@0.1.95':
|
||||||
resolution: {integrity: sha512-unVFo8CUlUeJCCxt50+j4yy91NF4x6n9zdGcvEsOFAWzowtZm3mgx8X2D7xjwV0cFSfxmpGPoe+JS77uzeFsxg==}
|
resolution: {integrity: sha512-lkg23ge+rgyhgUwXmlbkPEhuhHq/hUi/gXKH+4I7vO+lJrbNfEYcQdJLIGjKyXLQzgFiiyDAwh5vAe/tITAE+w==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@1.0.7':
|
'@napi-rs/wasm-runtime@1.0.7':
|
||||||
@@ -888,8 +894,8 @@ packages:
|
|||||||
'@push.rocks/smartdata@5.16.7':
|
'@push.rocks/smartdata@5.16.7':
|
||||||
resolution: {integrity: sha512-bu/YSIjQcwxWXkAsuhqE6zs7eT+bTIKV8+/H7TbbjpzeioLCyB3dZ/41cLZk37c/EYt4d4GHgZ0ww80OiKOUMg==}
|
resolution: {integrity: sha512-bu/YSIjQcwxWXkAsuhqE6zs7eT+bTIKV8+/H7TbbjpzeioLCyB3dZ/41cLZk37c/EYt4d4GHgZ0ww80OiKOUMg==}
|
||||||
|
|
||||||
'@push.rocks/smartdata@7.0.15':
|
'@push.rocks/smartdata@7.1.0':
|
||||||
resolution: {integrity: sha512-j09BUekmjiGZuvXmdGBiIpBTXFFnxrzG4rOBjZvPO/hG1BwNrvSkIVq20mIwdYomn8JGgya6oJ4Y7NL+FKTqEA==}
|
resolution: {integrity: sha512-ots0g7/96R2xs4ww4F2/2rIwAOPT5AmzP3ciD31YsF02o5WA4Gg6C5laLBUjV3hXCjazhzFsRVQTfwbjmPQe4w==}
|
||||||
|
|
||||||
'@push.rocks/smartdelay@3.0.5':
|
'@push.rocks/smartdelay@3.0.5':
|
||||||
resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==}
|
resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==}
|
||||||
@@ -990,12 +996,11 @@ packages:
|
|||||||
'@push.rocks/smartmongo@5.1.0':
|
'@push.rocks/smartmongo@5.1.0':
|
||||||
resolution: {integrity: sha512-2tpKf8K+SMdLHOEpafgKPIN+ypWTLwHc33hCUDNMQ1KaL7vokkavA44+fHxQydOGPMtDi22tSMFeVMCcUSzs4w==}
|
resolution: {integrity: sha512-2tpKf8K+SMdLHOEpafgKPIN+ypWTLwHc33hCUDNMQ1KaL7vokkavA44+fHxQydOGPMtDi22tSMFeVMCcUSzs4w==}
|
||||||
|
|
||||||
'@push.rocks/smartmta@5.2.2':
|
'@push.rocks/smartmta@5.3.0':
|
||||||
resolution: {integrity: sha512-0xKUi2BMM0HFYIPdNeNJZFitAiJ9CNbLlOJ8TenT+xInp7DKcSQ7ABER1rJKinPtvDjRDSiSqiF2iQR+O7299g==}
|
resolution: {integrity: sha512-uJI25fslzvrcenU36WCdt5gB8cCfkjUlY7PqlxEtFp474/l/kZxNnvirv1gnZLRNNa+ioe5aH18HKE+KcAjuxA==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
cpu: [x64, arm64]
|
cpu: [x64, arm64]
|
||||||
os: [darwin, linux, win32]
|
os: [darwin, linux, win32]
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
'@push.rocks/smartmustache@3.0.2':
|
'@push.rocks/smartmustache@3.0.2':
|
||||||
resolution: {integrity: sha512-G3LyRXoJhyM+iQhkvP/MR/2WYMvC9U7zc2J44JxUM5tPdkQ+o3++FbfRtnZj6rz5X/A7q03//vsxPitVQwoi2Q==}
|
resolution: {integrity: sha512-G3LyRXoJhyM+iQhkvP/MR/2WYMvC9U7zc2J44JxUM5tPdkQ+o3++FbfRtnZj6rz5X/A7q03//vsxPitVQwoi2Q==}
|
||||||
@@ -1030,8 +1035,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.9':
|
'@push.rocks/smartproxy@25.8.5':
|
||||||
resolution: {integrity: sha512-5esFvD72TEyveaEQbDYRgD7C5hDfWMSBvurNx3KPi02CBKG1gnhx/WWT7RHDS3KRF5fEQh9YxvI9aMkOwjc7sQ==}
|
resolution: {integrity: sha512-oLmV+Bq7sSgQP9McTao/imb6Xb62QM7wlTFt5kNynrS5WK2wAe8cEjDKOcyu8N/WmzNCEClT5f/0xAtI6JxtkA==}
|
||||||
|
|
||||||
'@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==}
|
||||||
@@ -1054,6 +1059,9 @@ packages:
|
|||||||
'@push.rocks/smartrust@1.2.1':
|
'@push.rocks/smartrust@1.2.1':
|
||||||
resolution: {integrity: sha512-ANwXXibUwoHNWF1hhXhXVVrfzYlhgHYRa2205Jkd/s/wXzcWHftYZthilJj+52B7nkzSB76umfxKfK5eBYY2Ug==}
|
resolution: {integrity: sha512-ANwXXibUwoHNWF1hhXhXVVrfzYlhgHYRa2205Jkd/s/wXzcWHftYZthilJj+52B7nkzSB76umfxKfK5eBYY2Ug==}
|
||||||
|
|
||||||
|
'@push.rocks/smartrust@1.3.1':
|
||||||
|
resolution: {integrity: sha512-3ApbgF6yGeE2TRQxBY9Y48H1JlpcRheIp7QDBLSSfk80Uoe6fjdgBAfNz3Ir8hW3RZ3b7hA3sm1ZshCok58SEA==}
|
||||||
|
|
||||||
'@push.rocks/smartrx@3.0.10':
|
'@push.rocks/smartrx@3.0.10':
|
||||||
resolution: {integrity: sha512-USjIYcsSfzn14cwOsxgq/bBmWDTTzy3ouWAnW5NdMyRRzEbmeNrvmy6TRqNeDlJ2PsYNTt1rr/zGUqvIy72ITg==}
|
resolution: {integrity: sha512-USjIYcsSfzn14cwOsxgq/bBmWDTTzy3ouWAnW5NdMyRRzEbmeNrvmy6TRqNeDlJ2PsYNTt1rr/zGUqvIy72ITg==}
|
||||||
|
|
||||||
@@ -1118,8 +1126,8 @@ packages:
|
|||||||
'@push.rocks/taskbuffer@3.5.0':
|
'@push.rocks/taskbuffer@3.5.0':
|
||||||
resolution: {integrity: sha512-Y9WwIEIyp6oVFdj06j84tfrZIvjhbMb3DF52rYxlTeYLk3W7RPhSg1bGPCbtkXWeKdBrSe37V90BkOG7Qq8Pqg==}
|
resolution: {integrity: sha512-Y9WwIEIyp6oVFdj06j84tfrZIvjhbMb3DF52rYxlTeYLk3W7RPhSg1bGPCbtkXWeKdBrSe37V90BkOG7Qq8Pqg==}
|
||||||
|
|
||||||
'@push.rocks/taskbuffer@4.2.0':
|
'@push.rocks/taskbuffer@4.2.1':
|
||||||
resolution: {integrity: sha512-ttoBe5y/WXkAo5/wSMcC/Y4Zbyw4XG8kwAsEaqnAPCxa3M9MI1oV/yM1e9gU1IH97HVPidzbTxRU5/PcHDdUsg==}
|
resolution: {integrity: sha512-F3aizWLGWdAz7wSJqOzjwVgo1VQJcxTbHUjDN/Pqxw0WMQUwODRGbhgy4zLag7bOyE4tc8Jv7yid7Bjmn5hKdg==}
|
||||||
|
|
||||||
'@push.rocks/taskbuffer@6.1.2':
|
'@push.rocks/taskbuffer@6.1.2':
|
||||||
resolution: {integrity: sha512-sdqKd8N/GidztQ1k3r8A86rLvD8Afyir5FjYCNJXDD9837JLoqzHaOKGltUSBsCGh2gjsZn6GydsY6HhXQgvZQ==}
|
resolution: {integrity: sha512-sdqKd8N/GidztQ1k3r8A86rLvD8Afyir5FjYCNJXDD9837JLoqzHaOKGltUSBsCGh2gjsZn6GydsY6HhXQgvZQ==}
|
||||||
@@ -1330,11 +1338,14 @@ packages:
|
|||||||
'@selderee/plugin-htmlparser2@0.11.0':
|
'@selderee/plugin-htmlparser2@0.11.0':
|
||||||
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
||||||
|
|
||||||
|
'@serve.zone/catalog@2.5.0':
|
||||||
|
resolution: {integrity: sha512-bRwk7pbDxUB471wUAS7p22MTOOBCHlMWijsE43K9tDAPcxlRarhtf2Dgx0Y25s/dFXqj2JHwe6jjE84S80jFzg==}
|
||||||
|
|
||||||
'@serve.zone/interfaces@5.3.0':
|
'@serve.zone/interfaces@5.3.0':
|
||||||
resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==}
|
resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==}
|
||||||
|
|
||||||
'@serve.zone/remoteingress@4.0.0':
|
'@serve.zone/remoteingress@4.2.0':
|
||||||
resolution: {integrity: sha512-kcC9pnQdS5Mw0ypmwT3GIYwrjVQJOjWw5TJ+j46t6Y98S4Mb7wEAAPsU+YRXozpkqUxWUDBeDK1fQHLvoF9PmQ==}
|
resolution: {integrity: sha512-VJsWAaTuNGtschbvnDP3PciZ5CGE8KLwrTXC8GN3tcDa2wGj7mJQ+JFlTaQRVoRLtCEMkxMYnLIrhJwal9yEYg==}
|
||||||
|
|
||||||
'@sindresorhus/is@5.6.0':
|
'@sindresorhus/is@5.6.0':
|
||||||
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
|
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
|
||||||
@@ -2000,6 +2011,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==}
|
resolution: {integrity: sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
|
balanced-match@4.0.4:
|
||||||
|
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
|
||||||
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
bare-events@2.8.2:
|
bare-events@2.8.2:
|
||||||
resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==}
|
resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2066,6 +2081,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==}
|
resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
|
brace-expansion@5.0.3:
|
||||||
|
resolution: {integrity: sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==}
|
||||||
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
broadcast-channel@7.3.0:
|
broadcast-channel@7.3.0:
|
||||||
resolution: {integrity: sha512-UHPhLBQKfQ8OmMFMpmPfO5dRakyA1vsfiDGWTYNvChYol65tbuhivPEGgZZiuetorvExdvxaWiBy/ym1Ty08yA==}
|
resolution: {integrity: sha512-UHPhLBQKfQ8OmMFMpmPfO5dRakyA1vsfiDGWTYNvChYol65tbuhivPEGgZZiuetorvExdvxaWiBy/ym1Ty08yA==}
|
||||||
|
|
||||||
@@ -3225,8 +3244,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==}
|
resolution: {integrity: sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
minimatch@3.1.2:
|
minimatch@10.2.2:
|
||||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
resolution: {integrity: sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==}
|
||||||
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
|
minimatch@3.1.3:
|
||||||
|
resolution: {integrity: sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==}
|
||||||
|
|
||||||
minimatch@9.0.5:
|
minimatch@9.0.5:
|
||||||
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||||
@@ -4159,7 +4182,7 @@ packages:
|
|||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
wordwrap@1.0.0:
|
wordwrap@1.0.0:
|
||||||
resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
|
resolution: {integrity: sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=}
|
||||||
|
|
||||||
wrap-ansi@6.2.0:
|
wrap-ansi@6.2.0:
|
||||||
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
||||||
@@ -4218,13 +4241,11 @@ packages:
|
|||||||
|
|
||||||
xterm-addon-fit@0.8.0:
|
xterm-addon-fit@0.8.0:
|
||||||
resolution: {integrity: sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==}
|
resolution: {integrity: sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==}
|
||||||
deprecated: This package is now deprecated. Move to @xterm/addon-fit instead.
|
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
xterm: ^5.0.0
|
xterm: ^5.0.0
|
||||||
|
|
||||||
xterm@5.3.0:
|
xterm@5.3.0:
|
||||||
resolution: {integrity: sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==}
|
resolution: {integrity: sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==}
|
||||||
deprecated: This package is now deprecated. Move to @xterm/xterm instead.
|
|
||||||
|
|
||||||
y18n@5.0.8:
|
y18n@5.0.8:
|
||||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||||
@@ -4330,13 +4351,13 @@ snapshots:
|
|||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@api.global/typedserver@8.3.0(@tiptap/pm@2.27.2)':
|
'@api.global/typedserver@8.4.0(@tiptap/pm@2.27.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedrequest': 3.2.6
|
'@api.global/typedrequest': 3.2.6
|
||||||
'@api.global/typedrequest-interfaces': 3.0.19
|
'@api.global/typedrequest-interfaces': 3.0.19
|
||||||
'@api.global/typedsocket': 4.1.0(@push.rocks/smartserve@2.0.1)
|
'@api.global/typedsocket': 4.1.2(@push.rocks/smartserve@2.0.1)
|
||||||
'@cloudflare/workers-types': 4.20260210.0
|
'@cloudflare/workers-types': 4.20260303.0
|
||||||
'@design.estate/dees-catalog': 3.43.0(@tiptap/pm@2.27.2)
|
'@design.estate/dees-catalog': 3.43.3(@tiptap/pm@2.27.2)
|
||||||
'@design.estate/dees-comms': 1.0.30
|
'@design.estate/dees-comms': 1.0.30
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
@@ -4360,7 +4381,7 @@ snapshots:
|
|||||||
'@push.rocks/smartserve': 2.0.1
|
'@push.rocks/smartserve': 2.0.1
|
||||||
'@push.rocks/smartsitemap': 2.0.4
|
'@push.rocks/smartsitemap': 2.0.4
|
||||||
'@push.rocks/smartstream': 3.2.5
|
'@push.rocks/smartstream': 3.2.5
|
||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.2.3
|
||||||
'@push.rocks/smartwatch': 6.3.0
|
'@push.rocks/smartwatch': 6.3.0
|
||||||
'@push.rocks/taskbuffer': 3.5.0
|
'@push.rocks/taskbuffer': 3.5.0
|
||||||
'@push.rocks/webrequest': 4.0.2
|
'@push.rocks/webrequest': 4.0.2
|
||||||
@@ -4396,7 +4417,7 @@ snapshots:
|
|||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@api.global/typedsocket@4.1.0(@push.rocks/smartserve@2.0.1)':
|
'@api.global/typedsocket@4.1.2(@push.rocks/smartserve@2.0.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedrequest': 3.2.6
|
'@api.global/typedrequest': 3.2.6
|
||||||
'@api.global/typedrequest-interfaces': 3.0.19
|
'@api.global/typedrequest-interfaces': 3.0.19
|
||||||
@@ -4930,11 +4951,13 @@ snapshots:
|
|||||||
|
|
||||||
'@cloudflare/workers-types@4.20260210.0': {}
|
'@cloudflare/workers-types@4.20260210.0': {}
|
||||||
|
|
||||||
|
'@cloudflare/workers-types@4.20260303.0': {}
|
||||||
|
|
||||||
'@configvault.io/interfaces@1.0.17':
|
'@configvault.io/interfaces@1.0.17':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedrequest-interfaces': 3.0.19
|
'@api.global/typedrequest-interfaces': 3.0.19
|
||||||
|
|
||||||
'@design.estate/dees-catalog@3.43.0(@tiptap/pm@2.27.2)':
|
'@design.estate/dees-catalog@3.43.3(@tiptap/pm@2.27.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@design.estate/dees-domtools': 2.3.8
|
'@design.estate/dees-domtools': 2.3.8
|
||||||
'@design.estate/dees-element': 2.1.6
|
'@design.estate/dees-element': 2.1.6
|
||||||
@@ -5159,7 +5182,7 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@git.zone/tsbundle@2.8.3':
|
'@git.zone/tsbundle@2.9.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/early': 4.0.4
|
'@push.rocks/early': 4.0.4
|
||||||
'@push.rocks/npmextra': 5.3.3
|
'@push.rocks/npmextra': 5.3.3
|
||||||
@@ -5216,7 +5239,7 @@ snapshots:
|
|||||||
'@git.zone/tstest@3.1.8(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)':
|
'@git.zone/tstest@3.1.8(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
|
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
|
||||||
'@git.zone/tsbundle': 2.8.3
|
'@git.zone/tsbundle': 2.9.0
|
||||||
'@git.zone/tsrun': 2.0.1
|
'@git.zone/tsrun': 2.0.1
|
||||||
'@push.rocks/consolecolor': 2.0.3
|
'@push.rocks/consolecolor': 2.0.3
|
||||||
'@push.rocks/qenv': 6.1.3
|
'@push.rocks/qenv': 6.1.3
|
||||||
@@ -5262,10 +5285,10 @@ snapshots:
|
|||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@git.zone/tswatch@3.1.0(@tiptap/pm@2.27.2)':
|
'@git.zone/tswatch@3.2.0(@tiptap/pm@2.27.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedserver': 8.3.0(@tiptap/pm@2.27.2)
|
'@api.global/typedserver': 8.4.0(@tiptap/pm@2.27.2)
|
||||||
'@git.zone/tsbundle': 2.8.3
|
'@git.zone/tsbundle': 2.9.0
|
||||||
'@git.zone/tsrun': 2.0.1
|
'@git.zone/tsrun': 2.0.1
|
||||||
'@push.rocks/early': 4.0.4
|
'@push.rocks/early': 4.0.4
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
@@ -5278,7 +5301,7 @@ snapshots:
|
|||||||
'@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
|
||||||
'@push.rocks/taskbuffer': 4.2.0
|
'@push.rocks/taskbuffer': 4.2.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@nuxt/kit'
|
- '@nuxt/kit'
|
||||||
- '@swc/helpers'
|
- '@swc/helpers'
|
||||||
@@ -5443,52 +5466,52 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
sparse-bitfield: 3.0.3
|
sparse-bitfield: 3.0.3
|
||||||
|
|
||||||
'@napi-rs/canvas-android-arm64@0.1.93':
|
'@napi-rs/canvas-android-arm64@0.1.95':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-darwin-arm64@0.1.93':
|
'@napi-rs/canvas-darwin-arm64@0.1.95':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-darwin-x64@0.1.93':
|
'@napi-rs/canvas-darwin-x64@0.1.95':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.93':
|
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.95':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.93':
|
'@napi-rs/canvas-linux-arm64-gnu@0.1.95':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm64-musl@0.1.93':
|
'@napi-rs/canvas-linux-arm64-musl@0.1.95':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.93':
|
'@napi-rs/canvas-linux-riscv64-gnu@0.1.95':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-x64-gnu@0.1.93':
|
'@napi-rs/canvas-linux-x64-gnu@0.1.95':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-x64-musl@0.1.93':
|
'@napi-rs/canvas-linux-x64-musl@0.1.95':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-win32-arm64-msvc@0.1.93':
|
'@napi-rs/canvas-win32-arm64-msvc@0.1.95':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-win32-x64-msvc@0.1.93':
|
'@napi-rs/canvas-win32-x64-msvc@0.1.95':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas@0.1.93':
|
'@napi-rs/canvas@0.1.95':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@napi-rs/canvas-android-arm64': 0.1.93
|
'@napi-rs/canvas-android-arm64': 0.1.95
|
||||||
'@napi-rs/canvas-darwin-arm64': 0.1.93
|
'@napi-rs/canvas-darwin-arm64': 0.1.95
|
||||||
'@napi-rs/canvas-darwin-x64': 0.1.93
|
'@napi-rs/canvas-darwin-x64': 0.1.95
|
||||||
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.93
|
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.95
|
||||||
'@napi-rs/canvas-linux-arm64-gnu': 0.1.93
|
'@napi-rs/canvas-linux-arm64-gnu': 0.1.95
|
||||||
'@napi-rs/canvas-linux-arm64-musl': 0.1.93
|
'@napi-rs/canvas-linux-arm64-musl': 0.1.95
|
||||||
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.93
|
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.95
|
||||||
'@napi-rs/canvas-linux-x64-gnu': 0.1.93
|
'@napi-rs/canvas-linux-x64-gnu': 0.1.95
|
||||||
'@napi-rs/canvas-linux-x64-musl': 0.1.93
|
'@napi-rs/canvas-linux-x64-musl': 0.1.95
|
||||||
'@napi-rs/canvas-win32-arm64-msvc': 0.1.93
|
'@napi-rs/canvas-win32-arm64-msvc': 0.1.95
|
||||||
'@napi-rs/canvas-win32-x64-msvc': 0.1.93
|
'@napi-rs/canvas-win32-x64-msvc': 0.1.95
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@1.0.7':
|
'@napi-rs/wasm-runtime@1.0.7':
|
||||||
@@ -5739,7 +5762,7 @@ snapshots:
|
|||||||
'@apiclient.xyz/cloudflare': 7.1.0
|
'@apiclient.xyz/cloudflare': 7.1.0
|
||||||
'@peculiar/x509': 1.14.3
|
'@peculiar/x509': 1.14.3
|
||||||
'@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.1.0(socks@2.8.7)
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartdns': 7.9.0
|
'@push.rocks/smartdns': 7.9.0
|
||||||
'@push.rocks/smartlog': 3.2.1
|
'@push.rocks/smartlog': 3.2.1
|
||||||
@@ -5902,7 +5925,7 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@push.rocks/smartdata@7.0.15(socks@2.8.7)':
|
'@push.rocks/smartdata@7.1.0(socks@2.8.7)':
|
||||||
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
|
||||||
@@ -5911,7 +5934,7 @@ snapshots:
|
|||||||
'@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/smartstring': 4.1.0
|
'@push.rocks/smartstring': 4.1.0
|
||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.2.3
|
||||||
'@push.rocks/smartunique': 3.0.9
|
'@push.rocks/smartunique': 3.0.9
|
||||||
'@push.rocks/taskbuffer': 3.5.0
|
'@push.rocks/taskbuffer': 3.5.0
|
||||||
'@tsclass/tsclass': 9.3.0
|
'@tsclass/tsclass': 9.3.0
|
||||||
@@ -6210,14 +6233,14 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@push.rocks/smartmta@5.2.2':
|
'@push.rocks/smartmta@5.3.0':
|
||||||
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.2.1
|
'@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.3.1
|
||||||
'@tsclass/tsclass': 9.3.0
|
'@tsclass/tsclass': 9.3.0
|
||||||
lru-cache: 11.2.6
|
lru-cache: 11.2.6
|
||||||
mailparser: 3.9.3
|
mailparser: 3.9.3
|
||||||
@@ -6317,13 +6340,13 @@ snapshots:
|
|||||||
|
|
||||||
'@push.rocks/smartpromise@4.2.3': {}
|
'@push.rocks/smartpromise@4.2.3': {}
|
||||||
|
|
||||||
'@push.rocks/smartproxy@25.7.9':
|
'@push.rocks/smartproxy@25.8.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartcrypto': 2.0.4
|
'@push.rocks/smartcrypto': 2.0.4
|
||||||
'@push.rocks/smartlog': 3.2.1
|
'@push.rocks/smartlog': 3.2.1
|
||||||
'@push.rocks/smartrust': 1.2.1
|
'@push.rocks/smartrust': 1.3.1
|
||||||
'@tsclass/tsclass': 9.3.0
|
'@tsclass/tsclass': 9.3.0
|
||||||
minimatch: 10.2.1
|
minimatch: 10.2.2
|
||||||
|
|
||||||
'@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)':
|
'@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6380,6 +6403,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
|
|
||||||
|
'@push.rocks/smartrust@1.3.1':
|
||||||
|
dependencies:
|
||||||
|
'@push.rocks/smartpath': 6.0.0
|
||||||
|
|
||||||
'@push.rocks/smartrx@3.0.10':
|
'@push.rocks/smartrx@3.0.10':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
@@ -6562,7 +6589,7 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@push.rocks/taskbuffer@4.2.0':
|
'@push.rocks/taskbuffer@4.2.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@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
|
||||||
@@ -6570,7 +6597,7 @@ snapshots:
|
|||||||
'@push.rocks/smartlog': 3.2.1
|
'@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.2.3
|
||||||
'@push.rocks/smartunique': 3.0.9
|
'@push.rocks/smartunique': 3.0.9
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@nuxt/kit'
|
- '@nuxt/kit'
|
||||||
@@ -6781,16 +6808,29 @@ snapshots:
|
|||||||
domhandler: 5.0.3
|
domhandler: 5.0.3
|
||||||
selderee: 0.11.0
|
selderee: 0.11.0
|
||||||
|
|
||||||
|
'@serve.zone/catalog@2.5.0(@tiptap/pm@2.27.2)':
|
||||||
|
dependencies:
|
||||||
|
'@design.estate/dees-catalog': 3.43.3(@tiptap/pm@2.27.2)
|
||||||
|
'@design.estate/dees-domtools': 2.3.8
|
||||||
|
'@design.estate/dees-element': 2.1.6
|
||||||
|
'@design.estate/dees-wcctools': 3.8.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@nuxt/kit'
|
||||||
|
- '@tiptap/pm'
|
||||||
|
- react
|
||||||
|
- supports-color
|
||||||
|
- vue
|
||||||
|
|
||||||
'@serve.zone/interfaces@5.3.0':
|
'@serve.zone/interfaces@5.3.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedrequest-interfaces': 3.0.19
|
'@api.global/typedrequest-interfaces': 3.0.19
|
||||||
'@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@4.0.0':
|
'@serve.zone/remoteingress@4.2.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.3.1
|
||||||
|
|
||||||
'@sindresorhus/is@5.6.0': {}
|
'@sindresorhus/is@5.6.0': {}
|
||||||
|
|
||||||
@@ -7604,6 +7644,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
jackspeak: 4.2.3
|
jackspeak: 4.2.3
|
||||||
|
|
||||||
|
balanced-match@4.0.4: {}
|
||||||
|
|
||||||
bare-events@2.8.2: {}
|
bare-events@2.8.2: {}
|
||||||
|
|
||||||
bare-fs@4.5.3:
|
bare-fs@4.5.3:
|
||||||
@@ -7676,6 +7718,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
balanced-match: 4.0.2
|
balanced-match: 4.0.2
|
||||||
|
|
||||||
|
brace-expansion@5.0.3:
|
||||||
|
dependencies:
|
||||||
|
balanced-match: 4.0.4
|
||||||
|
|
||||||
broadcast-channel@7.3.0:
|
broadcast-channel@7.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.28.6
|
'@babel/runtime': 7.28.6
|
||||||
@@ -8323,7 +8369,7 @@ snapshots:
|
|||||||
fs.realpath: 1.0.0
|
fs.realpath: 1.0.0
|
||||||
inflight: 1.0.6
|
inflight: 1.0.6
|
||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
minimatch: 3.1.2
|
minimatch: 3.1.3
|
||||||
once: 1.4.0
|
once: 1.4.0
|
||||||
path-is-absolute: 1.0.1
|
path-is-absolute: 1.0.1
|
||||||
|
|
||||||
@@ -9120,7 +9166,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 5.0.2
|
brace-expansion: 5.0.2
|
||||||
|
|
||||||
minimatch@3.1.2:
|
minimatch@10.2.2:
|
||||||
|
dependencies:
|
||||||
|
brace-expansion: 5.0.3
|
||||||
|
|
||||||
|
minimatch@3.1.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 1.1.12
|
brace-expansion: 1.1.12
|
||||||
|
|
||||||
@@ -9380,7 +9430,7 @@ snapshots:
|
|||||||
|
|
||||||
pdfjs-dist@4.10.38:
|
pdfjs-dist@4.10.38:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@napi-rs/canvas': 0.1.93
|
'@napi-rs/canvas': 0.1.95
|
||||||
|
|
||||||
peberminta@0.9.0: {}
|
peberminta@0.9.0: {}
|
||||||
|
|
||||||
|
|||||||
@@ -55,10 +55,14 @@ tap.test('should respond to configuration request', async () => {
|
|||||||
const response = await configRequest.fire({});
|
const response = await configRequest.fire({});
|
||||||
|
|
||||||
expect(response).toHaveProperty('config');
|
expect(response).toHaveProperty('config');
|
||||||
|
expect(response.config).toHaveProperty('system');
|
||||||
|
expect(response.config).toHaveProperty('smartProxy');
|
||||||
expect(response.config).toHaveProperty('email');
|
expect(response.config).toHaveProperty('email');
|
||||||
expect(response.config).toHaveProperty('dns');
|
expect(response.config).toHaveProperty('dns');
|
||||||
expect(response.config).toHaveProperty('proxy');
|
expect(response.config).toHaveProperty('tls');
|
||||||
expect(response.config).toHaveProperty('security');
|
expect(response.config).toHaveProperty('cache');
|
||||||
|
expect(response.config).toHaveProperty('radius');
|
||||||
|
expect(response.config).toHaveProperty('remoteIngress');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should handle log retrieval request', async () => {
|
tap.test('should handle log retrieval request', async () => {
|
||||||
|
|||||||
@@ -106,10 +106,14 @@ tap.test('should allow read-only config access', async () => {
|
|||||||
const response = await configRequest.fire({});
|
const response = await configRequest.fire({});
|
||||||
|
|
||||||
expect(response).toHaveProperty('config');
|
expect(response).toHaveProperty('config');
|
||||||
|
expect(response.config).toHaveProperty('system');
|
||||||
|
expect(response.config).toHaveProperty('smartProxy');
|
||||||
expect(response.config).toHaveProperty('email');
|
expect(response.config).toHaveProperty('email');
|
||||||
expect(response.config).toHaveProperty('dns');
|
expect(response.config).toHaveProperty('dns');
|
||||||
expect(response.config).toHaveProperty('proxy');
|
expect(response.config).toHaveProperty('tls');
|
||||||
expect(response.config).toHaveProperty('security');
|
expect(response.config).toHaveProperty('cache');
|
||||||
|
expect(response.config).toHaveProperty('radius');
|
||||||
|
expect(response.config).toHaveProperty('remoteIngress');
|
||||||
console.log('Configuration read successfully');
|
console.log('Configuration read successfully');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,32 @@
|
|||||||
import { DcRouter } from '../ts/index.js';
|
import { DcRouter } from '../ts/index.js';
|
||||||
|
|
||||||
const devRouter = new DcRouter({
|
const devRouter = new DcRouter({
|
||||||
// Configure services as needed for development
|
// SmartProxy routes for development/demo
|
||||||
// OpsServer always starts on port 3000
|
smartProxyConfig: {
|
||||||
|
routes: [
|
||||||
// Example: Add SmartProxy routes
|
{
|
||||||
// smartProxyConfig: {
|
name: 'web-traffic',
|
||||||
// routes: [...]
|
match: { ports: [18080], domains: ['example.com', '*.example.com'] },
|
||||||
// },
|
action: { type: 'forward', targets: [{ host: 'localhost', port: 3001 }] },
|
||||||
|
},
|
||||||
// Example: Add email configuration
|
{
|
||||||
// emailConfig: {
|
name: 'api-gateway',
|
||||||
// ports: [2525],
|
match: { ports: [18080], domains: ['api.example.com'], path: '/v1/*' },
|
||||||
// hostname: 'localhost',
|
action: { type: 'forward', targets: [{ host: 'localhost', port: 4000 }] },
|
||||||
// domains: [],
|
},
|
||||||
// routes: []
|
{
|
||||||
// },
|
name: 'tls-passthrough',
|
||||||
|
match: { ports: [18443], domains: ['secure.example.com'] },
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
targets: [{ host: 'localhost', port: 4443 }],
|
||||||
|
tls: { mode: 'passthrough' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Disable cache/mongo for dev
|
||||||
|
cacheConfig: { enabled: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Starting DcRouter in development mode...');
|
console.log('Starting DcRouter in development mode...');
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '7.4.0',
|
version: '9.2.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { OpsServer } from './opsserver/index.js';
|
|||||||
import { MetricsManager } from './monitoring/index.js';
|
import { MetricsManager } from './monitoring/index.js';
|
||||||
import { RadiusServer, type IRadiusServerConfig } from './radius/index.js';
|
import { RadiusServer, type IRadiusServerConfig } from './radius/index.js';
|
||||||
import { RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
|
import { RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
|
||||||
|
import { RouteConfigManager, ApiTokenManager } from './config/index.js';
|
||||||
|
|
||||||
export interface IDcRouterOptions {
|
export interface IDcRouterOptions {
|
||||||
/** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
|
/** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
|
||||||
@@ -212,6 +213,18 @@ export class DcRouter {
|
|||||||
public remoteIngressManager?: RemoteIngressManager;
|
public remoteIngressManager?: RemoteIngressManager;
|
||||||
public tunnelManager?: TunnelManager;
|
public tunnelManager?: TunnelManager;
|
||||||
|
|
||||||
|
// Programmatic config API
|
||||||
|
public routeConfigManager?: RouteConfigManager;
|
||||||
|
public apiTokenManager?: ApiTokenManager;
|
||||||
|
|
||||||
|
// Auto-discovered public IP (populated by generateAuthoritativeRecords)
|
||||||
|
public detectedPublicIp: string | null = null;
|
||||||
|
|
||||||
|
// DNS query logging rate limiter state
|
||||||
|
private dnsLogWindow: number[] = [];
|
||||||
|
private dnsBatchCount: number = 0;
|
||||||
|
private dnsBatchTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
// Certificate status tracking from SmartProxy events (keyed by domain)
|
// Certificate status tracking from SmartProxy events (keyed by domain)
|
||||||
public certificateStatusMap = new Map<string, {
|
public certificateStatusMap = new Map<string, {
|
||||||
status: 'valid' | 'failed';
|
status: 'valid' | 'failed';
|
||||||
@@ -228,6 +241,9 @@ export class DcRouter {
|
|||||||
// TypedRouter for API endpoints
|
// TypedRouter for API endpoints
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
|
||||||
|
// Cached constructor routes (computed once during setupSmartProxy, used by RouteConfigManager)
|
||||||
|
private constructorRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
||||||
|
|
||||||
// Environment access
|
// Environment access
|
||||||
private qenv = new plugins.qenv.Qenv('./', '.nogit/');
|
private qenv = new plugins.qenv.Qenv('./', '.nogit/');
|
||||||
|
|
||||||
@@ -252,9 +268,7 @@ export class DcRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async start() {
|
public async start() {
|
||||||
console.log('╔═══════════════════════════════════════════════════════════════════╗');
|
logger.log('info', 'Starting DcRouter Services');
|
||||||
console.log('║ Starting DcRouter Services ║');
|
|
||||||
console.log('╚═══════════════════════════════════════════════════════════════════╝');
|
|
||||||
|
|
||||||
|
|
||||||
this.opsServer = new OpsServer(this);
|
this.opsServer = new OpsServer(this);
|
||||||
@@ -273,6 +287,16 @@ export class DcRouter {
|
|||||||
// Set up SmartProxy for HTTP/HTTPS and all traffic including email routes
|
// Set up SmartProxy for HTTP/HTTPS and all traffic including email routes
|
||||||
await this.setupSmartProxy();
|
await this.setupSmartProxy();
|
||||||
|
|
||||||
|
// Initialize programmatic config API managers
|
||||||
|
this.routeConfigManager = new RouteConfigManager(
|
||||||
|
this.storageManager,
|
||||||
|
() => this.getConstructorRoutes(),
|
||||||
|
() => this.smartProxy,
|
||||||
|
);
|
||||||
|
this.apiTokenManager = new ApiTokenManager(this.storageManager);
|
||||||
|
await this.apiTokenManager.initialize();
|
||||||
|
await this.routeConfigManager.initialize();
|
||||||
|
|
||||||
// Set up unified email handling if configured
|
// Set up unified email handling if configured
|
||||||
if (this.options.emailConfig) {
|
if (this.options.emailConfig) {
|
||||||
await this.setupUnifiedEmailHandling();
|
await this.setupUnifiedEmailHandling();
|
||||||
@@ -296,7 +320,7 @@ export class DcRouter {
|
|||||||
|
|
||||||
this.logStartupSummary();
|
this.logStartupSummary();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error starting DcRouter:', error);
|
logger.log('error', 'Error starting DcRouter', { error: String(error) });
|
||||||
// Try to clean up any services that may have started
|
// Try to clean up any services that may have started
|
||||||
await this.stop();
|
await this.stop();
|
||||||
throw error;
|
throw error;
|
||||||
@@ -307,104 +331,60 @@ export class DcRouter {
|
|||||||
* Log comprehensive startup summary
|
* Log comprehensive startup summary
|
||||||
*/
|
*/
|
||||||
private logStartupSummary(): void {
|
private logStartupSummary(): void {
|
||||||
console.log('\n╔═══════════════════════════════════════════════════════════════════╗');
|
logger.log('info', 'DcRouter Started Successfully');
|
||||||
console.log('║ DcRouter Started Successfully ║');
|
|
||||||
console.log('╚═══════════════════════════════════════════════════════════════════╝\n');
|
|
||||||
|
|
||||||
// Metrics summary
|
// Metrics summary
|
||||||
if (this.metricsManager) {
|
if (this.metricsManager) {
|
||||||
console.log('📊 Metrics Service:');
|
logger.log('info', 'Metrics Service: SmartMetrics active, SmartProxy stats active, real-time tracking enabled');
|
||||||
console.log(' ├─ SmartMetrics: Active');
|
|
||||||
console.log(' ├─ SmartProxy Stats: Active');
|
|
||||||
console.log(' └─ Real-time tracking: Enabled');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SmartProxy summary
|
// SmartProxy summary
|
||||||
if (this.smartProxy) {
|
if (this.smartProxy) {
|
||||||
console.log('🌐 SmartProxy Service:');
|
|
||||||
const routeCount = this.options.smartProxyConfig?.routes?.length || 0;
|
const routeCount = this.options.smartProxyConfig?.routes?.length || 0;
|
||||||
console.log(` ├─ Routes configured: ${routeCount}`);
|
const acmeEnabled = this.options.smartProxyConfig?.acme?.enabled || false;
|
||||||
console.log(` ├─ ACME enabled: ${this.options.smartProxyConfig?.acme?.enabled || false}`);
|
const acmeMode = acmeEnabled
|
||||||
if (this.options.smartProxyConfig?.acme?.enabled) {
|
? `email=${this.options.smartProxyConfig!.acme!.email || 'not set'}, mode=${this.options.smartProxyConfig!.acme!.useProduction ? 'production' : 'staging'}`
|
||||||
console.log(` ├─ ACME email: ${this.options.smartProxyConfig.acme.email || 'not set'}`);
|
: 'disabled';
|
||||||
console.log(` └─ ACME mode: ${this.options.smartProxyConfig.acme.useProduction ? 'production' : 'staging'}`);
|
logger.log('info', `SmartProxy Service: ${routeCount} routes, ACME: ${acmeMode}`);
|
||||||
} else {
|
|
||||||
console.log(' └─ ACME: disabled');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Email service summary
|
// Email service summary
|
||||||
if (this.emailServer && this.options.emailConfig) {
|
if (this.emailServer && this.options.emailConfig) {
|
||||||
console.log('\n📧 Email Service:');
|
|
||||||
const ports = this.options.emailConfig.ports || [];
|
const ports = this.options.emailConfig.ports || [];
|
||||||
console.log(` ├─ Ports: ${ports.join(', ')}`);
|
const domainCount = this.options.emailConfig.domains?.length || 0;
|
||||||
console.log(` ├─ Hostname: ${this.options.emailConfig.hostname || 'localhost'}`);
|
const domainNames = this.options.emailConfig.domains?.map(d => `${d.domain} (${d.dnsMode || 'default'})`).join(', ') || 'none';
|
||||||
console.log(` ├─ Domains configured: ${this.options.emailConfig.domains?.length || 0}`);
|
logger.log('info', `Email Service: ports=[${ports.join(', ')}], hostname=${this.options.emailConfig.hostname || 'localhost'}, domains=${domainCount} [${domainNames}], DKIM initialized`);
|
||||||
if (this.options.emailConfig.domains && this.options.emailConfig.domains.length > 0) {
|
|
||||||
this.options.emailConfig.domains.forEach((domain, index) => {
|
|
||||||
const isLast = index === this.options.emailConfig!.domains!.length - 1;
|
|
||||||
console.log(` ${isLast ? '└─' : '├─'} ${domain.domain} (${domain.dnsMode || 'default'})`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log(` └─ DKIM: Initialized for all domains`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DNS service summary
|
// DNS service summary
|
||||||
if (this.dnsServer && this.options.dnsNsDomains && this.options.dnsScopes) {
|
if (this.dnsServer && this.options.dnsNsDomains && this.options.dnsScopes) {
|
||||||
console.log('\n🌍 DNS Service:');
|
logger.log('info', `DNS Service: nameservers=[${this.options.dnsNsDomains.join(', ')}], authoritative for ${this.options.dnsScopes.length} domains [${this.options.dnsScopes.join(', ')}], UDP:53, DoH enabled`);
|
||||||
console.log(` ├─ Nameservers: ${this.options.dnsNsDomains.join(', ')}`);
|
|
||||||
console.log(` ├─ Primary NS: ${this.options.dnsNsDomains[0]}`);
|
|
||||||
console.log(` ├─ Authoritative for: ${this.options.dnsScopes.length} domains`);
|
|
||||||
console.log(` ├─ UDP Port: 53`);
|
|
||||||
console.log(` ├─ DNS-over-HTTPS: Enabled via socket handler`);
|
|
||||||
console.log(` └─ DNSSEC: ${this.options.dnsNsDomains[0] ? 'Enabled' : 'Disabled'}`);
|
|
||||||
|
|
||||||
// Show authoritative domains
|
|
||||||
if (this.options.dnsScopes.length > 0) {
|
|
||||||
console.log('\n Authoritative Domains:');
|
|
||||||
this.options.dnsScopes.forEach((domain, index) => {
|
|
||||||
const isLast = index === this.options.dnsScopes!.length - 1;
|
|
||||||
console.log(` ${isLast ? '└─' : '├─'} ${domain}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RADIUS service summary
|
// RADIUS service summary
|
||||||
if (this.radiusServer && this.options.radiusConfig) {
|
if (this.radiusServer && this.options.radiusConfig) {
|
||||||
console.log('\n🔐 RADIUS Service:');
|
|
||||||
console.log(` ├─ Auth Port: ${this.options.radiusConfig.authPort || 1812}`);
|
|
||||||
console.log(` ├─ Acct Port: ${this.options.radiusConfig.acctPort || 1813}`);
|
|
||||||
console.log(` ├─ Clients configured: ${this.options.radiusConfig.clients?.length || 0}`);
|
|
||||||
const vlanStats = this.radiusServer.getVlanManager().getStats();
|
const vlanStats = this.radiusServer.getVlanManager().getStats();
|
||||||
console.log(` ├─ VLAN mappings: ${vlanStats.totalMappings}`);
|
logger.log('info', `RADIUS Service: auth=${this.options.radiusConfig.authPort || 1812}, acct=${this.options.radiusConfig.acctPort || 1813}, clients=${this.options.radiusConfig.clients?.length || 0}, VLANs=${vlanStats.totalMappings}, accounting=${this.options.radiusConfig.accounting?.enabled ? 'enabled' : 'disabled'}`);
|
||||||
console.log(` └─ Accounting: ${this.options.radiusConfig.accounting?.enabled ? 'Enabled' : 'Disabled'}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remote Ingress summary
|
// Remote Ingress summary
|
||||||
if (this.tunnelManager && this.options.remoteIngressConfig?.enabled) {
|
if (this.tunnelManager && this.options.remoteIngressConfig?.enabled) {
|
||||||
console.log('\n🌐 Remote Ingress:');
|
|
||||||
console.log(` ├─ Tunnel Port: ${this.options.remoteIngressConfig.tunnelPort || 8443}`);
|
|
||||||
const edgeCount = this.remoteIngressManager?.getAllEdges().length || 0;
|
const edgeCount = this.remoteIngressManager?.getAllEdges().length || 0;
|
||||||
const connectedCount = this.tunnelManager.getConnectedCount();
|
const connectedCount = this.tunnelManager.getConnectedCount();
|
||||||
console.log(` ├─ Registered Edges: ${edgeCount}`);
|
logger.log('info', `Remote Ingress: tunnel port=${this.options.remoteIngressConfig.tunnelPort || 8443}, edges=${edgeCount} registered/${connectedCount} connected`);
|
||||||
console.log(` └─ Connected Edges: ${connectedCount}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Storage summary
|
// Storage summary
|
||||||
if (this.storageManager && this.options.storage) {
|
if (this.storageManager && this.options.storage) {
|
||||||
console.log('\n💾 Storage:');
|
logger.log('info', `Storage: path=${this.options.storage.fsPath || 'default'}`);
|
||||||
console.log(` └─ Path: ${this.options.storage.fsPath || 'default'}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache database summary
|
// Cache database summary
|
||||||
if (this.cacheDb) {
|
if (this.cacheDb) {
|
||||||
console.log('\n🗄️ Cache Database (smartdata + LocalTsmDb):');
|
logger.log('info', `Cache Database: storage=${this.cacheDb.getStoragePath()}, db=${this.cacheDb.getDbName()}, cleaner=${this.cacheCleaner?.isActive() ? 'active' : 'inactive'} (${(this.options.cacheConfig?.cleanupIntervalHours || 1)}h interval)`);
|
||||||
console.log(` ├─ Storage: ${this.cacheDb.getStoragePath()}`);
|
|
||||||
console.log(` ├─ Database: ${this.cacheDb.getDbName()}`);
|
|
||||||
console.log(` └─ Cleaner: ${this.cacheCleaner?.isActive() ? 'Active' : 'Inactive'} (${(this.options.cacheConfig?.cleanupIntervalHours || 1)}h interval)`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('\n✅ All services are running\n');
|
logger.log('info', 'All services are running');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -439,7 +419,7 @@ export class DcRouter {
|
|||||||
* Set up SmartProxy with direct configuration and automatic email routes
|
* Set up SmartProxy with direct configuration and automatic email routes
|
||||||
*/
|
*/
|
||||||
private async setupSmartProxy(): Promise<void> {
|
private async setupSmartProxy(): Promise<void> {
|
||||||
console.log('[DcRouter] Setting up SmartProxy...');
|
logger.log('info', 'Setting up SmartProxy...');
|
||||||
let routes: plugins.smartproxy.IRouteConfig[] = [];
|
let routes: plugins.smartproxy.IRouteConfig[] = [];
|
||||||
let acmeConfig: plugins.smartproxy.IAcmeOptions | undefined;
|
let acmeConfig: plugins.smartproxy.IAcmeOptions | undefined;
|
||||||
|
|
||||||
@@ -447,22 +427,20 @@ export class DcRouter {
|
|||||||
if (this.options.smartProxyConfig) {
|
if (this.options.smartProxyConfig) {
|
||||||
routes = this.options.smartProxyConfig.routes || [];
|
routes = this.options.smartProxyConfig.routes || [];
|
||||||
acmeConfig = this.options.smartProxyConfig.acme;
|
acmeConfig = this.options.smartProxyConfig.acme;
|
||||||
console.log(`[DcRouter] Found ${routes.length} routes in config`);
|
logger.log('info', `Found ${routes.length} routes in config, ACME config present: ${!!acmeConfig}`);
|
||||||
console.log(`[DcRouter] ACME config present: ${!!acmeConfig}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If email config exists, automatically add email routes
|
// If email config exists, automatically add email routes
|
||||||
if (this.options.emailConfig) {
|
if (this.options.emailConfig) {
|
||||||
const emailRoutes = this.generateEmailRoutes(this.options.emailConfig);
|
const emailRoutes = this.generateEmailRoutes(this.options.emailConfig);
|
||||||
console.log(`Email Routes are:`)
|
logger.log('debug', 'Email routes generated', { routes: JSON.stringify(emailRoutes) });
|
||||||
console.log(emailRoutes)
|
|
||||||
routes = [...routes, ...emailRoutes]; // Enable email routing through SmartProxy
|
routes = [...routes, ...emailRoutes]; // Enable email routing through SmartProxy
|
||||||
}
|
}
|
||||||
|
|
||||||
// If DNS is configured, add DNS routes
|
// If DNS is configured, add DNS routes
|
||||||
if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0) {
|
if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0) {
|
||||||
const dnsRoutes = this.generateDnsRoutes();
|
const dnsRoutes = this.generateDnsRoutes();
|
||||||
console.log(`DNS Routes for nameservers ${this.options.dnsNsDomains.join(', ')}:`, dnsRoutes);
|
logger.log('debug', `DNS routes for nameservers ${this.options.dnsNsDomains.join(', ')}`, { routes: JSON.stringify(dnsRoutes) });
|
||||||
routes = [...routes, ...dnsRoutes];
|
routes = [...routes, ...dnsRoutes];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,15 +458,18 @@ export class DcRouter {
|
|||||||
// Configure DNS challenge if available
|
// Configure DNS challenge if available
|
||||||
let challengeHandlers: any[] = [];
|
let challengeHandlers: any[] = [];
|
||||||
if (this.options.dnsChallenge?.cloudflareApiKey) {
|
if (this.options.dnsChallenge?.cloudflareApiKey) {
|
||||||
console.log('Configuring Cloudflare DNS challenge for ACME');
|
logger.log('info', 'Configuring Cloudflare DNS challenge for ACME');
|
||||||
const cloudflareAccount = new plugins.cloudflare.CloudflareAccount(this.options.dnsChallenge.cloudflareApiKey);
|
const cloudflareAccount = new plugins.cloudflare.CloudflareAccount(this.options.dnsChallenge.cloudflareApiKey);
|
||||||
const dns01Handler = new plugins.smartacme.handlers.Dns01Handler(cloudflareAccount);
|
const dns01Handler = new plugins.smartacme.handlers.Dns01Handler(cloudflareAccount);
|
||||||
challengeHandlers.push(dns01Handler);
|
challengeHandlers.push(dns01Handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache constructor routes for RouteConfigManager
|
||||||
|
this.constructorRoutes = [...routes];
|
||||||
|
|
||||||
// If we have routes or need a basic SmartProxy instance, create it
|
// If we have routes or need a basic SmartProxy instance, create it
|
||||||
if (routes.length > 0 || this.options.smartProxyConfig) {
|
if (routes.length > 0 || this.options.smartProxyConfig) {
|
||||||
console.log('Setting up SmartProxy with combined configuration');
|
logger.log('info', 'Setting up SmartProxy with combined configuration');
|
||||||
|
|
||||||
// Track cert entries loaded from cert store so we can populate certificateStatusMap after start
|
// Track cert entries loaded from cert store so we can populate certificateStatusMap after start
|
||||||
const loadedCertEntries: Array<{domain: string; publicKey: string; validUntil?: number; validFrom?: number}> = [];
|
const loadedCertEntries: Array<{domain: string; publicKey: string; validUntil?: number; validFrom?: number}> = [];
|
||||||
@@ -537,7 +518,7 @@ export class DcRouter {
|
|||||||
// Stop old SmartAcme if it exists (e.g., during updateSmartProxyConfig)
|
// Stop old SmartAcme if it exists (e.g., during updateSmartProxyConfig)
|
||||||
if (this.smartAcme) {
|
if (this.smartAcme) {
|
||||||
await this.smartAcme.stop().catch(err =>
|
await this.smartAcme.stop().catch(err =>
|
||||||
console.error('[DcRouter] Error stopping old SmartAcme:', err)
|
logger.log('error', 'Error stopping old SmartAcme', { error: String(err) })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.smartAcme = new plugins.smartacme.SmartAcme({
|
this.smartAcme = new plugins.smartacme.SmartAcme({
|
||||||
@@ -600,25 +581,19 @@ export class DcRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create SmartProxy instance
|
// Create SmartProxy instance
|
||||||
console.log('[DcRouter] Creating SmartProxy instance with config:', JSON.stringify({
|
logger.log('info', `Creating SmartProxy instance: routes=${smartProxyConfig.routes?.length}, acme=${smartProxyConfig.acme?.enabled}, certProvisionFunction=${!!smartProxyConfig.certProvisionFunction}`);
|
||||||
routeCount: smartProxyConfig.routes?.length,
|
|
||||||
acmeEnabled: smartProxyConfig.acme?.enabled,
|
|
||||||
acmeEmail: smartProxyConfig.acme?.email,
|
|
||||||
certProvisionFunction: !!smartProxyConfig.certProvisionFunction
|
|
||||||
}, null, 2));
|
|
||||||
|
|
||||||
this.smartProxy = new plugins.smartproxy.SmartProxy(smartProxyConfig);
|
this.smartProxy = new plugins.smartproxy.SmartProxy(smartProxyConfig);
|
||||||
|
|
||||||
// Set up event listeners
|
// Set up event listeners
|
||||||
this.smartProxy.on('error', (err) => {
|
this.smartProxy.on('error', (err) => {
|
||||||
console.error('[DcRouter] SmartProxy error:', err);
|
logger.log('error', `SmartProxy error: ${err.message}`, { stack: err.stack });
|
||||||
console.error('[DcRouter] Error stack:', err.stack);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Always listen for certificate events — emitted by both ACME and certProvisionFunction paths
|
// Always listen for certificate events — emitted by both ACME and certProvisionFunction paths
|
||||||
// Events are keyed by domain for domain-centric certificate tracking
|
// Events are keyed by domain for domain-centric certificate tracking
|
||||||
this.smartProxy.on('certificate-issued', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
|
this.smartProxy.on('certificate-issued', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
|
||||||
console.log(`[DcRouter] Certificate issued for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
|
logger.log('info', `Certificate issued for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
|
||||||
const routeNames = this.findRouteNamesForDomain(event.domain);
|
const routeNames = this.findRouteNamesForDomain(event.domain);
|
||||||
this.certificateStatusMap.set(event.domain, {
|
this.certificateStatusMap.set(event.domain, {
|
||||||
status: 'valid', routeNames,
|
status: 'valid', routeNames,
|
||||||
@@ -628,7 +603,7 @@ export class DcRouter {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.smartProxy.on('certificate-renewed', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
|
this.smartProxy.on('certificate-renewed', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
|
||||||
console.log(`[DcRouter] Certificate renewed for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
|
logger.log('info', `Certificate renewed for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
|
||||||
const routeNames = this.findRouteNamesForDomain(event.domain);
|
const routeNames = this.findRouteNamesForDomain(event.domain);
|
||||||
this.certificateStatusMap.set(event.domain, {
|
this.certificateStatusMap.set(event.domain, {
|
||||||
status: 'valid', routeNames,
|
status: 'valid', routeNames,
|
||||||
@@ -638,7 +613,7 @@ export class DcRouter {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.smartProxy.on('certificate-failed', (event: plugins.smartproxy.ICertificateFailedEvent) => {
|
this.smartProxy.on('certificate-failed', (event: plugins.smartproxy.ICertificateFailedEvent) => {
|
||||||
console.error(`[DcRouter] Certificate failed for ${event.domain} (${event.source}):`, event.error);
|
logger.log('error', `Certificate failed for ${event.domain} (${event.source}): ${event.error}`);
|
||||||
const routeNames = this.findRouteNamesForDomain(event.domain);
|
const routeNames = this.findRouteNamesForDomain(event.domain);
|
||||||
this.certificateStatusMap.set(event.domain, {
|
this.certificateStatusMap.set(event.domain, {
|
||||||
status: 'failed', routeNames, error: event.error,
|
status: 'failed', routeNames, error: event.error,
|
||||||
@@ -647,9 +622,9 @@ export class DcRouter {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Start SmartProxy
|
// Start SmartProxy
|
||||||
console.log('[DcRouter] Starting SmartProxy...');
|
logger.log('info', 'Starting SmartProxy...');
|
||||||
await this.smartProxy.start();
|
await this.smartProxy.start();
|
||||||
console.log('[DcRouter] SmartProxy started successfully');
|
logger.log('info', 'SmartProxy started successfully');
|
||||||
|
|
||||||
// Populate certificateStatusMap for certs loaded from store at startup
|
// Populate certificateStatusMap for certs loaded from store at startup
|
||||||
for (const entry of loadedCertEntries) {
|
for (const entry of loadedCertEntries) {
|
||||||
@@ -701,10 +676,10 @@ export class DcRouter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (loadedCertEntries.length > 0) {
|
if (loadedCertEntries.length > 0) {
|
||||||
console.log(`[DcRouter] Populated certificate status for ${loadedCertEntries.length} store-loaded domain(s)`);
|
logger.log('info', `Populated certificate status for ${loadedCertEntries.length} store-loaded domain(s)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`SmartProxy started with ${routes.length} routes`);
|
logger.log('info', `SmartProxy started with ${routes.length} routes`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -906,48 +881,81 @@ export class DcRouter {
|
|||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the routes derived from constructor config (smartProxy + email + DNS).
|
||||||
|
* Used by RouteConfigManager as the "hardcoded" base.
|
||||||
|
*/
|
||||||
|
public getConstructorRoutes(): plugins.smartproxy.IRouteConfig[] {
|
||||||
|
return this.constructorRoutes;
|
||||||
|
}
|
||||||
|
|
||||||
public async stop() {
|
public async stop() {
|
||||||
console.log('Stopping DcRouter services...');
|
logger.log('info', 'Stopping DcRouter services...');
|
||||||
|
|
||||||
|
// Flush pending DNS batch log
|
||||||
|
if (this.dnsBatchTimer) {
|
||||||
|
clearTimeout(this.dnsBatchTimer);
|
||||||
|
if (this.dnsBatchCount > 0) {
|
||||||
|
logger.log('info', `DNS: ${this.dnsBatchCount} queries processed (rate limited, final flush)`, { zone: 'dns' });
|
||||||
|
}
|
||||||
|
this.dnsBatchTimer = null;
|
||||||
|
this.dnsBatchCount = 0;
|
||||||
|
this.dnsLogWindow = [];
|
||||||
|
}
|
||||||
|
|
||||||
await this.opsServer.stop();
|
await this.opsServer.stop();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Remove event listeners before stopping services to prevent leaks
|
||||||
|
if (this.smartProxy) {
|
||||||
|
this.smartProxy.removeAllListeners();
|
||||||
|
}
|
||||||
|
if (this.emailServer) {
|
||||||
|
if ((this.emailServer as any).deliverySystem) {
|
||||||
|
(this.emailServer as any).deliverySystem.removeAllListeners();
|
||||||
|
}
|
||||||
|
this.emailServer.removeAllListeners();
|
||||||
|
}
|
||||||
|
if (this.dnsServer) {
|
||||||
|
this.dnsServer.removeAllListeners();
|
||||||
|
}
|
||||||
|
|
||||||
// Stop all services in parallel for faster shutdown
|
// Stop all services in parallel for faster shutdown
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
// Stop cache cleaner if running
|
// Stop cache cleaner if running
|
||||||
this.cacheCleaner ? Promise.resolve(this.cacheCleaner.stop()) : Promise.resolve(),
|
this.cacheCleaner ? Promise.resolve(this.cacheCleaner.stop()) : Promise.resolve(),
|
||||||
|
|
||||||
// Stop metrics manager if running
|
// Stop metrics manager if running
|
||||||
this.metricsManager ? this.metricsManager.stop().catch(err => console.error('Error stopping MetricsManager:', err)) : Promise.resolve(),
|
this.metricsManager ? this.metricsManager.stop().catch(err => logger.log('error', 'Error stopping MetricsManager', { error: String(err) })) : Promise.resolve(),
|
||||||
|
|
||||||
// Stop unified email server if running
|
// Stop unified email server if running
|
||||||
this.emailServer ? this.emailServer.stop().catch(err => console.error('Error stopping email server:', err)) : Promise.resolve(),
|
this.emailServer ? this.emailServer.stop().catch(err => logger.log('error', 'Error stopping email server', { error: String(err) })) : Promise.resolve(),
|
||||||
|
|
||||||
// Stop SmartAcme if running
|
// Stop SmartAcme if running
|
||||||
this.smartAcme ? this.smartAcme.stop().catch(err => console.error('Error stopping SmartAcme:', err)) : Promise.resolve(),
|
this.smartAcme ? this.smartAcme.stop().catch(err => logger.log('error', 'Error stopping SmartAcme', { error: String(err) })) : Promise.resolve(),
|
||||||
|
|
||||||
// Stop HTTP SmartProxy if running
|
// Stop HTTP SmartProxy if running
|
||||||
this.smartProxy ? this.smartProxy.stop().catch(err => console.error('Error stopping SmartProxy:', err)) : Promise.resolve(),
|
this.smartProxy ? this.smartProxy.stop().catch(err => logger.log('error', 'Error stopping SmartProxy', { error: String(err) })) : Promise.resolve(),
|
||||||
|
|
||||||
// Stop DNS server if running
|
// Stop DNS server if running
|
||||||
this.dnsServer ?
|
this.dnsServer ?
|
||||||
this.dnsServer.stop().catch(err => console.error('Error stopping DNS server:', err)) :
|
this.dnsServer.stop().catch(err => logger.log('error', 'Error stopping DNS server', { error: String(err) })) :
|
||||||
Promise.resolve(),
|
Promise.resolve(),
|
||||||
|
|
||||||
// Stop RADIUS server if running
|
// Stop RADIUS server if running
|
||||||
this.radiusServer ?
|
this.radiusServer ?
|
||||||
this.radiusServer.stop().catch(err => console.error('Error stopping RADIUS server:', err)) :
|
this.radiusServer.stop().catch(err => logger.log('error', 'Error stopping RADIUS server', { error: String(err) })) :
|
||||||
Promise.resolve(),
|
Promise.resolve(),
|
||||||
|
|
||||||
// Stop Remote Ingress tunnel manager if running
|
// Stop Remote Ingress tunnel manager if running
|
||||||
this.tunnelManager ?
|
this.tunnelManager ?
|
||||||
this.tunnelManager.stop().catch(err => console.error('Error stopping TunnelManager:', err)) :
|
this.tunnelManager.stop().catch(err => logger.log('error', 'Error stopping TunnelManager', { error: String(err) })) :
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Stop cache database after other services (they may need it during shutdown)
|
// Stop cache database after other services (they may need it during shutdown)
|
||||||
if (this.cacheDb) {
|
if (this.cacheDb) {
|
||||||
await this.cacheDb.stop().catch(err => console.error('Error stopping CacheDb:', err));
|
await this.cacheDb.stop().catch(err => logger.log('error', 'Error stopping CacheDb', { error: String(err) }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear backoff cache in cert scheduler
|
// Clear backoff cache in cert scheduler
|
||||||
@@ -967,11 +975,13 @@ export class DcRouter {
|
|||||||
this.smartAcme = undefined;
|
this.smartAcme = undefined;
|
||||||
this.certProvisionScheduler = undefined;
|
this.certProvisionScheduler = undefined;
|
||||||
this.remoteIngressManager = undefined;
|
this.remoteIngressManager = undefined;
|
||||||
|
this.routeConfigManager = undefined;
|
||||||
|
this.apiTokenManager = undefined;
|
||||||
this.certificateStatusMap.clear();
|
this.certificateStatusMap.clear();
|
||||||
|
|
||||||
console.log('All DcRouter services stopped');
|
logger.log('info', 'All DcRouter services stopped');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during DcRouter shutdown:', error);
|
logger.log('error', 'Error during DcRouter shutdown', { error: String(error) });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -983,6 +993,7 @@ export class DcRouter {
|
|||||||
public async updateSmartProxyConfig(config: plugins.smartproxy.ISmartProxyOptions): Promise<void> {
|
public async updateSmartProxyConfig(config: plugins.smartproxy.ISmartProxyOptions): Promise<void> {
|
||||||
// Stop existing SmartProxy if running
|
// Stop existing SmartProxy if running
|
||||||
if (this.smartProxy) {
|
if (this.smartProxy) {
|
||||||
|
this.smartProxy.removeAllListeners();
|
||||||
await this.smartProxy.stop();
|
await this.smartProxy.stop();
|
||||||
this.smartProxy = undefined;
|
this.smartProxy = undefined;
|
||||||
}
|
}
|
||||||
@@ -998,7 +1009,12 @@ export class DcRouter {
|
|||||||
// Start new SmartProxy with updated configuration (will include email routes if configured)
|
// Start new SmartProxy with updated configuration (will include email routes if configured)
|
||||||
await this.setupSmartProxy();
|
await this.setupSmartProxy();
|
||||||
|
|
||||||
console.log('SmartProxy configuration updated');
|
// Re-apply programmatic routes and overrides after SmartProxy restart
|
||||||
|
if (this.routeConfigManager) {
|
||||||
|
await this.routeConfigManager.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('info', 'SmartProxy configuration updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1056,21 +1072,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
|
// Wire delivery events to MetricsManager and logger
|
||||||
if (this.metricsManager && this.emailServer.deliverySystem) {
|
if (this.metricsManager && this.emailServer.deliverySystem) {
|
||||||
this.emailServer.deliverySystem.on('deliveryStart', (item: any) => {
|
this.emailServer.deliverySystem.on('deliveryStart', (item: any) => {
|
||||||
this.metricsManager.trackEmailReceived(item?.from);
|
this.metricsManager.trackEmailReceived(item?.from);
|
||||||
|
logger.log('info', `Email delivery started: ${item?.from} → ${item?.to}`, { zone: 'email' });
|
||||||
});
|
});
|
||||||
this.emailServer.deliverySystem.on('deliverySuccess', (item: any) => {
|
this.emailServer.deliverySystem.on('deliverySuccess', (item: any) => {
|
||||||
this.metricsManager.trackEmailSent(item?.to);
|
this.metricsManager.trackEmailSent(item?.to);
|
||||||
|
logger.log('info', `Email delivered to ${item?.to}`, { zone: 'email' });
|
||||||
});
|
});
|
||||||
this.emailServer.deliverySystem.on('deliveryFailed', (item: any, error: any) => {
|
this.emailServer.deliverySystem.on('deliveryFailed', (item: any, error: any) => {
|
||||||
this.metricsManager.trackEmailFailed(item?.to, error?.message);
|
this.metricsManager.trackEmailFailed(item?.to, error?.message);
|
||||||
|
logger.log('warn', `Email delivery failed to ${item?.to}: ${error?.message}`, { zone: 'email' });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (this.metricsManager && this.emailServer) {
|
if (this.metricsManager && this.emailServer) {
|
||||||
this.emailServer.on('bounceProcessed', () => {
|
this.emailServer.on('bounceProcessed', () => {
|
||||||
this.metricsManager.trackEmailBounced();
|
this.metricsManager.trackEmailBounced();
|
||||||
|
logger.log('warn', 'Email bounce processed', { zone: 'email' });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1091,7 +1111,7 @@ export class DcRouter {
|
|||||||
// Start email handling with new configuration
|
// Start email handling with new configuration
|
||||||
await this.setupUnifiedEmailHandling();
|
await this.setupUnifiedEmailHandling();
|
||||||
|
|
||||||
console.log('Unified email configuration updated');
|
logger.log('info', 'Unified email configuration updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1101,6 +1121,11 @@ export class DcRouter {
|
|||||||
try {
|
try {
|
||||||
// Stop the unified email server which contains all components
|
// Stop the unified email server which contains all components
|
||||||
if (this.emailServer) {
|
if (this.emailServer) {
|
||||||
|
// Remove listeners before stopping to prevent leaks on config update cycles
|
||||||
|
if ((this.emailServer as any).deliverySystem) {
|
||||||
|
(this.emailServer as any).deliverySystem.removeAllListeners();
|
||||||
|
}
|
||||||
|
this.emailServer.removeAllListeners();
|
||||||
await this.emailServer.stop();
|
await this.emailServer.stop();
|
||||||
logger.log('info', 'Unified email server stopped');
|
logger.log('info', 'Unified email server stopped');
|
||||||
this.emailServer = undefined;
|
this.emailServer = undefined;
|
||||||
@@ -1131,7 +1156,7 @@ export class DcRouter {
|
|||||||
this.emailServer.updateEmailRoutes(routes);
|
this.emailServer.updateEmailRoutes(routes);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Email routes updated with ${routes.length} routes`);
|
logger.log('info', `Email routes updated with ${routes.length} routes`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1257,9 +1282,18 @@ export class DcRouter {
|
|||||||
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
|
// Wire DNS query events to MetricsManager and logger with adaptive rate limiting
|
||||||
if (this.metricsManager && this.dnsServer) {
|
if (this.metricsManager && this.dnsServer) {
|
||||||
|
const flushDnsBatch = () => {
|
||||||
|
if (this.dnsBatchCount > 0) {
|
||||||
|
logger.log('info', `DNS: ${this.dnsBatchCount} queries processed (rate limited)`, { zone: 'dns' });
|
||||||
|
this.dnsBatchCount = 0;
|
||||||
|
}
|
||||||
|
this.dnsBatchTimer = null;
|
||||||
|
};
|
||||||
|
|
||||||
this.dnsServer.on('query', (event: plugins.smartdns.dnsServerMod.IDnsQueryCompletedEvent) => {
|
this.dnsServer.on('query', (event: plugins.smartdns.dnsServerMod.IDnsQueryCompletedEvent) => {
|
||||||
|
// Metrics tracking
|
||||||
for (const question of event.questions) {
|
for (const question of event.questions) {
|
||||||
this.metricsManager.trackDnsQuery(
|
this.metricsManager.trackDnsQuery(
|
||||||
question.type,
|
question.type,
|
||||||
@@ -1269,6 +1303,21 @@ export class DcRouter {
|
|||||||
event.answered,
|
event.answered,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Adaptive logging: individual logs up to 2/sec, then batch
|
||||||
|
const now = Date.now();
|
||||||
|
this.dnsLogWindow = this.dnsLogWindow.filter(t => now - t < 1000);
|
||||||
|
|
||||||
|
if (this.dnsLogWindow.length < 2) {
|
||||||
|
this.dnsLogWindow.push(now);
|
||||||
|
const summary = event.questions.map(q => `${q.type} ${q.name}`).join(', ');
|
||||||
|
logger.log('info', `DNS query: ${summary} (${event.responseTimeMs}ms, ${event.answered ? 'answered' : 'unanswered'})`, { zone: 'dns' });
|
||||||
|
} else {
|
||||||
|
this.dnsBatchCount++;
|
||||||
|
if (!this.dnsBatchTimer) {
|
||||||
|
this.dnsBatchTimer = setTimeout(flushDnsBatch, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1528,6 +1577,7 @@ export class DcRouter {
|
|||||||
} else if (this.options.publicIp) {
|
} else if (this.options.publicIp) {
|
||||||
// Use explicitly configured public IP
|
// Use explicitly configured public IP
|
||||||
publicIp = this.options.publicIp;
|
publicIp = this.options.publicIp;
|
||||||
|
this.detectedPublicIp = publicIp;
|
||||||
logger.log('info', `Using configured public IP for nameserver A records: ${publicIp}`);
|
logger.log('info', `Using configured public IP for nameserver A records: ${publicIp}`);
|
||||||
} else {
|
} else {
|
||||||
// Auto-discover public IP using smartnetwork
|
// Auto-discover public IP using smartnetwork
|
||||||
@@ -1538,6 +1588,7 @@ export class DcRouter {
|
|||||||
|
|
||||||
if (publicIps.v4) {
|
if (publicIps.v4) {
|
||||||
publicIp = publicIps.v4;
|
publicIp = publicIps.v4;
|
||||||
|
this.detectedPublicIp = publicIp;
|
||||||
logger.log('info', `Auto-discovered public IPv4: ${publicIp}`);
|
logger.log('info', `Auto-discovered public IPv4: ${publicIp}`);
|
||||||
} else {
|
} else {
|
||||||
logger.log('warn', 'Could not auto-discover public IPv4 address');
|
logger.log('warn', 'Could not auto-discover public IPv4 address');
|
||||||
|
|||||||
155
ts/config/classes.api-token-manager.ts
Normal file
155
ts/config/classes.api-token-manager.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { logger } from '../logger.js';
|
||||||
|
import type { StorageManager } from '../storage/index.js';
|
||||||
|
import type {
|
||||||
|
IStoredApiToken,
|
||||||
|
IApiTokenInfo,
|
||||||
|
TApiTokenScope,
|
||||||
|
} from '../../ts_interfaces/data/route-management.js';
|
||||||
|
|
||||||
|
const TOKENS_PREFIX = '/config-api/tokens/';
|
||||||
|
const TOKEN_PREFIX_STR = 'dcr_';
|
||||||
|
|
||||||
|
export class ApiTokenManager {
|
||||||
|
private tokens = new Map<string, IStoredApiToken>();
|
||||||
|
|
||||||
|
constructor(private storageManager: StorageManager) {}
|
||||||
|
|
||||||
|
public async initialize(): Promise<void> {
|
||||||
|
await this.loadTokens();
|
||||||
|
if (this.tokens.size > 0) {
|
||||||
|
logger.log('info', `Loaded ${this.tokens.size} API token(s) from storage`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Token lifecycle
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new API token. Returns the raw token value (shown once).
|
||||||
|
*/
|
||||||
|
public async createToken(
|
||||||
|
name: string,
|
||||||
|
scopes: TApiTokenScope[],
|
||||||
|
expiresInDays: number | null,
|
||||||
|
createdBy: string,
|
||||||
|
): Promise<{ id: string; rawToken: string }> {
|
||||||
|
const id = plugins.uuid.v4();
|
||||||
|
const randomBytes = plugins.crypto.randomBytes(32);
|
||||||
|
const rawPayload = `${id}:${randomBytes.toString('base64url')}`;
|
||||||
|
const rawToken = `${TOKEN_PREFIX_STR}${rawPayload}`;
|
||||||
|
|
||||||
|
const tokenHash = plugins.crypto.createHash('sha256').update(rawToken).digest('hex');
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const stored: IStoredApiToken = {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
tokenHash,
|
||||||
|
scopes,
|
||||||
|
createdAt: now,
|
||||||
|
expiresAt: expiresInDays != null ? now + expiresInDays * 86400000 : null,
|
||||||
|
lastUsedAt: null,
|
||||||
|
createdBy,
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.tokens.set(id, stored);
|
||||||
|
await this.persistToken(stored);
|
||||||
|
logger.log('info', `API token '${name}' created (id: ${id})`);
|
||||||
|
return { id, rawToken };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a raw token string. Returns the stored token if valid, null otherwise.
|
||||||
|
* Also updates lastUsedAt.
|
||||||
|
*/
|
||||||
|
public async validateToken(rawToken: string): Promise<IStoredApiToken | null> {
|
||||||
|
if (!rawToken.startsWith(TOKEN_PREFIX_STR)) return null;
|
||||||
|
|
||||||
|
const hash = plugins.crypto.createHash('sha256').update(rawToken).digest('hex');
|
||||||
|
|
||||||
|
for (const stored of this.tokens.values()) {
|
||||||
|
if (stored.tokenHash === hash) {
|
||||||
|
if (!stored.enabled) return null;
|
||||||
|
if (stored.expiresAt !== null && stored.expiresAt < Date.now()) return null;
|
||||||
|
|
||||||
|
// Update lastUsedAt (fire and forget)
|
||||||
|
stored.lastUsedAt = Date.now();
|
||||||
|
this.persistToken(stored).catch(() => {});
|
||||||
|
return stored;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a token has a specific scope.
|
||||||
|
*/
|
||||||
|
public hasScope(token: IStoredApiToken, scope: TApiTokenScope): boolean {
|
||||||
|
return token.scopes.includes(scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all tokens (safe info only, no hashes).
|
||||||
|
*/
|
||||||
|
public listTokens(): IApiTokenInfo[] {
|
||||||
|
const result: IApiTokenInfo[] = [];
|
||||||
|
for (const stored of this.tokens.values()) {
|
||||||
|
result.push({
|
||||||
|
id: stored.id,
|
||||||
|
name: stored.name,
|
||||||
|
scopes: stored.scopes,
|
||||||
|
createdAt: stored.createdAt,
|
||||||
|
expiresAt: stored.expiresAt,
|
||||||
|
lastUsedAt: stored.lastUsedAt,
|
||||||
|
enabled: stored.enabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke (delete) a token.
|
||||||
|
*/
|
||||||
|
public async revokeToken(id: string): Promise<boolean> {
|
||||||
|
if (!this.tokens.has(id)) return false;
|
||||||
|
const token = this.tokens.get(id)!;
|
||||||
|
this.tokens.delete(id);
|
||||||
|
await this.storageManager.delete(`${TOKENS_PREFIX}${id}.json`);
|
||||||
|
logger.log('info', `API token '${token.name}' revoked (id: ${id})`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable a token.
|
||||||
|
*/
|
||||||
|
public async toggleToken(id: string, enabled: boolean): Promise<boolean> {
|
||||||
|
const stored = this.tokens.get(id);
|
||||||
|
if (!stored) return false;
|
||||||
|
stored.enabled = enabled;
|
||||||
|
await this.persistToken(stored);
|
||||||
|
logger.log('info', `API token '${stored.name}' ${enabled ? 'enabled' : 'disabled'} (id: ${id})`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Private
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
private async loadTokens(): Promise<void> {
|
||||||
|
const keys = await this.storageManager.list(TOKENS_PREFIX);
|
||||||
|
for (const key of keys) {
|
||||||
|
if (!key.endsWith('.json')) continue;
|
||||||
|
const stored = await this.storageManager.getJSON<IStoredApiToken>(key);
|
||||||
|
if (stored?.id) {
|
||||||
|
this.tokens.set(stored.id, stored);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async persistToken(stored: IStoredApiToken): Promise<void> {
|
||||||
|
await this.storageManager.setJSON(`${TOKENS_PREFIX}${stored.id}.json`, stored);
|
||||||
|
}
|
||||||
|
}
|
||||||
271
ts/config/classes.route-config-manager.ts
Normal file
271
ts/config/classes.route-config-manager.ts
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { logger } from '../logger.js';
|
||||||
|
import type { StorageManager } from '../storage/index.js';
|
||||||
|
import type {
|
||||||
|
IStoredRoute,
|
||||||
|
IRouteOverride,
|
||||||
|
IMergedRoute,
|
||||||
|
IRouteWarning,
|
||||||
|
} from '../../ts_interfaces/data/route-management.js';
|
||||||
|
|
||||||
|
const ROUTES_PREFIX = '/config-api/routes/';
|
||||||
|
const OVERRIDES_PREFIX = '/config-api/overrides/';
|
||||||
|
|
||||||
|
export class RouteConfigManager {
|
||||||
|
private storedRoutes = new Map<string, IStoredRoute>();
|
||||||
|
private overrides = new Map<string, IRouteOverride>();
|
||||||
|
private warnings: IRouteWarning[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private storageManager: StorageManager,
|
||||||
|
private getHardcodedRoutes: () => plugins.smartproxy.IRouteConfig[],
|
||||||
|
private getSmartProxy: () => plugins.smartproxy.SmartProxy | undefined,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load persisted routes and overrides, compute warnings, apply to SmartProxy.
|
||||||
|
*/
|
||||||
|
public async initialize(): Promise<void> {
|
||||||
|
await this.loadStoredRoutes();
|
||||||
|
await this.loadOverrides();
|
||||||
|
this.computeWarnings();
|
||||||
|
this.logWarnings();
|
||||||
|
await this.applyRoutes();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Merged view
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public getMergedRoutes(): { routes: IMergedRoute[]; warnings: IRouteWarning[] } {
|
||||||
|
const merged: IMergedRoute[] = [];
|
||||||
|
|
||||||
|
// Hardcoded routes
|
||||||
|
for (const route of this.getHardcodedRoutes()) {
|
||||||
|
const name = route.name || '';
|
||||||
|
const override = this.overrides.get(name);
|
||||||
|
merged.push({
|
||||||
|
route,
|
||||||
|
source: 'hardcoded',
|
||||||
|
enabled: override ? override.enabled : true,
|
||||||
|
overridden: !!override,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Programmatic routes
|
||||||
|
for (const stored of this.storedRoutes.values()) {
|
||||||
|
merged.push({
|
||||||
|
route: stored.route,
|
||||||
|
source: 'programmatic',
|
||||||
|
enabled: stored.enabled,
|
||||||
|
overridden: false,
|
||||||
|
storedRouteId: stored.id,
|
||||||
|
createdAt: stored.createdAt,
|
||||||
|
updatedAt: stored.updatedAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { routes: merged, warnings: [...this.warnings] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Programmatic route CRUD
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public async createRoute(
|
||||||
|
route: plugins.smartproxy.IRouteConfig,
|
||||||
|
createdBy: string,
|
||||||
|
enabled = true,
|
||||||
|
): Promise<string> {
|
||||||
|
const id = plugins.uuid.v4();
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Ensure route has a name
|
||||||
|
if (!route.name) {
|
||||||
|
route.name = `programmatic-${id.slice(0, 8)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stored: IStoredRoute = {
|
||||||
|
id,
|
||||||
|
route,
|
||||||
|
enabled,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
createdBy,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.storedRoutes.set(id, stored);
|
||||||
|
await this.persistRoute(stored);
|
||||||
|
await this.applyRoutes();
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateRoute(
|
||||||
|
id: string,
|
||||||
|
patch: { route?: Partial<plugins.smartproxy.IRouteConfig>; enabled?: boolean },
|
||||||
|
): Promise<boolean> {
|
||||||
|
const stored = this.storedRoutes.get(id);
|
||||||
|
if (!stored) return false;
|
||||||
|
|
||||||
|
if (patch.route) {
|
||||||
|
stored.route = { ...stored.route, ...patch.route } as plugins.smartproxy.IRouteConfig;
|
||||||
|
}
|
||||||
|
if (patch.enabled !== undefined) {
|
||||||
|
stored.enabled = patch.enabled;
|
||||||
|
}
|
||||||
|
stored.updatedAt = Date.now();
|
||||||
|
|
||||||
|
await this.persistRoute(stored);
|
||||||
|
await this.applyRoutes();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteRoute(id: string): Promise<boolean> {
|
||||||
|
if (!this.storedRoutes.has(id)) return false;
|
||||||
|
this.storedRoutes.delete(id);
|
||||||
|
await this.storageManager.delete(`${ROUTES_PREFIX}${id}.json`);
|
||||||
|
await this.applyRoutes();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async toggleRoute(id: string, enabled: boolean): Promise<boolean> {
|
||||||
|
return this.updateRoute(id, { enabled });
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Hardcoded route overrides
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public async setOverride(routeName: string, enabled: boolean, updatedBy: string): Promise<void> {
|
||||||
|
const override: IRouteOverride = {
|
||||||
|
routeName,
|
||||||
|
enabled,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
updatedBy,
|
||||||
|
};
|
||||||
|
this.overrides.set(routeName, override);
|
||||||
|
await this.storageManager.setJSON(`${OVERRIDES_PREFIX}${routeName}.json`, override);
|
||||||
|
this.computeWarnings();
|
||||||
|
await this.applyRoutes();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async removeOverride(routeName: string): Promise<boolean> {
|
||||||
|
if (!this.overrides.has(routeName)) return false;
|
||||||
|
this.overrides.delete(routeName);
|
||||||
|
await this.storageManager.delete(`${OVERRIDES_PREFIX}${routeName}.json`);
|
||||||
|
this.computeWarnings();
|
||||||
|
await this.applyRoutes();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Private: persistence
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
private async loadStoredRoutes(): Promise<void> {
|
||||||
|
const keys = await this.storageManager.list(ROUTES_PREFIX);
|
||||||
|
for (const key of keys) {
|
||||||
|
if (!key.endsWith('.json')) continue;
|
||||||
|
const stored = await this.storageManager.getJSON<IStoredRoute>(key);
|
||||||
|
if (stored?.id) {
|
||||||
|
this.storedRoutes.set(stored.id, stored);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.storedRoutes.size > 0) {
|
||||||
|
logger.log('info', `Loaded ${this.storedRoutes.size} programmatic route(s) from storage`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadOverrides(): Promise<void> {
|
||||||
|
const keys = await this.storageManager.list(OVERRIDES_PREFIX);
|
||||||
|
for (const key of keys) {
|
||||||
|
if (!key.endsWith('.json')) continue;
|
||||||
|
const override = await this.storageManager.getJSON<IRouteOverride>(key);
|
||||||
|
if (override?.routeName) {
|
||||||
|
this.overrides.set(override.routeName, override);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.overrides.size > 0) {
|
||||||
|
logger.log('info', `Loaded ${this.overrides.size} route override(s) from storage`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async persistRoute(stored: IStoredRoute): Promise<void> {
|
||||||
|
await this.storageManager.setJSON(`${ROUTES_PREFIX}${stored.id}.json`, stored);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Private: warnings
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
private computeWarnings(): void {
|
||||||
|
this.warnings = [];
|
||||||
|
const hardcodedNames = new Set(this.getHardcodedRoutes().map((r) => r.name || ''));
|
||||||
|
|
||||||
|
// Check overrides
|
||||||
|
for (const [routeName, override] of this.overrides) {
|
||||||
|
if (!hardcodedNames.has(routeName)) {
|
||||||
|
this.warnings.push({
|
||||||
|
type: 'orphaned-override',
|
||||||
|
routeName,
|
||||||
|
message: `Orphaned override for route '${routeName}' — hardcoded route no longer exists`,
|
||||||
|
});
|
||||||
|
} else if (!override.enabled) {
|
||||||
|
this.warnings.push({
|
||||||
|
type: 'disabled-hardcoded',
|
||||||
|
routeName,
|
||||||
|
message: `Route '${routeName}' is disabled via API override`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check disabled programmatic routes
|
||||||
|
for (const stored of this.storedRoutes.values()) {
|
||||||
|
if (!stored.enabled) {
|
||||||
|
const name = stored.route.name || stored.id;
|
||||||
|
this.warnings.push({
|
||||||
|
type: 'disabled-programmatic',
|
||||||
|
routeName: name,
|
||||||
|
message: `Programmatic route '${name}' (id: ${stored.id}) is disabled`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private logWarnings(): void {
|
||||||
|
for (const w of this.warnings) {
|
||||||
|
logger.log('warn', w.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Private: apply merged routes to SmartProxy
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
private async applyRoutes(): Promise<void> {
|
||||||
|
const smartProxy = this.getSmartProxy();
|
||||||
|
if (!smartProxy) return;
|
||||||
|
|
||||||
|
const enabledRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
||||||
|
|
||||||
|
// Add enabled hardcoded routes (respecting overrides)
|
||||||
|
for (const route of this.getHardcodedRoutes()) {
|
||||||
|
const name = route.name || '';
|
||||||
|
const override = this.overrides.get(name);
|
||||||
|
if (override && !override.enabled) {
|
||||||
|
continue; // Skip disabled hardcoded route
|
||||||
|
}
|
||||||
|
enabledRoutes.push(route);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add enabled programmatic routes
|
||||||
|
for (const stored of this.storedRoutes.values()) {
|
||||||
|
if (stored.enabled) {
|
||||||
|
enabledRoutes.push(stored.route);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await smartProxy.updateRoutes(enabledRoutes);
|
||||||
|
logger.log('info', `Applied ${enabledRoutes.length} routes to SmartProxy (${this.storedRoutes.size} programmatic, ${this.overrides.size} overrides)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
// Export validation tools only
|
// Export validation tools only
|
||||||
export * from './validator.js';
|
export * from './validator.js';
|
||||||
|
export { RouteConfigManager } from './classes.route-config-manager.js';
|
||||||
|
export { ApiTokenManager } from './classes.api-token-manager.js';
|
||||||
@@ -2,9 +2,10 @@ 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';
|
import { SecurityLogger, SecurityEventType } from '../security/classes.securitylogger.js';
|
||||||
|
import { logger } from '../logger.js';
|
||||||
|
|
||||||
export class MetricsManager {
|
export class MetricsManager {
|
||||||
private logger: plugins.smartlog.Smartlog;
|
private metricsLogger: plugins.smartlog.Smartlog;
|
||||||
private smartMetrics: plugins.smartmetrics.SmartMetrics;
|
private smartMetrics: plugins.smartmetrics.SmartMetrics;
|
||||||
private dcRouter: DcRouter;
|
private dcRouter: DcRouter;
|
||||||
private resetInterval?: NodeJS.Timeout;
|
private resetInterval?: NodeJS.Timeout;
|
||||||
@@ -56,15 +57,15 @@ export class MetricsManager {
|
|||||||
|
|
||||||
constructor(dcRouter: DcRouter) {
|
constructor(dcRouter: DcRouter) {
|
||||||
this.dcRouter = dcRouter;
|
this.dcRouter = dcRouter;
|
||||||
// Create a new Smartlog instance for metrics
|
// Create a Smartlog instance for SmartMetrics (requires its own instance)
|
||||||
this.logger = new plugins.smartlog.Smartlog({
|
this.metricsLogger = new plugins.smartlog.Smartlog({
|
||||||
logContext: {
|
logContext: {
|
||||||
environment: 'production',
|
environment: 'production',
|
||||||
runtime: 'node',
|
runtime: 'node',
|
||||||
zone: 'dcrouter-metrics',
|
zone: 'dcrouter-metrics',
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.smartMetrics = new plugins.smartmetrics.SmartMetrics(this.logger, 'dcrouter');
|
this.smartMetrics = new plugins.smartmetrics.SmartMetrics(this.metricsLogger, 'dcrouter');
|
||||||
// Initialize metrics cache with 500ms TTL
|
// Initialize metrics cache with 500ms TTL
|
||||||
this.metricsCache = new MetricsCache(500);
|
this.metricsCache = new MetricsCache(500);
|
||||||
}
|
}
|
||||||
@@ -109,9 +110,12 @@ export class MetricsManager {
|
|||||||
this.securityMetrics.incidents = [];
|
this.securityMetrics.incidents = [];
|
||||||
this.securityMetrics.lastResetDate = currentDate;
|
this.securityMetrics.lastResetDate = currentDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prune old time-series buckets every minute (don't wait for lazy query)
|
||||||
|
this.pruneOldBuckets();
|
||||||
}, 60000); // Check every minute
|
}, 60000); // Check every minute
|
||||||
|
|
||||||
this.logger.log('info', 'MetricsManager started');
|
logger.log('info', 'MetricsManager started');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async stop(): Promise<void> {
|
public async stop(): Promise<void> {
|
||||||
@@ -122,7 +126,13 @@ export class MetricsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.smartMetrics.stop();
|
this.smartMetrics.stop();
|
||||||
this.logger.log('info', 'MetricsManager stopped');
|
|
||||||
|
// Clear caches and time-series buckets on shutdown
|
||||||
|
this.metricsCache.clear();
|
||||||
|
this.emailMinuteBuckets.clear();
|
||||||
|
this.dnsMinuteBuckets.clear();
|
||||||
|
|
||||||
|
logger.log('info', 'MetricsManager stopped');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get server metrics from SmartMetrics and SmartProxy
|
// Get server metrics from SmartMetrics and SmartProxy
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ export class OpsServer {
|
|||||||
private emailOpsHandler: handlers.EmailOpsHandler;
|
private emailOpsHandler: handlers.EmailOpsHandler;
|
||||||
private certificateHandler: handlers.CertificateHandler;
|
private certificateHandler: handlers.CertificateHandler;
|
||||||
private remoteIngressHandler: handlers.RemoteIngressHandler;
|
private remoteIngressHandler: handlers.RemoteIngressHandler;
|
||||||
|
private routeManagementHandler: handlers.RouteManagementHandler;
|
||||||
|
private apiTokenHandler: handlers.ApiTokenHandler;
|
||||||
|
|
||||||
constructor(dcRouterRefArg: DcRouter) {
|
constructor(dcRouterRefArg: DcRouter) {
|
||||||
this.dcRouterRef = dcRouterRefArg;
|
this.dcRouterRef = dcRouterRefArg;
|
||||||
@@ -61,11 +63,17 @@ export class OpsServer {
|
|||||||
this.emailOpsHandler = new handlers.EmailOpsHandler(this);
|
this.emailOpsHandler = new handlers.EmailOpsHandler(this);
|
||||||
this.certificateHandler = new handlers.CertificateHandler(this);
|
this.certificateHandler = new handlers.CertificateHandler(this);
|
||||||
this.remoteIngressHandler = new handlers.RemoteIngressHandler(this);
|
this.remoteIngressHandler = new handlers.RemoteIngressHandler(this);
|
||||||
|
this.routeManagementHandler = new handlers.RouteManagementHandler(this);
|
||||||
|
this.apiTokenHandler = new handlers.ApiTokenHandler(this);
|
||||||
|
|
||||||
console.log('✅ OpsServer TypedRequest handlers initialized');
|
console.log('✅ OpsServer TypedRequest handlers initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async stop() {
|
public async stop() {
|
||||||
|
// Clean up log handler streams and push destination before stopping the server
|
||||||
|
if (this.logsHandler) {
|
||||||
|
this.logsHandler.cleanup();
|
||||||
|
}
|
||||||
if (this.server) {
|
if (this.server) {
|
||||||
await this.server.stop();
|
await this.server.stop();
|
||||||
}
|
}
|
||||||
|
|||||||
96
ts/opsserver/handlers/api-token.handler.ts
Normal file
96
ts/opsserver/handlers/api-token.handler.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import * as plugins from '../../plugins.js';
|
||||||
|
import type { OpsServer } from '../classes.opsserver.js';
|
||||||
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||||
|
|
||||||
|
export class ApiTokenHandler {
|
||||||
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
|
||||||
|
constructor(private opsServerRef: OpsServer) {
|
||||||
|
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
|
this.registerHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token management requires admin JWT only (tokens cannot manage tokens).
|
||||||
|
*/
|
||||||
|
private async requireAdmin(identity?: interfaces.data.IIdentity): Promise<string> {
|
||||||
|
if (!identity?.jwt) {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('unauthorized');
|
||||||
|
}
|
||||||
|
const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ identity });
|
||||||
|
if (!isAdmin) {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('admin access required');
|
||||||
|
}
|
||||||
|
return identity.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerHandlers(): void {
|
||||||
|
// Create API token
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateApiToken>(
|
||||||
|
'createApiToken',
|
||||||
|
async (dataArg) => {
|
||||||
|
const userId = await this.requireAdmin(dataArg.identity);
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
||||||
|
if (!manager) {
|
||||||
|
return { success: false, message: 'Token management not initialized' };
|
||||||
|
}
|
||||||
|
const result = await manager.createToken(
|
||||||
|
dataArg.name,
|
||||||
|
dataArg.scopes,
|
||||||
|
dataArg.expiresInDays ?? null,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
return { success: true, tokenId: result.id, tokenValue: result.rawToken };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// List API tokens
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListApiTokens>(
|
||||||
|
'listApiTokens',
|
||||||
|
async (dataArg) => {
|
||||||
|
await this.requireAdmin(dataArg.identity);
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
||||||
|
if (!manager) {
|
||||||
|
return { tokens: [] };
|
||||||
|
}
|
||||||
|
return { tokens: manager.listTokens() };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Revoke API token
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RevokeApiToken>(
|
||||||
|
'revokeApiToken',
|
||||||
|
async (dataArg) => {
|
||||||
|
await this.requireAdmin(dataArg.identity);
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
||||||
|
if (!manager) {
|
||||||
|
return { success: false, message: 'Token management not initialized' };
|
||||||
|
}
|
||||||
|
const ok = await manager.revokeToken(dataArg.id);
|
||||||
|
return { success: ok, message: ok ? undefined : 'Token not found' };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Toggle API token
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ToggleApiToken>(
|
||||||
|
'toggleApiToken',
|
||||||
|
async (dataArg) => {
|
||||||
|
await this.requireAdmin(dataArg.identity);
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
||||||
|
if (!manager) {
|
||||||
|
return { success: false, message: 'Token management not initialized' };
|
||||||
|
}
|
||||||
|
const ok = await manager.toggleToken(dataArg.id, dataArg.enabled);
|
||||||
|
return { success: ok, message: ok ? undefined : 'Token not found' };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
|
import * as paths from '../../paths.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';
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ export class ConfigHandler {
|
|||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetConfiguration>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetConfiguration>(
|
||||||
'getConfiguration',
|
'getConfiguration',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
const config = await this.getConfiguration(dataArg.section);
|
const config = await this.getConfiguration();
|
||||||
return {
|
return {
|
||||||
config,
|
config,
|
||||||
section: dataArg.section,
|
section: dataArg.section,
|
||||||
@@ -27,82 +28,174 @@ export class ConfigHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getConfiguration(section?: string): Promise<{
|
private async getConfiguration(): Promise<interfaces.requests.IConfigData> {
|
||||||
email: {
|
|
||||||
enabled: boolean;
|
|
||||||
ports: number[];
|
|
||||||
maxMessageSize: number;
|
|
||||||
rateLimits: {
|
|
||||||
perMinute: number;
|
|
||||||
perHour: number;
|
|
||||||
perDay: number;
|
|
||||||
};
|
|
||||||
domains?: string[];
|
|
||||||
};
|
|
||||||
dns: {
|
|
||||||
enabled: boolean;
|
|
||||||
port: number;
|
|
||||||
nameservers: string[];
|
|
||||||
caching: boolean;
|
|
||||||
ttl: number;
|
|
||||||
};
|
|
||||||
proxy: {
|
|
||||||
enabled: boolean;
|
|
||||||
httpPort: number;
|
|
||||||
httpsPort: number;
|
|
||||||
maxConnections: number;
|
|
||||||
};
|
|
||||||
security: {
|
|
||||||
blockList: string[];
|
|
||||||
rateLimit: boolean;
|
|
||||||
spamDetection: boolean;
|
|
||||||
tlsRequired: boolean;
|
|
||||||
};
|
|
||||||
}> {
|
|
||||||
const dcRouter = this.opsServerRef.dcRouterRef;
|
const dcRouter = this.opsServerRef.dcRouterRef;
|
||||||
|
const opts = dcRouter.options;
|
||||||
|
const resolvedPaths = dcRouter.resolvedPaths;
|
||||||
|
|
||||||
// Get email domains if email server is configured
|
// --- System ---
|
||||||
|
const storageBackend: 'filesystem' | 'custom' | 'memory' = opts.storage?.readFunction
|
||||||
|
? 'custom'
|
||||||
|
: opts.storage?.fsPath
|
||||||
|
? 'filesystem'
|
||||||
|
: 'memory';
|
||||||
|
|
||||||
|
// Resolve proxy IPs: fall back to SmartProxy's runtime proxyIPs if not in opts
|
||||||
|
let proxyIps = opts.proxyIps || [];
|
||||||
|
if (proxyIps.length === 0 && dcRouter.smartProxy) {
|
||||||
|
const spSettings = (dcRouter.smartProxy as any).settings;
|
||||||
|
if (spSettings?.proxyIPs?.length > 0) {
|
||||||
|
proxyIps = spSettings.proxyIPs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const system: interfaces.requests.IConfigData['system'] = {
|
||||||
|
baseDir: resolvedPaths.dcrouterHomeDir,
|
||||||
|
dataDir: resolvedPaths.dataDir,
|
||||||
|
publicIp: opts.publicIp || dcRouter.detectedPublicIp || null,
|
||||||
|
proxyIps,
|
||||||
|
uptime: Math.floor(process.uptime()),
|
||||||
|
storageBackend,
|
||||||
|
storagePath: opts.storage?.fsPath || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- SmartProxy ---
|
||||||
|
let acmeInfo: interfaces.requests.IConfigData['smartProxy']['acme'] = null;
|
||||||
|
if (opts.smartProxyConfig?.acme) {
|
||||||
|
const acme = opts.smartProxyConfig.acme;
|
||||||
|
acmeInfo = {
|
||||||
|
enabled: acme.enabled !== false,
|
||||||
|
accountEmail: acme.accountEmail || '',
|
||||||
|
useProduction: acme.useProduction !== false,
|
||||||
|
autoRenew: acme.autoRenew !== false,
|
||||||
|
renewThresholdDays: acme.renewThresholdDays || 30,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let routeCount = 0;
|
||||||
|
if (dcRouter.routeConfigManager) {
|
||||||
|
try {
|
||||||
|
const merged = await dcRouter.routeConfigManager.getMergedRoutes();
|
||||||
|
routeCount = merged.routes.length;
|
||||||
|
} catch {
|
||||||
|
routeCount = opts.smartProxyConfig?.routes?.length || 0;
|
||||||
|
}
|
||||||
|
} else if (opts.smartProxyConfig?.routes) {
|
||||||
|
routeCount = opts.smartProxyConfig.routes.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const smartProxy: interfaces.requests.IConfigData['smartProxy'] = {
|
||||||
|
enabled: !!dcRouter.smartProxy,
|
||||||
|
routeCount,
|
||||||
|
acme: acmeInfo,
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Email ---
|
||||||
let emailDomains: string[] = [];
|
let emailDomains: string[] = [];
|
||||||
if (dcRouter.emailServer && dcRouter.emailServer.domainRegistry) {
|
if (dcRouter.emailServer && (dcRouter.emailServer as any).domainRegistry) {
|
||||||
emailDomains = dcRouter.emailServer.domainRegistry.getAllDomains();
|
emailDomains = (dcRouter.emailServer as any).domainRegistry.getAllDomains();
|
||||||
} else if (dcRouter.options.emailConfig?.domains) {
|
} else if (opts.emailConfig?.domains) {
|
||||||
// Fallback: get domains from email config options
|
emailDomains = opts.emailConfig.domains.map((d: any) =>
|
||||||
emailDomains = dcRouter.options.emailConfig.domains.map(d =>
|
|
||||||
typeof d === 'string' ? d : d.domain
|
typeof d === 'string' ? d : d.domain
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
let portMapping: Record<string, number> | null = null;
|
||||||
email: {
|
if (opts.emailPortConfig?.portMapping) {
|
||||||
|
portMapping = {};
|
||||||
|
for (const [ext, int] of Object.entries(opts.emailPortConfig.portMapping)) {
|
||||||
|
portMapping[String(ext)] = int as number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const email: interfaces.requests.IConfigData['email'] = {
|
||||||
enabled: !!dcRouter.emailServer,
|
enabled: !!dcRouter.emailServer,
|
||||||
ports: dcRouter.emailServer ? [25, 465, 587, 2525] : [],
|
ports: opts.emailConfig?.ports || [],
|
||||||
maxMessageSize: 10 * 1024 * 1024, // 10MB default
|
portMapping,
|
||||||
rateLimits: {
|
hostname: opts.emailConfig?.hostname || null,
|
||||||
perMinute: 10,
|
|
||||||
perHour: 100,
|
|
||||||
perDay: 1000,
|
|
||||||
},
|
|
||||||
domains: emailDomains,
|
domains: emailDomains,
|
||||||
},
|
emailRouteCount: opts.emailConfig?.routes?.length || 0,
|
||||||
dns: {
|
receivedEmailsPath: opts.emailPortConfig?.receivedEmailsPath || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- DNS ---
|
||||||
|
const dnsRecords = (opts.dnsRecords || []).map(r => ({
|
||||||
|
name: r.name,
|
||||||
|
type: r.type,
|
||||||
|
value: r.value,
|
||||||
|
ttl: r.ttl,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const dns: interfaces.requests.IConfigData['dns'] = {
|
||||||
enabled: !!dcRouter.dnsServer,
|
enabled: !!dcRouter.dnsServer,
|
||||||
port: 53,
|
port: 53,
|
||||||
nameservers: dcRouter.options.dnsNsDomains || [],
|
nsDomains: opts.dnsNsDomains || [],
|
||||||
caching: true,
|
scopes: opts.dnsScopes || [],
|
||||||
ttl: 300,
|
recordCount: dnsRecords.length,
|
||||||
},
|
records: dnsRecords,
|
||||||
proxy: {
|
dnsChallenge: !!opts.dnsChallenge?.cloudflareApiKey,
|
||||||
enabled: !!dcRouter.smartProxy,
|
};
|
||||||
httpPort: 80,
|
|
||||||
httpsPort: 443,
|
// --- TLS ---
|
||||||
maxConnections: 1000,
|
let tlsSource: 'acme' | 'static' | 'none' = 'none';
|
||||||
},
|
if (opts.tls?.certPath && opts.tls?.keyPath) {
|
||||||
security: {
|
tlsSource = 'static';
|
||||||
blockList: [],
|
} else if (opts.smartProxyConfig?.acme?.enabled !== false && opts.smartProxyConfig?.acme) {
|
||||||
rateLimit: true,
|
tlsSource = 'acme';
|
||||||
spamDetection: true,
|
}
|
||||||
tlsRequired: false,
|
|
||||||
},
|
const tls: interfaces.requests.IConfigData['tls'] = {
|
||||||
|
contactEmail: opts.tls?.contactEmail || opts.smartProxyConfig?.acme?.accountEmail || null,
|
||||||
|
domain: opts.tls?.domain || null,
|
||||||
|
source: tlsSource,
|
||||||
|
certPath: opts.tls?.certPath || null,
|
||||||
|
keyPath: opts.tls?.keyPath || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Cache ---
|
||||||
|
const cacheConfig = opts.cacheConfig;
|
||||||
|
const cache: interfaces.requests.IConfigData['cache'] = {
|
||||||
|
enabled: cacheConfig?.enabled !== false,
|
||||||
|
storagePath: cacheConfig?.storagePath || resolvedPaths.defaultTsmDbPath,
|
||||||
|
dbName: cacheConfig?.dbName || 'dcrouter',
|
||||||
|
defaultTTLDays: cacheConfig?.defaultTTLDays || 30,
|
||||||
|
cleanupIntervalHours: cacheConfig?.cleanupIntervalHours || 1,
|
||||||
|
ttlConfig: cacheConfig?.ttlConfig ? { ...cacheConfig.ttlConfig } as Record<string, number> : {},
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- RADIUS ---
|
||||||
|
const radiusCfg = opts.radiusConfig;
|
||||||
|
const radius: interfaces.requests.IConfigData['radius'] = {
|
||||||
|
enabled: !!dcRouter.radiusServer,
|
||||||
|
authPort: radiusCfg?.authPort || null,
|
||||||
|
acctPort: radiusCfg?.acctPort || null,
|
||||||
|
bindAddress: radiusCfg?.bindAddress || null,
|
||||||
|
clientCount: radiusCfg?.clients?.length || 0,
|
||||||
|
vlanDefaultVlan: radiusCfg?.vlanAssignment?.defaultVlan ?? null,
|
||||||
|
vlanAllowUnknownMacs: radiusCfg?.vlanAssignment?.allowUnknownMacs ?? null,
|
||||||
|
vlanMappingCount: radiusCfg?.vlanAssignment?.mappings?.length || 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Remote Ingress ---
|
||||||
|
const riCfg = opts.remoteIngressConfig;
|
||||||
|
const connectedEdgeIps = dcRouter.tunnelManager?.getConnectedEdgeIps() || [];
|
||||||
|
const remoteIngress: interfaces.requests.IConfigData['remoteIngress'] = {
|
||||||
|
enabled: !!dcRouter.remoteIngressManager,
|
||||||
|
tunnelPort: riCfg?.tunnelPort || null,
|
||||||
|
hubDomain: riCfg?.hubDomain || null,
|
||||||
|
tlsConfigured: !!(riCfg?.tls?.certPath && riCfg?.tls?.keyPath),
|
||||||
|
connectedEdgeIps,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
system,
|
||||||
|
smartProxy,
|
||||||
|
email,
|
||||||
|
dns,
|
||||||
|
tls,
|
||||||
|
cache,
|
||||||
|
radius,
|
||||||
|
remoteIngress,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
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 { SecurityLogger } from '../../security/index.js';
|
|
||||||
|
|
||||||
export class EmailOpsHandler {
|
export class EmailOpsHandler {
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
@@ -13,68 +12,24 @@ export class EmailOpsHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private registerHandlers(): void {
|
private registerHandlers(): void {
|
||||||
// Get Queued Emails Handler
|
// Get All Emails Handler
|
||||||
this.typedrouter.addTypedHandler(
|
this.typedrouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetQueuedEmails>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetAllEmails>(
|
||||||
'getQueuedEmails',
|
'getAllEmails',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
|
const emails = this.getAllQueueEmails();
|
||||||
if (!emailServer?.deliveryQueue) {
|
return { emails };
|
||||||
return { items: [], total: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
const queue = emailServer.deliveryQueue;
|
|
||||||
const stats = queue.getStats();
|
|
||||||
|
|
||||||
// Get all queue items and filter by status if provided
|
|
||||||
const items = this.getQueueItems(
|
|
||||||
dataArg.status,
|
|
||||||
dataArg.limit || 50,
|
|
||||||
dataArg.offset || 0
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
items,
|
|
||||||
total: stats.queueSize,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get Sent Emails Handler
|
// Get Email Detail Handler
|
||||||
this.typedrouter.addTypedHandler(
|
this.typedrouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSentEmails>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetEmailDetail>(
|
||||||
'getSentEmails',
|
'getEmailDetail',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
const items = this.getQueueItems(
|
const email = this.getEmailDetail(dataArg.emailId);
|
||||||
'delivered',
|
return { email };
|
||||||
dataArg.limit || 50,
|
|
||||||
dataArg.offset || 0
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
items,
|
|
||||||
total: items.length, // Note: total would ideally come from a counter
|
|
||||||
};
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get Failed Emails Handler
|
|
||||||
this.typedrouter.addTypedHandler(
|
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetFailedEmails>(
|
|
||||||
'getFailedEmails',
|
|
||||||
async (dataArg) => {
|
|
||||||
const items = this.getQueueItems(
|
|
||||||
'failed',
|
|
||||||
dataArg.limit || 50,
|
|
||||||
dataArg.offset || 0
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
items,
|
|
||||||
total: items.length,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -101,17 +56,12 @@ export class EmailOpsHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Re-enqueue the failed email by creating a new queue entry
|
|
||||||
// with the same data but reset attempt count
|
|
||||||
const newQueueId = await queue.enqueue(
|
const newQueueId = await queue.enqueue(
|
||||||
item.processingResult,
|
item.processingResult,
|
||||||
item.processingMode,
|
item.processingMode,
|
||||||
item.route
|
item.route
|
||||||
);
|
);
|
||||||
|
|
||||||
// Optionally remove the old failed entry
|
|
||||||
await queue.removeItem(dataArg.emailId);
|
await queue.removeItem(dataArg.emailId);
|
||||||
|
|
||||||
return { success: true, newQueueId };
|
return { success: true, newQueueId };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
@@ -122,197 +72,199 @@ export class EmailOpsHandler {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get Security Incidents Handler
|
|
||||||
this.typedrouter.addTypedHandler(
|
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSecurityIncidents>(
|
|
||||||
'getSecurityIncidents',
|
|
||||||
async (dataArg) => {
|
|
||||||
const securityLogger = SecurityLogger.getInstance();
|
|
||||||
|
|
||||||
const filter: {
|
|
||||||
level?: any;
|
|
||||||
type?: any;
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
if (dataArg.level) {
|
|
||||||
filter.level = dataArg.level;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dataArg.type) {
|
|
||||||
filter.type = dataArg.type;
|
|
||||||
}
|
|
||||||
|
|
||||||
const incidents = securityLogger.getRecentEvents(
|
|
||||||
dataArg.limit || 100,
|
|
||||||
Object.keys(filter).length > 0 ? filter : undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
incidents: incidents.map(event => ({
|
|
||||||
timestamp: event.timestamp,
|
|
||||||
level: event.level as interfaces.requests.TSecurityLogLevel,
|
|
||||||
type: event.type as interfaces.requests.TSecurityEventType,
|
|
||||||
message: event.message,
|
|
||||||
details: event.details,
|
|
||||||
ipAddress: event.ipAddress,
|
|
||||||
userId: event.userId,
|
|
||||||
sessionId: event.sessionId,
|
|
||||||
emailId: event.emailId,
|
|
||||||
domain: event.domain,
|
|
||||||
action: event.action,
|
|
||||||
result: event.result,
|
|
||||||
success: event.success,
|
|
||||||
})),
|
|
||||||
total: incidents.length,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get Bounce Records Handler
|
|
||||||
this.typedrouter.addTypedHandler(
|
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetBounceRecords>(
|
|
||||||
'getBounceRecords',
|
|
||||||
async (dataArg) => {
|
|
||||||
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
|
|
||||||
|
|
||||||
if (!emailServer) {
|
|
||||||
return { records: [], suppressionList: [], total: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use smartmta's public API for bounce/suppression data
|
|
||||||
const suppressionList = emailServer.getSuppressionList();
|
|
||||||
const hardBouncedAddresses = emailServer.getHardBouncedAddresses();
|
|
||||||
|
|
||||||
// Create bounce records from the available data
|
|
||||||
const records: interfaces.requests.IBounceRecord[] = [];
|
|
||||||
|
|
||||||
for (const email of hardBouncedAddresses) {
|
|
||||||
const bounceInfo = emailServer.getBounceHistory(email);
|
|
||||||
if (bounceInfo) {
|
|
||||||
records.push({
|
|
||||||
id: `bounce-${email}`,
|
|
||||||
recipient: email,
|
|
||||||
sender: '',
|
|
||||||
domain: email.split('@')[1] || '',
|
|
||||||
bounceType: (bounceInfo as any).type as interfaces.requests.TBounceType,
|
|
||||||
bounceCategory: (bounceInfo as any).category as interfaces.requests.TBounceCategory,
|
|
||||||
timestamp: (bounceInfo as any).lastBounce,
|
|
||||||
processed: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply limit and offset
|
|
||||||
const limit = dataArg.limit || 50;
|
|
||||||
const offset = dataArg.offset || 0;
|
|
||||||
const paginatedRecords = records.slice(offset, offset + limit);
|
|
||||||
|
|
||||||
return {
|
|
||||||
records: paginatedRecords,
|
|
||||||
suppressionList,
|
|
||||||
total: records.length,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Remove from Suppression List Handler
|
|
||||||
this.typedrouter.addTypedHandler(
|
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RemoveFromSuppressionList>(
|
|
||||||
'removeFromSuppressionList',
|
|
||||||
async (dataArg) => {
|
|
||||||
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
|
|
||||||
|
|
||||||
if (!emailServer) {
|
|
||||||
return { success: false, error: 'Email server not available' };
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
emailServer.removeFromSuppressionList(dataArg.email);
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Failed to remove from suppression list'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method to get queue items with filtering and pagination
|
* Get all queue items mapped to catalog IEmail format
|
||||||
*/
|
*/
|
||||||
private getQueueItems(
|
private getAllQueueEmails(): interfaces.requests.IEmail[] {
|
||||||
status?: interfaces.requests.TEmailQueueStatus,
|
|
||||||
limit: number = 50,
|
|
||||||
offset: number = 0
|
|
||||||
): interfaces.requests.IEmailQueueItem[] {
|
|
||||||
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
|
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
|
||||||
if (!emailServer?.deliveryQueue) {
|
if (!emailServer?.deliveryQueue) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const queue = emailServer.deliveryQueue;
|
const queue = emailServer.deliveryQueue;
|
||||||
const items: interfaces.requests.IEmailQueueItem[] = [];
|
|
||||||
|
|
||||||
// Access the internal queue map via reflection
|
|
||||||
// This is necessary because the queue doesn't expose iteration methods
|
|
||||||
const queueMap = (queue as any).queue as Map<string, any>;
|
const queueMap = (queue as any).queue as Map<string, any>;
|
||||||
|
|
||||||
if (!queueMap) {
|
if (!queueMap) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter and convert items
|
const emails: interfaces.requests.IEmail[] = [];
|
||||||
|
|
||||||
for (const [id, item] of queueMap.entries()) {
|
for (const [id, item] of queueMap.entries()) {
|
||||||
// Apply status filter if provided
|
emails.push(this.mapQueueItemToEmail(item));
|
||||||
if (status && item.status !== status) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract email details from processingResult if available
|
|
||||||
const processingResult = item.processingResult;
|
|
||||||
let from = '';
|
|
||||||
let to: string[] = [];
|
|
||||||
let subject = '';
|
|
||||||
|
|
||||||
if (processingResult) {
|
|
||||||
// Check if it's an Email object or raw email data
|
|
||||||
if (processingResult.email) {
|
|
||||||
from = processingResult.email.from || '';
|
|
||||||
to = processingResult.email.to || [];
|
|
||||||
subject = processingResult.email.subject || '';
|
|
||||||
} else if (processingResult.from) {
|
|
||||||
from = processingResult.from;
|
|
||||||
to = processingResult.to || [];
|
|
||||||
subject = processingResult.subject || '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
items.push({
|
|
||||||
id: item.id,
|
|
||||||
processingMode: item.processingMode,
|
|
||||||
status: item.status,
|
|
||||||
attempts: item.attempts,
|
|
||||||
nextAttempt: item.nextAttempt instanceof Date ? item.nextAttempt.getTime() : item.nextAttempt,
|
|
||||||
lastError: item.lastError,
|
|
||||||
createdAt: item.createdAt instanceof Date ? item.createdAt.getTime() : item.createdAt,
|
|
||||||
updatedAt: item.updatedAt instanceof Date ? item.updatedAt.getTime() : item.updatedAt,
|
|
||||||
deliveredAt: item.deliveredAt instanceof Date ? item.deliveredAt.getTime() : item.deliveredAt,
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
subject,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by createdAt descending (newest first)
|
// Sort by createdAt descending (newest first)
|
||||||
items.sort((a, b) => b.createdAt - a.createdAt);
|
emails.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
||||||
|
|
||||||
// Apply pagination
|
return emails;
|
||||||
return items.slice(offset, offset + limit);
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single email detail by ID
|
||||||
|
*/
|
||||||
|
private getEmailDetail(emailId: string): interfaces.requests.IEmailDetail | null {
|
||||||
|
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
|
||||||
|
if (!emailServer?.deliveryQueue) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const queue = emailServer.deliveryQueue;
|
||||||
|
const item = queue.getItem(emailId);
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.mapQueueItemToEmailDetail(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map a queue item to catalog IEmail format
|
||||||
|
*/
|
||||||
|
private mapQueueItemToEmail(item: any): interfaces.requests.IEmail {
|
||||||
|
const processingResult = item.processingResult;
|
||||||
|
let from = '';
|
||||||
|
let to = '';
|
||||||
|
let subject = '';
|
||||||
|
let messageId = '';
|
||||||
|
let size = '0 B';
|
||||||
|
|
||||||
|
if (processingResult) {
|
||||||
|
if (processingResult.email) {
|
||||||
|
from = processingResult.email.from || '';
|
||||||
|
to = (processingResult.email.to || [])[0] || '';
|
||||||
|
subject = processingResult.email.subject || '';
|
||||||
|
} else if (processingResult.from) {
|
||||||
|
from = processingResult.from;
|
||||||
|
to = (processingResult.to || [])[0] || '';
|
||||||
|
subject = processingResult.subject || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get messageId
|
||||||
|
if (typeof processingResult.getMessageId === 'function') {
|
||||||
|
try {
|
||||||
|
messageId = processingResult.getMessageId() || '';
|
||||||
|
} catch {
|
||||||
|
messageId = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute approximate size
|
||||||
|
const textLen = processingResult.text?.length || 0;
|
||||||
|
const htmlLen = processingResult.html?.length || 0;
|
||||||
|
let attachSize = 0;
|
||||||
|
if (typeof processingResult.getAttachmentsSize === 'function') {
|
||||||
|
try {
|
||||||
|
attachSize = processingResult.getAttachmentsSize() || 0;
|
||||||
|
} catch {
|
||||||
|
attachSize = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
size = this.formatSize(textLen + htmlLen + attachSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map queue status to catalog TEmailStatus
|
||||||
|
const status = this.mapStatus(item.status);
|
||||||
|
|
||||||
|
const createdAt = item.createdAt instanceof Date ? item.createdAt.getTime() : item.createdAt;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
direction: 'outbound' as interfaces.requests.TEmailDirection,
|
||||||
|
status,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
subject,
|
||||||
|
timestamp: new Date(createdAt).toISOString(),
|
||||||
|
messageId,
|
||||||
|
size,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map a queue item to catalog IEmailDetail format
|
||||||
|
*/
|
||||||
|
private mapQueueItemToEmailDetail(item: any): interfaces.requests.IEmailDetail {
|
||||||
|
const base = this.mapQueueItemToEmail(item);
|
||||||
|
const processingResult = item.processingResult;
|
||||||
|
|
||||||
|
let toList: string[] = [];
|
||||||
|
let cc: string[] = [];
|
||||||
|
let headers: Record<string, string> = {};
|
||||||
|
let body = '';
|
||||||
|
|
||||||
|
if (processingResult) {
|
||||||
|
if (processingResult.email) {
|
||||||
|
toList = processingResult.email.to || [];
|
||||||
|
cc = processingResult.email.cc || [];
|
||||||
|
} else {
|
||||||
|
toList = processingResult.to || [];
|
||||||
|
cc = processingResult.cc || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = processingResult.headers || {};
|
||||||
|
body = processingResult.html || processingResult.text || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
toList,
|
||||||
|
cc,
|
||||||
|
smtpLog: [],
|
||||||
|
connectionInfo: {
|
||||||
|
sourceIp: '',
|
||||||
|
sourceHostname: '',
|
||||||
|
destinationIp: '',
|
||||||
|
destinationPort: 0,
|
||||||
|
tlsVersion: '',
|
||||||
|
tlsCipher: '',
|
||||||
|
authenticated: false,
|
||||||
|
authMethod: '',
|
||||||
|
authUser: '',
|
||||||
|
},
|
||||||
|
authenticationResults: {
|
||||||
|
spf: 'none',
|
||||||
|
spfDomain: '',
|
||||||
|
dkim: 'none',
|
||||||
|
dkimDomain: '',
|
||||||
|
dmarc: 'none',
|
||||||
|
dmarcPolicy: '',
|
||||||
|
},
|
||||||
|
rejectionReason: item.status === 'failed' ? item.lastError : undefined,
|
||||||
|
bounceMessage: item.status === 'failed' ? item.lastError : undefined,
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map queue status to catalog TEmailStatus
|
||||||
|
*/
|
||||||
|
private mapStatus(queueStatus: string): interfaces.requests.TEmailStatus {
|
||||||
|
switch (queueStatus) {
|
||||||
|
case 'pending':
|
||||||
|
case 'processing':
|
||||||
|
return 'pending';
|
||||||
|
case 'delivered':
|
||||||
|
return 'delivered';
|
||||||
|
case 'failed':
|
||||||
|
return 'bounced';
|
||||||
|
case 'deferred':
|
||||||
|
return 'deferred';
|
||||||
|
default:
|
||||||
|
return 'pending';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format byte size to human-readable string
|
||||||
|
*/
|
||||||
|
private formatSize(bytes: number): string {
|
||||||
|
if (bytes < 1024) return `${bytes} B`;
|
||||||
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||||
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,3 +7,5 @@ export * from './radius.handler.js';
|
|||||||
export * from './email-ops.handler.js';
|
export * from './email-ops.handler.js';
|
||||||
export * from './certificate.handler.js';
|
export * from './certificate.handler.js';
|
||||||
export * from './remoteingress.handler.js';
|
export * from './remoteingress.handler.js';
|
||||||
|
export * from './route-management.handler.js';
|
||||||
|
export * from './api-token.handler.js';
|
||||||
@@ -3,8 +3,15 @@ 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';
|
import { logBuffer, baseLogger } from '../../logger.js';
|
||||||
|
|
||||||
|
// Module-level singleton: the log push destination is added once and reuses
|
||||||
|
// the current OpsServer reference so it survives OpsServer restarts without
|
||||||
|
// accumulating duplicate destinations.
|
||||||
|
let logPushDestinationInstalled = false;
|
||||||
|
let currentOpsServerRef: OpsServer | null = null;
|
||||||
|
|
||||||
export class LogsHandler {
|
export class LogsHandler {
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
private activeStreamStops: Set<() => void> = new Set();
|
||||||
|
|
||||||
constructor(private opsServerRef: OpsServer) {
|
constructor(private opsServerRef: OpsServer) {
|
||||||
// Add this handler's router to the parent
|
// Add this handler's router to the parent
|
||||||
@@ -13,6 +20,20 @@ export class LogsHandler {
|
|||||||
this.setupLogPushDestination();
|
this.setupLogPushDestination();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up all active log streams and deactivate the push destination.
|
||||||
|
* Called when OpsServer stops.
|
||||||
|
*/
|
||||||
|
public cleanup(): void {
|
||||||
|
// Stop all active follow-mode log streams
|
||||||
|
for (const stop of this.activeStreamStops) {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
this.activeStreamStops.clear();
|
||||||
|
// Deactivate the push destination (it stays registered but becomes a no-op)
|
||||||
|
currentOpsServerRef = null;
|
||||||
|
}
|
||||||
|
|
||||||
private registerHandlers(): void {
|
private registerHandlers(): void {
|
||||||
// Get Recent Logs Handler
|
// Get Recent Logs Handler
|
||||||
this.typedrouter.addTypedHandler(
|
this.typedrouter.addTypedHandler(
|
||||||
@@ -30,8 +51,8 @@ export class LogsHandler {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
logs,
|
logs,
|
||||||
total: logs.length, // TODO: Implement proper total count
|
total: logs.length,
|
||||||
hasMore: false, // TODO: Implement proper pagination
|
hasMore: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -56,10 +77,11 @@ export class LogsHandler {
|
|||||||
// Start streaming
|
// Start streaming
|
||||||
streamLogs.start();
|
streamLogs.start();
|
||||||
|
|
||||||
// VirtualStream handles cleanup automatically
|
// Track the stop function so we can clean up on shutdown
|
||||||
|
this.activeStreamStops.add(streamLogs.stop);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
logStream: virtualStream as any, // Cast to IVirtualStream interface
|
logStream: virtualStream as any,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -169,14 +191,26 @@ export class LogsHandler {
|
|||||||
/**
|
/**
|
||||||
* Add a log destination to the base logger that pushes entries
|
* Add a log destination to the base logger that pushes entries
|
||||||
* to all connected ops_dashboard TypedSocket clients.
|
* to all connected ops_dashboard TypedSocket clients.
|
||||||
|
*
|
||||||
|
* Uses a module-level singleton so the destination is added only once,
|
||||||
|
* even across OpsServer restart cycles. The destination reads
|
||||||
|
* `currentOpsServerRef` dynamically so it always uses the active server.
|
||||||
*/
|
*/
|
||||||
private setupLogPushDestination(): void {
|
private setupLogPushDestination(): void {
|
||||||
const opsServerRef = this.opsServerRef;
|
// Update the module-level reference so the existing destination uses the new server
|
||||||
|
currentOpsServerRef = this.opsServerRef;
|
||||||
|
|
||||||
|
if (logPushDestinationInstalled) {
|
||||||
|
return; // destination already registered — just updated the ref
|
||||||
|
}
|
||||||
|
logPushDestinationInstalled = true;
|
||||||
|
|
||||||
baseLogger.addLogDestination({
|
baseLogger.addLogDestination({
|
||||||
async handleLog(logPackage: any) {
|
async handleLog(logPackage: any) {
|
||||||
// Access the TypedSocket server instance from OpsServer
|
const opsServer = currentOpsServerRef;
|
||||||
const typedsocket = opsServerRef.server?.typedserver?.typedsocket;
|
if (!opsServer) return;
|
||||||
|
|
||||||
|
const typedsocket = opsServer.server?.typedserver?.typedsocket;
|
||||||
if (!typedsocket) return;
|
if (!typedsocket) return;
|
||||||
|
|
||||||
let connections: any[];
|
let connections: any[];
|
||||||
@@ -220,8 +254,18 @@ export class LogsHandler {
|
|||||||
stop: () => void;
|
stop: () => void;
|
||||||
} {
|
} {
|
||||||
let intervalId: NodeJS.Timeout | null = null;
|
let intervalId: NodeJS.Timeout | null = null;
|
||||||
|
let stopped = false;
|
||||||
let logIndex = 0;
|
let logIndex = 0;
|
||||||
|
|
||||||
|
const stop = () => {
|
||||||
|
stopped = true;
|
||||||
|
if (intervalId) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
intervalId = null;
|
||||||
|
}
|
||||||
|
this.activeStreamStops.delete(stop);
|
||||||
|
};
|
||||||
|
|
||||||
const start = () => {
|
const start = () => {
|
||||||
if (!follow) {
|
if (!follow) {
|
||||||
// Send existing logs and close
|
// Send existing logs and close
|
||||||
@@ -236,13 +280,19 @@ export class LogsHandler {
|
|||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
virtualStream.sendData(encoder.encode(logData));
|
virtualStream.sendData(encoder.encode(logData));
|
||||||
});
|
});
|
||||||
// VirtualStream doesn't have end() method - it closes automatically
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For follow mode, simulate real-time log streaming
|
// For follow mode, simulate real-time log streaming
|
||||||
intervalId = setInterval(async () => {
|
intervalId = setInterval(async () => {
|
||||||
|
if (stopped) {
|
||||||
|
// Guard: clear interval if stop() was called between ticks
|
||||||
|
clearInterval(intervalId!);
|
||||||
|
intervalId = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const categories: Array<'smtp' | 'dns' | 'security' | 'system' | 'email'> = ['smtp', 'dns', 'security', 'system', 'email'];
|
const 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'];
|
||||||
|
|
||||||
@@ -266,28 +316,19 @@ export class LogsHandler {
|
|||||||
const logData = JSON.stringify(logEntry);
|
const logData = JSON.stringify(logEntry);
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
try {
|
try {
|
||||||
await virtualStream.sendData(encoder.encode(logData));
|
// Use a timeout to detect hung streams (sendData can hang if the
|
||||||
|
// VirtualStream's keepAlive loop has ended)
|
||||||
|
await Promise.race([
|
||||||
|
virtualStream.sendData(encoder.encode(logData)),
|
||||||
|
new Promise<never>((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error('stream send timeout')), 10_000)
|
||||||
|
),
|
||||||
|
]);
|
||||||
} catch {
|
} catch {
|
||||||
// Stream closed or errored — clean up to prevent interval leak
|
// Stream closed, errored, or timed out — clean up
|
||||||
clearInterval(intervalId!);
|
stop();
|
||||||
intervalId = null;
|
|
||||||
}
|
}
|
||||||
}, 2000); // Send a log every 2 seconds
|
}, 2000);
|
||||||
|
|
||||||
// TODO: Hook into actual logger events
|
|
||||||
// logger.on('log', (logEntry) => {
|
|
||||||
// if (matchesCriteria(logEntry, level, service)) {
|
|
||||||
// virtualStream.sendData(formatLogEntry(logEntry));
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
};
|
|
||||||
|
|
||||||
const stop = () => {
|
|
||||||
if (intervalId) {
|
|
||||||
clearInterval(intervalId);
|
|
||||||
intervalId = null;
|
|
||||||
}
|
|
||||||
// TODO: Unhook from logger events
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return { start, stop };
|
return { start, stop };
|
||||||
|
|||||||
163
ts/opsserver/handlers/route-management.handler.ts
Normal file
163
ts/opsserver/handlers/route-management.handler.ts
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import * as plugins from '../../plugins.js';
|
||||||
|
import type { OpsServer } from '../classes.opsserver.js';
|
||||||
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||||
|
|
||||||
|
export class RouteManagementHandler {
|
||||||
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
|
||||||
|
constructor(private opsServerRef: OpsServer) {
|
||||||
|
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
|
this.registerHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate auth: JWT identity OR API token with required scope.
|
||||||
|
* Returns a userId string on success, throws on failure.
|
||||||
|
*/
|
||||||
|
private async requireAuth(
|
||||||
|
request: { identity?: interfaces.data.IIdentity; apiToken?: string },
|
||||||
|
requiredScope?: interfaces.data.TApiTokenScope,
|
||||||
|
): Promise<string> {
|
||||||
|
// Try JWT identity first
|
||||||
|
if (request.identity?.jwt) {
|
||||||
|
try {
|
||||||
|
const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({
|
||||||
|
identity: request.identity,
|
||||||
|
});
|
||||||
|
if (isAdmin) return request.identity.userId;
|
||||||
|
} catch { /* fall through */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try API token
|
||||||
|
if (request.apiToken) {
|
||||||
|
const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
||||||
|
if (tokenManager) {
|
||||||
|
const token = await tokenManager.validateToken(request.apiToken);
|
||||||
|
if (token) {
|
||||||
|
if (!requiredScope || tokenManager.hasScope(token, requiredScope)) {
|
||||||
|
return token.createdBy;
|
||||||
|
}
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('insufficient scope');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerHandlers(): void {
|
||||||
|
// Get merged routes
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetMergedRoutes>(
|
||||||
|
'getMergedRoutes',
|
||||||
|
async (dataArg) => {
|
||||||
|
await this.requireAuth(dataArg, 'routes:read');
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
|
||||||
|
if (!manager) {
|
||||||
|
return { routes: [], warnings: [] };
|
||||||
|
}
|
||||||
|
return manager.getMergedRoutes();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create route
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateRoute>(
|
||||||
|
'createRoute',
|
||||||
|
async (dataArg) => {
|
||||||
|
const userId = await this.requireAuth(dataArg, 'routes:write');
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
|
||||||
|
if (!manager) {
|
||||||
|
return { success: false, message: 'Route management not initialized' };
|
||||||
|
}
|
||||||
|
const id = await manager.createRoute(dataArg.route, userId, dataArg.enabled ?? true);
|
||||||
|
return { success: true, storedRouteId: id };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update route
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateRoute>(
|
||||||
|
'updateRoute',
|
||||||
|
async (dataArg) => {
|
||||||
|
await this.requireAuth(dataArg, 'routes:write');
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
|
||||||
|
if (!manager) {
|
||||||
|
return { success: false, message: 'Route management not initialized' };
|
||||||
|
}
|
||||||
|
const ok = await manager.updateRoute(dataArg.id, {
|
||||||
|
route: dataArg.route as any,
|
||||||
|
enabled: dataArg.enabled,
|
||||||
|
});
|
||||||
|
return { success: ok, message: ok ? undefined : 'Route not found' };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete route
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteRoute>(
|
||||||
|
'deleteRoute',
|
||||||
|
async (dataArg) => {
|
||||||
|
await this.requireAuth(dataArg, 'routes:write');
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
|
||||||
|
if (!manager) {
|
||||||
|
return { success: false, message: 'Route management not initialized' };
|
||||||
|
}
|
||||||
|
const ok = await manager.deleteRoute(dataArg.id);
|
||||||
|
return { success: ok, message: ok ? undefined : 'Route not found' };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set override on a hardcoded route
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SetRouteOverride>(
|
||||||
|
'setRouteOverride',
|
||||||
|
async (dataArg) => {
|
||||||
|
const userId = await this.requireAuth(dataArg, 'routes:write');
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
|
||||||
|
if (!manager) {
|
||||||
|
return { success: false, message: 'Route management not initialized' };
|
||||||
|
}
|
||||||
|
await manager.setOverride(dataArg.routeName, dataArg.enabled, userId);
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove override from a hardcoded route
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RemoveRouteOverride>(
|
||||||
|
'removeRouteOverride',
|
||||||
|
async (dataArg) => {
|
||||||
|
await this.requireAuth(dataArg, 'routes:write');
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
|
||||||
|
if (!manager) {
|
||||||
|
return { success: false, message: 'Route management not initialized' };
|
||||||
|
}
|
||||||
|
const ok = await manager.removeOverride(dataArg.routeName);
|
||||||
|
return { success: ok, message: ok ? undefined : 'Override not found' };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Toggle programmatic route
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ToggleRoute>(
|
||||||
|
'toggleRoute',
|
||||||
|
async (dataArg) => {
|
||||||
|
await this.requireAuth(dataArg, 'routes:write');
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
|
||||||
|
if (!manager) {
|
||||||
|
return { success: false, message: 'Route management not initialized' };
|
||||||
|
}
|
||||||
|
const ok = await manager.toggleRoute(dataArg.id, dataArg.enabled);
|
||||||
|
return { success: ok, message: ok ? undefined : 'Route not found' };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ export class TunnelManager {
|
|||||||
private manager: RemoteIngressManager;
|
private manager: RemoteIngressManager;
|
||||||
private config: ITunnelManagerConfig;
|
private config: ITunnelManagerConfig;
|
||||||
private edgeStatuses: Map<string, IRemoteIngressStatus> = new Map();
|
private edgeStatuses: Map<string, IRemoteIngressStatus> = new Map();
|
||||||
|
private reconcileInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
constructor(manager: RemoteIngressManager, config: ITunnelManagerConfig = {}) {
|
constructor(manager: RemoteIngressManager, config: ITunnelManagerConfig = {}) {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
@@ -22,12 +23,11 @@ export class TunnelManager {
|
|||||||
this.hub = new plugins.remoteingress.RemoteIngressHub();
|
this.hub = new plugins.remoteingress.RemoteIngressHub();
|
||||||
|
|
||||||
// Listen for edge connect/disconnect events
|
// Listen for edge connect/disconnect events
|
||||||
this.hub.on('edgeConnected', (data: { edgeId: string }) => {
|
this.hub.on('edgeConnected', (data: { edgeId: string; peerAddr: string }) => {
|
||||||
const existing = this.edgeStatuses.get(data.edgeId);
|
|
||||||
this.edgeStatuses.set(data.edgeId, {
|
this.edgeStatuses.set(data.edgeId, {
|
||||||
edgeId: data.edgeId,
|
edgeId: data.edgeId,
|
||||||
connected: true,
|
connected: true,
|
||||||
publicIp: existing?.publicIp ?? null,
|
publicIp: data.peerAddr || null,
|
||||||
activeTunnels: 0,
|
activeTunnels: 0,
|
||||||
lastHeartbeat: Date.now(),
|
lastHeartbeat: Date.now(),
|
||||||
connectedAt: Date.now(),
|
connectedAt: Date.now(),
|
||||||
@@ -35,11 +35,7 @@ export class TunnelManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.hub.on('edgeDisconnected', (data: { edgeId: string }) => {
|
this.hub.on('edgeDisconnected', (data: { edgeId: string }) => {
|
||||||
const existing = this.edgeStatuses.get(data.edgeId);
|
this.edgeStatuses.delete(data.edgeId);
|
||||||
if (existing) {
|
|
||||||
existing.connected = false;
|
|
||||||
existing.activeTunnels = 0;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.hub.on('streamOpened', (data: { edgeId: string; streamId: number }) => {
|
this.hub.on('streamOpened', (data: { edgeId: string; streamId: number }) => {
|
||||||
@@ -69,16 +65,68 @@ export class TunnelManager {
|
|||||||
|
|
||||||
// Send allowed edges to the hub
|
// Send allowed edges to the hub
|
||||||
await this.syncAllowedEdges();
|
await this.syncAllowedEdges();
|
||||||
|
|
||||||
|
// Periodically reconcile with authoritative Rust hub status
|
||||||
|
this.reconcileInterval = setInterval(() => {
|
||||||
|
this.reconcile().catch(() => {});
|
||||||
|
}, 15_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the tunnel hub.
|
* Stop the tunnel hub.
|
||||||
*/
|
*/
|
||||||
public async stop(): Promise<void> {
|
public async stop(): Promise<void> {
|
||||||
|
if (this.reconcileInterval) {
|
||||||
|
clearInterval(this.reconcileInterval);
|
||||||
|
this.reconcileInterval = null;
|
||||||
|
}
|
||||||
|
// Remove event listeners before stopping to prevent leaks
|
||||||
|
this.hub.removeAllListeners();
|
||||||
await this.hub.stop();
|
await this.hub.stop();
|
||||||
this.edgeStatuses.clear();
|
this.edgeStatuses.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconcile TS-side edge statuses with the authoritative Rust hub status.
|
||||||
|
* Overwrites event-derived activeTunnels with the real activeStreams count.
|
||||||
|
*/
|
||||||
|
private async reconcile(): Promise<void> {
|
||||||
|
const hubStatus = await this.hub.getStatus();
|
||||||
|
if (!hubStatus || !hubStatus.connectedEdges) return;
|
||||||
|
|
||||||
|
const rustEdgeIds = new Set<string>();
|
||||||
|
|
||||||
|
for (const rustEdge of hubStatus.connectedEdges) {
|
||||||
|
rustEdgeIds.add(rustEdge.edgeId);
|
||||||
|
const existing = this.edgeStatuses.get(rustEdge.edgeId);
|
||||||
|
if (existing) {
|
||||||
|
existing.activeTunnels = rustEdge.activeStreams;
|
||||||
|
existing.lastHeartbeat = Date.now();
|
||||||
|
// Update peer address if available from Rust hub
|
||||||
|
if (rustEdge.peerAddr) {
|
||||||
|
existing.publicIp = rustEdge.peerAddr;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Missed edgeConnected event — add entry
|
||||||
|
this.edgeStatuses.set(rustEdge.edgeId, {
|
||||||
|
edgeId: rustEdge.edgeId,
|
||||||
|
connected: true,
|
||||||
|
publicIp: rustEdge.peerAddr || null,
|
||||||
|
activeTunnels: rustEdge.activeStreams,
|
||||||
|
lastHeartbeat: Date.now(),
|
||||||
|
connectedAt: rustEdge.connectedAt * 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove entries for edges no longer connected in Rust (missed edgeDisconnected)
|
||||||
|
for (const edgeId of this.edgeStatuses.keys()) {
|
||||||
|
if (!rustEdgeIds.has(edgeId)) {
|
||||||
|
this.edgeStatuses.delete(edgeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync allowed edges from the manager to the hub.
|
* Sync allowed edges from the manager to the hub.
|
||||||
* Call this after creating/deleting/updating edges.
|
* Call this after creating/deleting/updating edges.
|
||||||
@@ -113,6 +161,19 @@ export class TunnelManager {
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the public IPs of all connected edges.
|
||||||
|
*/
|
||||||
|
public getConnectedEdgeIps(): string[] {
|
||||||
|
const ips: string[] = [];
|
||||||
|
for (const status of this.edgeStatuses.values()) {
|
||||||
|
if (status.connected && status.publicIp) {
|
||||||
|
ips.push(status.publicIp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ips;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the total number of active tunnels across all edges.
|
* Get the total number of active tunnels across all edges.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './auth.js';
|
export * from './auth.js';
|
||||||
export * from './stats.js';
|
export * from './stats.js';
|
||||||
export * from './remoteingress.js';
|
export * from './remoteingress.js';
|
||||||
|
export * from './route-management.js';
|
||||||
83
ts_interfaces/data/route-management.ts
Normal file
83
ts_interfaces/data/route-management.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import type { IRouteConfig } from '@push.rocks/smartproxy';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Route Management Data Types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export type TApiTokenScope = 'routes:read' | 'routes:write' | 'config:read' | 'tokens:read' | 'tokens:manage';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A merged route combining hardcoded and programmatic sources.
|
||||||
|
*/
|
||||||
|
export interface IMergedRoute {
|
||||||
|
route: IRouteConfig;
|
||||||
|
source: 'hardcoded' | 'programmatic';
|
||||||
|
enabled: boolean;
|
||||||
|
overridden: boolean;
|
||||||
|
storedRouteId?: string;
|
||||||
|
createdAt?: number;
|
||||||
|
updatedAt?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A warning generated during route merge/startup.
|
||||||
|
*/
|
||||||
|
export interface IRouteWarning {
|
||||||
|
type: 'disabled-hardcoded' | 'disabled-programmatic' | 'orphaned-override';
|
||||||
|
routeName: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public info about an API token (never includes the hash).
|
||||||
|
*/
|
||||||
|
export interface IApiTokenInfo {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
scopes: TApiTokenScope[];
|
||||||
|
createdAt: number;
|
||||||
|
expiresAt: number | null;
|
||||||
|
lastUsedAt: number | null;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Storage Schemas (persisted via StorageManager)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A programmatic route stored in /config-api/routes/{id}.json
|
||||||
|
*/
|
||||||
|
export interface IStoredRoute {
|
||||||
|
id: string;
|
||||||
|
route: IRouteConfig;
|
||||||
|
enabled: boolean;
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
createdBy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An override for a hardcoded route, stored in /config-api/overrides/{routeName}.json
|
||||||
|
*/
|
||||||
|
export interface IRouteOverride {
|
||||||
|
routeName: string;
|
||||||
|
enabled: boolean;
|
||||||
|
updatedAt: number;
|
||||||
|
updatedBy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A stored API token, stored in /config-api/tokens/{id}.json
|
||||||
|
*/
|
||||||
|
export interface IStoredApiToken {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
tokenHash: string;
|
||||||
|
scopes: TApiTokenScope[];
|
||||||
|
createdAt: number;
|
||||||
|
expiresAt: number | null;
|
||||||
|
lastUsedAt: number | null;
|
||||||
|
createdBy: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
83
ts_interfaces/requests/api-tokens.ts
Normal file
83
ts_interfaces/requests/api-tokens.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import type * as authInterfaces from '../data/auth.js';
|
||||||
|
import type { IApiTokenInfo, TApiTokenScope } from '../data/route-management.js';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// API Token Management Endpoints
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new API token. Returns the raw token value once (never shown again).
|
||||||
|
* Admin JWT only — tokens cannot create tokens.
|
||||||
|
*/
|
||||||
|
export interface IReq_CreateApiToken extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_CreateApiToken
|
||||||
|
> {
|
||||||
|
method: 'createApiToken';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
name: string;
|
||||||
|
scopes: TApiTokenScope[];
|
||||||
|
expiresInDays?: number | null;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
tokenId?: string;
|
||||||
|
tokenValue?: string;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all API tokens (without hashes).
|
||||||
|
*/
|
||||||
|
export interface IReq_ListApiTokens extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_ListApiTokens
|
||||||
|
> {
|
||||||
|
method: 'listApiTokens';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
tokens: IApiTokenInfo[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke (delete) an API token.
|
||||||
|
*/
|
||||||
|
export interface IReq_RevokeApiToken extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_RevokeApiToken
|
||||||
|
> {
|
||||||
|
method: 'revokeApiToken';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable an API token.
|
||||||
|
*/
|
||||||
|
export interface IReq_ToggleApiToken extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_ToggleApiToken
|
||||||
|
> {
|
||||||
|
method: 'toggleApiToken';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
id: string;
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,79 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import * as authInterfaces from '../data/auth.js';
|
import * as authInterfaces from '../data/auth.js';
|
||||||
|
|
||||||
|
export interface IConfigData {
|
||||||
|
system: {
|
||||||
|
baseDir: string;
|
||||||
|
dataDir: string;
|
||||||
|
publicIp: string | null;
|
||||||
|
proxyIps: string[];
|
||||||
|
uptime: number;
|
||||||
|
storageBackend: 'filesystem' | 'custom' | 'memory';
|
||||||
|
storagePath: string | null;
|
||||||
|
};
|
||||||
|
smartProxy: {
|
||||||
|
enabled: boolean;
|
||||||
|
routeCount: number;
|
||||||
|
acme: {
|
||||||
|
enabled: boolean;
|
||||||
|
accountEmail: string;
|
||||||
|
useProduction: boolean;
|
||||||
|
autoRenew: boolean;
|
||||||
|
renewThresholdDays: number;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
email: {
|
||||||
|
enabled: boolean;
|
||||||
|
ports: number[];
|
||||||
|
portMapping: Record<string, number> | null;
|
||||||
|
hostname: string | null;
|
||||||
|
domains: string[];
|
||||||
|
emailRouteCount: number;
|
||||||
|
receivedEmailsPath: string | null;
|
||||||
|
};
|
||||||
|
dns: {
|
||||||
|
enabled: boolean;
|
||||||
|
port: number;
|
||||||
|
nsDomains: string[];
|
||||||
|
scopes: string[];
|
||||||
|
recordCount: number;
|
||||||
|
records: Array<{ name: string; type: string; value: string; ttl?: number }>;
|
||||||
|
dnsChallenge: boolean;
|
||||||
|
};
|
||||||
|
tls: {
|
||||||
|
contactEmail: string | null;
|
||||||
|
domain: string | null;
|
||||||
|
source: 'acme' | 'static' | 'none';
|
||||||
|
certPath: string | null;
|
||||||
|
keyPath: string | null;
|
||||||
|
};
|
||||||
|
cache: {
|
||||||
|
enabled: boolean;
|
||||||
|
storagePath: string | null;
|
||||||
|
dbName: string | null;
|
||||||
|
defaultTTLDays: number;
|
||||||
|
cleanupIntervalHours: number;
|
||||||
|
ttlConfig: Record<string, number>;
|
||||||
|
};
|
||||||
|
radius: {
|
||||||
|
enabled: boolean;
|
||||||
|
authPort: number | null;
|
||||||
|
acctPort: number | null;
|
||||||
|
bindAddress: string | null;
|
||||||
|
clientCount: number;
|
||||||
|
vlanDefaultVlan: number | null;
|
||||||
|
vlanAllowUnknownMacs: boolean | null;
|
||||||
|
vlanMappingCount: number;
|
||||||
|
};
|
||||||
|
remoteIngress: {
|
||||||
|
enabled: boolean;
|
||||||
|
tunnelPort: number | null;
|
||||||
|
hubDomain: string | null;
|
||||||
|
tlsConfigured: boolean;
|
||||||
|
connectedEdgeIps: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Get Configuration (read-only)
|
// Get Configuration (read-only)
|
||||||
export interface IReq_GetConfiguration extends plugins.typedrequestInterfaces.implementsTR<
|
export interface IReq_GetConfiguration extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
plugins.typedrequestInterfaces.ITypedRequest,
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
@@ -12,7 +85,7 @@ export interface IReq_GetConfiguration extends plugins.typedrequestInterfaces.im
|
|||||||
section?: string;
|
section?: string;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
config: any;
|
config: IConfigData;
|
||||||
section?: string;
|
section?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -2,162 +2,93 @@ import * as plugins from '../plugins.js';
|
|||||||
import * as authInterfaces from '../data/auth.js';
|
import * as authInterfaces from '../data/auth.js';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Email Queue Item Interface (matches backend IQueueItem)
|
// Catalog-compatible email types (matches @serve.zone/catalog IEmail/IEmailDetail)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
export type TEmailQueueStatus = 'pending' | 'processing' | 'delivered' | 'failed' | 'deferred';
|
export type TEmailStatus = 'delivered' | 'bounced' | 'rejected' | 'deferred' | 'pending';
|
||||||
|
export type TEmailDirection = 'inbound' | 'outbound';
|
||||||
|
|
||||||
export interface IEmailQueueItem {
|
export interface IEmail {
|
||||||
id: string;
|
id: string;
|
||||||
processingMode: 'forward' | 'mta' | 'process';
|
direction: TEmailDirection;
|
||||||
status: TEmailQueueStatus;
|
status: TEmailStatus;
|
||||||
attempts: number;
|
from: string;
|
||||||
nextAttempt: number; // timestamp
|
to: string;
|
||||||
lastError?: string;
|
subject: string;
|
||||||
createdAt: number; // timestamp
|
timestamp: string;
|
||||||
updatedAt: number; // timestamp
|
messageId: string;
|
||||||
deliveredAt?: number; // timestamp
|
size: string;
|
||||||
// Email details extracted from processingResult
|
}
|
||||||
from?: string;
|
|
||||||
to?: string[];
|
export interface ISmtpLogEntry {
|
||||||
subject?: string;
|
timestamp: string;
|
||||||
|
direction: 'client' | 'server';
|
||||||
|
command: string;
|
||||||
|
responseCode?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IConnectionInfo {
|
||||||
|
sourceIp: string;
|
||||||
|
sourceHostname: string;
|
||||||
|
destinationIp: string;
|
||||||
|
destinationPort: number;
|
||||||
|
tlsVersion: string;
|
||||||
|
tlsCipher: string;
|
||||||
|
authenticated: boolean;
|
||||||
|
authMethod: string;
|
||||||
|
authUser: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAuthenticationResults {
|
||||||
|
spf: 'pass' | 'fail' | 'softfail' | 'neutral' | 'none';
|
||||||
|
spfDomain: string;
|
||||||
|
dkim: 'pass' | 'fail' | 'none';
|
||||||
|
dkimDomain: string;
|
||||||
|
dmarc: 'pass' | 'fail' | 'none';
|
||||||
|
dmarcPolicy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEmailDetail extends IEmail {
|
||||||
|
toList: string[];
|
||||||
|
cc?: string[];
|
||||||
|
smtpLog: ISmtpLogEntry[];
|
||||||
|
connectionInfo: IConnectionInfo;
|
||||||
|
authenticationResults: IAuthenticationResults;
|
||||||
|
rejectionReason?: string;
|
||||||
|
bounceMessage?: string;
|
||||||
|
headers: Record<string, string>;
|
||||||
|
body: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Bounce Record Interface (matches backend BounceRecord)
|
// Get All Emails Request
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
export type TBounceType =
|
export interface IReq_GetAllEmails extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
| 'invalid_recipient'
|
|
||||||
| 'domain_not_found'
|
|
||||||
| 'mailbox_full'
|
|
||||||
| 'mailbox_inactive'
|
|
||||||
| 'blocked'
|
|
||||||
| 'spam_related'
|
|
||||||
| 'policy_related'
|
|
||||||
| 'server_unavailable'
|
|
||||||
| 'temporary_failure'
|
|
||||||
| 'quota_exceeded'
|
|
||||||
| 'network_error'
|
|
||||||
| 'timeout'
|
|
||||||
| 'auto_response'
|
|
||||||
| 'challenge_response'
|
|
||||||
| 'unknown';
|
|
||||||
|
|
||||||
export type TBounceCategory = 'hard' | 'soft' | 'auto_response' | 'unknown';
|
|
||||||
|
|
||||||
export interface IBounceRecord {
|
|
||||||
id: string;
|
|
||||||
originalEmailId?: string;
|
|
||||||
recipient: string;
|
|
||||||
sender: string;
|
|
||||||
domain: string;
|
|
||||||
subject?: string;
|
|
||||||
bounceType: TBounceType;
|
|
||||||
bounceCategory: TBounceCategory;
|
|
||||||
timestamp: number;
|
|
||||||
smtpResponse?: string;
|
|
||||||
diagnosticCode?: string;
|
|
||||||
statusCode?: string;
|
|
||||||
processed: boolean;
|
|
||||||
retryCount?: number;
|
|
||||||
nextRetryTime?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Security Incident Interface (matches backend ISecurityEvent)
|
|
||||||
// ============================================================================
|
|
||||||
export type TSecurityLogLevel = 'info' | 'warn' | 'error' | 'critical';
|
|
||||||
|
|
||||||
export type TSecurityEventType =
|
|
||||||
| 'authentication'
|
|
||||||
| 'access_control'
|
|
||||||
| 'email_validation'
|
|
||||||
| 'email_processing'
|
|
||||||
| 'email_forwarding'
|
|
||||||
| 'email_delivery'
|
|
||||||
| 'dkim'
|
|
||||||
| 'spf'
|
|
||||||
| 'dmarc'
|
|
||||||
| 'rate_limit'
|
|
||||||
| 'rate_limiting'
|
|
||||||
| 'spam'
|
|
||||||
| 'malware'
|
|
||||||
| 'connection'
|
|
||||||
| 'data_exposure'
|
|
||||||
| 'configuration'
|
|
||||||
| 'ip_reputation'
|
|
||||||
| 'rejected_connection';
|
|
||||||
|
|
||||||
export interface ISecurityIncident {
|
|
||||||
timestamp: number;
|
|
||||||
level: TSecurityLogLevel;
|
|
||||||
type: TSecurityEventType;
|
|
||||||
message: string;
|
|
||||||
details?: any;
|
|
||||||
ipAddress?: string;
|
|
||||||
userId?: string;
|
|
||||||
sessionId?: string;
|
|
||||||
emailId?: string;
|
|
||||||
domain?: string;
|
|
||||||
action?: string;
|
|
||||||
result?: string;
|
|
||||||
success?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Get Queued Emails Request
|
|
||||||
// ============================================================================
|
|
||||||
export interface IReq_GetQueuedEmails extends plugins.typedrequestInterfaces.implementsTR<
|
|
||||||
plugins.typedrequestInterfaces.ITypedRequest,
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
IReq_GetQueuedEmails
|
IReq_GetAllEmails
|
||||||
> {
|
> {
|
||||||
method: 'getQueuedEmails';
|
method: 'getAllEmails';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity?: authInterfaces.IIdentity;
|
||||||
status?: TEmailQueueStatus;
|
|
||||||
limit?: number;
|
|
||||||
offset?: number;
|
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
items: IEmailQueueItem[];
|
emails: IEmail[];
|
||||||
total: number;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Get Sent Emails Request
|
// Get Email Detail Request
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
export interface IReq_GetSentEmails extends plugins.typedrequestInterfaces.implementsTR<
|
export interface IReq_GetEmailDetail extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
plugins.typedrequestInterfaces.ITypedRequest,
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
IReq_GetSentEmails
|
IReq_GetEmailDetail
|
||||||
> {
|
> {
|
||||||
method: 'getSentEmails';
|
method: 'getEmailDetail';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity?: authInterfaces.IIdentity;
|
||||||
limit?: number;
|
emailId: string;
|
||||||
offset?: number;
|
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
items: IEmailQueueItem[];
|
email: IEmailDetail | null;
|
||||||
total: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Get Failed Emails Request
|
|
||||||
// ============================================================================
|
|
||||||
export interface IReq_GetFailedEmails extends plugins.typedrequestInterfaces.implementsTR<
|
|
||||||
plugins.typedrequestInterfaces.ITypedRequest,
|
|
||||||
IReq_GetFailedEmails
|
|
||||||
> {
|
|
||||||
method: 'getFailedEmails';
|
|
||||||
request: {
|
|
||||||
identity?: authInterfaces.IIdentity;
|
|
||||||
limit?: number;
|
|
||||||
offset?: number;
|
|
||||||
};
|
|
||||||
response: {
|
|
||||||
items: IEmailQueueItem[];
|
|
||||||
total: number;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,61 +110,3 @@ export interface IReq_ResendEmail extends plugins.typedrequestInterfaces.impleme
|
|||||||
error?: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Get Security Incidents Request
|
|
||||||
// ============================================================================
|
|
||||||
export interface IReq_GetSecurityIncidents extends plugins.typedrequestInterfaces.implementsTR<
|
|
||||||
plugins.typedrequestInterfaces.ITypedRequest,
|
|
||||||
IReq_GetSecurityIncidents
|
|
||||||
> {
|
|
||||||
method: 'getSecurityIncidents';
|
|
||||||
request: {
|
|
||||||
identity?: authInterfaces.IIdentity;
|
|
||||||
type?: TSecurityEventType;
|
|
||||||
level?: TSecurityLogLevel;
|
|
||||||
limit?: number;
|
|
||||||
};
|
|
||||||
response: {
|
|
||||||
incidents: ISecurityIncident[];
|
|
||||||
total: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Get Bounce Records Request
|
|
||||||
// ============================================================================
|
|
||||||
export interface IReq_GetBounceRecords extends plugins.typedrequestInterfaces.implementsTR<
|
|
||||||
plugins.typedrequestInterfaces.ITypedRequest,
|
|
||||||
IReq_GetBounceRecords
|
|
||||||
> {
|
|
||||||
method: 'getBounceRecords';
|
|
||||||
request: {
|
|
||||||
identity?: authInterfaces.IIdentity;
|
|
||||||
limit?: number;
|
|
||||||
offset?: number;
|
|
||||||
};
|
|
||||||
response: {
|
|
||||||
records: IBounceRecord[];
|
|
||||||
suppressionList: string[];
|
|
||||||
total: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Remove from Suppression List Request
|
|
||||||
// ============================================================================
|
|
||||||
export interface IReq_RemoveFromSuppressionList extends plugins.typedrequestInterfaces.implementsTR<
|
|
||||||
plugins.typedrequestInterfaces.ITypedRequest,
|
|
||||||
IReq_RemoveFromSuppressionList
|
|
||||||
> {
|
|
||||||
method: 'removeFromSuppressionList';
|
|
||||||
request: {
|
|
||||||
identity?: authInterfaces.IIdentity;
|
|
||||||
email: string;
|
|
||||||
};
|
|
||||||
response: {
|
|
||||||
success: boolean;
|
|
||||||
error?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,3 +7,5 @@ export * from './radius.js';
|
|||||||
export * from './email-ops.js';
|
export * from './email-ops.js';
|
||||||
export * from './certificate.js';
|
export * from './certificate.js';
|
||||||
export * from './remoteingress.js';
|
export * from './remoteingress.js';
|
||||||
|
export * from './route-management.js';
|
||||||
|
export * from './api-tokens.js';
|
||||||
146
ts_interfaces/requests/route-management.ts
Normal file
146
ts_interfaces/requests/route-management.ts
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import type * as authInterfaces from '../data/auth.js';
|
||||||
|
import type { IMergedRoute, IRouteWarning } from '../data/route-management.js';
|
||||||
|
import type { IRouteConfig } from '@push.rocks/smartproxy';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Route Management Endpoints
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all merged routes (hardcoded + programmatic) with warnings.
|
||||||
|
*/
|
||||||
|
export interface IReq_GetMergedRoutes extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_GetMergedRoutes
|
||||||
|
> {
|
||||||
|
method: 'getMergedRoutes';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
apiToken?: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
routes: IMergedRoute[];
|
||||||
|
warnings: IRouteWarning[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new programmatic route.
|
||||||
|
*/
|
||||||
|
export interface IReq_CreateRoute extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_CreateRoute
|
||||||
|
> {
|
||||||
|
method: 'createRoute';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
apiToken?: string;
|
||||||
|
route: IRouteConfig;
|
||||||
|
enabled?: boolean;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
storedRouteId?: string;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a programmatic route.
|
||||||
|
*/
|
||||||
|
export interface IReq_UpdateRoute extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_UpdateRoute
|
||||||
|
> {
|
||||||
|
method: 'updateRoute';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
apiToken?: string;
|
||||||
|
id: string;
|
||||||
|
route?: Partial<IRouteConfig>;
|
||||||
|
enabled?: boolean;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a programmatic route.
|
||||||
|
*/
|
||||||
|
export interface IReq_DeleteRoute extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_DeleteRoute
|
||||||
|
> {
|
||||||
|
method: 'deleteRoute';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
apiToken?: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an override on a hardcoded route (disable/enable by name).
|
||||||
|
*/
|
||||||
|
export interface IReq_SetRouteOverride extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_SetRouteOverride
|
||||||
|
> {
|
||||||
|
method: 'setRouteOverride';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
apiToken?: string;
|
||||||
|
routeName: string;
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an override from a hardcoded route (restore default behavior).
|
||||||
|
*/
|
||||||
|
export interface IReq_RemoveRouteOverride extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_RemoveRouteOverride
|
||||||
|
> {
|
||||||
|
method: 'removeRouteOverride';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
apiToken?: string;
|
||||||
|
routeName: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle a programmatic route on/off by id.
|
||||||
|
*/
|
||||||
|
export interface IReq_ToggleRoute extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_ToggleRoute
|
||||||
|
> {
|
||||||
|
method: 'toggleRoute';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
apiToken?: string;
|
||||||
|
id: string;
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '7.4.0',
|
version: '9.2.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export interface IStatsState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IConfigState {
|
export interface IConfigState {
|
||||||
config: any | null;
|
config: interfaces.requests.IConfigData | null;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
@@ -67,14 +67,7 @@ export interface ICertificateState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IEmailOpsState {
|
export interface IEmailOpsState {
|
||||||
currentView: 'queued' | 'sent' | 'failed' | 'received' | 'security';
|
emails: interfaces.requests.IEmail[];
|
||||||
queuedEmails: interfaces.requests.IEmailQueueItem[];
|
|
||||||
sentEmails: interfaces.requests.IEmailQueueItem[];
|
|
||||||
failedEmails: interfaces.requests.IEmailQueueItem[];
|
|
||||||
securityIncidents: interfaces.requests.ISecurityIncident[];
|
|
||||||
bounceRecords: interfaces.requests.IBounceRecord[];
|
|
||||||
suppressionList: string[];
|
|
||||||
selectedEmailId: string | null;
|
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
lastUpdated: number;
|
lastUpdated: number;
|
||||||
@@ -116,7 +109,7 @@ export const configStatePart = await appState.getStatePart<IConfigState>(
|
|||||||
// Determine initial view from URL path
|
// Determine initial view from URL path
|
||||||
const getInitialView = (): string => {
|
const getInitialView = (): string => {
|
||||||
const path = typeof window !== 'undefined' ? window.location.pathname : '/';
|
const path = typeof window !== 'undefined' ? window.location.pathname : '/';
|
||||||
const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates', 'remoteingress'];
|
const validViews = ['overview', 'network', 'emails', 'logs', 'routes', 'apitokens', 'configuration', 'security', 'certificates', 'remoteingress'];
|
||||||
const segments = path.split('/').filter(Boolean);
|
const segments = path.split('/').filter(Boolean);
|
||||||
const view = segments[0];
|
const view = segments[0];
|
||||||
return validViews.includes(view) ? view : 'overview';
|
return validViews.includes(view) ? view : 'overview';
|
||||||
@@ -165,14 +158,7 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
|
|||||||
export const emailOpsStatePart = await appState.getStatePart<IEmailOpsState>(
|
export const emailOpsStatePart = await appState.getStatePart<IEmailOpsState>(
|
||||||
'emailOps',
|
'emailOps',
|
||||||
{
|
{
|
||||||
currentView: 'queued',
|
emails: [],
|
||||||
queuedEmails: [],
|
|
||||||
sentEmails: [],
|
|
||||||
failedEmails: [],
|
|
||||||
securityIncidents: [],
|
|
||||||
bounceRecords: [],
|
|
||||||
suppressionList: [],
|
|
||||||
selectedEmailId: null,
|
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
lastUpdated: 0,
|
lastUpdated: 0,
|
||||||
@@ -220,6 +206,32 @@ export const remoteIngressStatePart = await appState.getStatePart<IRemoteIngress
|
|||||||
'soft'
|
'soft'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Route Management State
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface IRouteManagementState {
|
||||||
|
mergedRoutes: interfaces.data.IMergedRoute[];
|
||||||
|
warnings: interfaces.data.IRouteWarning[];
|
||||||
|
apiTokens: interfaces.data.IApiTokenInfo[];
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
lastUpdated: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const routeManagementStatePart = await appState.getStatePart<IRouteManagementState>(
|
||||||
|
'routeManagement',
|
||||||
|
{
|
||||||
|
mergedRoutes: [],
|
||||||
|
warnings: [],
|
||||||
|
apiTokens: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
lastUpdated: 0,
|
||||||
|
},
|
||||||
|
'soft'
|
||||||
|
);
|
||||||
|
|
||||||
// Actions for state management
|
// Actions for state management
|
||||||
interface IActionContext {
|
interface IActionContext {
|
||||||
identity: interfaces.data.IIdentity | null;
|
identity: interfaces.data.IIdentity | null;
|
||||||
@@ -406,6 +418,20 @@ export const setActiveViewAction = uiStatePart.createAction<string>(async (state
|
|||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If switching to routes view, ensure we fetch route data
|
||||||
|
if (viewName === 'routes' && currentState.activeView !== 'routes') {
|
||||||
|
setTimeout(() => {
|
||||||
|
routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If switching to apitokens view, ensure we fetch token data
|
||||||
|
if (viewName === 'apitokens' && currentState.activeView !== 'apitokens') {
|
||||||
|
setTimeout(() => {
|
||||||
|
routeManagementStatePart.dispatchAction(fetchApiTokensAction, null);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
// If switching to remoteingress view, ensure we fetch edge data
|
// If switching to remoteingress view, ensure we fetch edge data
|
||||||
if (viewName === 'remoteingress' && currentState.activeView !== 'remoteingress') {
|
if (viewName === 'remoteingress' && currentState.activeView !== 'remoteingress') {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -492,35 +518,22 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
|||||||
// Email Operations Actions
|
// Email Operations Actions
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// Set Email Ops View Action
|
// Fetch All Emails Action
|
||||||
export const setEmailOpsViewAction = emailOpsStatePart.createAction<IEmailOpsState['currentView']>(
|
export const fetchAllEmailsAction = emailOpsStatePart.createAction(async (statePartArg) => {
|
||||||
async (statePartArg, view) => {
|
|
||||||
return {
|
|
||||||
...statePartArg.getState(),
|
|
||||||
currentView: view,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Fetch Queued Emails Action
|
|
||||||
export const fetchQueuedEmailsAction = emailOpsStatePart.createAction(async (statePartArg) => {
|
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
interfaces.requests.IReq_GetQueuedEmails
|
interfaces.requests.IReq_GetAllEmails
|
||||||
>('/typedrequest', 'getQueuedEmails');
|
>('/typedrequest', 'getAllEmails');
|
||||||
|
|
||||||
const response = await request.fire({
|
const response = await request.fire({
|
||||||
identity: context.identity,
|
identity: context.identity,
|
||||||
status: 'pending',
|
|
||||||
limit: 100,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...currentState,
|
emails: response.emails,
|
||||||
queuedEmails: response.items,
|
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
@@ -529,197 +542,11 @@ export const fetchQueuedEmailsAction = emailOpsStatePart.createAction(async (sta
|
|||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: error instanceof Error ? error.message : 'Failed to fetch queued emails',
|
error: error instanceof Error ? error.message : 'Failed to fetch emails',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch Sent Emails Action
|
|
||||||
export const fetchSentEmailsAction = emailOpsStatePart.createAction(async (statePartArg) => {
|
|
||||||
const context = getActionContext();
|
|
||||||
const currentState = statePartArg.getState();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
||||||
interfaces.requests.IReq_GetSentEmails
|
|
||||||
>('/typedrequest', 'getSentEmails');
|
|
||||||
|
|
||||||
const response = await request.fire({
|
|
||||||
identity: context.identity,
|
|
||||||
limit: 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...currentState,
|
|
||||||
sentEmails: response.items,
|
|
||||||
isLoading: false,
|
|
||||||
error: null,
|
|
||||||
lastUpdated: Date.now(),
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
...currentState,
|
|
||||||
isLoading: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Failed to fetch sent emails',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fetch Failed Emails Action
|
|
||||||
export const fetchFailedEmailsAction = emailOpsStatePart.createAction(async (statePartArg) => {
|
|
||||||
const context = getActionContext();
|
|
||||||
const currentState = statePartArg.getState();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
||||||
interfaces.requests.IReq_GetFailedEmails
|
|
||||||
>('/typedrequest', 'getFailedEmails');
|
|
||||||
|
|
||||||
const response = await request.fire({
|
|
||||||
identity: context.identity,
|
|
||||||
limit: 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...currentState,
|
|
||||||
failedEmails: response.items,
|
|
||||||
isLoading: false,
|
|
||||||
error: null,
|
|
||||||
lastUpdated: Date.now(),
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
...currentState,
|
|
||||||
isLoading: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Failed to fetch failed emails',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fetch Security Incidents Action
|
|
||||||
export const fetchSecurityIncidentsAction = emailOpsStatePart.createAction(async (statePartArg) => {
|
|
||||||
const context = getActionContext();
|
|
||||||
const currentState = statePartArg.getState();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
||||||
interfaces.requests.IReq_GetSecurityIncidents
|
|
||||||
>('/typedrequest', 'getSecurityIncidents');
|
|
||||||
|
|
||||||
const response = await request.fire({
|
|
||||||
identity: context.identity,
|
|
||||||
limit: 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...currentState,
|
|
||||||
securityIncidents: response.incidents,
|
|
||||||
isLoading: false,
|
|
||||||
error: null,
|
|
||||||
lastUpdated: Date.now(),
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
...currentState,
|
|
||||||
isLoading: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Failed to fetch security incidents',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fetch Bounce Records Action
|
|
||||||
export const fetchBounceRecordsAction = emailOpsStatePart.createAction(async (statePartArg) => {
|
|
||||||
const context = getActionContext();
|
|
||||||
const currentState = statePartArg.getState();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
||||||
interfaces.requests.IReq_GetBounceRecords
|
|
||||||
>('/typedrequest', 'getBounceRecords');
|
|
||||||
|
|
||||||
const response = await request.fire({
|
|
||||||
identity: context.identity,
|
|
||||||
limit: 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...currentState,
|
|
||||||
bounceRecords: response.records,
|
|
||||||
suppressionList: response.suppressionList,
|
|
||||||
isLoading: false,
|
|
||||||
error: null,
|
|
||||||
lastUpdated: Date.now(),
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
...currentState,
|
|
||||||
isLoading: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Failed to fetch bounce records',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Resend Failed Email Action
|
|
||||||
export const resendEmailAction = emailOpsStatePart.createAction<string>(async (statePartArg, emailId) => {
|
|
||||||
const context = getActionContext();
|
|
||||||
const currentState = statePartArg.getState();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
||||||
interfaces.requests.IReq_ResendEmail
|
|
||||||
>('/typedrequest', 'resendEmail');
|
|
||||||
|
|
||||||
const response = await request.fire({
|
|
||||||
identity: context.identity,
|
|
||||||
emailId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
// Refresh failed emails list
|
|
||||||
await emailOpsStatePart.dispatchAction(fetchFailedEmailsAction, null);
|
|
||||||
await emailOpsStatePart.dispatchAction(fetchQueuedEmailsAction, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return statePartArg.getState();
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
...currentState,
|
|
||||||
error: error instanceof Error ? error.message : 'Failed to resend email',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove from Suppression List Action
|
|
||||||
export const removeFromSuppressionListAction = emailOpsStatePart.createAction<string>(
|
|
||||||
async (statePartArg, email) => {
|
|
||||||
const context = getActionContext();
|
|
||||||
const currentState = statePartArg.getState();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
||||||
interfaces.requests.IReq_RemoveFromSuppressionList
|
|
||||||
>('/typedrequest', 'removeFromSuppressionList');
|
|
||||||
|
|
||||||
const response = await request.fire({
|
|
||||||
identity: context.identity,
|
|
||||||
email,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
// Refresh bounce records
|
|
||||||
await emailOpsStatePart.dispatchAction(fetchBounceRecordsAction, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return statePartArg.getState();
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
...currentState,
|
|
||||||
error: error instanceof Error ? error.message : 'Failed to remove from suppression list',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Certificate Actions
|
// Certificate Actions
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -1075,6 +902,273 @@ export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Route Management Actions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const fetchMergedRoutesAction = routeManagementStatePart.createAction(async (statePartArg) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_GetMergedRoutes
|
||||||
|
>('/typedrequest', 'getMergedRoutes');
|
||||||
|
|
||||||
|
const response = await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
mergedRoutes: response.routes,
|
||||||
|
warnings: response.warnings,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
isLoading: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to fetch routes',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createRouteAction = routeManagementStatePart.createAction<{
|
||||||
|
route: any;
|
||||||
|
enabled?: boolean;
|
||||||
|
}>(async (statePartArg, dataArg) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_CreateRoute
|
||||||
|
>('/typedrequest', 'createRoute');
|
||||||
|
|
||||||
|
await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
route: dataArg.route,
|
||||||
|
enabled: dataArg.enabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
|
||||||
|
return statePartArg.getState();
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to create route',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteRouteAction = routeManagementStatePart.createAction<string>(
|
||||||
|
async (statePartArg, routeId) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_DeleteRoute
|
||||||
|
>('/typedrequest', 'deleteRoute');
|
||||||
|
|
||||||
|
await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
id: routeId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
|
||||||
|
return statePartArg.getState();
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to delete route',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const toggleRouteAction = routeManagementStatePart.createAction<{
|
||||||
|
id: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}>(async (statePartArg, dataArg) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_ToggleRoute
|
||||||
|
>('/typedrequest', 'toggleRoute');
|
||||||
|
|
||||||
|
await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
id: dataArg.id,
|
||||||
|
enabled: dataArg.enabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
|
||||||
|
return statePartArg.getState();
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to toggle route',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setRouteOverrideAction = routeManagementStatePart.createAction<{
|
||||||
|
routeName: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}>(async (statePartArg, dataArg) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_SetRouteOverride
|
||||||
|
>('/typedrequest', 'setRouteOverride');
|
||||||
|
|
||||||
|
await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
routeName: dataArg.routeName,
|
||||||
|
enabled: dataArg.enabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
|
||||||
|
return statePartArg.getState();
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to set override',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const removeRouteOverrideAction = routeManagementStatePart.createAction<string>(
|
||||||
|
async (statePartArg, routeName) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_RemoveRouteOverride
|
||||||
|
>('/typedrequest', 'removeRouteOverride');
|
||||||
|
|
||||||
|
await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
routeName,
|
||||||
|
});
|
||||||
|
|
||||||
|
await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
|
||||||
|
return statePartArg.getState();
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to remove override',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// API Token Actions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const fetchApiTokensAction = routeManagementStatePart.createAction(async (statePartArg) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_ListApiTokens
|
||||||
|
>('/typedrequest', 'listApiTokens');
|
||||||
|
|
||||||
|
const response = await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
apiTokens: response.tokens,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to fetch tokens',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function createApiToken(name: string, scopes: interfaces.data.TApiTokenScope[], expiresInDays?: number | null) {
|
||||||
|
const context = getActionContext();
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_CreateApiToken
|
||||||
|
>('/typedrequest', 'createApiToken');
|
||||||
|
|
||||||
|
return request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
name,
|
||||||
|
scopes,
|
||||||
|
expiresInDays,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const revokeApiTokenAction = routeManagementStatePart.createAction<string>(
|
||||||
|
async (statePartArg, tokenId) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_RevokeApiToken
|
||||||
|
>('/typedrequest', 'revokeApiToken');
|
||||||
|
|
||||||
|
await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
id: tokenId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await routeManagementStatePart.dispatchAction(fetchApiTokensAction, null);
|
||||||
|
return statePartArg.getState();
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to revoke token',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const toggleApiTokenAction = routeManagementStatePart.createAction<{
|
||||||
|
id: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}>(async (statePartArg, dataArg) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_ToggleApiToken
|
||||||
|
>('/typedrequest', 'toggleApiToken');
|
||||||
|
|
||||||
|
await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
id: dataArg.id,
|
||||||
|
enabled: dataArg.enabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
await routeManagementStatePart.dispatchAction(fetchApiTokensAction, null);
|
||||||
|
return statePartArg.getState();
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to toggle token',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TypedSocket Client for Real-time Log Streaming
|
// TypedSocket Client for Real-time Log Streaming
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -1227,6 +1321,15 @@ async function dispatchCombinedRefreshAction() {
|
|||||||
console.error('Certificate refresh failed:', error);
|
console.error('Certificate refresh failed:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refresh remote ingress data if on remoteingress view
|
||||||
|
if (currentView === 'remoteingress') {
|
||||||
|
try {
|
||||||
|
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Remote ingress refresh failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Combined refresh failed:', error);
|
console.error('Combined refresh failed:', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ export * from './ops-view-network.js';
|
|||||||
export * from './ops-view-emails.js';
|
export * from './ops-view-emails.js';
|
||||||
export * from './ops-view-logs.js';
|
export * from './ops-view-logs.js';
|
||||||
export * from './ops-view-config.js';
|
export * from './ops-view-config.js';
|
||||||
|
export * from './ops-view-routes.js';
|
||||||
|
export * from './ops-view-apitokens.js';
|
||||||
export * from './ops-view-security.js';
|
export * from './ops-view-security.js';
|
||||||
export * from './ops-view-certificates.js';
|
export * from './ops-view-certificates.js';
|
||||||
export * from './ops-view-remoteingress.js';
|
export * from './ops-view-remoteingress.js';
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import { OpsViewNetwork } from './ops-view-network.js';
|
|||||||
import { OpsViewEmails } from './ops-view-emails.js';
|
import { OpsViewEmails } from './ops-view-emails.js';
|
||||||
import { OpsViewLogs } from './ops-view-logs.js';
|
import { OpsViewLogs } from './ops-view-logs.js';
|
||||||
import { OpsViewConfig } from './ops-view-config.js';
|
import { OpsViewConfig } from './ops-view-config.js';
|
||||||
|
import { OpsViewRoutes } from './ops-view-routes.js';
|
||||||
|
import { OpsViewApiTokens } from './ops-view-apitokens.js';
|
||||||
import { OpsViewSecurity } from './ops-view-security.js';
|
import { OpsViewSecurity } from './ops-view-security.js';
|
||||||
import { OpsViewCertificates } from './ops-view-certificates.js';
|
import { OpsViewCertificates } from './ops-view-certificates.js';
|
||||||
import { OpsViewRemoteIngress } from './ops-view-remoteingress.js';
|
import { OpsViewRemoteIngress } from './ops-view-remoteingress.js';
|
||||||
@@ -41,34 +43,52 @@ export class OpsDashboard extends DeesElement {
|
|||||||
private viewTabs = [
|
private viewTabs = [
|
||||||
{
|
{
|
||||||
name: 'Overview',
|
name: 'Overview',
|
||||||
|
iconName: 'lucide:layoutDashboard',
|
||||||
element: OpsViewOverview,
|
element: OpsViewOverview,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Configuration',
|
||||||
|
iconName: 'lucide:settings',
|
||||||
|
element: OpsViewConfig,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Network',
|
name: 'Network',
|
||||||
|
iconName: 'lucide:network',
|
||||||
element: OpsViewNetwork,
|
element: OpsViewNetwork,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Emails',
|
name: 'Emails',
|
||||||
|
iconName: 'lucide:mail',
|
||||||
element: OpsViewEmails,
|
element: OpsViewEmails,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Logs',
|
name: 'Logs',
|
||||||
|
iconName: 'lucide:scrollText',
|
||||||
element: OpsViewLogs,
|
element: OpsViewLogs,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Configuration',
|
name: 'Routes',
|
||||||
element: OpsViewConfig,
|
iconName: 'lucide:route',
|
||||||
|
element: OpsViewRoutes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ApiTokens',
|
||||||
|
iconName: 'lucide:key',
|
||||||
|
element: OpsViewApiTokens,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Security',
|
name: 'Security',
|
||||||
|
iconName: 'lucide:shield',
|
||||||
element: OpsViewSecurity,
|
element: OpsViewSecurity,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Certificates',
|
name: 'Certificates',
|
||||||
|
iconName: 'lucide:badgeCheck',
|
||||||
element: OpsViewCertificates,
|
element: OpsViewCertificates,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'RemoteIngress',
|
name: 'RemoteIngress',
|
||||||
|
iconName: 'lucide:globe',
|
||||||
element: OpsViewRemoteIngress,
|
element: OpsViewRemoteIngress,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
285
ts_web/elements/ops-view-apitokens.ts
Normal file
285
ts_web/elements/ops-view-apitokens.ts
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
import * as appstate from '../appstate.js';
|
||||||
|
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||||
|
import { viewHostCss } from './shared/css.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
state,
|
||||||
|
type TemplateResult,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
type TApiTokenScope = interfaces.data.TApiTokenScope;
|
||||||
|
|
||||||
|
@customElement('ops-view-apitokens')
|
||||||
|
export class OpsViewApiTokens extends DeesElement {
|
||||||
|
@state() accessor routeState: appstate.IRouteManagementState = {
|
||||||
|
mergedRoutes: [],
|
||||||
|
warnings: [],
|
||||||
|
apiTokens: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
lastUpdated: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const sub = appstate.routeManagementStatePart
|
||||||
|
.select((s) => s)
|
||||||
|
.subscribe((routeState) => {
|
||||||
|
this.routeState = routeState;
|
||||||
|
});
|
||||||
|
this.rxSubscriptions.push(sub);
|
||||||
|
|
||||||
|
// Re-fetch tokens when user logs in (fixes race condition where
|
||||||
|
// the view is created before authentication completes)
|
||||||
|
const loginSub = appstate.loginStatePart
|
||||||
|
.select((s) => s.isLoggedIn)
|
||||||
|
.subscribe((isLoggedIn) => {
|
||||||
|
if (isLoggedIn) {
|
||||||
|
appstate.routeManagementStatePart.dispatchAction(appstate.fetchApiTokensAction, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.rxSubscriptions.push(loginSub);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
viewHostCss,
|
||||||
|
css`
|
||||||
|
.apiTokensContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scopePill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
background: ${cssManager.bdTheme('rgba(0, 130, 200, 0.1)', 'rgba(0, 170, 255, 0.1)')};
|
||||||
|
color: ${cssManager.bdTheme('#0369a1', '#0af')};
|
||||||
|
margin-right: 4px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBadge.active {
|
||||||
|
background: ${cssManager.bdTheme('#dcfce7', '#14532d')};
|
||||||
|
color: ${cssManager.bdTheme('#166534', '#4ade80')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBadge.disabled {
|
||||||
|
background: ${cssManager.bdTheme('#fef2f2', '#450a0a')};
|
||||||
|
color: ${cssManager.bdTheme('#991b1b', '#f87171')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBadge.expired {
|
||||||
|
background: ${cssManager.bdTheme('#f3f4f6', '#374151')};
|
||||||
|
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
const { apiTokens } = this.routeState;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ops-sectionheading>API Tokens</ops-sectionheading>
|
||||||
|
|
||||||
|
<div class="apiTokensContainer">
|
||||||
|
<dees-table
|
||||||
|
.heading1=${'API Tokens'}
|
||||||
|
.heading2=${'Manage programmatic access tokens'}
|
||||||
|
.data=${apiTokens}
|
||||||
|
.dataName=${'token'}
|
||||||
|
.searchable=${true}
|
||||||
|
.displayFunction=${(token: interfaces.data.IApiTokenInfo) => ({
|
||||||
|
name: token.name,
|
||||||
|
scopes: this.renderScopePills(token.scopes),
|
||||||
|
status: this.renderStatusBadge(token),
|
||||||
|
created: new Date(token.createdAt).toLocaleDateString(),
|
||||||
|
expires: token.expiresAt ? new Date(token.expiresAt).toLocaleDateString() : 'Never',
|
||||||
|
lastUsed: token.lastUsedAt ? new Date(token.lastUsedAt).toLocaleDateString() : 'Never',
|
||||||
|
})}
|
||||||
|
.dataActions=${[
|
||||||
|
{
|
||||||
|
name: 'Create Token',
|
||||||
|
iconName: 'lucide:plus',
|
||||||
|
type: ['header'],
|
||||||
|
actionFunc: async () => {
|
||||||
|
await this.showCreateTokenDialog();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Enable',
|
||||||
|
iconName: 'lucide:play',
|
||||||
|
type: ['inRow', 'contextmenu'] as any,
|
||||||
|
actionRelevancyCheckFunc: (actionData: any) => !actionData.item.enabled,
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const token = actionData.item as interfaces.data.IApiTokenInfo;
|
||||||
|
await appstate.routeManagementStatePart.dispatchAction(
|
||||||
|
appstate.toggleApiTokenAction,
|
||||||
|
{ id: token.id, enabled: true },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Disable',
|
||||||
|
iconName: 'lucide:pause',
|
||||||
|
type: ['inRow', 'contextmenu'] as any,
|
||||||
|
actionRelevancyCheckFunc: (actionData: any) => actionData.item.enabled,
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const token = actionData.item as interfaces.data.IApiTokenInfo;
|
||||||
|
await appstate.routeManagementStatePart.dispatchAction(
|
||||||
|
appstate.toggleApiTokenAction,
|
||||||
|
{ id: token.id, enabled: false },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Revoke',
|
||||||
|
iconName: 'lucide:trash2',
|
||||||
|
type: ['inRow', 'contextmenu'] as any,
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const token = actionData.item as interfaces.data.IApiTokenInfo;
|
||||||
|
await appstate.routeManagementStatePart.dispatchAction(
|
||||||
|
appstate.revokeApiTokenAction,
|
||||||
|
token.id,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
></dees-table>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderScopePills(scopes: TApiTokenScope[]): TemplateResult {
|
||||||
|
return html`<div style="display: flex; flex-wrap: wrap; gap: 2px;">${scopes.map(
|
||||||
|
(s) => html`<span class="scopePill">${s}</span>`,
|
||||||
|
)}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderStatusBadge(token: interfaces.data.IApiTokenInfo): TemplateResult {
|
||||||
|
if (!token.enabled) {
|
||||||
|
return html`<span class="statusBadge disabled">Disabled</span>`;
|
||||||
|
}
|
||||||
|
if (token.expiresAt && token.expiresAt < Date.now()) {
|
||||||
|
return html`<span class="statusBadge expired">Expired</span>`;
|
||||||
|
}
|
||||||
|
return html`<span class="statusBadge active">Active</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async showCreateTokenDialog() {
|
||||||
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
|
|
||||||
|
const allScopes: TApiTokenScope[] = [
|
||||||
|
'routes:read',
|
||||||
|
'routes:write',
|
||||||
|
'config:read',
|
||||||
|
'tokens:read',
|
||||||
|
'tokens:manage',
|
||||||
|
];
|
||||||
|
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: 'Create API Token',
|
||||||
|
content: html`
|
||||||
|
<div style="color: #888; margin-bottom: 12px; font-size: 13px;">
|
||||||
|
The token value will be shown once after creation. Copy it immediately.
|
||||||
|
</div>
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text .key=${'name'} .label=${'Token Name'} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-tags
|
||||||
|
.key=${'scopes'}
|
||||||
|
.label=${'Token Scopes'}
|
||||||
|
.value=${['routes:read', 'routes:write']}
|
||||||
|
.suggestions=${allScopes}
|
||||||
|
.required=${true}
|
||||||
|
></dees-input-tags>
|
||||||
|
<dees-input-text .key=${'expiresInDays'} .label=${'Expires in (days, blank = never)'}></dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Cancel',
|
||||||
|
iconName: 'lucide:x',
|
||||||
|
action: async (modalArg: any) => await modalArg.destroy(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Create',
|
||||||
|
iconName: 'lucide:key',
|
||||||
|
action: async (modalArg: any) => {
|
||||||
|
const contentEl = modalArg.shadowRoot?.querySelector('.content');
|
||||||
|
const form = contentEl?.querySelector('dees-form');
|
||||||
|
if (!form) return;
|
||||||
|
const formData = await form.collectFormData();
|
||||||
|
if (!formData.name) return;
|
||||||
|
|
||||||
|
// dees-input-tags is not in dees-form's FORM_INPUT_TYPES, so collectFormData() won't
|
||||||
|
// include it. Query the tags input directly and call getValue().
|
||||||
|
const tagsInput = form.querySelector('dees-input-tags') as any;
|
||||||
|
const rawScopes: string[] = tagsInput?.getValue?.() || tagsInput?.value || formData.scopes || [];
|
||||||
|
const scopes = rawScopes
|
||||||
|
.filter((s: string) => allScopes.includes(s as any)) as TApiTokenScope[];
|
||||||
|
|
||||||
|
const expiresInDays = formData.expiresInDays
|
||||||
|
? parseInt(formData.expiresInDays, 10)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
await modalArg.destroy();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await appstate.createApiToken(formData.name, scopes, expiresInDays);
|
||||||
|
if (response.success && response.tokenValue) {
|
||||||
|
// Refresh the list first so it's ready when user dismisses the modal
|
||||||
|
await appstate.routeManagementStatePart.dispatchAction(appstate.fetchApiTokensAction, null);
|
||||||
|
|
||||||
|
// Show the token value in a new modal
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: 'Token Created',
|
||||||
|
content: html`
|
||||||
|
<div style="color: #ccc; padding: 8px 0;">
|
||||||
|
<p>Copy this token now. It will not be shown again.</p>
|
||||||
|
<div style="background: #111; padding: 12px; border-radius: 6px; margin-top: 8px;">
|
||||||
|
<code style="color: #0f8; word-break: break-all; font-size: 13px;">${response.tokenValue}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Done',
|
||||||
|
iconName: 'lucide:check',
|
||||||
|
action: async (m: any) => await m.destroy(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create token:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async firstUpdated() {
|
||||||
|
await appstate.routeManagementStatePart.dispatchAction(appstate.fetchApiTokensAction, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import * as shared from './shared/index.js';
|
import * as shared from './shared/index.js';
|
||||||
import * as appstate from '../appstate.js';
|
import * as appstate from '../appstate.js';
|
||||||
|
import { appRouter } from '../router.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DeesElement,
|
DeesElement,
|
||||||
@@ -12,6 +13,8 @@ import {
|
|||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import type { IConfigField, IConfigSectionAction } from '@serve.zone/catalog';
|
||||||
|
|
||||||
@customElement('ops-view-config')
|
@customElement('ops-view-config')
|
||||||
export class OpsViewConfig extends DeesElement {
|
export class OpsViewConfig extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
@@ -35,165 +38,19 @@ export class OpsViewConfig extends DeesElement {
|
|||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
shared.viewHostCss,
|
shared.viewHostCss,
|
||||||
css`
|
css`
|
||||||
.configSection {
|
|
||||||
background: ${cssManager.bdTheme('#fff', '#222')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sectionHeader {
|
|
||||||
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
|
|
||||||
padding: 16px 24px;
|
|
||||||
border-bottom: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sectionTitle {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sectionTitle dees-icon {
|
|
||||||
font-size: 20px;
|
|
||||||
color: ${cssManager.bdTheme('#666', '#888')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.sectionContent {
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.configField {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.configField:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fieldLabel {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#666', '#999')};
|
|
||||||
margin-bottom: 8px;
|
|
||||||
display: block;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fieldValue {
|
|
||||||
font-family: 'Consolas', 'Monaco', monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
|
||||||
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
|
|
||||||
padding: 10px 14px;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.fieldValue.empty {
|
|
||||||
color: ${cssManager.bdTheme('#999', '#666')};
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nestedFields {
|
|
||||||
margin-left: 16px;
|
|
||||||
padding-left: 16px;
|
|
||||||
border-left: 2px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Status badge styles */
|
|
||||||
.statusBadge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.statusBadge.enabled {
|
|
||||||
background: ${cssManager.bdTheme('#d4edda', '#1a3d1a')};
|
|
||||||
color: ${cssManager.bdTheme('#155724', '#66cc66')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.statusBadge.disabled {
|
|
||||||
background: ${cssManager.bdTheme('#f8d7da', '#3d1a1a')};
|
|
||||||
color: ${cssManager.bdTheme('#721c24', '#cc6666')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.statusBadge dees-icon {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Array/list display */
|
|
||||||
.arrayItems {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrayItem {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
background: ${cssManager.bdTheme('#e7f3ff', '#1a2a3d')};
|
|
||||||
color: ${cssManager.bdTheme('#0066cc', '#66aaff')};
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 16px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-family: 'Consolas', 'Monaco', monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrayCount {
|
|
||||||
font-size: 12px;
|
|
||||||
color: ${cssManager.bdTheme('#999', '#666')};
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Numeric value formatting */
|
|
||||||
.numericValue {
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#0066cc', '#66aaff')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.errorMessage {
|
|
||||||
background: ${cssManager.bdTheme('#fee', '#4a1f1f')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#fcc', '#6a2f2f')};
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 16px;
|
|
||||||
color: ${cssManager.bdTheme('#c00', '#ff6666')};
|
|
||||||
margin: 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loadingMessage {
|
.loadingMessage {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
color: ${cssManager.bdTheme('#666', '#999')};
|
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.infoNote {
|
.errorMessage {
|
||||||
background: ${cssManager.bdTheme('#e7f3ff', '#1a2a3d')};
|
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239,68,68,0.1)')};
|
||||||
border: 1px solid ${cssManager.bdTheme('#b3d7ff', '#2a4a6d')};
|
border: 1px solid ${cssManager.bdTheme('#fecaca', 'rgba(239,68,68,0.3)')};
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
margin-bottom: 24px;
|
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
|
||||||
color: ${cssManager.bdTheme('#004085', '#88ccff')};
|
margin: 16px 0;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infoNote dees-icon {
|
|
||||||
font-size: 20px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
@@ -202,185 +59,276 @@ export class OpsViewConfig extends DeesElement {
|
|||||||
return html`
|
return html`
|
||||||
<ops-sectionheading>Configuration</ops-sectionheading>
|
<ops-sectionheading>Configuration</ops-sectionheading>
|
||||||
|
|
||||||
${this.configState.isLoading ? html`
|
${this.configState.isLoading
|
||||||
|
? html`
|
||||||
<div class="loadingMessage">
|
<div class="loadingMessage">
|
||||||
<dees-spinner></dees-spinner>
|
<dees-spinner></dees-spinner>
|
||||||
<p>Loading configuration...</p>
|
<p>Loading configuration...</p>
|
||||||
</div>
|
</div>
|
||||||
` : this.configState.error ? html`
|
`
|
||||||
|
: this.configState.error
|
||||||
|
? html`
|
||||||
<div class="errorMessage">
|
<div class="errorMessage">
|
||||||
Error loading configuration: ${this.configState.error}
|
Error loading configuration: ${this.configState.error}
|
||||||
</div>
|
</div>
|
||||||
` : this.configState.config ? html`
|
`
|
||||||
<div class="infoNote">
|
: this.configState.config
|
||||||
<dees-icon icon="lucide:info"></dees-icon>
|
? this.renderConfig()
|
||||||
<span>This view displays the current running configuration. DcRouter is configured through code or remote management.</span>
|
: html`<div class="errorMessage">No configuration loaded</div>`}
|
||||||
</div>
|
|
||||||
|
|
||||||
${this.renderConfigSection('email', 'Email', 'lucide:mail', this.configState.config?.email)}
|
|
||||||
${this.renderConfigSection('dns', 'DNS', 'lucide:globe', this.configState.config?.dns)}
|
|
||||||
${this.renderConfigSection('proxy', 'Proxy', 'lucide:network', this.configState.config?.proxy)}
|
|
||||||
${this.renderConfigSection('security', 'Security', 'lucide:shield', this.configState.config?.security)}
|
|
||||||
` : html`
|
|
||||||
<div class="errorMessage">No configuration loaded</div>
|
|
||||||
`}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderConfigSection(key: string, title: string, icon: string, config: any) {
|
private renderConfig(): TemplateResult {
|
||||||
const isEnabled = config?.enabled ?? false;
|
const cfg = this.configState.config!;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="configSection">
|
<sz-config-overview
|
||||||
<div class="sectionHeader">
|
infoText="This view displays the current running configuration. DcRouter is configured through code or remote management."
|
||||||
<h3 class="sectionTitle">
|
@navigate=${(e: CustomEvent) => {
|
||||||
<dees-icon icon="${icon}"></dees-icon>
|
if (e.detail?.view) {
|
||||||
${title}
|
appRouter.navigateToView(e.detail.view);
|
||||||
</h3>
|
}
|
||||||
${this.renderStatusBadge(isEnabled)}
|
}}
|
||||||
</div>
|
>
|
||||||
<div class="sectionContent">
|
${this.renderSystemSection(cfg.system)}
|
||||||
${config ? this.renderConfigFields(config) : html`
|
${this.renderSmartProxySection(cfg.smartProxy)}
|
||||||
<div class="fieldValue empty">Not configured</div>
|
${this.renderEmailSection(cfg.email)}
|
||||||
`}
|
${this.renderDnsSection(cfg.dns)}
|
||||||
</div>
|
${this.renderTlsSection(cfg.tls)}
|
||||||
</div>
|
${this.renderCacheSection(cfg.cache)}
|
||||||
|
${this.renderRadiusSection(cfg.radius)}
|
||||||
|
${this.renderRemoteIngressSection(cfg.remoteIngress)}
|
||||||
|
</sz-config-overview>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderStatusBadge(enabled: boolean): TemplateResult {
|
private renderSystemSection(sys: appstate.IConfigState['config']['system']): TemplateResult {
|
||||||
return enabled
|
// Annotate proxy IPs with source hint when Remote Ingress is active
|
||||||
? html`<span class="statusBadge enabled"><dees-icon icon="lucide:check"></dees-icon>Enabled</span>`
|
const ri = this.configState.config?.remoteIngress;
|
||||||
: html`<span class="statusBadge disabled"><dees-icon icon="lucide:x"></dees-icon>Disabled</span>`;
|
let proxyIpValues: string[] | null = sys.proxyIps.length > 0 ? [...sys.proxyIps] : null;
|
||||||
|
if (proxyIpValues && ri?.enabled && proxyIpValues.includes('127.0.0.1')) {
|
||||||
|
proxyIpValues = proxyIpValues.map(ip =>
|
||||||
|
ip === '127.0.0.1' ? '127.0.0.1 (Remote Ingress)' : ip
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderConfigFields(config: any, prefix = ''): TemplateResult | TemplateResult[] {
|
const fields: IConfigField[] = [
|
||||||
if (!config || typeof config !== 'object') {
|
{ key: 'Base Directory', value: sys.baseDir },
|
||||||
return html`<div class="fieldValue">${this.formatValue(config)}</div>`;
|
{ key: 'Data Directory', value: sys.dataDir },
|
||||||
}
|
{ key: 'Public IP', value: sys.publicIp },
|
||||||
|
{ key: 'Proxy IPs', value: proxyIpValues, type: 'pills' },
|
||||||
|
{ key: 'Uptime', value: this.formatUptime(sys.uptime) },
|
||||||
|
{ key: 'Storage Backend', value: sys.storageBackend, type: 'badge' },
|
||||||
|
{ key: 'Storage Path', value: sys.storagePath },
|
||||||
|
];
|
||||||
|
|
||||||
return Object.entries(config).map(([key, value]) => {
|
|
||||||
const fieldName = prefix ? `${prefix}.${key}` : key;
|
|
||||||
const displayName = this.formatFieldName(key);
|
|
||||||
|
|
||||||
// Handle boolean values with badges
|
|
||||||
if (typeof value === 'boolean') {
|
|
||||||
return html`
|
return html`
|
||||||
<div class="configField">
|
<sz-config-section
|
||||||
<label class="fieldLabel">${displayName}</label>
|
title="System"
|
||||||
${this.renderStatusBadge(value)}
|
subtitle="Base paths and infrastructure"
|
||||||
</div>
|
icon="lucide:server"
|
||||||
|
status="enabled"
|
||||||
|
.fields=${fields}
|
||||||
|
></sz-config-section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle arrays
|
private renderSmartProxySection(proxy: appstate.IConfigState['config']['smartProxy']): TemplateResult {
|
||||||
if (Array.isArray(value)) {
|
const fields: IConfigField[] = [
|
||||||
|
{ key: 'Route Count', value: proxy.routeCount },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (proxy.acme) {
|
||||||
|
fields.push(
|
||||||
|
{ key: 'ACME Enabled', value: proxy.acme.enabled, type: 'boolean' },
|
||||||
|
{ key: 'Account Email', value: proxy.acme.accountEmail || null },
|
||||||
|
{ key: 'Use Production', value: proxy.acme.useProduction, type: 'boolean' },
|
||||||
|
{ key: 'Auto Renew', value: proxy.acme.autoRenew, type: 'boolean' },
|
||||||
|
{ key: 'Renew Threshold', value: `${proxy.acme.renewThresholdDays} days` },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions: IConfigSectionAction[] = [
|
||||||
|
{ label: 'View Routes', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'routes' } },
|
||||||
|
];
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="configField">
|
<sz-config-section
|
||||||
<label class="fieldLabel">${displayName}</label>
|
title="SmartProxy"
|
||||||
${this.renderArrayValue(value, key)}
|
subtitle="HTTP/HTTPS and TCP/SNI reverse proxy"
|
||||||
</div>
|
icon="lucide:network"
|
||||||
|
.status=${proxy.enabled ? 'enabled' : 'disabled'}
|
||||||
|
.fields=${fields}
|
||||||
|
.actions=${actions}
|
||||||
|
></sz-config-section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle nested objects
|
private renderEmailSection(email: appstate.IConfigState['config']['email']): TemplateResult {
|
||||||
if (typeof value === 'object' && value !== null) {
|
const fields: IConfigField[] = [
|
||||||
|
{ key: 'Ports', value: email.ports.length > 0 ? email.ports.map(String) : null, type: 'pills' },
|
||||||
|
{ key: 'Hostname', value: email.hostname },
|
||||||
|
{ key: 'Domains', value: email.domains.length > 0 ? email.domains : null, type: 'pills' },
|
||||||
|
{ key: 'Email Routes', value: email.emailRouteCount },
|
||||||
|
{ key: 'Received Emails Path', value: email.receivedEmailsPath },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (email.portMapping) {
|
||||||
|
const mappingStr = Object.entries(email.portMapping)
|
||||||
|
.map(([ext, int]) => `${ext} → ${int}`)
|
||||||
|
.join(', ');
|
||||||
|
fields.splice(1, 0, { key: 'Port Mapping', value: mappingStr, type: 'code' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions: IConfigSectionAction[] = [
|
||||||
|
{ label: 'View Emails', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'emails' } },
|
||||||
|
];
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="configField">
|
<sz-config-section
|
||||||
<label class="fieldLabel">${displayName}</label>
|
title="Email Server"
|
||||||
<div class="nestedFields">
|
subtitle="SMTP email handling with smartmta"
|
||||||
${this.renderConfigFields(value, fieldName)}
|
icon="lucide:mail"
|
||||||
</div>
|
.status=${email.enabled ? 'enabled' : 'disabled'}
|
||||||
</div>
|
.fields=${fields}
|
||||||
|
.actions=${actions}
|
||||||
|
></sz-config-section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle primitive values
|
private renderDnsSection(dns: appstate.IConfigState['config']['dns']): TemplateResult {
|
||||||
|
const fields: IConfigField[] = [
|
||||||
|
{ key: 'Port', value: dns.port },
|
||||||
|
{ key: 'NS Domains', value: dns.nsDomains.length > 0 ? dns.nsDomains : null, type: 'pills' },
|
||||||
|
{ key: 'Scopes', value: dns.scopes.length > 0 ? dns.scopes : null, type: 'pills' },
|
||||||
|
{ key: 'Record Count', value: dns.recordCount },
|
||||||
|
{ key: 'DNS Challenge', value: dns.dnsChallenge, type: 'boolean' },
|
||||||
|
];
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="configField">
|
<sz-config-section
|
||||||
<label class="fieldLabel">${displayName}</label>
|
title="DNS Server"
|
||||||
<div class="fieldValue">${this.formatValue(value, key)}</div>
|
subtitle="Authoritative DNS with smartdns"
|
||||||
</div>
|
icon="lucide:globe"
|
||||||
|
.status=${dns.enabled ? 'enabled' : 'disabled'}
|
||||||
|
.fields=${fields}
|
||||||
|
></sz-config-section>
|
||||||
`;
|
`;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderArrayValue(arr: any[], fieldKey: string): TemplateResult {
|
private renderTlsSection(tls: appstate.IConfigState['config']['tls']): TemplateResult {
|
||||||
if (arr.length === 0) {
|
const fields: IConfigField[] = [
|
||||||
return html`<div class="fieldValue empty">None configured</div>`;
|
{ key: 'Contact Email', value: tls.contactEmail },
|
||||||
}
|
{ key: 'Domain', value: tls.domain },
|
||||||
|
{ key: 'Source', value: tls.source, type: 'badge' },
|
||||||
|
{ key: 'Certificate Path', value: tls.certPath },
|
||||||
|
{ key: 'Key Path', value: tls.keyPath },
|
||||||
|
];
|
||||||
|
|
||||||
// Determine if we should show as pills/tags
|
const status = tls.source === 'none' ? 'not-configured' : 'enabled';
|
||||||
const showAsPills = arr.every(item => typeof item === 'string' || typeof item === 'number');
|
const actions: IConfigSectionAction[] = [
|
||||||
|
{ label: 'View Certificates', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'certificates' } },
|
||||||
|
];
|
||||||
|
|
||||||
if (showAsPills) {
|
|
||||||
const itemLabel = this.getArrayItemLabel(fieldKey, arr.length);
|
|
||||||
return html`
|
return html`
|
||||||
<div class="arrayCount">${arr.length} ${itemLabel}</div>
|
<sz-config-section
|
||||||
<div class="arrayItems">
|
title="TLS / Certificates"
|
||||||
${arr.map(item => html`<span class="arrayItem">${item}</span>`)}
|
subtitle="Certificate management and ACME"
|
||||||
</div>
|
icon="lucide:shield-check"
|
||||||
|
.status=${status as any}
|
||||||
|
.fields=${fields}
|
||||||
|
.actions=${actions}
|
||||||
|
></sz-config-section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For complex arrays, show as JSON
|
private renderCacheSection(cache: appstate.IConfigState['config']['cache']): TemplateResult {
|
||||||
|
const fields: IConfigField[] = [
|
||||||
|
{ key: 'Storage Path', value: cache.storagePath },
|
||||||
|
{ key: 'DB Name', value: cache.dbName },
|
||||||
|
{ key: 'Default TTL', value: `${cache.defaultTTLDays} days` },
|
||||||
|
{ key: 'Cleanup Interval', value: `${cache.cleanupIntervalHours} hours` },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (cache.ttlConfig && Object.keys(cache.ttlConfig).length > 0) {
|
||||||
|
for (const [key, val] of Object.entries(cache.ttlConfig)) {
|
||||||
|
fields.push({ key: `TTL: ${key}`, value: `${val} days` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="fieldValue">
|
<sz-config-section
|
||||||
${arr.length} items configured
|
title="Cache Database"
|
||||||
</div>
|
subtitle="Persistent caching with smartdata"
|
||||||
|
icon="lucide:database"
|
||||||
|
.status=${cache.enabled ? 'enabled' : 'disabled'}
|
||||||
|
.fields=${fields}
|
||||||
|
></sz-config-section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getArrayItemLabel(fieldKey: string, count: number): string {
|
private renderRadiusSection(radius: appstate.IConfigState['config']['radius']): TemplateResult {
|
||||||
const labels: Record<string, [string, string]> = {
|
const fields: IConfigField[] = [
|
||||||
ports: ['port', 'ports'],
|
{ key: 'Auth Port', value: radius.authPort },
|
||||||
domains: ['domain', 'domains'],
|
{ key: 'Accounting Port', value: radius.acctPort },
|
||||||
nameservers: ['nameserver', 'nameservers'],
|
{ key: 'Bind Address', value: radius.bindAddress },
|
||||||
blockList: ['IP', 'IPs'],
|
{ key: 'Client Count', value: radius.clientCount },
|
||||||
};
|
];
|
||||||
|
|
||||||
const label = labels[fieldKey] || ['item', 'items'];
|
if (radius.vlanDefaultVlan !== null) {
|
||||||
return count === 1 ? label[0] : label[1];
|
fields.push(
|
||||||
|
{ key: 'Default VLAN', value: radius.vlanDefaultVlan },
|
||||||
|
{ key: 'Allow Unknown MACs', value: radius.vlanAllowUnknownMacs, type: 'boolean' },
|
||||||
|
{ key: 'VLAN Mappings', value: radius.vlanMappingCount },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatFieldName(key: string): string {
|
const status = radius.enabled ? 'enabled' : 'not-configured';
|
||||||
// Convert camelCase to readable format
|
|
||||||
return key
|
return html`
|
||||||
.replace(/([A-Z])/g, ' $1')
|
<sz-config-section
|
||||||
.replace(/^./, str => str.toUpperCase())
|
title="RADIUS Server"
|
||||||
.trim();
|
subtitle="Network authentication and VLAN assignment"
|
||||||
|
icon="lucide:wifi"
|
||||||
|
.status=${status as any}
|
||||||
|
.fields=${fields}
|
||||||
|
></sz-config-section>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatValue(value: any, fieldKey?: string): string | TemplateResult {
|
private renderRemoteIngressSection(ri: appstate.IConfigState['config']['remoteIngress']): TemplateResult {
|
||||||
if (value === null || value === undefined) {
|
const fields: IConfigField[] = [
|
||||||
return html`<span class="empty">Not set</span>`;
|
{ key: 'Tunnel Port', value: ri.tunnelPort },
|
||||||
|
{ key: 'Hub Domain', value: ri.hubDomain },
|
||||||
|
{ key: 'TLS Configured', value: ri.tlsConfigured, type: 'boolean' },
|
||||||
|
{ key: 'Connected Edge IPs', value: ri.connectedEdgeIps?.length > 0 ? ri.connectedEdgeIps : null, type: 'pills' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const actions: IConfigSectionAction[] = [
|
||||||
|
{ label: 'View Remote Ingress', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'remoteingress' } },
|
||||||
|
];
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<sz-config-section
|
||||||
|
title="Remote Ingress"
|
||||||
|
subtitle="Edge tunnel nodes"
|
||||||
|
icon="lucide:cloud"
|
||||||
|
.status=${ri.enabled ? 'enabled' : 'disabled'}
|
||||||
|
.fields=${fields}
|
||||||
|
.actions=${actions}
|
||||||
|
></sz-config-section>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === 'number') {
|
private formatUptime(seconds: number): string {
|
||||||
// Format bytes
|
const days = Math.floor(seconds / 86400);
|
||||||
if (fieldKey?.toLowerCase().includes('size') || fieldKey?.toLowerCase().includes('bytes')) {
|
const hours = Math.floor((seconds % 86400) / 3600);
|
||||||
return html`<span class="numericValue">${this.formatBytes(value)}</span>`;
|
const mins = Math.floor((seconds % 3600) / 60);
|
||||||
}
|
|
||||||
// Format time values
|
|
||||||
if (fieldKey?.toLowerCase().includes('ttl') || fieldKey?.toLowerCase().includes('timeout')) {
|
|
||||||
return html`<span class="numericValue">${value} seconds</span>`;
|
|
||||||
}
|
|
||||||
// Format port numbers
|
|
||||||
if (fieldKey?.toLowerCase().includes('port')) {
|
|
||||||
return html`<span class="numericValue">${value}</span>`;
|
|
||||||
}
|
|
||||||
// Format counts with separators
|
|
||||||
return html`<span class="numericValue">${value.toLocaleString()}</span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return String(value);
|
const parts: string[] = [];
|
||||||
}
|
if (days > 0) parts.push(`${days}d`);
|
||||||
|
if (hours > 0) parts.push(`${hours}h`);
|
||||||
private formatBytes(bytes: number): string {
|
parts.push(`${mins}m`);
|
||||||
if (bytes === 0) return '0 B';
|
return parts.join(' ');
|
||||||
const k = 1024;
|
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { DeesElement, property, html, customElement, type TemplateResult, css, state, cssManager } from '@design.estate/dees-element';
|
import { DeesElement, property, html, customElement, type TemplateResult, css, state, cssManager } from '@design.estate/dees-element';
|
||||||
|
import * as plugins from '../plugins.js';
|
||||||
import * as appstate from '../appstate.js';
|
import * as appstate from '../appstate.js';
|
||||||
import * as shared from './shared/index.js';
|
import * as shared from './shared/index.js';
|
||||||
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||||
import { appRouter } from '../router.js';
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -10,67 +10,30 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type TEmailFolder = 'queued' | 'sent' | 'failed' | 'received' | 'security';
|
|
||||||
|
|
||||||
@customElement('ops-view-emails')
|
@customElement('ops-view-emails')
|
||||||
export class OpsViewEmails extends DeesElement {
|
export class OpsViewEmails extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
accessor selectedFolder: TEmailFolder = 'queued';
|
accessor emails: interfaces.requests.IEmail[] = [];
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
accessor queuedEmails: interfaces.requests.IEmailQueueItem[] = [];
|
accessor selectedEmail: interfaces.requests.IEmailDetail | null = null;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
accessor sentEmails: interfaces.requests.IEmailQueueItem[] = [];
|
accessor currentView: 'list' | 'detail' = 'list';
|
||||||
|
|
||||||
@state()
|
|
||||||
accessor failedEmails: interfaces.requests.IEmailQueueItem[] = [];
|
|
||||||
|
|
||||||
@state()
|
|
||||||
accessor securityIncidents: interfaces.requests.ISecurityIncident[] = [];
|
|
||||||
|
|
||||||
@state()
|
|
||||||
accessor selectedEmail: interfaces.requests.IEmailQueueItem | null = null;
|
|
||||||
|
|
||||||
@state()
|
|
||||||
accessor selectedIncident: interfaces.requests.ISecurityIncident | null = null;
|
|
||||||
|
|
||||||
@state()
|
|
||||||
accessor showCompose = false;
|
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
accessor isLoading = false;
|
accessor isLoading = false;
|
||||||
|
|
||||||
@state()
|
|
||||||
accessor searchTerm = '';
|
|
||||||
|
|
||||||
@state()
|
|
||||||
accessor emailDomains: string[] = [];
|
|
||||||
|
|
||||||
private stateSubscription: any;
|
private stateSubscription: any;
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.loadData();
|
|
||||||
this.loadEmailDomains();
|
|
||||||
}
|
|
||||||
|
|
||||||
async connectedCallback() {
|
async connectedCallback() {
|
||||||
await super.connectedCallback();
|
await super.connectedCallback();
|
||||||
// Subscribe to state changes
|
|
||||||
this.stateSubscription = appstate.emailOpsStatePart.state.subscribe((state) => {
|
this.stateSubscription = appstate.emailOpsStatePart.state.subscribe((state) => {
|
||||||
this.queuedEmails = state.queuedEmails;
|
this.emails = state.emails;
|
||||||
this.sentEmails = state.sentEmails;
|
|
||||||
this.failedEmails = state.failedEmails;
|
|
||||||
this.securityIncidents = state.securityIncidents;
|
|
||||||
this.isLoading = state.isLoading;
|
this.isLoading = state.isLoading;
|
||||||
|
|
||||||
// Sync folder from state (e.g., when URL changes)
|
|
||||||
if (state.currentView !== this.selectedFolder) {
|
|
||||||
this.selectedFolder = state.currentView as TEmailFolder;
|
|
||||||
this.loadFolderData(state.currentView as TEmailFolder);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
// Initial fetch
|
||||||
|
await appstate.emailOpsStatePart.dispatchAction(appstate.fetchAllEmailsAction, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
async disconnectedCallback() {
|
async disconnectedCallback() {
|
||||||
@@ -89,730 +52,58 @@ export class OpsViewEmails extends DeesElement {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emailLayout {
|
.viewContainer {
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 280px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mainArea {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emailToolbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchBox {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 200px;
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emailList {
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emailPreview {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background: ${cssManager.bdTheme('#fff', '#222')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emailHeader {
|
|
||||||
padding: 24px;
|
|
||||||
border-bottom: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.emailSubject {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.emailMeta {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: ${cssManager.bdTheme('#666', '#999')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.emailMetaRow {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emailMetaLabel {
|
|
||||||
font-weight: 600;
|
|
||||||
min-width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emailBody {
|
|
||||||
flex: 1;
|
|
||||||
padding: 24px;
|
|
||||||
overflow-y: auto;
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emailActions {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 16px 24px;
|
|
||||||
border-top: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
|
||||||
background: ${cssManager.bdTheme('#fafafa', '#1a1a1a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.emptyState {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 400px;
|
|
||||||
color: ${cssManager.bdTheme('#999', '#666')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.emptyIcon {
|
|
||||||
font-size: 64px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emptyText {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-pending {
|
|
||||||
color: ${cssManager.bdTheme('#f59e0b', '#fbbf24')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-processing {
|
|
||||||
color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-delivered {
|
|
||||||
color: ${cssManager.bdTheme('#10b981', '#34d399')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-failed {
|
|
||||||
color: ${cssManager.bdTheme('#ef4444', '#f87171')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-deferred {
|
|
||||||
color: ${cssManager.bdTheme('#f97316', '#fb923c')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.severity-info {
|
|
||||||
color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.severity-warn {
|
|
||||||
color: ${cssManager.bdTheme('#f59e0b', '#fbbf24')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.severity-error {
|
|
||||||
color: ${cssManager.bdTheme('#ef4444', '#f87171')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.severity-critical {
|
|
||||||
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.incidentDetails {
|
|
||||||
padding: 24px;
|
|
||||||
background: ${cssManager.bdTheme('#fff', '#222')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.incidentHeader {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.incidentTitle {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.incidentMeta {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
||||||
gap: 12px;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.incidentField {
|
|
||||||
padding: 12px;
|
|
||||||
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.incidentFieldLabel {
|
|
||||||
font-size: 12px;
|
|
||||||
color: ${cssManager.bdTheme('#666', '#999')};
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.incidentFieldValue {
|
|
||||||
font-size: 14px;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
if (this.selectedEmail) {
|
|
||||||
return this.renderEmailDetail();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.selectedIncident) {
|
|
||||||
return this.renderIncidentDetail();
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ops-sectionheading>Email Operations</ops-sectionheading>
|
<ops-sectionheading>Email Operations</ops-sectionheading>
|
||||||
|
<div class="viewContainer">
|
||||||
<!-- Toolbar -->
|
${this.currentView === 'detail' && this.selectedEmail
|
||||||
<div class="emailToolbar" style="margin-bottom: 16px;">
|
? html`
|
||||||
<dees-button @click=${() => this.openComposeModal()} type="highlighted">
|
<sz-mta-detail-view
|
||||||
<dees-icon icon="lucide:penLine" slot="iconSlot"></dees-icon>
|
.email=${this.selectedEmail}
|
||||||
Compose
|
@back=${this.handleBack}
|
||||||
</dees-button>
|
></sz-mta-detail-view>
|
||||||
|
`
|
||||||
<dees-input-text
|
: html`
|
||||||
class="searchBox"
|
<sz-mta-list-view
|
||||||
placeholder="Search..."
|
.emails=${this.emails}
|
||||||
.value=${this.searchTerm}
|
@email-click=${this.handleEmailClick}
|
||||||
@input=${(e: Event) => this.searchTerm = (e.target as any).value}
|
></sz-mta-list-view>
|
||||||
>
|
`
|
||||||
<dees-icon icon="lucide:search" slot="iconSlot"></dees-icon>
|
|
||||||
</dees-input-text>
|
|
||||||
|
|
||||||
<dees-button @click=${() => this.refreshData()}>
|
|
||||||
${this.isLoading ? html`<dees-spinner slot="iconSlot" size="small"></dees-spinner>` : html`<dees-icon slot="iconSlot" icon="lucide:refreshCw"></dees-icon>`}
|
|
||||||
Refresh
|
|
||||||
</dees-button>
|
|
||||||
|
|
||||||
<div style="margin-left: auto; display: flex; gap: 8px;">
|
|
||||||
<dees-button-group>
|
|
||||||
<dees-button
|
|
||||||
@click=${() => this.selectFolder('queued')}
|
|
||||||
.type=${this.selectedFolder === 'queued' ? 'highlighted' : 'normal'}
|
|
||||||
>
|
|
||||||
Queued ${this.queuedEmails.length > 0 ? `(${this.queuedEmails.length})` : ''}
|
|
||||||
</dees-button>
|
|
||||||
<dees-button
|
|
||||||
@click=${() => this.selectFolder('sent')}
|
|
||||||
.type=${this.selectedFolder === 'sent' ? 'highlighted' : 'normal'}
|
|
||||||
>
|
|
||||||
Sent
|
|
||||||
</dees-button>
|
|
||||||
<dees-button
|
|
||||||
@click=${() => this.selectFolder('failed')}
|
|
||||||
.type=${this.selectedFolder === 'failed' ? 'highlighted' : 'normal'}
|
|
||||||
>
|
|
||||||
Failed ${this.failedEmails.length > 0 ? `(${this.failedEmails.length})` : ''}
|
|
||||||
</dees-button>
|
|
||||||
<dees-button
|
|
||||||
@click=${() => this.selectFolder('security')}
|
|
||||||
.type=${this.selectedFolder === 'security' ? 'highlighted' : 'normal'}
|
|
||||||
>
|
|
||||||
Security ${this.securityIncidents.length > 0 ? `(${this.securityIncidents.length})` : ''}
|
|
||||||
</dees-button>
|
|
||||||
</dees-button-group>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${this.renderContent()}
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderContent() {
|
|
||||||
switch (this.selectedFolder) {
|
|
||||||
case 'queued':
|
|
||||||
return this.renderEmailTable(this.queuedEmails, 'Queued Emails', 'Emails waiting to be delivered');
|
|
||||||
case 'sent':
|
|
||||||
return this.renderEmailTable(this.sentEmails, 'Sent Emails', 'Successfully delivered emails');
|
|
||||||
case 'failed':
|
|
||||||
return this.renderEmailTable(this.failedEmails, 'Failed Emails', 'Emails that failed to deliver', true);
|
|
||||||
case 'security':
|
|
||||||
return this.renderSecurityIncidents();
|
|
||||||
default:
|
|
||||||
return this.renderEmptyState('Select a folder');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderEmailTable(
|
|
||||||
emails: interfaces.requests.IEmailQueueItem[],
|
|
||||||
heading1: string,
|
|
||||||
heading2: string,
|
|
||||||
showResend = false
|
|
||||||
) {
|
|
||||||
const filteredEmails = this.filterEmails(emails);
|
|
||||||
|
|
||||||
if (filteredEmails.length === 0) {
|
|
||||||
return this.renderEmptyState(`No emails in ${this.selectedFolder}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const actions = [
|
|
||||||
{
|
|
||||||
name: 'View Details',
|
|
||||||
iconName: 'lucide:eye',
|
|
||||||
type: ['doubleClick', 'inRow'] as any,
|
|
||||||
actionFunc: async (actionData: any) => {
|
|
||||||
this.selectedEmail = actionData.item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
if (showResend) {
|
|
||||||
actions.push({
|
|
||||||
name: 'Resend',
|
|
||||||
iconName: 'lucide:send',
|
|
||||||
type: ['inRow'] as any,
|
|
||||||
actionFunc: async (actionData: any) => {
|
|
||||||
await this.resendEmail(actionData.item.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<dees-table
|
|
||||||
.data=${filteredEmails}
|
|
||||||
.displayFunction=${(email: interfaces.requests.IEmailQueueItem) => ({
|
|
||||||
'Status': html`<span class="status-${email.status}">${email.status}</span>`,
|
|
||||||
'From': email.from || 'N/A',
|
|
||||||
'To': email.to?.join(', ') || 'N/A',
|
|
||||||
'Subject': email.subject || 'No subject',
|
|
||||||
'Attempts': email.attempts,
|
|
||||||
'Created': this.formatDate(email.createdAt),
|
|
||||||
})}
|
|
||||||
.dataActions=${actions}
|
|
||||||
.selectionMode=${'single'}
|
|
||||||
heading1=${heading1}
|
|
||||||
heading2=${`${filteredEmails.length} emails - ${heading2}`}
|
|
||||||
></dees-table>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderSecurityIncidents() {
|
|
||||||
const incidents = this.securityIncidents;
|
|
||||||
|
|
||||||
if (incidents.length === 0) {
|
|
||||||
return this.renderEmptyState('No security incidents');
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<dees-table
|
|
||||||
.data=${incidents}
|
|
||||||
.displayFunction=${(incident: interfaces.requests.ISecurityIncident) => ({
|
|
||||||
'Severity': html`<span class="severity-${incident.level}">${incident.level.toUpperCase()}</span>`,
|
|
||||||
'Type': incident.type,
|
|
||||||
'Message': incident.message,
|
|
||||||
'IP': incident.ipAddress || 'N/A',
|
|
||||||
'Domain': incident.domain || 'N/A',
|
|
||||||
'Time': this.formatDate(incident.timestamp),
|
|
||||||
})}
|
|
||||||
.dataActions=${[
|
|
||||||
{
|
|
||||||
name: 'View Details',
|
|
||||||
iconName: 'lucide:eye',
|
|
||||||
type: ['doubleClick', 'inRow'],
|
|
||||||
actionFunc: async (actionData: any) => {
|
|
||||||
this.selectedIncident = actionData.item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
.selectionMode=${'single'}
|
|
||||||
heading1="Security Incidents"
|
|
||||||
heading2=${`${incidents.length} incidents`}
|
|
||||||
></dees-table>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderEmailDetail() {
|
|
||||||
if (!this.selectedEmail) return '';
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<ops-sectionheading>Email Details</ops-sectionheading>
|
|
||||||
<div class="emailLayout">
|
|
||||||
<div class="sidebar">
|
|
||||||
<dees-windowbox>
|
|
||||||
<dees-button @click=${() => this.selectedEmail = null} type="secondary" style="width: 100%;">
|
|
||||||
<dees-icon icon="lucide:arrowLeft" slot="iconSlot"></dees-icon>
|
|
||||||
Back to List
|
|
||||||
</dees-button>
|
|
||||||
</dees-windowbox>
|
|
||||||
</div>
|
|
||||||
<div class="mainArea">
|
|
||||||
<div class="emailPreview">
|
|
||||||
<div class="emailHeader">
|
|
||||||
<div class="emailSubject">${this.selectedEmail.subject || 'No subject'}</div>
|
|
||||||
<div class="emailMeta">
|
|
||||||
<div class="emailMetaRow">
|
|
||||||
<span class="emailMetaLabel">Status:</span>
|
|
||||||
<span class="status-${this.selectedEmail.status}">${this.selectedEmail.status}</span>
|
|
||||||
</div>
|
|
||||||
<div class="emailMetaRow">
|
|
||||||
<span class="emailMetaLabel">From:</span>
|
|
||||||
<span>${this.selectedEmail.from || 'N/A'}</span>
|
|
||||||
</div>
|
|
||||||
<div class="emailMetaRow">
|
|
||||||
<span class="emailMetaLabel">To:</span>
|
|
||||||
<span>${this.selectedEmail.to?.join(', ') || 'N/A'}</span>
|
|
||||||
</div>
|
|
||||||
<div class="emailMetaRow">
|
|
||||||
<span class="emailMetaLabel">Mode:</span>
|
|
||||||
<span>${this.selectedEmail.processingMode}</span>
|
|
||||||
</div>
|
|
||||||
<div class="emailMetaRow">
|
|
||||||
<span class="emailMetaLabel">Attempts:</span>
|
|
||||||
<span>${this.selectedEmail.attempts}</span>
|
|
||||||
</div>
|
|
||||||
<div class="emailMetaRow">
|
|
||||||
<span class="emailMetaLabel">Created:</span>
|
|
||||||
<span>${new Date(this.selectedEmail.createdAt).toLocaleString()}</span>
|
|
||||||
</div>
|
|
||||||
${this.selectedEmail.deliveredAt ? html`
|
|
||||||
<div class="emailMetaRow">
|
|
||||||
<span class="emailMetaLabel">Delivered:</span>
|
|
||||||
<span>${new Date(this.selectedEmail.deliveredAt).toLocaleString()}</span>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
${this.selectedEmail.lastError ? html`
|
|
||||||
<div class="emailMetaRow">
|
|
||||||
<span class="emailMetaLabel">Last Error:</span>
|
|
||||||
<span style="color: #ef4444;">${this.selectedEmail.lastError}</span>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="emailActions">
|
|
||||||
${this.selectedEmail.status === 'failed' ? html`
|
|
||||||
<dees-button @click=${() => this.resendEmail(this.selectedEmail!.id)} type="highlighted">
|
|
||||||
<dees-icon icon="lucide:send" slot="iconSlot"></dees-icon>
|
|
||||||
Resend
|
|
||||||
</dees-button>
|
|
||||||
` : ''}
|
|
||||||
<dees-button @click=${() => this.selectedEmail = null}>
|
|
||||||
<dees-icon icon="lucide:x" slot="iconSlot"></dees-icon>
|
|
||||||
Close
|
|
||||||
</dees-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderIncidentDetail() {
|
private async handleEmailClick(e: CustomEvent<interfaces.requests.IEmail>) {
|
||||||
if (!this.selectedIncident) return '';
|
const emailSummary = e.detail;
|
||||||
|
|
||||||
const incident = this.selectedIncident;
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<ops-sectionheading>Security Incident Details</ops-sectionheading>
|
|
||||||
<div style="margin-bottom: 16px;">
|
|
||||||
<dees-button @click=${() => this.selectedIncident = null} type="secondary">
|
|
||||||
<dees-icon icon="lucide:arrowLeft" slot="iconSlot"></dees-icon>
|
|
||||||
Back to List
|
|
||||||
</dees-button>
|
|
||||||
</div>
|
|
||||||
<div class="incidentDetails">
|
|
||||||
<div class="incidentHeader">
|
|
||||||
<div>
|
|
||||||
<div class="incidentTitle">${incident.message}</div>
|
|
||||||
<div style="margin-top: 8px; color: #666;">
|
|
||||||
${new Date(incident.timestamp).toLocaleString()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="severity-${incident.level}" style="font-size: 16px; padding: 4px 12px; background: rgba(0,0,0,0.1); border-radius: 4px;">
|
|
||||||
${incident.level.toUpperCase()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="incidentMeta">
|
|
||||||
<div class="incidentField">
|
|
||||||
<div class="incidentFieldLabel">Type</div>
|
|
||||||
<div class="incidentFieldValue">${incident.type}</div>
|
|
||||||
</div>
|
|
||||||
${incident.ipAddress ? html`
|
|
||||||
<div class="incidentField">
|
|
||||||
<div class="incidentFieldLabel">IP Address</div>
|
|
||||||
<div class="incidentFieldValue">${incident.ipAddress}</div>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
${incident.domain ? html`
|
|
||||||
<div class="incidentField">
|
|
||||||
<div class="incidentFieldLabel">Domain</div>
|
|
||||||
<div class="incidentFieldValue">${incident.domain}</div>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
${incident.emailId ? html`
|
|
||||||
<div class="incidentField">
|
|
||||||
<div class="incidentFieldLabel">Email ID</div>
|
|
||||||
<div class="incidentFieldValue">${incident.emailId}</div>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
${incident.userId ? html`
|
|
||||||
<div class="incidentField">
|
|
||||||
<div class="incidentFieldLabel">User ID</div>
|
|
||||||
<div class="incidentFieldValue">${incident.userId}</div>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
${incident.action ? html`
|
|
||||||
<div class="incidentField">
|
|
||||||
<div class="incidentFieldLabel">Action</div>
|
|
||||||
<div class="incidentFieldValue">${incident.action}</div>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
${incident.result ? html`
|
|
||||||
<div class="incidentField">
|
|
||||||
<div class="incidentFieldLabel">Result</div>
|
|
||||||
<div class="incidentFieldValue">${incident.result}</div>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
${incident.success !== undefined ? html`
|
|
||||||
<div class="incidentField">
|
|
||||||
<div class="incidentFieldLabel">Success</div>
|
|
||||||
<div class="incidentFieldValue">${incident.success ? 'Yes' : 'No'}</div>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${incident.details ? html`
|
|
||||||
<div style="margin-top: 24px;">
|
|
||||||
<div class="incidentFieldLabel" style="margin-bottom: 8px;">Details</div>
|
|
||||||
<pre style="background: #1a1a1a; color: #e5e5e5; padding: 16px; border-radius: 6px; overflow-x: auto; font-size: 13px;">
|
|
||||||
${JSON.stringify(incident.details, null, 2)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderEmptyState(message: string) {
|
|
||||||
return html`
|
|
||||||
<div class="emptyState">
|
|
||||||
<dees-icon class="emptyIcon" icon="lucide:inbox"></dees-icon>
|
|
||||||
<div class="emptyText">${message}</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async openComposeModal() {
|
|
||||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
||||||
|
|
||||||
// Ensure domains are loaded before opening modal
|
|
||||||
if (this.emailDomains.length === 0) {
|
|
||||||
await this.loadEmailDomains();
|
|
||||||
}
|
|
||||||
|
|
||||||
await DeesModal.createAndShow({
|
|
||||||
heading: 'New Email',
|
|
||||||
width: 'large',
|
|
||||||
content: html`
|
|
||||||
<div>
|
|
||||||
<dees-form @formData=${async (e: CustomEvent) => {
|
|
||||||
await this.sendEmail(e.detail);
|
|
||||||
const modals = document.querySelectorAll('dees-modal');
|
|
||||||
modals.forEach(m => (m as any).destroy?.());
|
|
||||||
}}>
|
|
||||||
<div style="display: flex; gap: 8px; align-items: flex-end;">
|
|
||||||
<dees-input-text
|
|
||||||
key="fromUsername"
|
|
||||||
label="From"
|
|
||||||
placeholder="username"
|
|
||||||
.value=${'admin'}
|
|
||||||
required
|
|
||||||
style="flex: 1;"
|
|
||||||
></dees-input-text>
|
|
||||||
<span style="padding-bottom: 12px; font-size: 18px; color: #666;">@</span>
|
|
||||||
<dees-input-dropdown
|
|
||||||
key="fromDomain"
|
|
||||||
label=" "
|
|
||||||
.options=${this.emailDomains.length > 0
|
|
||||||
? this.emailDomains.map(domain => ({ key: domain, value: domain }))
|
|
||||||
: [{ key: 'dcrouter.local', value: 'dcrouter.local' }]}
|
|
||||||
.selectedKey=${this.emailDomains[0] || 'dcrouter.local'}
|
|
||||||
required
|
|
||||||
style="flex: 1;"
|
|
||||||
></dees-input-dropdown>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<dees-input-tags
|
|
||||||
key="to"
|
|
||||||
label="To"
|
|
||||||
placeholder="Enter recipient email addresses..."
|
|
||||||
required
|
|
||||||
></dees-input-tags>
|
|
||||||
|
|
||||||
<dees-input-tags
|
|
||||||
key="cc"
|
|
||||||
label="CC"
|
|
||||||
placeholder="Enter CC recipients..."
|
|
||||||
></dees-input-tags>
|
|
||||||
|
|
||||||
<dees-input-text
|
|
||||||
key="subject"
|
|
||||||
label="Subject"
|
|
||||||
placeholder="Enter email subject..."
|
|
||||||
required
|
|
||||||
></dees-input-text>
|
|
||||||
|
|
||||||
<dees-input-wysiwyg
|
|
||||||
key="body"
|
|
||||||
label="Message"
|
|
||||||
outputFormat="html"
|
|
||||||
></dees-input-wysiwyg>
|
|
||||||
</dees-form>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{
|
|
||||||
name: 'Send',
|
|
||||||
iconName: 'lucide:send',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
const form = modalArg.shadowRoot?.querySelector('dees-form') as any;
|
|
||||||
form?.submit();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Cancel',
|
|
||||||
iconName: 'lucide:x',
|
|
||||||
action: async (modalArg) => await modalArg.destroy()
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private filterEmails(emails: interfaces.requests.IEmailQueueItem[]): interfaces.requests.IEmailQueueItem[] {
|
|
||||||
if (!this.searchTerm) {
|
|
||||||
return emails;
|
|
||||||
}
|
|
||||||
|
|
||||||
const search = this.searchTerm.toLowerCase();
|
|
||||||
return emails.filter(e =>
|
|
||||||
(e.subject?.toLowerCase().includes(search)) ||
|
|
||||||
(e.from?.toLowerCase().includes(search)) ||
|
|
||||||
(e.to?.some(t => t.toLowerCase().includes(search)))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private selectFolder(folder: TEmailFolder) {
|
|
||||||
// Use router for navigation to update URL
|
|
||||||
appRouter.navigateToEmailFolder(folder);
|
|
||||||
// Clear selections
|
|
||||||
this.selectedEmail = null;
|
|
||||||
this.selectedIncident = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatDate(timestamp: number): string {
|
|
||||||
const date = new Date(timestamp);
|
|
||||||
const now = new Date();
|
|
||||||
const diff = now.getTime() - date.getTime();
|
|
||||||
const hours = diff / (1000 * 60 * 60);
|
|
||||||
|
|
||||||
if (hours < 24) {
|
|
||||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
||||||
} else if (hours < 168) { // 7 days
|
|
||||||
return date.toLocaleDateString([], { weekday: 'short', hour: '2-digit', minute: '2-digit' });
|
|
||||||
} else {
|
|
||||||
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async loadData() {
|
|
||||||
this.isLoading = true;
|
|
||||||
await this.loadFolderData(this.selectedFolder);
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async loadFolderData(folder: TEmailFolder) {
|
|
||||||
switch (folder) {
|
|
||||||
case 'queued':
|
|
||||||
await appstate.emailOpsStatePart.dispatchAction(appstate.fetchQueuedEmailsAction, null);
|
|
||||||
break;
|
|
||||||
case 'sent':
|
|
||||||
await appstate.emailOpsStatePart.dispatchAction(appstate.fetchSentEmailsAction, null);
|
|
||||||
break;
|
|
||||||
case 'failed':
|
|
||||||
await appstate.emailOpsStatePart.dispatchAction(appstate.fetchFailedEmailsAction, null);
|
|
||||||
break;
|
|
||||||
case 'security':
|
|
||||||
await appstate.emailOpsStatePart.dispatchAction(appstate.fetchSecurityIncidentsAction, null);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async loadEmailDomains() {
|
|
||||||
try {
|
try {
|
||||||
await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null);
|
const context = appstate.loginStatePart.getState();
|
||||||
const config = appstate.configStatePart.getState().config;
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_GetEmailDetail
|
||||||
|
>('/typedrequest', 'getEmailDetail');
|
||||||
|
|
||||||
if (config?.email?.domains && Array.isArray(config.email.domains) && config.email.domains.length > 0) {
|
const response = await request.fire({
|
||||||
this.emailDomains = config.email.domains;
|
identity: context.identity,
|
||||||
} else {
|
emailId: emailSummary.id,
|
||||||
this.emailDomains = ['dcrouter.local'];
|
});
|
||||||
|
|
||||||
|
if (response.email) {
|
||||||
|
this.selectedEmail = response.email;
|
||||||
|
this.currentView = 'detail';
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load email domains:', error);
|
console.error('Failed to fetch email detail:', error);
|
||||||
this.emailDomains = ['dcrouter.local'];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async refreshData() {
|
private handleBack() {
|
||||||
this.isLoading = true;
|
|
||||||
await this.loadFolderData(this.selectedFolder);
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async sendEmail(formData: any) {
|
|
||||||
try {
|
|
||||||
console.log('Sending email:', formData);
|
|
||||||
// TODO: Implement actual email sending via API
|
|
||||||
// For now, just log the data
|
|
||||||
const fromEmail = `${formData.fromUsername || 'admin'}@${formData.fromDomain || this.emailDomains[0] || 'dcrouter.local'}`;
|
|
||||||
console.log('From:', fromEmail);
|
|
||||||
console.log('To:', formData.to);
|
|
||||||
console.log('Subject:', formData.subject);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Failed to send email', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async resendEmail(emailId: string) {
|
|
||||||
try {
|
|
||||||
await appstate.emailOpsStatePart.dispatchAction(appstate.resendEmailAction, emailId);
|
|
||||||
this.selectedEmail = null;
|
this.selectedEmail = null;
|
||||||
} catch (error) {
|
this.currentView = 'list';
|
||||||
console.error('Failed to resend email:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as plugins from '../plugins.js';
|
|
||||||
import * as shared from './shared/index.js';
|
import * as shared from './shared/index.js';
|
||||||
import * as appstate from '../appstate.js';
|
import * as appstate from '../appstate.js';
|
||||||
|
|
||||||
@@ -20,15 +19,6 @@ 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;
|
private lastPushedCount = 0;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -44,63 +34,13 @@ export class OpsViewLogs extends DeesElement {
|
|||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
shared.viewHostCss,
|
shared.viewHostCss,
|
||||||
css`
|
css``,
|
||||||
.controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filterGroup {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
return html`
|
return html`
|
||||||
<ops-sectionheading>Logs</ops-sectionheading>
|
<ops-sectionheading>Logs</ops-sectionheading>
|
||||||
|
|
||||||
<div class="controls">
|
|
||||||
<div class="filterGroup">
|
|
||||||
<dees-button
|
|
||||||
@click=${() => this.fetchLogs()}
|
|
||||||
>
|
|
||||||
Refresh Logs
|
|
||||||
</dees-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filterGroup">
|
|
||||||
<label>Level:</label>
|
|
||||||
<dees-input-dropdown
|
|
||||||
.options=${['all', 'debug', 'info', 'warn', 'error']}
|
|
||||||
.selectedOption=${'all'}
|
|
||||||
@selectedOption=${(e: any) => this.updateFilter('level', e.detail)}
|
|
||||||
></dees-input-dropdown>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filterGroup">
|
|
||||||
<label>Category:</label>
|
|
||||||
<dees-input-dropdown
|
|
||||||
.options=${['all', 'smtp', 'dns', 'security', 'system', 'email']}
|
|
||||||
.selectedOption=${'all'}
|
|
||||||
@selectedOption=${(e: any) => this.updateFilter('category', e.detail)}
|
|
||||||
></dees-input-dropdown>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filterGroup">
|
|
||||||
<label>Limit:</label>
|
|
||||||
<dees-input-dropdown
|
|
||||||
.options=${['50', '100', '200', '500']}
|
|
||||||
.selectedOption=${'100'}
|
|
||||||
@selectedOption=${(e: any) => this.updateFilter('limit', e.detail)}
|
|
||||||
></dees-input-dropdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<dees-chart-log
|
<dees-chart-log
|
||||||
.label=${'Application Logs'}
|
.label=${'Application Logs'}
|
||||||
.autoScroll=${true}
|
.autoScroll=${true}
|
||||||
@@ -115,7 +55,7 @@ export class OpsViewLogs extends DeesElement {
|
|||||||
this.lastPushedCount = 0;
|
this.lastPushedCount = 0;
|
||||||
// Only fetch if state is empty (streaming will handle new entries)
|
// Only fetch if state is empty (streaming will handle new entries)
|
||||||
if (this.logState.recentLogs.length === 0) {
|
if (this.logState.recentLogs.length === 0) {
|
||||||
this.fetchLogs();
|
await appstate.logStatePart.dispatchAction(appstate.fetchRecentLogsAction, { limit: 100 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,6 +73,17 @@ export class OpsViewLogs extends DeesElement {
|
|||||||
// Ensure the chart element has finished its own initialization
|
// Ensure the chart element has finished its own initialization
|
||||||
await chartLog.updateComplete;
|
await chartLog.updateComplete;
|
||||||
|
|
||||||
|
// Wait for xterm terminal to finish initializing (CDN load)
|
||||||
|
if (!chartLog.terminalReady) {
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
const check = () => {
|
||||||
|
if (chartLog.terminalReady) { resolve(); return; }
|
||||||
|
setTimeout(check, 50);
|
||||||
|
};
|
||||||
|
check();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const allEntries = this.getMappedLogEntries();
|
const allEntries = this.getMappedLogEntries();
|
||||||
if (this.lastPushedCount === 0 && allEntries.length > 0) {
|
if (this.lastPushedCount === 0 && allEntries.length > 0) {
|
||||||
// Initial load: push all entries
|
// Initial load: push all entries
|
||||||
@@ -155,29 +106,4 @@ export class OpsViewLogs extends DeesElement {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchLogs() {
|
|
||||||
await appstate.logStatePart.dispatchAction(appstate.fetchRecentLogsAction, {
|
|
||||||
limit: this.filterLimit,
|
|
||||||
level: this.filterLevel as 'debug' | 'info' | 'warn' | 'error' | undefined,
|
|
||||||
category: this.filterCategory as 'smtp' | 'dns' | 'security' | 'system' | 'email' | undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateFilter(type: string, value: string) {
|
|
||||||
const resolved = value === 'all' ? undefined : value;
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case 'level':
|
|
||||||
this.filterLevel = resolved;
|
|
||||||
break;
|
|
||||||
case 'category':
|
|
||||||
this.filterCategory = resolved;
|
|
||||||
break;
|
|
||||||
case 'limit':
|
|
||||||
this.filterLimit = resolved ? parseInt(resolved, 10) : 100;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.fetchLogs();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,9 +47,6 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
private lastChartUpdate = 0;
|
private lastChartUpdate = 0;
|
||||||
private chartUpdateThreshold = 1000; // Minimum ms between chart updates
|
private chartUpdateThreshold = 1000; // Minimum ms between chart updates
|
||||||
|
|
||||||
private lastTrafficUpdateTime = 0;
|
|
||||||
private trafficUpdateInterval = 1000; // Update every 1 second
|
|
||||||
private requestCountHistory = new Map<number, number>(); // Track requests per time bucket
|
|
||||||
private trafficUpdateTimer: any = null;
|
private trafficUpdateTimer: any = null;
|
||||||
private requestsPerSecHistory: number[] = []; // Track requests/sec over time for trend
|
private requestsPerSecHistory: number[] = []; // Track requests/sec over time for trend
|
||||||
private historyLoaded = false; // Whether server-side throughput history has been loaded
|
private historyLoaded = false; // Whether server-side throughput history has been loaded
|
||||||
@@ -106,8 +103,6 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
|
|
||||||
this.trafficDataIn = [...emptyData];
|
this.trafficDataIn = [...emptyData];
|
||||||
this.trafficDataOut = emptyData.map(point => ({ ...point }));
|
this.trafficDataOut = emptyData.map(point => ({ ...point }));
|
||||||
|
|
||||||
this.lastTrafficUpdateTime = now;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -413,11 +408,7 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
const throughput = this.calculateThroughput();
|
const throughput = this.calculateThroughput();
|
||||||
const activeConnections = this.statsState.serverStats?.activeConnections || 0;
|
const activeConnections = this.statsState.serverStats?.activeConnections || 0;
|
||||||
|
|
||||||
// Track requests/sec history for the trend sparkline
|
// Build trend data from pre-computed history (mutated in updateNetworkData, not here)
|
||||||
this.requestsPerSecHistory.push(reqPerSec);
|
|
||||||
if (this.requestsPerSecHistory.length > 20) {
|
|
||||||
this.requestsPerSecHistory.shift();
|
|
||||||
}
|
|
||||||
const trendData = [...this.requestsPerSecHistory];
|
const trendData = [...this.requestsPerSecHistory];
|
||||||
while (trendData.length < 20) {
|
while (trendData.length < 20) {
|
||||||
trendData.unshift(0);
|
trendData.unshift(0);
|
||||||
@@ -529,6 +520,13 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async updateNetworkData() {
|
private async updateNetworkData() {
|
||||||
|
// Track requests/sec history for the trend sparkline (moved out of render)
|
||||||
|
const reqPerSec = this.networkState.requestsPerSecond || 0;
|
||||||
|
this.requestsPerSecHistory.push(reqPerSec);
|
||||||
|
if (this.requestsPerSecHistory.length > 20) {
|
||||||
|
this.requestsPerSecHistory.shift();
|
||||||
|
}
|
||||||
|
|
||||||
// Only update if connections changed significantly
|
// Only update if connections changed significantly
|
||||||
const newConnectionCount = this.networkState.connections.length;
|
const newConnectionCount = this.networkState.connections.length;
|
||||||
const oldConnectionCount = this.networkRequests.length;
|
const oldConnectionCount = this.networkRequests.length;
|
||||||
@@ -602,16 +600,13 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
y: Math.round(throughputOutMbps * 10) / 10
|
y: Math.round(throughputOutMbps * 10) / 10
|
||||||
};
|
};
|
||||||
|
|
||||||
// Efficient array updates - modify in place when possible
|
// In-place mutation then reassign for Lit reactivity (avoids 4 intermediate arrays)
|
||||||
if (this.trafficDataIn.length >= 60) {
|
if (this.trafficDataIn.length >= 60) {
|
||||||
// Remove oldest and add newest
|
this.trafficDataIn.shift();
|
||||||
this.trafficDataIn = [...this.trafficDataIn.slice(1), newDataPointIn];
|
this.trafficDataOut.shift();
|
||||||
this.trafficDataOut = [...this.trafficDataOut.slice(1), newDataPointOut];
|
}
|
||||||
} else {
|
|
||||||
// Still filling up the initial data
|
|
||||||
this.trafficDataIn = [...this.trafficDataIn, newDataPointIn];
|
this.trafficDataIn = [...this.trafficDataIn, newDataPointIn];
|
||||||
this.trafficDataOut = [...this.trafficDataOut, newDataPointOut];
|
this.trafficDataOut = [...this.trafficDataOut, newDataPointOut];
|
||||||
}
|
|
||||||
|
|
||||||
this.lastChartUpdate = now;
|
this.lastChartUpdate = now;
|
||||||
}
|
}
|
||||||
|
|||||||
389
ts_web/elements/ops-view-routes.ts
Normal file
389
ts_web/elements/ops-view-routes.ts
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
import * as appstate from '../appstate.js';
|
||||||
|
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||||
|
import { viewHostCss } from './shared/css.js';
|
||||||
|
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
state,
|
||||||
|
type TemplateResult,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
@customElement('ops-view-routes')
|
||||||
|
export class OpsViewRoutes extends DeesElement {
|
||||||
|
@state() accessor routeState: appstate.IRouteManagementState = {
|
||||||
|
mergedRoutes: [],
|
||||||
|
warnings: [],
|
||||||
|
apiTokens: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
lastUpdated: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const sub = appstate.routeManagementStatePart
|
||||||
|
.select((s) => s)
|
||||||
|
.subscribe((routeState) => {
|
||||||
|
this.routeState = routeState;
|
||||||
|
});
|
||||||
|
this.rxSubscriptions.push(sub);
|
||||||
|
|
||||||
|
// Re-fetch routes when user logs in (fixes race condition where
|
||||||
|
// the view is created before authentication completes)
|
||||||
|
const loginSub = appstate.loginStatePart
|
||||||
|
.select((s) => s.isLoggedIn)
|
||||||
|
.subscribe((isLoggedIn) => {
|
||||||
|
if (isLoggedIn) {
|
||||||
|
appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.rxSubscriptions.push(loginSub);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
viewHostCss,
|
||||||
|
css`
|
||||||
|
.routesContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warnings-bar {
|
||||||
|
background: ${cssManager.bdTheme('rgba(255, 170, 0, 0.08)', 'rgba(255, 170, 0, 0.1)')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('rgba(255, 170, 0, 0.25)', 'rgba(255, 170, 0, 0.3)')};
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#b45309', '#fa0')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 48px 24px;
|
||||||
|
color: ${cssManager.bdTheme('#6b7280', '#666')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state p {
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
const { mergedRoutes, warnings } = this.routeState;
|
||||||
|
|
||||||
|
const hardcodedCount = mergedRoutes.filter((mr) => mr.source === 'hardcoded').length;
|
||||||
|
const programmaticCount = mergedRoutes.filter((mr) => mr.source === 'programmatic').length;
|
||||||
|
const disabledCount = mergedRoutes.filter((mr) => !mr.enabled).length;
|
||||||
|
|
||||||
|
const statsTiles: IStatsTile[] = [
|
||||||
|
{
|
||||||
|
id: 'totalRoutes',
|
||||||
|
title: 'Total Routes',
|
||||||
|
type: 'number',
|
||||||
|
value: mergedRoutes.length,
|
||||||
|
icon: 'lucide:route',
|
||||||
|
description: 'All configured routes',
|
||||||
|
color: '#3b82f6',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hardcoded',
|
||||||
|
title: 'Hardcoded',
|
||||||
|
type: 'number',
|
||||||
|
value: hardcodedCount,
|
||||||
|
icon: 'lucide:lock',
|
||||||
|
description: 'Routes from constructor config',
|
||||||
|
color: '#8b5cf6',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'programmatic',
|
||||||
|
title: 'Programmatic',
|
||||||
|
type: 'number',
|
||||||
|
value: programmaticCount,
|
||||||
|
icon: 'lucide:code',
|
||||||
|
description: 'Routes added via API',
|
||||||
|
color: '#0ea5e9',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'disabled',
|
||||||
|
title: 'Disabled',
|
||||||
|
type: 'number',
|
||||||
|
value: disabledCount,
|
||||||
|
icon: 'lucide:pauseCircle',
|
||||||
|
description: 'Currently disabled routes',
|
||||||
|
color: disabledCount > 0 ? '#ef4444' : '#6b7280',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Map merged routes to sz-route-list-view format
|
||||||
|
const szRoutes = mergedRoutes.map((mr) => {
|
||||||
|
const tags = [...(mr.route.tags || [])];
|
||||||
|
tags.push(mr.source);
|
||||||
|
if (!mr.enabled) tags.push('disabled');
|
||||||
|
if (mr.overridden) tags.push('overridden');
|
||||||
|
|
||||||
|
return {
|
||||||
|
...mr.route,
|
||||||
|
enabled: mr.enabled,
|
||||||
|
tags,
|
||||||
|
id: mr.storedRouteId || mr.route.name || undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ops-sectionheading>Route Management</ops-sectionheading>
|
||||||
|
|
||||||
|
<div class="routesContainer">
|
||||||
|
<dees-statsgrid
|
||||||
|
.tiles=${statsTiles}
|
||||||
|
.gridActions=${[
|
||||||
|
{
|
||||||
|
name: 'Add Route',
|
||||||
|
iconName: 'lucide:plus',
|
||||||
|
action: () => this.showCreateRouteDialog(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Refresh',
|
||||||
|
iconName: 'lucide:refreshCw',
|
||||||
|
action: () => this.refreshData(),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
></dees-statsgrid>
|
||||||
|
|
||||||
|
${warnings.length > 0
|
||||||
|
? html`
|
||||||
|
<div class="warnings-bar">
|
||||||
|
${warnings.map(
|
||||||
|
(w) => html`
|
||||||
|
<div class="warning-item">
|
||||||
|
<span class="warning-icon">⚠</span>
|
||||||
|
<span>${w.message}</span>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ''}
|
||||||
|
|
||||||
|
${szRoutes.length > 0
|
||||||
|
? html`
|
||||||
|
<sz-route-list-view
|
||||||
|
.routes=${szRoutes}
|
||||||
|
@route-click=${(e: CustomEvent) => this.handleRouteClick(e)}
|
||||||
|
></sz-route-list-view>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<div class="empty-state">
|
||||||
|
<p>No routes configured</p>
|
||||||
|
<p>Add a programmatic route or check your constructor configuration.</p>
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleRouteClick(e: CustomEvent) {
|
||||||
|
const clickedRoute = e.detail;
|
||||||
|
if (!clickedRoute) return;
|
||||||
|
|
||||||
|
// Find the corresponding merged route
|
||||||
|
const merged = this.routeState.mergedRoutes.find(
|
||||||
|
(mr) => mr.route.name === clickedRoute.name,
|
||||||
|
);
|
||||||
|
if (!merged) return;
|
||||||
|
|
||||||
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
|
|
||||||
|
if (merged.source === 'hardcoded') {
|
||||||
|
const menuOptions = merged.enabled
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: 'Disable Route',
|
||||||
|
iconName: 'lucide:pause',
|
||||||
|
action: async (modalArg: any) => {
|
||||||
|
await appstate.routeManagementStatePart.dispatchAction(
|
||||||
|
appstate.setRouteOverrideAction,
|
||||||
|
{ routeName: merged.route.name!, enabled: false },
|
||||||
|
);
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Close',
|
||||||
|
iconName: 'lucide:x',
|
||||||
|
action: async (modalArg: any) => await modalArg.destroy(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
name: 'Enable Route',
|
||||||
|
iconName: 'lucide:play',
|
||||||
|
action: async (modalArg: any) => {
|
||||||
|
await appstate.routeManagementStatePart.dispatchAction(
|
||||||
|
appstate.setRouteOverrideAction,
|
||||||
|
{ routeName: merged.route.name!, enabled: true },
|
||||||
|
);
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Remove Override',
|
||||||
|
iconName: 'lucide:undo',
|
||||||
|
action: async (modalArg: any) => {
|
||||||
|
await appstate.routeManagementStatePart.dispatchAction(
|
||||||
|
appstate.removeRouteOverrideAction,
|
||||||
|
merged.route.name!,
|
||||||
|
);
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Close',
|
||||||
|
iconName: 'lucide:x',
|
||||||
|
action: async (modalArg: any) => await modalArg.destroy(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: `Route: ${merged.route.name}`,
|
||||||
|
content: html`
|
||||||
|
<div style="color: #ccc; padding: 8px 0;">
|
||||||
|
<p>Source: <strong style="color: #88f;">hardcoded</strong></p>
|
||||||
|
<p>Status: <strong>${merged.enabled ? 'Enabled' : 'Disabled (overridden)'}</strong></p>
|
||||||
|
<p style="color: #888; font-size: 13px;">Hardcoded routes cannot be edited or deleted, but they can be disabled via an override.</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
menuOptions,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Programmatic route
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: `Route: ${merged.route.name}`,
|
||||||
|
content: html`
|
||||||
|
<div style="color: #ccc; padding: 8px 0;">
|
||||||
|
<p>Source: <strong style="color: #0af;">programmatic</strong></p>
|
||||||
|
<p>Status: <strong>${merged.enabled ? 'Enabled' : 'Disabled'}</strong></p>
|
||||||
|
<p>ID: <code style="color: #888;">${merged.storedRouteId}</code></p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: merged.enabled ? 'Disable' : 'Enable',
|
||||||
|
iconName: merged.enabled ? 'lucide:pause' : 'lucide:play',
|
||||||
|
action: async (modalArg: any) => {
|
||||||
|
await appstate.routeManagementStatePart.dispatchAction(
|
||||||
|
appstate.toggleRouteAction,
|
||||||
|
{ id: merged.storedRouteId!, enabled: !merged.enabled },
|
||||||
|
);
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delete',
|
||||||
|
iconName: 'lucide:trash-2',
|
||||||
|
action: async (modalArg: any) => {
|
||||||
|
await appstate.routeManagementStatePart.dispatchAction(
|
||||||
|
appstate.deleteRouteAction,
|
||||||
|
merged.storedRouteId!,
|
||||||
|
);
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Close',
|
||||||
|
iconName: 'lucide:x',
|
||||||
|
action: async (modalArg: any) => await modalArg.destroy(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async showCreateRouteDialog() {
|
||||||
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
|
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: 'Add Programmatic Route',
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text .key=${'name'} .label=${'Route Name'} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'ports'} .label=${'Ports (comma-separated)'} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'domains'} .label=${'Domains (comma-separated, optional)'}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'targetHost'} .label=${'Target Host'} .value=${'localhost'} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'targetPort'} .label=${'Target Port'} .required=${true}></dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Cancel',
|
||||||
|
iconName: 'lucide:x',
|
||||||
|
action: async (modalArg: any) => await modalArg.destroy(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Create',
|
||||||
|
iconName: 'lucide:plus',
|
||||||
|
action: async (modalArg: any) => {
|
||||||
|
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
||||||
|
if (!form) return;
|
||||||
|
const formData = await form.collectFormData();
|
||||||
|
if (!formData.name || !formData.ports) return;
|
||||||
|
|
||||||
|
const ports = formData.ports.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p));
|
||||||
|
const domains = formData.domains
|
||||||
|
? formData.domains.split(',').map((d: string) => d.trim()).filter(Boolean)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const route: any = {
|
||||||
|
name: formData.name,
|
||||||
|
match: {
|
||||||
|
ports,
|
||||||
|
...(domains && domains.length > 0 ? { domains } : {}),
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
targets: [
|
||||||
|
{
|
||||||
|
host: formData.targetHost || 'localhost',
|
||||||
|
port: parseInt(formData.targetPort, 10),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await appstate.routeManagementStatePart.dispatchAction(
|
||||||
|
appstate.createRouteAction,
|
||||||
|
{ route },
|
||||||
|
);
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private refreshData() {
|
||||||
|
appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async firstUpdated() {
|
||||||
|
await appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,16 @@
|
|||||||
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';
|
||||||
|
|
||||||
|
// @serve.zone scope
|
||||||
|
import * as szCatalog from '@serve.zone/catalog';
|
||||||
|
|
||||||
// TypedSocket for real-time push communication
|
// TypedSocket for real-time push communication
|
||||||
import * as typedsocket from '@api.global/typedsocket';
|
import * as typedsocket from '@api.global/typedsocket';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
deesElement,
|
deesElement,
|
||||||
deesCatalog,
|
deesCatalog,
|
||||||
|
szCatalog,
|
||||||
typedsocket,
|
typedsocket,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ import * as appstate from './appstate.js';
|
|||||||
|
|
||||||
const SmartRouter = plugins.domtools.plugins.smartrouter.SmartRouter;
|
const SmartRouter = plugins.domtools.plugins.smartrouter.SmartRouter;
|
||||||
|
|
||||||
export const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates', 'remoteingress'] as const;
|
export const validViews = ['overview', 'network', 'emails', 'logs', 'routes', 'apitokens', 'configuration', 'security', 'certificates', 'remoteingress'] as const;
|
||||||
export const validEmailFolders = ['queued', 'sent', 'failed', 'security'] as const;
|
|
||||||
|
|
||||||
export type TValidView = typeof validViews[number];
|
export type TValidView = typeof validViews[number];
|
||||||
export type TValidEmailFolder = typeof validEmailFolders[number];
|
|
||||||
|
|
||||||
class AppRouter {
|
class AppRouter {
|
||||||
private router: InstanceType<typeof SmartRouter>;
|
private router: InstanceType<typeof SmartRouter>;
|
||||||
@@ -27,32 +25,11 @@ class AppRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setupRoutes(): void {
|
private setupRoutes(): void {
|
||||||
// Main views
|
|
||||||
for (const view of validViews) {
|
for (const view of validViews) {
|
||||||
if (view === 'emails') {
|
|
||||||
// Email root - default to queued
|
|
||||||
this.router.on('/emails', async () => {
|
|
||||||
this.updateViewState('emails');
|
|
||||||
this.updateEmailFolder('queued');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Email with folder parameter
|
|
||||||
this.router.on('/emails/:folder', async (routeInfo) => {
|
|
||||||
const folder = routeInfo.params.folder as string;
|
|
||||||
if (validEmailFolders.includes(folder as TValidEmailFolder)) {
|
|
||||||
this.updateViewState('emails');
|
|
||||||
this.updateEmailFolder(folder as TValidEmailFolder);
|
|
||||||
} else {
|
|
||||||
// Invalid folder, redirect to queued
|
|
||||||
this.navigateTo('/emails/queued');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.router.on(`/${view}`, async () => {
|
this.router.on(`/${view}`, async () => {
|
||||||
this.updateViewState(view);
|
this.updateViewState(view);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Root redirect
|
// Root redirect
|
||||||
this.router.on('/', async () => {
|
this.router.on('/', async () => {
|
||||||
@@ -61,60 +38,32 @@ class AppRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setupStateSync(): void {
|
private setupStateSync(): void {
|
||||||
// Sync URL when state changes programmatically (not from router)
|
|
||||||
appstate.uiStatePart.state.subscribe((uiState) => {
|
appstate.uiStatePart.state.subscribe((uiState) => {
|
||||||
if (this.suppressStateUpdate) return;
|
if (this.suppressStateUpdate) return;
|
||||||
|
|
||||||
const currentPath = window.location.pathname;
|
const currentPath = window.location.pathname;
|
||||||
const expectedPath = this.getExpectedPath(uiState.activeView);
|
const expectedPath = `/${uiState.activeView}`;
|
||||||
|
|
||||||
// Only update URL if it doesn't match current state
|
if (currentPath !== expectedPath) {
|
||||||
if (!currentPath.startsWith(expectedPath)) {
|
|
||||||
this.suppressStateUpdate = true;
|
this.suppressStateUpdate = true;
|
||||||
if (uiState.activeView === 'emails') {
|
this.router.pushUrl(expectedPath);
|
||||||
const emailState = appstate.emailOpsStatePart.getState();
|
|
||||||
this.router.pushUrl(`/emails/${emailState.currentView}`);
|
|
||||||
} else {
|
|
||||||
this.router.pushUrl(`/${uiState.activeView}`);
|
|
||||||
}
|
|
||||||
this.suppressStateUpdate = false;
|
this.suppressStateUpdate = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private getExpectedPath(view: string): string {
|
|
||||||
if (view === 'emails') {
|
|
||||||
return '/emails';
|
|
||||||
}
|
|
||||||
return `/${view}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleInitialRoute(): void {
|
private handleInitialRoute(): void {
|
||||||
const path = window.location.pathname;
|
const path = window.location.pathname;
|
||||||
|
|
||||||
if (!path || path === '/') {
|
if (!path || path === '/') {
|
||||||
// Redirect root to overview
|
|
||||||
this.router.pushUrl('/overview');
|
this.router.pushUrl('/overview');
|
||||||
} else {
|
} else {
|
||||||
// Parse current path and update state
|
|
||||||
const segments = path.split('/').filter(Boolean);
|
const segments = path.split('/').filter(Boolean);
|
||||||
const view = segments[0];
|
const view = segments[0];
|
||||||
|
|
||||||
if (validViews.includes(view as TValidView)) {
|
if (validViews.includes(view as TValidView)) {
|
||||||
this.updateViewState(view as TValidView);
|
this.updateViewState(view as TValidView);
|
||||||
|
|
||||||
if (view === 'emails' && segments[1]) {
|
|
||||||
const folder = segments[1];
|
|
||||||
if (validEmailFolders.includes(folder as TValidEmailFolder)) {
|
|
||||||
this.updateEmailFolder(folder as TValidEmailFolder);
|
|
||||||
} else {
|
} else {
|
||||||
this.updateEmailFolder('queued');
|
|
||||||
}
|
|
||||||
} else if (view === 'emails') {
|
|
||||||
this.updateEmailFolder('queued');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Invalid view, redirect to overview
|
|
||||||
this.router.pushUrl('/overview');
|
this.router.pushUrl('/overview');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,18 +81,6 @@ class AppRouter {
|
|||||||
this.suppressStateUpdate = false;
|
this.suppressStateUpdate = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateEmailFolder(folder: TValidEmailFolder): void {
|
|
||||||
this.suppressStateUpdate = true;
|
|
||||||
const currentState = appstate.emailOpsStatePart.getState();
|
|
||||||
if (currentState.currentView !== folder) {
|
|
||||||
appstate.emailOpsStatePart.setState({
|
|
||||||
...currentState,
|
|
||||||
currentView: folder as appstate.IEmailOpsState['currentView'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.suppressStateUpdate = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public navigateTo(path: string): void {
|
public navigateTo(path: string): void {
|
||||||
this.router.pushUrl(path);
|
this.router.pushUrl(path);
|
||||||
}
|
}
|
||||||
@@ -156,22 +93,10 @@ class AppRouter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public navigateToEmailFolder(folder: string): void {
|
|
||||||
if (validEmailFolders.includes(folder as TValidEmailFolder)) {
|
|
||||||
this.navigateTo(`/emails/${folder}`);
|
|
||||||
} else {
|
|
||||||
this.navigateTo('/emails/queued');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCurrentView(): string {
|
public getCurrentView(): string {
|
||||||
return appstate.uiStatePart.getState().activeView;
|
return appstate.uiStatePart.getState().activeView;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCurrentEmailFolder(): string {
|
|
||||||
return appstate.emailOpsStatePart.getState().currentView;
|
|
||||||
}
|
|
||||||
|
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
this.router.destroy();
|
this.router.destroy();
|
||||||
this.initialized = false;
|
this.initialized = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user