@serve.zone/dcrouter 7.4.3 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/dist_serve/bundle.js +11567 -3516
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.d.ts +9 -0
  4. package/dist_ts/classes.dcrouter.js +27 -1
  5. package/dist_ts/config/classes.api-token-manager.d.ts +38 -0
  6. package/dist_ts/config/classes.api-token-manager.js +134 -0
  7. package/dist_ts/config/classes.route-config-manager.d.ts +35 -0
  8. package/dist_ts/config/classes.route-config-manager.js +231 -0
  9. package/dist_ts/config/index.d.ts +2 -0
  10. package/dist_ts/config/index.js +3 -1
  11. package/dist_ts/opsserver/classes.opsserver.d.ts +2 -0
  12. package/dist_ts/opsserver/classes.opsserver.js +5 -1
  13. package/dist_ts/opsserver/handlers/{email-ops.handler.d.ts → api-token.handler.d.ts} +4 -4
  14. package/dist_ts/opsserver/handlers/api-token.handler.js +66 -0
  15. package/dist_ts/opsserver/handlers/index.d.ts +2 -0
  16. package/dist_ts/opsserver/handlers/index.js +3 -1
  17. package/dist_ts/opsserver/handlers/{radius.handler.d.ts → route-management.handler.d.ts} +6 -1
  18. package/dist_ts/opsserver/handlers/route-management.handler.js +117 -0
  19. package/dist_ts_interfaces/data/index.d.ts +1 -0
  20. package/dist_ts_interfaces/data/index.js +2 -1
  21. package/dist_ts_interfaces/data/route-management.d.ts +68 -0
  22. package/dist_ts_interfaces/data/route-management.js +2 -0
  23. package/dist_ts_interfaces/requests/api-tokens.d.ts +63 -0
  24. package/dist_ts_interfaces/requests/api-tokens.js +2 -0
  25. package/dist_ts_interfaces/requests/email-ops.d.ts +51 -108
  26. package/dist_ts_interfaces/requests/index.d.ts +2 -0
  27. package/dist_ts_interfaces/requests/index.js +3 -1
  28. package/dist_ts_interfaces/requests/route-management.d.ts +114 -0
  29. package/dist_ts_interfaces/requests/route-management.js +2 -0
  30. package/dist_ts_web/00_commitinfo_data.js +1 -1
  31. package/dist_ts_web/appstate.d.ts +38 -16
  32. package/dist_ts_web/appstate.js +226 -177
  33. package/dist_ts_web/elements/index.d.ts +2 -0
  34. package/dist_ts_web/elements/index.js +3 -1
  35. package/dist_ts_web/elements/ops-dashboard.js +11 -1
  36. package/dist_ts_web/elements/ops-view-apitokens.d.ts +12 -0
  37. package/dist_ts_web/elements/ops-view-apitokens.js +306 -0
  38. package/dist_ts_web/elements/ops-view-emails.d.ts +8 -31
  39. package/dist_ts_web/elements/ops-view-emails.js +54 -769
  40. package/dist_ts_web/elements/ops-view-logs.d.ts +2 -8
  41. package/dist_ts_web/elements/ops-view-logs.js +4 -101
  42. package/dist_ts_web/elements/ops-view-routes.d.ts +12 -0
  43. package/dist_ts_web/elements/ops-view-routes.js +404 -0
  44. package/dist_ts_web/plugins.d.ts +2 -1
  45. package/dist_ts_web/plugins.js +4 -2
  46. package/dist_ts_web/router.d.ts +1 -7
  47. package/dist_ts_web/router.js +8 -82
  48. package/package.json +2 -1
  49. package/ts/00_commitinfo_data.ts +1 -1
  50. package/ts/classes.dcrouter.ts +37 -1
  51. package/ts/config/classes.api-token-manager.ts +155 -0
  52. package/ts/config/classes.route-config-manager.ts +271 -0
  53. package/ts/config/index.ts +3 -1
  54. package/ts/opsserver/classes.opsserver.ts +4 -0
  55. package/ts/opsserver/handlers/api-token.handler.ts +96 -0
  56. package/ts/opsserver/handlers/email-ops.handler.ts +177 -225
  57. package/ts/opsserver/handlers/index.ts +3 -1
  58. package/ts/opsserver/handlers/route-management.handler.ts +163 -0
  59. package/ts_web/00_commitinfo_data.ts +1 -1
  60. package/ts_web/appstate.ts +316 -222
  61. package/ts_web/elements/index.ts +2 -0
  62. package/ts_web/elements/ops-dashboard.ts +10 -0
  63. package/ts_web/elements/ops-view-apitokens.ts +281 -0
  64. package/ts_web/elements/ops-view-emails.ts +40 -749
  65. package/ts_web/elements/ops-view-logs.ts +2 -87
  66. package/ts_web/elements/ops-view-routes.ts +389 -0
  67. package/ts_web/plugins.ts +4 -0
  68. package/ts_web/router.ts +7 -82
  69. package/dist_ts/cache/classes.cache.cleaner.d.ts +0 -47
  70. package/dist_ts/cache/classes.cache.cleaner.js +0 -130
  71. package/dist_ts/cache/classes.cached.document.d.ts +0 -76
  72. package/dist_ts/cache/classes.cached.document.js +0 -100
  73. package/dist_ts/cache/classes.cachedb.d.ts +0 -60
  74. package/dist_ts/cache/classes.cachedb.js +0 -126
  75. package/dist_ts/cache/documents/classes.cached.email.d.ts +0 -125
  76. package/dist_ts/cache/documents/classes.cached.email.js +0 -337
  77. package/dist_ts/cache/documents/classes.cached.ip.reputation.d.ts +0 -119
  78. package/dist_ts/cache/documents/classes.cached.ip.reputation.js +0 -323
  79. package/dist_ts/cache/documents/index.d.ts +0 -2
  80. package/dist_ts/cache/documents/index.js +0 -3
  81. package/dist_ts/cache/index.d.ts +0 -4
  82. package/dist_ts/cache/index.js +0 -7
  83. package/dist_ts/monitoring/classes.metricscache.d.ts +0 -32
  84. package/dist_ts/monitoring/classes.metricscache.js +0 -63
  85. package/dist_ts/monitoring/classes.metricsmanager.d.ts +0 -169
  86. package/dist_ts/monitoring/classes.metricsmanager.js +0 -591
  87. package/dist_ts/monitoring/index.d.ts +0 -1
  88. package/dist_ts/monitoring/index.js +0 -2
  89. package/dist_ts/opsserver/handlers/admin.handler.d.ts +0 -31
  90. package/dist_ts/opsserver/handlers/admin.handler.js +0 -180
  91. package/dist_ts/opsserver/handlers/certificate.handler.d.ts +0 -34
  92. package/dist_ts/opsserver/handlers/certificate.handler.js +0 -419
  93. package/dist_ts/opsserver/handlers/config.handler.d.ts +0 -9
  94. package/dist_ts/opsserver/handlers/config.handler.js +0 -67
  95. package/dist_ts/opsserver/handlers/email-ops.handler.js +0 -219
  96. package/dist_ts/opsserver/handlers/logs.handler.d.ts +0 -17
  97. package/dist_ts/opsserver/handlers/logs.handler.js +0 -215
  98. package/dist_ts/opsserver/handlers/radius.handler.js +0 -296
  99. package/dist_ts/opsserver/handlers/remoteingress.handler.d.ts +0 -8
  100. package/dist_ts/opsserver/handlers/remoteingress.handler.js +0 -154
  101. package/dist_ts/opsserver/handlers/security.handler.d.ts +0 -11
  102. package/dist_ts/opsserver/handlers/security.handler.js +0 -232
  103. package/dist_ts/opsserver/handlers/stats.handler.d.ts +0 -13
  104. package/dist_ts/opsserver/handlers/stats.handler.js +0 -400
  105. package/dist_ts/security/classes.securitylogger.d.ts +0 -140
  106. package/dist_ts/security/classes.securitylogger.js +0 -235
  107. package/dist_ts/storage/classes.storagemanager.d.ts +0 -82
  108. package/dist_ts/storage/classes.storagemanager.js +0 -344
  109. package/dist_ts/storage/index.d.ts +0 -1
  110. package/dist_ts/storage/index.js +0 -3
