Compare commits

...

12 Commits

Author SHA1 Message Date
9b7426f1e6 v9.1.3
Some checks failed
Docker (tags) / security (push) Failing after 2s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-24 23:29:26 +00:00
3c9c865841 fix(deps): bump @api.global/typedserver to ^8.4.0 and @push.rocks/smartproxy to ^25.8.0 2026-02-24 23:29:26 +00:00
8421c9fe46 v9.1.2
Some checks failed
Docker (tags) / security (push) Failing after 2s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-24 20:58:43 +00:00
907e3df156 fix(deps): bump dependency versions for build and runtime packages 2026-02-24 20:58:43 +00:00
aaa0956148 v9.1.1
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-23 21:49:21 +00:00
118019fcf5 fix(dcrouter): no changes detected — no files modified, no release necessary 2026-02-23 21:49:21 +00:00
deb80f4fd0 v9.1.0
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-23 21:40:34 +00:00
7d28cea937 feat(ops-dashboard): add lucide icons to Ops dashboard view tabs 2026-02-23 21:40:34 +00:00
2bd5e5c7c5 v9.0.0
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-23 21:34:50 +00:00
4d6ac81c59 BREAKING CHANGE(opsserver): Return structured configuration (IConfigData) from opsserver and update UI to render detailed config sections 2026-02-23 21:34:50 +00:00
2ebe0de92d v8.1.0
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-23 12:40:26 +00:00
f5028ffb60 feat(route-management): add programmatic route management API with API tokens and admin UI 2026-02-23 12:40:26 +00:00
43 changed files with 2968 additions and 526 deletions

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -1,5 +1,53 @@
# Changelog # Changelog
## 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) ## 2026-02-22 - 8.0.0 - BREAKING CHANGE(email-ops)
migrate email operations to catalog-compatible email model and simplify UI/router migrate email operations to catalog-compatible email model and simplify UI/router

View File

@@ -1,7 +1,7 @@
{ {
"name": "@serve.zone/dcrouter", "name": "@serve.zone/dcrouter",
"private": false, "private": false,
"version": "8.0.0", "version": "9.1.3",
"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,19 +20,19 @@
}, },
"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.0",
"@apiclient.xyz/cloudflare": "^7.1.0", "@apiclient.xyz/cloudflare": "^7.1.0",
"@design.estate/dees-catalog": "^3.43.2", "@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",
@@ -49,13 +49,13 @@
"@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.0",
"@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.2.0", "@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.0.0",
"@tsclass/tsclass": "^9.3.0", "@tsclass/tsclass": "^9.3.0",

217
pnpm-lock.yaml generated
View File