@@ -1,4 +1,3 @@
1
- import * as plugins from '../plugins.js';
2
1
  import * as shared from './shared/index.js';
3
2
  import * as appstate from '../appstate.js';
4
3
 
@@ -20,15 +19,6 @@ export class OpsViewLogs extends DeesElement {
20
19
  filters: {},
21
20
  };
22
21
 
23
- @state()
24
- accessor filterLevel: string | undefined;
25
-
26
- @state()
27
- accessor filterCategory: string | undefined;
28
-
29
- @state()
30
- accessor filterLimit: number = 100;
31
-
32
22
  private lastPushedCount = 0;
33
23
 
34
24
  constructor() {
@@ -44,63 +34,13 @@ export class OpsViewLogs extends DeesElement {
44
34
  public static styles = [
45
35
  cssManager.defaultStyles,
46
36
  shared.viewHostCss,
47
- css`
48
- .controls {
49
- display: flex;
50
- gap: 16px;
51
- margin-bottom: 24px;
52
- flex-wrap: wrap;
53
- }
54
-
55
- .filterGroup {
56
- display: flex;
57
- align-items: center;
58
- gap: 8px;
59
- }
60
- `,
37
+ css``,
61
38
  ];
62
39
 
63
40
  public render() {
64
41
  return html`
65
42
  <ops-sectionheading>Logs</ops-sectionheading>
66
43
 
67
- <div class="controls">
68
- <div class="filterGroup">
69
- <dees-button
70
- @click=${() => this.fetchLogs()}
71
- >
72
- Refresh Logs
73
- </dees-button>
74
- </div>
75
-
76
- <div class="filterGroup">
77
- <label>Level:</label>
78
- <dees-input-dropdown
79
- .options=${['all', 'debug', 'info', 'warn', 'error']}
80
- .selectedOption=${'all'}
81
- @selectedOption=${(e: any) => this.updateFilter('level', e.detail)}
82
- ></dees-input-dropdown>
83
- </div>
84
-
85
- <div class="filterGroup">
86
- <label>Category:</label>
87
- <dees-input-dropdown
88
- .options=${['all', 'smtp', 'dns', 'security', 'system', 'email']}
89
- .selectedOption=${'all'}
90
- @selectedOption=${(e: any) => this.updateFilter('category', e.detail)}
91
- ></dees-input-dropdown>
92
- </div>
93
-
94
- <div class="filterGroup">
95
- <label>Limit:</label>
96
- <dees-input-dropdown
97
- .options=${['50', '100', '200', '500']}
98
- .selectedOption=${'100'}
99
- @selectedOption=${(e: any) => this.updateFilter('limit', e.detail)}
100
- ></dees-input-dropdown>
101
- </div>
102
- </div>
103
-
104
44
  <dees-chart-log
105
45
  .label=${'Application Logs'}
106
46
  .autoScroll=${true}
@@ -115,7 +55,7 @@ export class OpsViewLogs extends DeesElement {
115
55
  this.lastPushedCount = 0;
116
56
  // Only fetch if state is empty (streaming will handle new entries)
117
57
  if (this.logState.recentLogs.length === 0) {
118
- this.fetchLogs();
58
+ await appstate.logStatePart.dispatchAction(appstate.fetchRecentLogsAction, { limit: 100 });
119
59
  }
120
60
  }
121
61
 
@@ -166,29 +106,4 @@ export class OpsViewLogs extends DeesElement {
166
106
  }));
167
107
  }
168
108
 
169
- private async fetchLogs() {
170
- await appstate.logStatePart.dispatchAction(appstate.fetchRecentLogsAction, {
171
- limit: this.filterLimit,
172
- level: this.filterLevel as 'debug' | 'info' | 'warn' | 'error' | undefined,
173
- category: this.filterCategory as 'smtp' | 'dns' | 'security' | 'system' | 'email' | undefined,
174
- });
175
- }
176
-
177
- private updateFilter(type: string, value: string) {
178
- const resolved = value === 'all' ? undefined : value;
179
-
180
- switch (type) {
181
- case 'level':
182
- this.filterLevel = resolved;
183
- break;
184
- case 'category':
185
- this.filterCategory = resolved;
186
- break;
187
- case 'limit':
188
- this.filterLimit = resolved ? parseInt(resolved, 10) : 100;
189
- break;
190
- }
191
-
192
- this.fetchLogs();
193
- }
194
109
  }
@@ -0,0 +1,389 @@
1
+ import * as appstate from '../appstate.js';
2
+ import * as interfaces from '../../dist_ts_interfaces/index.js';
3
+ import { viewHostCss } from './shared/css.js';
4
+ import { type IStatsTile } from '@design.estate/dees-catalog';
5
+
6
+ import {
7
+ DeesElement,
8
+ css,
9
+ cssManager,
10
+ customElement,
11
+ html,
12
+ state,
13
+ type TemplateResult,
14
+ } from '@design.estate/dees-element';
15
+
16
+ @customElement('ops-view-routes')
17
+ export class OpsViewRoutes extends DeesElement {
18
+ @state() accessor routeState: appstate.IRouteManagementState = {
19
+ mergedRoutes: [],
20
+ warnings: [],
21
+ apiTokens: [],
22
+ isLoading: false,
23
+ error: null,
24
+ lastUpdated: 0,
25
+ };
26
+
27
+ constructor() {
28
+ super();
29
+ const sub = appstate.routeManagementStatePart
30
+ .select((s) => s)
31
+ .subscribe((routeState) => {
32
+ this.routeState = routeState;
33
+ });
34
+ this.rxSubscriptions.push(sub);
35
+
36
+ // Re-fetch routes when user logs in (fixes race condition where
37
+ // the view is created before authentication completes)
38
+ const loginSub = appstate.loginStatePart
39
+ .select((s) => s.isLoggedIn)
40
+ .subscribe((isLoggedIn) => {
41
+ if (isLoggedIn) {
42
+ appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
43
+ }
44
+ });
45
+ this.rxSubscriptions.push(loginSub);
46
+ }
47
+
48
+ public static styles = [
49
+ cssManager.defaultStyles,
50
+ viewHostCss,
51
+ css`
52
+ .routesContainer {
53
+ display: flex;
54
+ flex-direction: column;
55
+ gap: 24px;
56
+ }
57
+
58
+ .warnings-bar {
59
+ background: ${cssManager.bdTheme('rgba(255, 170, 0, 0.08)', 'rgba(255, 170, 0, 0.1)')};
60
+ border: 1px solid ${cssManager.bdTheme('rgba(255, 170, 0, 0.25)', 'rgba(255, 170, 0, 0.3)')};
61
+ border-radius: 8px;
62
+ padding: 12px 16px;
63
+ }
64
+
65
+ .warning-item {
66
+ display: flex;
67
+ align-items: center;
68
+ gap: 8px;
69
+ padding: 4px 0;
70
+ font-size: 13px;
71
+ color: ${cssManager.bdTheme('#b45309', '#fa0')};
72
+ }
73
+
74
+ .warning-icon {
75
+ flex-shrink: 0;
76
+ }
77
+
78
+ .empty-state {
79
+ text-align: center;
80
+ padding: 48px 24px;
81
+ color: ${cssManager.bdTheme('#6b7280', '#666')};
82
+ }
83
+
84
+ .empty-state p {
85
+ margin: 8px 0;
86
+ }
87
+ `,
88
+ ];
89
+
90
+ public render(): TemplateResult {
91
+ const { mergedRoutes, warnings } = this.routeState;
92
+
93
+ const hardcodedCount = mergedRoutes.filter((mr) => mr.source === 'hardcoded').length;
94
+ const programmaticCount = mergedRoutes.filter((mr) => mr.source === 'programmatic').length;
95
+ const disabledCount = mergedRoutes.filter((mr) => !mr.enabled).length;
96
+
97
+ const statsTiles: IStatsTile[] = [
98
+ {
99
+ id: 'totalRoutes',
100
+ title: 'Total Routes',
101
+ type: 'number',
102
+ value: mergedRoutes.length,
103
+ icon: 'lucide:route',
104
+ description: 'All configured routes',
105
+ color: '#3b82f6',
106
+ },
107
+ {
108
+ id: 'hardcoded',
109
+ title: 'Hardcoded',
110
+ type: 'number',
111
+ value: hardcodedCount,
112
+ icon: 'lucide:lock',
113
+ description: 'Routes from constructor config',
114
+ color: '#8b5cf6',
115
+ },
116
+ {
117
+ id: 'programmatic',
118
+ title: 'Programmatic',
119
+ type: 'number',
120
+ value: programmaticCount,
121
+ icon: 'lucide:code',
122
+ description: 'Routes added via API',
123
+ color: '#0ea5e9',
124
+ },
125
+ {
126
+ id: 'disabled',
127
+ title: 'Disabled',
128
+ type: 'number',
129
+ value: disabledCount,
130
+ icon: 'lucide:pauseCircle',
131
+ description: 'Currently disabled routes',
132
+ color: disabledCount > 0 ? '#ef4444' : '#6b7280',
133
+ },
134
+ ];
135
+
136
+ // Map merged routes to sz-route-list-view format
137
+ const szRoutes = mergedRoutes.map((mr) => {
138
+ const tags = [...(mr.route.tags || [])];
139
+ tags.push(mr.source);
140
+ if (!mr.enabled) tags.push('disabled');
141
+ if (mr.overridden) tags.push('overridden');
142
+
143
+ return {
144
+ ...mr.route,
145
+ enabled: mr.enabled,
146
+ tags,
147
+ id: mr.storedRouteId || mr.route.name || undefined,
148
+ };
149
+ });
150
+
151
+ return html`
152
+ <ops-sectionheading>Route Management</ops-sectionheading>
153
+
154
+ <div class="routesContainer">
155
+ <dees-statsgrid
156
+ .tiles=${statsTiles}
157
+ .gridActions=${[
158
+ {
159
+ name: 'Add Route',
160
+ iconName: 'lucide:plus',
161
+ action: () => this.showCreateRouteDialog(),
162
+ },
163
+ {
164
+ name: 'Refresh',
165
+ iconName: 'lucide:refreshCw',
166
+ action: () => this.refreshData(),
167
+ },
168
+ ]}
169
+ ></dees-statsgrid>
170
+
171
+ ${warnings.length > 0
172
+ ? html`
173
+ <div class="warnings-bar">
174
+ ${warnings.map(
175
+ (w) => html`
176
+ <div class="warning-item">
177
+ <span class="warning-icon">&#9888;</span>
178
+ <span>${w.message}</span>
179
+ </div>
180
+ `,
181
+ )}
182
+ </div>
183
+ `
184
+ : ''}
185
+
186
+ ${szRoutes.length > 0
187
+ ? html`
188
+ <sz-route-list-view
189
+ .routes=${szRoutes}
190
+ @route-click=${(e: CustomEvent) => this.handleRouteClick(e)}
191
+ ></sz-route-list-view>
192
+ `
193
+ : html`
194
+ <div class="empty-state">
195
+ <p>No routes configured</p>
196
+ <p>Add a programmatic route or check your constructor configuration.</p>
197
+ </div>
198
+ `}
199
+ </div>
200
+ `;
201
+ }
202
+
203
+ private async handleRouteClick(e: CustomEvent) {
204
+ const clickedRoute = e.detail;
205
+ if (!clickedRoute) return;
206
+
207
+ // Find the corresponding merged route
208
+ const merged = this.routeState.mergedRoutes.find(
209
+ (mr) => mr.route.name === clickedRoute.name,
210
+ );
211
+ if (!merged) return;
212
+
213
+ const { DeesModal } = await import('@design.estate/dees-catalog');
214
+
215
+ if (merged.source === 'hardcoded') {
216
+ const menuOptions = merged.enabled
217
+ ? [
218
+ {
219
+ name: 'Disable Route',
220
+ iconName: 'lucide:pause',
221
+ action: async (modalArg: any) => {
222
+ await appstate.routeManagementStatePart.dispatchAction(
223
+ appstate.setRouteOverrideAction,
224
+ { routeName: merged.route.name!, enabled: false },
225
+ );
226
+ await modalArg.destroy();
227
+ },
228
+ },
229
+ {
230
+ name: 'Close',
231
+ iconName: 'lucide:x',
232
+ action: async (modalArg: any) => await modalArg.destroy(),
233
+ },
234
+ ]
235
+ : [
236
+ {
237
+ name: 'Enable Route',
238
+ iconName: 'lucide:play',
239
+ action: async (modalArg: any) => {
240
+ await appstate.routeManagementStatePart.dispatchAction(
241
+ appstate.setRouteOverrideAction,
242
+ { routeName: merged.route.name!, enabled: true },
243
+ );
244
+ await modalArg.destroy();
245
+ },
246
+ },
247
+ {
248
+ name: 'Remove Override',
249
+ iconName: 'lucide:undo',
250
+ action: async (modalArg: any) => {
251
+ await appstate.routeManagementStatePart.dispatchAction(
252
+ appstate.removeRouteOverrideAction,
253
+ merged.route.name!,
254
+ );
255
+ await modalArg.destroy();
256
+ },
257
+ },
258
+ {
259
+ name: 'Close',
260
+ iconName: 'lucide:x',
261
+ action: async (modalArg: any) => await modalArg.destroy(),
262
+ },
263
+ ];
264
+
265
+ await DeesModal.createAndShow({
266
+ heading: `Route: ${merged.route.name}`,
267
+ content: html`
268
+ <div style="color: #ccc; padding: 8px 0;">
269
+ <p>Source: <strong style="color: #88f;">hardcoded</strong></p>
270
+ <p>Status: <strong>${merged.enabled ? 'Enabled' : 'Disabled (overridden)'}</strong></p>
271
+ <p style="color: #888; font-size: 13px;">Hardcoded routes cannot be edited or deleted, but they can be disabled via an override.</p>
272
+ </div>
273
+ `,
274
+ menuOptions,
275
+ });
276
+ } else {
277
+ // Programmatic route
278
+ await DeesModal.createAndShow({
279
+ heading: `Route: ${merged.route.name}`,
280
+ content: html`
281
+ <div style="color: #ccc; padding: 8px 0;">
282
+ <p>Source: <strong style="color: #0af;">programmatic</strong></p>
283
+ <p>Status: <strong>${merged.enabled ? 'Enabled' : 'Disabled'}</strong></p>
284
+ <p>ID: <code style="color: #888;">${merged.storedRouteId}</code></p>
285
+ </div>
286
+ `,
287
+ menuOptions: [
288
+ {
289
+ name: merged.enabled ? 'Disable' : 'Enable',
290
+ iconName: merged.enabled ? 'lucide:pause' : 'lucide:play',
291
+ action: async (modalArg: any) => {
292
+ await appstate.routeManagementStatePart.dispatchAction(
293
+ appstate.toggleRouteAction,
294
+ { id: merged.storedRouteId!, enabled: !merged.enabled },
295
+ );
296
+ await modalArg.destroy();
297
+ },
298
+ },
299
+ {
300
+ name: 'Delete',
301
+ iconName: 'lucide:trash-2',
302
+ action: async (modalArg: any) => {
303
+ await appstate.routeManagementStatePart.dispatchAction(
304
+ appstate.deleteRouteAction,
305
+ merged.storedRouteId!,
306
+ );
307
+ await modalArg.destroy();
308
+ },
309
+ },
310
+ {
311
+ name: 'Close',
312
+ iconName: 'lucide:x',
313
+ action: async (modalArg: any) => await modalArg.destroy(),
314
+ },
315
+ ],
316
+ });
317
+ }
318
+ }
319
+
320
+ private async showCreateRouteDialog() {
321
+ const { DeesModal } = await import('@design.estate/dees-catalog');
322
+
323
+ await DeesModal.createAndShow({
324
+ heading: 'Add Programmatic Route',
325
+ content: html`
326
+ <dees-form>
327
+ <dees-input-text .key=${'name'} .label=${'Route Name'} .required=${true}></dees-input-text>
328
+ <dees-input-text .key=${'ports'} .label=${'Ports (comma-separated)'} .required=${true}></dees-input-text>
329
+ <dees-input-text .key=${'domains'} .label=${'Domains (comma-separated, optional)'}></dees-input-text>
330
+ <dees-input-text .key=${'targetHost'} .label=${'Target Host'} .value=${'localhost'} .required=${true}></dees-input-text>
331
+ <dees-input-text .key=${'targetPort'} .label=${'Target Port'} .required=${true}></dees-input-text>
332
+ </dees-form>
333
+ `,
334
+ menuOptions: [
335
+ {
336
+ name: 'Cancel',
337
+ iconName: 'lucide:x',
338
+ action: async (modalArg: any) => await modalArg.destroy(),
339
+ },
340
+ {
341
+ name: 'Create',
342
+ iconName: 'lucide:plus',
343
+ action: async (modalArg: any) => {
344
+ const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
345
+ if (!form) return;
346
+ const formData = await form.collectFormData();
347
+ if (!formData.name || !formData.ports) return;
348
+
349
+ const ports = formData.ports.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p));
350
+ const domains = formData.domains
351
+ ? formData.domains.split(',').map((d: string) => d.trim()).filter(Boolean)
352
+ : undefined;
353
+
354
+ const route: any = {
355
+ name: formData.name,
356
+ match: {
357
+ ports,
358
+ ...(domains && domains.length > 0 ? { domains } : {}),
359
+ },
360
+ action: {
361
+ type: 'forward',
362
+ targets: [
363
+ {
364
+ host: formData.targetHost || 'localhost',
365
+ port: parseInt(formData.targetPort, 10),
366
+ },
367
+ ],
368
+ },
369
+ };
370
+
371
+ await appstate.routeManagementStatePart.dispatchAction(
372
+ appstate.createRouteAction,
373
+ { route },
374
+ );
375
+ await modalArg.destroy();
376
+ },
377
+ },
378
+ ],
379
+ });
380
+ }
381
+
382
+ private refreshData() {
383
+ appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
384
+ }
385
+
386
+ async firstUpdated() {
387
+ await appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
388
+ }
389
+ }
package/ts_web/plugins.ts CHANGED
@@ -2,12 +2,16 @@
2
2
  import * as deesElement from '@design.estate/dees-element';
3
3
  import * as deesCatalog from '@design.estate/dees-catalog';
4
4
 
5
+ // @serve.zone scope
6
+ import * as szCatalog from '@serve.zone/catalog';
7
+
5
8
  // TypedSocket for real-time push communication
6
9
  import * as typedsocket from '@api.global/typedsocket';
7
10
 
8
11
  export {
9
12
  deesElement,
10
13
  deesCatalog,
14
+ szCatalog,
11
15
  typedsocket,
12
16
  }
13
17
 
package/ts_web/router.ts CHANGED
@@ -3,11 +3,9 @@ import * as appstate from './appstate.js';
3
3
 
4
4
  const SmartRouter = plugins.domtools.plugins.smartrouter.SmartRouter;
5
5
 
6
- export const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates', 'remoteingress'] as const;
7
- export const validEmailFolders = ['queued', 'sent', 'failed', 'security'] as const;
6
+ export const validViews = ['overview', 'network', 'emails', 'logs', 'routes', 'apitokens', 'configuration', 'security', 'certificates', 'remoteingress'] as const;
8
7
 
9
8
  export type TValidView = typeof validViews[number];
10
- export type TValidEmailFolder = typeof validEmailFolders[number];
11
9
 
12
10
  class AppRouter {
13
11
  private router: InstanceType<typeof SmartRouter>;
@@ -27,31 +25,10 @@ class AppRouter {
27
25
  }
28
26
 
29
27
  private setupRoutes(): void {
30
- // Main views
31
28
  for (const view of validViews) {
32
- if (view === 'emails') {
33
- // Email root - default to queued
34
- this.router.on('/emails', async () => {
35
- this.updateViewState('emails');
36
- this.updateEmailFolder('queued');
37
- });
38
-
39
- // Email with folder parameter
40
- this.router.on('/emails/:folder', async (routeInfo) => {
41
- const folder = routeInfo.params.folder as string;
42
- if (validEmailFolders.includes(folder as TValidEmailFolder)) {
43
- this.updateViewState('emails');
44
- this.updateEmailFolder(folder as TValidEmailFolder);
45
- } else {
46
- // Invalid folder, redirect to queued
47
- this.navigateTo('/emails/queued');
48
- }
49
- });
50
- } else {
51
- this.router.on(`/${view}`, async () => {
52
- this.updateViewState(view);
53
- });
54
- }
29
+ this.router.on(`/${view}`, async () => {
30
+ this.updateViewState(view);
31
+ });
55
32
  }
56
33
 
57
34
  // Root redirect
@@ -61,60 +38,32 @@ class AppRouter {
61
38
  }
62
39
 
63
40
  private setupStateSync(): void {
64
- // Sync URL when state changes programmatically (not from router)
65
41
  appstate.uiStatePart.state.subscribe((uiState) => {
66
42
  if (this.suppressStateUpdate) return;
67
43
 
68
44
  const currentPath = window.location.pathname;
69
- const expectedPath = this.getExpectedPath(uiState.activeView);
45
+ const expectedPath = `/${uiState.activeView}`;
70
46
 
71
- // Only update URL if it doesn't match current state
72
- if (!currentPath.startsWith(expectedPath)) {
47
+ if (currentPath !== expectedPath) {
73
48
  this.suppressStateUpdate = true;
74
- if (uiState.activeView === 'emails') {
75
- const emailState = appstate.emailOpsStatePart.getState();
76
- this.router.pushUrl(`/emails/${emailState.currentView}`);
77
- } else {
78
- this.router.pushUrl(`/${uiState.activeView}`);
79
- }
49
+ this.router.pushUrl(expectedPath);
80
50
  this.suppressStateUpdate = false;
81
51
  }
82
52
  });
83
53
  }
84
54
 
85
- private getExpectedPath(view: string): string {
86
- if (view === 'emails') {
87
- return '/emails';
88
- }
89
- return `/${view}`;
90
- }
91
-
92
55
  private handleInitialRoute(): void {
93
56
  const path = window.location.pathname;
94
57
 
95
58
  if (!path || path === '/') {
96
- // Redirect root to overview
97
59
  this.router.pushUrl('/overview');
98
60
  } else {
99
- // Parse current path and update state
100
61
  const segments = path.split('/').filter(Boolean);
101
62
  const view = segments[0];
102
63
 
103
64
  if (validViews.includes(view as TValidView)) {
104
65
  this.updateViewState(view as TValidView);
105
-
106
- if (view === 'emails' && segments[1]) {
107
- const folder = segments[1];
108
- if (validEmailFolders.includes(folder as TValidEmailFolder)) {
109
- this.updateEmailFolder(folder as TValidEmailFolder);
110
- } else {
111
- this.updateEmailFolder('queued');
112
- }
113
- } else if (view === 'emails') {
114
- this.updateEmailFolder('queued');
115
- }
116
66
  } else {
117
- // Invalid view, redirect to overview
118
67
  this.router.pushUrl('/overview');
119
68
  }
120
69
  }
@@ -132,18 +81,6 @@ class AppRouter {
132
81
  this.suppressStateUpdate = false;
133
82
  }
134
83
 
135
- private updateEmailFolder(folder: TValidEmailFolder): void {
136
- this.suppressStateUpdate = true;
137
- const currentState = appstate.emailOpsStatePart.getState();
138
- if (currentState.currentView !== folder) {
139
- appstate.emailOpsStatePart.setState({
140
- ...currentState,
141
- currentView: folder as appstate.IEmailOpsState['currentView'],
142
- });
143
- }
144
- this.suppressStateUpdate = false;
145
- }
146
-
147
84
  public navigateTo(path: string): void {
148
85
  this.router.pushUrl(path);
149
86
  }
@@ -156,22 +93,10 @@ class AppRouter {
156
93
  }
157
94
  }
158
95
 
159
- public navigateToEmailFolder(folder: string): void {
160
- if (validEmailFolders.includes(folder as TValidEmailFolder)) {
161
- this.navigateTo(`/emails/${folder}`);
162
- } else {
163
- this.navigateTo('/emails/queued');
164
- }
165
- }
166
-
167
96
  public getCurrentView(): string {
168
97
  return appstate.uiStatePart.getState().activeView;
169
98
  }
170
99
 
171
- public getCurrentEmailFolder(): string {
172
- return appstate.emailOpsStatePart.getState().currentView;
173
- }
174
-
175
100
  public destroy(): void {
176
101
  this.router.destroy();
177
102
  this.initialized = false;