@@ -15,8 +15,8 @@ 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.0
version: 4.1.0(@push.rocks/smartserve@2.0.1) version: 4.1.0(@push.rocks/smartserve@2.0.1)
@@ -24,8 +24,8 @@ importers:
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.2 specifier: ^3.43.3
version: 3.43.2(@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
@@ -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.0
version: 25.7.9 version: 25.8.0
'@push.rocks/smartradius': '@push.rocks/smartradius':
specifier: ^1.1.1 specifier: ^1.1.1
version: 1.1.1 version: 1.1.1
@@ -93,8 +93,8 @@ importers:
specifier: ^3.0.9 specifier: ^3.0.9
version: 3.0.9 version: 3.0.9
'@serve.zone/catalog': '@serve.zone/catalog':
specifier: ^2.2.0 specifier: ^2.5.0
version: 2.2.0(@tiptap/pm@2.27.2) 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
@@ -115,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
@@ -124,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
@@ -144,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==}
@@ -351,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.2': '@design.estate/dees-catalog@3.43.3':
resolution: {integrity: sha512-7pU+K+B70SxqR4DrBkpc/xvQGLDkxAV2jH7Qyh0TYgkCoxxjsxCuTMKM9JguA38wm6bEgBJVTvyg5S3wCwxm4Q==} 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==}
@@ -558,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':
@@ -574,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':
@@ -684,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':
@@ -1033,8 +1036,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.0':
resolution: {integrity: sha512-5esFvD72TEyveaEQbDYRgD7C5hDfWMSBvurNx3KPi02CBKG1gnhx/WWT7RHDS3KRF5fEQh9YxvI9aMkOwjc7sQ==} resolution: {integrity: sha512-wf/eMleuYf50/sgJ6yMK/4FakMeijV2NwGbI8AS8gJj0DvCCqMdz5tbtupMJUsf8KFtvRePpSG9Qys+u9j3Eow==}
'@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==}
@@ -1121,8 +1124,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==}
@@ -1333,8 +1336,8 @@ 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.2.0': '@serve.zone/catalog@2.5.0':
resolution: {integrity: sha512-FxRGjuz8PdOXnfjHAGuPWP4jUTVGl5r9rsnxZlGSgTT+dHAm6Ue9AoTCkwVTKV9hP/Ac4yy8KKeNtNYIlidfJQ==} 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==}
@@ -2006,6 +2009,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:
@@ -2072,6 +2079,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==}
@@ -3231,8 +3242,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==}
@@ -4334,13 +4349,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.0(@push.rocks/smartserve@2.0.1)
'@cloudflare/workers-types': 4.20260210.0 '@cloudflare/workers-types': 4.20260303.0
'@design.estate/dees-catalog': 3.43.2(@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
@@ -4364,7 +4379,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
@@ -4934,11 +4949,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.2(@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
@@ -5163,7 +5180,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
@@ -5220,7 +5237,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
@@ -5266,10 +5283,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
@@ -5282,7 +5299,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'
@@ -5447,52 +5464,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':
@@ -6321,13 +6338,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.0':
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.2.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:
@@ -6566,7 +6583,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
@@ -6574,7 +6591,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'
@@ -6785,9 +6802,9 @@ snapshots:
domhandler: 5.0.3 domhandler: 5.0.3
selderee: 0.11.0 selderee: 0.11.0
'@serve.zone/catalog@2.2.0(@tiptap/pm@2.27.2)': '@serve.zone/catalog@2.5.0(@tiptap/pm@2.27.2)':
dependencies: dependencies:
'@design.estate/dees-catalog': 3.43.2(@tiptap/pm@2.27.2) '@design.estate/dees-catalog': 3.43.3(@tiptap/pm@2.27.2)
'@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
'@design.estate/dees-wcctools': 3.8.0 '@design.estate/dees-wcctools': 3.8.0
@@ -7621,6 +7638,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:
@@ -7693,6 +7712,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
@@ -8340,7 +8363,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
@@ -9137,7 +9160,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
@@ -9397,7 +9424,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: {}

View File

@@ -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 () => {

View File

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

View File

@@ -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...');

View File

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

View File

@@ -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,10 @@ export class DcRouter {
public remoteIngressManager?: RemoteIngressManager; public remoteIngressManager?: RemoteIngressManager;
public tunnelManager?: TunnelManager; public tunnelManager?: TunnelManager;
// Programmatic config API
public routeConfigManager?: RouteConfigManager;
public apiTokenManager?: ApiTokenManager;
// DNS query logging rate limiter state // DNS query logging rate limiter state
private dnsLogWindow: number[] = []; private dnsLogWindow: number[] = [];
private dnsBatchCount: number = 0; private dnsBatchCount: number = 0;
@@ -233,6 +238,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/');
@@ -275,7 +283,17 @@ 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();
@@ -443,6 +461,9 @@ export class DcRouter {
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) {
logger.log('info', 'Setting up SmartProxy with combined configuration'); logger.log('info', 'Setting up SmartProxy with combined configuration');
@@ -857,6 +878,14 @@ 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() {
logger.log('info', 'Stopping DcRouter services...'); logger.log('info', 'Stopping DcRouter services...');
@@ -929,6 +958,8 @@ 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();
logger.log('info', 'All DcRouter services stopped'); logger.log('info', 'All DcRouter services stopped');
@@ -960,6 +991,11 @@ 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();
// Re-apply programmatic routes and overrides after SmartProxy restart
if (this.routeConfigManager) {
await this.routeConfigManager.initialize();
}
logger.log('info', 'SmartProxy configuration updated'); logger.log('info', 'SmartProxy configuration updated');
} }

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

View 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)`);
}
}

View File

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

View File

@@ -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,6 +63,8 @@ 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');
} }

View 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' };
},
),
);
}
}

View File

@@ -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,
@@ -26,83 +27,164 @@ 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;
// Get email domains if email server is configured const resolvedPaths = dcRouter.resolvedPaths;
// --- System ---
const storageBackend: 'filesystem' | 'custom' | 'memory' = opts.storage?.readFunction
? 'custom'
: opts.storage?.fsPath
? 'filesystem'
: 'memory';
const system: interfaces.requests.IConfigData['system'] = {
baseDir: resolvedPaths.dcrouterHomeDir,
dataDir: resolvedPaths.dataDir,
publicIp: opts.publicIp || null,
proxyIps: opts.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
); );
} }
let portMapping: Record<string, number> | null = null;
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,
ports: opts.emailConfig?.ports || [],
portMapping,
hostname: opts.emailConfig?.hostname || null,
domains: emailDomains,
emailRouteCount: opts.emailConfig?.routes?.length || 0,
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,
port: 53,
nsDomains: opts.dnsNsDomains || [],
scopes: opts.dnsScopes || [],
recordCount: dnsRecords.length,
records: dnsRecords,
dnsChallenge: !!opts.dnsChallenge?.cloudflareApiKey,
};
// --- TLS ---
let tlsSource: 'acme' | 'static' | 'none' = 'none';
if (opts.tls?.certPath && opts.tls?.keyPath) {
tlsSource = 'static';
} else if (opts.smartProxyConfig?.acme?.enabled !== false && opts.smartProxyConfig?.acme) {
tlsSource = 'acme';
}
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 remoteIngress: interfaces.requests.IConfigData['remoteIngress'] = {
enabled: !!dcRouter.remoteIngressManager,
tunnelPort: riCfg?.tunnelPort || null,
hubDomain: riCfg?.hubDomain || null,
tlsConfigured: !!(riCfg?.tls?.certPath && riCfg?.tls?.keyPath),
};
return { return {
email: { system,
enabled: !!dcRouter.emailServer, smartProxy,
ports: dcRouter.emailServer ? [25, 465, 587, 2525] : [], email,
maxMessageSize: 10 * 1024 * 1024, // 10MB default dns,
rateLimits: { tls,
perMinute: 10, cache,
perHour: 100, radius,
perDay: 1000, remoteIngress,
},
domains: emailDomains,
},
dns: {
enabled: !!dcRouter.dnsServer,
port: 53,
nameservers: dcRouter.options.dnsNsDomains || [],
caching: true,
ttl: 300,
},
proxy: {
enabled: !!dcRouter.smartProxy,
httpPort: 80,
httpsPort: 443,
maxConnections: 1000,
},
security: {
blockList: [],
rateLimit: true,
spamDetection: true,
tlsRequired: false,
},
}; };
} }
} }

View File

@@ -6,4 +6,6 @@ export * from './stats.handler.js';
export * from './radius.handler.js'; 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';

View 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' };
},
),
);
}
}

View File

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

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

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

View File

@@ -1,6 +1,78 @@
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;
};
}
// 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 +84,7 @@ export interface IReq_GetConfiguration extends plugins.typedrequestInterfaces.im
section?: string; section?: string;
}; };
response: { response: {
config: any; config: IConfigData;
section?: string; section?: string;
}; };
} }

View File

@@ -6,4 +6,6 @@ export * from './combined.stats.js';
export * from './radius.js'; 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';

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

View File

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

View File

@@ -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;
} }
@@ -109,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';
@@ -206,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;
@@ -392,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(() => {
@@ -862,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
// ============================================================================ // ============================================================================

View File

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

View File

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

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

View File

@@ -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,266 @@ 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
<div class="loadingMessage"> ? html`
<dees-spinner></dees-spinner> <div class="loadingMessage">
<p>Loading configuration...</p> <dees-spinner></dees-spinner>
</div> <p>Loading configuration...</p>
` : this.configState.error ? html`
<div class="errorMessage">
Error loading configuration: ${this.configState.error}
</div>
` : this.configState.config ? html`
<div class="infoNote">
<dees-icon icon="lucide:info"></dees-icon>
<span>This view displays the current running configuration. DcRouter is configured through code or remote management.</span>
</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) {
const isEnabled = config?.enabled ?? false;
return html`
<div class="configSection">
<div class="sectionHeader">
<h3 class="sectionTitle">
<dees-icon icon="${icon}"></dees-icon>
${title}
</h3>
${this.renderStatusBadge(isEnabled)}
</div>
<div class="sectionContent">
${config ? this.renderConfigFields(config) : html`
<div class="fieldValue empty">Not configured</div>
`}
</div>
</div>
`;
}
private renderStatusBadge(enabled: boolean): TemplateResult {
return enabled
? html`<span class="statusBadge enabled"><dees-icon icon="lucide:check"></dees-icon>Enabled</span>`
: html`<span class="statusBadge disabled"><dees-icon icon="lucide:x"></dees-icon>Disabled</span>`;
}
private renderConfigFields(config: any, prefix = ''): TemplateResult | TemplateResult[] {
if (!config || typeof config !== 'object') {
return html`<div class="fieldValue">${this.formatValue(config)}</div>`;
}
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`
<div class="configField">
<label class="fieldLabel">${displayName}</label>
${this.renderStatusBadge(value)}
</div>
`;
}
// Handle arrays
if (Array.isArray(value)) {
return html`
<div class="configField">
<label class="fieldLabel">${displayName}</label>
${this.renderArrayValue(value, key)}
</div>
`;
}
// Handle nested objects
if (typeof value === 'object' && value !== null) {
return html`
<div class="configField">
<label class="fieldLabel">${displayName}</label>
<div class="nestedFields">
${this.renderConfigFields(value, fieldName)}
</div> </div>
</div> `
`; : this.configState.error
} ? html`
<div class="errorMessage">
// Handle primitive values Error loading configuration: ${this.configState.error}
return html` </div>
<div class="configField"> `
<label class="fieldLabel">${displayName}</label> : this.configState.config
<div class="fieldValue">${this.formatValue(value, key)}</div> ? this.renderConfig()
</div> : html`<div class="errorMessage">No configuration loaded</div>`}
`;
});
}
private renderArrayValue(arr: any[], fieldKey: string): TemplateResult {
if (arr.length === 0) {
return html`<div class="fieldValue empty">None configured</div>`;
}
// Determine if we should show as pills/tags
const showAsPills = arr.every(item => typeof item === 'string' || typeof item === 'number');
if (showAsPills) {
const itemLabel = this.getArrayItemLabel(fieldKey, arr.length);
return html`
<div class="arrayCount">${arr.length} ${itemLabel}</div>
<div class="arrayItems">
${arr.map(item => html`<span class="arrayItem">${item}</span>`)}
</div>
`;
}
// For complex arrays, show as JSON
return html`
<div class="fieldValue">
${arr.length} items configured
</div>
`; `;
} }
private getArrayItemLabel(fieldKey: string, count: number): string { private renderConfig(): TemplateResult {
const labels: Record<string, [string, string]> = { const cfg = this.configState.config!;
ports: ['port', 'ports'],
domains: ['domain', 'domains'],
nameservers: ['nameserver', 'nameservers'],
blockList: ['IP', 'IPs'],
};
const label = labels[fieldKey] || ['item', 'items']; return html`
return count === 1 ? label[0] : label[1]; <sz-config-overview
infoText="This view displays the current running configuration. DcRouter is configured through code or remote management."
@navigate=${(e: CustomEvent) => {
if (e.detail?.view) {
appRouter.navigateToView(e.detail.view);
}
}}
>
${this.renderSystemSection(cfg.system)}
${this.renderSmartProxySection(cfg.smartProxy)}
${this.renderEmailSection(cfg.email)}
${this.renderDnsSection(cfg.dns)}
${this.renderTlsSection(cfg.tls)}
${this.renderCacheSection(cfg.cache)}
${this.renderRadiusSection(cfg.radius)}
${this.renderRemoteIngressSection(cfg.remoteIngress)}
</sz-config-overview>
`;
} }
private formatFieldName(key: string): string { private renderSystemSection(sys: appstate.IConfigState['config']['system']): TemplateResult {
// Convert camelCase to readable format const fields: IConfigField[] = [
return key { key: 'Base Directory', value: sys.baseDir },
.replace(/([A-Z])/g, ' $1') { key: 'Data Directory', value: sys.dataDir },
.replace(/^./, str => str.toUpperCase()) { key: 'Public IP', value: sys.publicIp },
.trim(); { key: 'Proxy IPs', value: sys.proxyIps.length > 0 ? sys.proxyIps : null, type: 'pills' },
{ key: 'Uptime', value: this.formatUptime(sys.uptime) },
{ key: 'Storage Backend', value: sys.storageBackend, type: 'badge' },
{ key: 'Storage Path', value: sys.storagePath },
];
return html`
<sz-config-section
title="System"
subtitle="Base paths and infrastructure"
icon="lucide:server"
status="enabled"
.fields=${fields}
></sz-config-section>
`;
} }
private formatValue(value: any, fieldKey?: string): string | TemplateResult { private renderSmartProxySection(proxy: appstate.IConfigState['config']['smartProxy']): TemplateResult {
if (value === null || value === undefined) { const fields: IConfigField[] = [
return html`<span class="empty">Not set</span>`; { 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` },
);
} }
if (typeof value === 'number') { const actions: IConfigSectionAction[] = [
// Format bytes { label: 'View Routes', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'routes' } },
if (fieldKey?.toLowerCase().includes('size') || fieldKey?.toLowerCase().includes('bytes')) { ];
return html`<span class="numericValue">${this.formatBytes(value)}</span>`;
}
// 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); return html`
<sz-config-section
title="SmartProxy"
subtitle="HTTP/HTTPS and TCP/SNI reverse proxy"
icon="lucide:network"
.status=${proxy.enabled ? 'enabled' : 'disabled'}
.fields=${fields}
.actions=${actions}
></sz-config-section>
`;
} }
private formatBytes(bytes: number): string { private renderEmailSection(email: appstate.IConfigState['config']['email']): TemplateResult {
if (bytes === 0) return '0 B'; const fields: IConfigField[] = [
const k = 1024; { key: 'Ports', value: email.ports.length > 0 ? email.ports.map(String) : null, type: 'pills' },
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; { key: 'Hostname', value: email.hostname },
const i = Math.floor(Math.log(bytes) / Math.log(k)); { key: 'Domains', value: email.domains.length > 0 ? email.domains : null, type: 'pills' },
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; { 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`
<sz-config-section
title="Email Server"
subtitle="SMTP email handling with smartmta"
icon="lucide:mail"
.status=${email.enabled ? 'enabled' : 'disabled'}
.fields=${fields}
.actions=${actions}
></sz-config-section>
`;
}
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`
<sz-config-section
title="DNS Server"
subtitle="Authoritative DNS with smartdns"
icon="lucide:globe"
.status=${dns.enabled ? 'enabled' : 'disabled'}
.fields=${fields}
></sz-config-section>
`;
}
private renderTlsSection(tls: appstate.IConfigState['config']['tls']): TemplateResult {
const fields: IConfigField[] = [
{ 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 },
];
const status = tls.source === 'none' ? 'not-configured' : 'enabled';
const actions: IConfigSectionAction[] = [
{ label: 'View Certificates', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'certificates' } },
];
return html`
<sz-config-section
title="TLS / Certificates"
subtitle="Certificate management and ACME"
icon="lucide:shield-check"
.status=${status as any}
.fields=${fields}
.actions=${actions}
></sz-config-section>
`;
}
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`
<sz-config-section
title="Cache Database"
subtitle="Persistent caching with smartdata"
icon="lucide:database"
.status=${cache.enabled ? 'enabled' : 'disabled'}
.fields=${fields}
></sz-config-section>
`;
}
private renderRadiusSection(radius: appstate.IConfigState['config']['radius']): TemplateResult {
const fields: IConfigField[] = [
{ key: 'Auth Port', value: radius.authPort },
{ key: 'Accounting Port', value: radius.acctPort },
{ key: 'Bind Address', value: radius.bindAddress },
{ key: 'Client Count', value: radius.clientCount },
];
if (radius.vlanDefaultVlan !== null) {
fields.push(
{ key: 'Default VLAN', value: radius.vlanDefaultVlan },
{ key: 'Allow Unknown MACs', value: radius.vlanAllowUnknownMacs, type: 'boolean' },
{ key: 'VLAN Mappings', value: radius.vlanMappingCount },
);
}
const status = radius.enabled ? 'enabled' : 'not-configured';
return html`
<sz-config-section
title="RADIUS Server"
subtitle="Network authentication and VLAN assignment"
icon="lucide:wifi"
.status=${status as any}
.fields=${fields}
></sz-config-section>
`;
}
private renderRemoteIngressSection(ri: appstate.IConfigState['config']['remoteIngress']): TemplateResult {
const fields: IConfigField[] = [
{ key: 'Tunnel Port', value: ri.tunnelPort },
{ key: 'Hub Domain', value: ri.hubDomain },
{ key: 'TLS Configured', value: ri.tlsConfigured, type: 'boolean' },
];
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>
`;
}
private formatUptime(seconds: number): string {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const parts: string[] = [];
if (days > 0) parts.push(`${days}d`);
if (hours > 0) parts.push(`${hours}h`);
parts.push(`${mins}m`);
return parts.join(' ');
} }
} }

View 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">&#9888;</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);
}
}

View File

@@ -3,7 +3,7 @@ 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 type TValidView = typeof validViews[number]; export type TValidView = typeof validViews[number];