@serve.zone/dcrouter 8.0.0 → 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 (99) hide show
  1. package/dist_serve/bundle.js +1659 -891
  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/{remoteingress.handler.d.ts → api-token.handler.d.ts} +5 -1
  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/index.d.ts +2 -0
  26. package/dist_ts_interfaces/requests/index.js +3 -1
  27. package/dist_ts_interfaces/requests/route-management.d.ts +114 -0
  28. package/dist_ts_interfaces/requests/route-management.js +2 -0
  29. package/dist_ts_web/00_commitinfo_data.js +1 -1
  30. package/dist_ts_web/appstate.d.ts +36 -0
  31. package/dist_ts_web/appstate.js +220 -2
  32. package/dist_ts_web/elements/index.d.ts +2 -0
  33. package/dist_ts_web/elements/index.js +3 -1
  34. package/dist_ts_web/elements/ops-dashboard.js +11 -1
  35. package/dist_ts_web/elements/ops-view-apitokens.d.ts +12 -0
  36. package/dist_ts_web/elements/ops-view-apitokens.js +306 -0
  37. package/dist_ts_web/elements/ops-view-routes.d.ts +12 -0
  38. package/dist_ts_web/elements/ops-view-routes.js +404 -0
  39. package/dist_ts_web/router.d.ts +1 -1
  40. package/dist_ts_web/router.js +2 -2
  41. package/package.json +2 -2
  42. package/ts/00_commitinfo_data.ts +1 -1
  43. package/ts/classes.dcrouter.ts +37 -1
  44. package/ts/config/classes.api-token-manager.ts +155 -0
  45. package/ts/config/classes.route-config-manager.ts +271 -0
  46. package/ts/config/index.ts +3 -1
  47. package/ts/opsserver/classes.opsserver.ts +4 -0
  48. package/ts/opsserver/handlers/api-token.handler.ts +96 -0
  49. package/ts/opsserver/handlers/index.ts +3 -1
  50. package/ts/opsserver/handlers/route-management.handler.ts +163 -0
  51. package/ts_web/00_commitinfo_data.ts +1 -1
  52. package/ts_web/appstate.ts +308 -1
  53. package/ts_web/elements/index.ts +2 -0
  54. package/ts_web/elements/ops-dashboard.ts +10 -0
  55. package/ts_web/elements/ops-view-apitokens.ts +281 -0
  56. package/ts_web/elements/ops-view-routes.ts +389 -0
  57. package/ts_web/router.ts +1 -1
  58. package/dist_ts/cache/classes.cache.cleaner.d.ts +0 -47
  59. package/dist_ts/cache/classes.cache.cleaner.js +0 -130
  60. package/dist_ts/cache/classes.cached.document.d.ts +0 -76
  61. package/dist_ts/cache/classes.cached.document.js +0 -100
  62. package/dist_ts/cache/classes.cachedb.d.ts +0 -60
  63. package/dist_ts/cache/classes.cachedb.js +0 -126
  64. package/dist_ts/cache/documents/classes.cached.email.d.ts +0 -125
  65. package/dist_ts/cache/documents/classes.cached.email.js +0 -337
  66. package/dist_ts/cache/documents/classes.cached.ip.reputation.d.ts +0 -119
  67. package/dist_ts/cache/documents/classes.cached.ip.reputation.js +0 -323
  68. package/dist_ts/cache/documents/index.d.ts +0 -2
  69. package/dist_ts/cache/documents/index.js +0 -3
  70. package/dist_ts/cache/index.d.ts +0 -4
  71. package/dist_ts/cache/index.js +0 -7
  72. package/dist_ts/monitoring/classes.metricscache.d.ts +0 -32
  73. package/dist_ts/monitoring/classes.metricscache.js +0 -63
  74. package/dist_ts/monitoring/classes.metricsmanager.d.ts +0 -169
  75. package/dist_ts/monitoring/classes.metricsmanager.js +0 -591
  76. package/dist_ts/monitoring/index.d.ts +0 -1
  77. package/dist_ts/monitoring/index.js +0 -2
  78. package/dist_ts/opsserver/handlers/admin.handler.d.ts +0 -31
  79. package/dist_ts/opsserver/handlers/admin.handler.js +0 -180
  80. package/dist_ts/opsserver/handlers/certificate.handler.d.ts +0 -34
  81. package/dist_ts/opsserver/handlers/certificate.handler.js +0 -419
  82. package/dist_ts/opsserver/handlers/config.handler.d.ts +0 -9
  83. package/dist_ts/opsserver/handlers/config.handler.js +0 -67
  84. package/dist_ts/opsserver/handlers/email-ops.handler.d.ts +0 -32
  85. package/dist_ts/opsserver/handlers/email-ops.handler.js +0 -226
  86. package/dist_ts/opsserver/handlers/logs.handler.d.ts +0 -17
  87. package/dist_ts/opsserver/handlers/logs.handler.js +0 -215
  88. package/dist_ts/opsserver/handlers/radius.handler.js +0 -296
  89. package/dist_ts/opsserver/handlers/remoteingress.handler.js +0 -154
  90. package/dist_ts/opsserver/handlers/security.handler.d.ts +0 -11
  91. package/dist_ts/opsserver/handlers/security.handler.js +0 -232
  92. package/dist_ts/opsserver/handlers/stats.handler.d.ts +0 -13
  93. package/dist_ts/opsserver/handlers/stats.handler.js +0 -400
  94. package/dist_ts/security/classes.securitylogger.d.ts +0 -140
  95. package/dist_ts/security/classes.securitylogger.js +0 -235
  96. package/dist_ts/storage/classes.storagemanager.d.ts +0 -82
  97. package/dist_ts/storage/classes.storagemanager.js +0 -344
  98. package/dist_ts/storage/index.d.ts +0 -1
  99. package/dist_ts/storage/index.js +0 -3
@@ -0,0 +1,281 @@
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
+
5
+ import {
6
+ DeesElement,
7
+ css,
8
+ cssManager,
9
+ customElement,
10
+ html,
11
+ state,
12
+ type TemplateResult,
13
+ } from '@design.estate/dees-element';
14
+
15
+ type TApiTokenScope = interfaces.data.TApiTokenScope;
16
+
17
+ @customElement('ops-view-apitokens')
18
+ export class OpsViewApiTokens extends DeesElement {
19
+ @state() accessor routeState: appstate.IRouteManagementState = {
20
+ mergedRoutes: [],
21
+ warnings: [],
22
+ apiTokens: [],
23
+ isLoading: false,
24
+ error: null,
25
+ lastUpdated: 0,
26
+ };
27
+
28
+ constructor() {
29
+ super();
30
+ const sub = appstate.routeManagementStatePart
31
+ .select((s) => s)
32
+ .subscribe((routeState) => {
33
+ this.routeState = routeState;
34
+ });
35
+ this.rxSubscriptions.push(sub);
36
+
37
+ // Re-fetch tokens when user logs in (fixes race condition where
38
+ // the view is created before authentication completes)
39
+ const loginSub = appstate.loginStatePart
40
+ .select((s) => s.isLoggedIn)
41
+ .subscribe((isLoggedIn) => {
42
+ if (isLoggedIn) {
43
+ appstate.routeManagementStatePart.dispatchAction(appstate.fetchApiTokensAction, null);
44
+ }
45
+ });
46
+ this.rxSubscriptions.push(loginSub);
47
+ }
48
+
49
+ public static styles = [
50
+ cssManager.defaultStyles,
51
+ viewHostCss,
52
+ css`
53
+ .apiTokensContainer {
54
+ display: flex;
55
+ flex-direction: column;
56
+ gap: 24px;
57
+ }
58
+
59
+ .scopePill {
60
+ display: inline-flex;
61
+ align-items: center;
62
+ padding: 2px 6px;
63
+ border-radius: 3px;
64
+ font-size: 11px;
65
+ background: ${cssManager.bdTheme('rgba(0, 130, 200, 0.1)', 'rgba(0, 170, 255, 0.1)')};
66
+ color: ${cssManager.bdTheme('#0369a1', '#0af')};
67
+ margin-right: 4px;
68
+ margin-bottom: 2px;
69
+ }
70
+
71
+ .statusBadge {
72
+ display: inline-flex;
73
+ align-items: center;
74
+ padding: 3px 10px;
75
+ border-radius: 12px;
76
+ font-size: 12px;
77
+ font-weight: 600;
78
+ letter-spacing: 0.02em;
79
+ text-transform: uppercase;
80
+ }
81
+
82
+ .statusBadge.active {
83
+ background: ${cssManager.bdTheme('#dcfce7', '#14532d')};
84
+ color: ${cssManager.bdTheme('#166534', '#4ade80')};
85
+ }
86
+
87
+ .statusBadge.disabled {
88
+ background: ${cssManager.bdTheme('#fef2f2', '#450a0a')};
89
+ color: ${cssManager.bdTheme('#991b1b', '#f87171')};
90
+ }
91
+
92
+ .statusBadge.expired {
93
+ background: ${cssManager.bdTheme('#f3f4f6', '#374151')};
94
+ color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
95
+ }
96
+ `,
97
+ ];
98
+
99
+ public render(): TemplateResult {
100
+ const { apiTokens } = this.routeState;
101
+
102
+ return html`
103
+ <ops-sectionheading>API Tokens</ops-sectionheading>
104
+
105
+ <div class="apiTokensContainer">
106
+ <dees-table
107
+ .heading1=${'API Tokens'}
108
+ .heading2=${'Manage programmatic access tokens'}
109
+ .data=${apiTokens}
110
+ .dataName=${'token'}
111
+ .searchable=${true}
112
+ .displayFunction=${(token: interfaces.data.IApiTokenInfo) => ({
113
+ name: token.name,
114
+ scopes: this.renderScopePills(token.scopes),
115
+ status: this.renderStatusBadge(token),
116
+ created: new Date(token.createdAt).toLocaleDateString(),
117
+ expires: token.expiresAt ? new Date(token.expiresAt).toLocaleDateString() : 'Never',
118
+ lastUsed: token.lastUsedAt ? new Date(token.lastUsedAt).toLocaleDateString() : 'Never',
119
+ })}
120
+ .dataActions=${[
121
+ {
122
+ name: 'Create Token',
123
+ iconName: 'lucide:plus',
124
+ type: ['header'],
125
+ actionFunc: async () => {
126
+ await this.showCreateTokenDialog();
127
+ },
128
+ },
129
+ {
130
+ name: 'Enable',
131
+ iconName: 'lucide:play',
132
+ type: ['inRow', 'contextmenu'] as any,
133
+ actionRelevancyCheckFunc: (actionData: any) => !actionData.item.enabled,
134
+ actionFunc: async (actionData: any) => {
135
+ const token = actionData.item as interfaces.data.IApiTokenInfo;
136
+ await appstate.routeManagementStatePart.dispatchAction(
137
+ appstate.toggleApiTokenAction,
138
+ { id: token.id, enabled: true },
139
+ );
140
+ },
141
+ },
142
+ {
143
+ name: 'Disable',
144
+ iconName: 'lucide:pause',
145
+ type: ['inRow', 'contextmenu'] as any,
146
+ actionRelevancyCheckFunc: (actionData: any) => actionData.item.enabled,
147
+ actionFunc: async (actionData: any) => {
148
+ const token = actionData.item as interfaces.data.IApiTokenInfo;
149
+ await appstate.routeManagementStatePart.dispatchAction(
150
+ appstate.toggleApiTokenAction,
151
+ { id: token.id, enabled: false },
152
+ );
153
+ },
154
+ },
155
+ {
156
+ name: 'Revoke',
157
+ iconName: 'lucide:trash2',
158
+ type: ['inRow', 'contextmenu'] as any,
159
+ actionFunc: async (actionData: any) => {
160
+ const token = actionData.item as interfaces.data.IApiTokenInfo;
161
+ await appstate.routeManagementStatePart.dispatchAction(
162
+ appstate.revokeApiTokenAction,
163
+ token.id,
164
+ );
165
+ },
166
+ },
167
+ ]}
168
+ ></dees-table>
169
+ </div>
170
+ `;
171
+ }
172
+
173
+ private renderScopePills(scopes: TApiTokenScope[]): TemplateResult {
174
+ return html`<div style="display: flex; flex-wrap: wrap; gap: 2px;">${scopes.map(
175
+ (s) => html`<span class="scopePill">${s}</span>`,
176
+ )}</div>`;
177
+ }
178
+
179
+ private renderStatusBadge(token: interfaces.data.IApiTokenInfo): TemplateResult {
180
+ if (!token.enabled) {
181
+ return html`<span class="statusBadge disabled">Disabled</span>`;
182
+ }
183
+ if (token.expiresAt && token.expiresAt < Date.now()) {
184
+ return html`<span class="statusBadge expired">Expired</span>`;
185
+ }
186
+ return html`<span class="statusBadge active">Active</span>`;
187
+ }
188
+
189
+ private async showCreateTokenDialog() {
190
+ const { DeesModal } = await import('@design.estate/dees-catalog');
191
+
192
+ const allScopes: TApiTokenScope[] = [
193
+ 'routes:read',
194
+ 'routes:write',
195
+ 'config:read',
196
+ 'tokens:read',
197
+ 'tokens:manage',
198
+ ];
199
+
200
+ await DeesModal.createAndShow({
201
+ heading: 'Create API Token',
202
+ content: html`
203
+ <div style="color: #888; margin-bottom: 12px; font-size: 13px;">
204
+ The token value will be shown once after creation. Copy it immediately.
205
+ </div>
206
+ <dees-form>
207
+ <dees-input-text .key=${'name'} .label=${'Token Name'} .required=${true}></dees-input-text>
208
+ <dees-input-tags
209
+ .key=${'scopes'}
210
+ .label=${'Token Scopes'}
211
+ .value=${['routes:read', 'routes:write']}
212
+ .suggestions=${allScopes}
213
+ .required=${true}
214
+ ></dees-input-tags>
215
+ <dees-input-text .key=${'expiresInDays'} .label=${'Expires in (days, blank = never)'}></dees-input-text>
216
+ </dees-form>
217
+ `,
218
+ menuOptions: [
219
+ {
220
+ name: 'Cancel',
221
+ iconName: 'lucide:x',
222
+ action: async (modalArg: any) => await modalArg.destroy(),
223
+ },
224
+ {
225
+ name: 'Create',
226
+ iconName: 'lucide:key',
227
+ action: async (modalArg: any) => {
228
+ const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
229
+ if (!form) return;
230
+ const formData = await form.collectFormData();
231
+ if (!formData.name) return;
232
+
233
+ // dees-input-tags returns string[] directly
234
+ const scopes = (formData.scopes || [])
235
+ .filter((s: string) => allScopes.includes(s as any)) as TApiTokenScope[];
236
+
237
+ const expiresInDays = formData.expiresInDays
238
+ ? parseInt(formData.expiresInDays, 10)
239
+ : null;
240
+
241
+ await modalArg.destroy();
242
+
243
+ try {
244
+ const response = await appstate.createApiToken(formData.name, scopes, expiresInDays);
245
+ if (response.success && response.tokenValue) {
246
+ // Refresh the list first so it's ready when user dismisses the modal
247
+ await appstate.routeManagementStatePart.dispatchAction(appstate.fetchApiTokensAction, null);
248
+
249
+ // Show the token value in a new modal
250
+ await DeesModal.createAndShow({
251
+ heading: 'Token Created',
252
+ content: html`
253
+ <div style="color: #ccc; padding: 8px 0;">
254
+ <p>Copy this token now. It will not be shown again.</p>
255
+ <div style="background: #111; padding: 12px; border-radius: 6px; margin-top: 8px;">
256
+ <code style="color: #0f8; word-break: break-all; font-size: 13px;">${response.tokenValue}</code>
257
+ </div>
258
+ </div>
259
+ `,
260
+ menuOptions: [
261
+ {
262
+ name: 'Done',
263
+ iconName: 'lucide:check',
264
+ action: async (m: any) => await m.destroy(),
265
+ },
266
+ ],
267
+ });
268
+ }
269
+ } catch (error) {
270
+ console.error('Failed to create token:', error);
271
+ }
272
+ },
273
+ },
274
+ ],
275
+ });
276
+ }
277
+
278
+ async firstUpdated() {
279
+ await appstate.routeManagementStatePart.dispatchAction(appstate.fetchApiTokensAction, null);
280
+ }
281
+ }
@@ -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/router.ts CHANGED
@@ -3,7 +3,7 @@ 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;
6
+ export const validViews = ['overview', 'network', 'emails', 'logs', 'routes', 'apitokens', 'configuration', 'security', 'certificates', 'remoteingress'] as const;
7
7
 
8
8
  export type TValidView = typeof validViews[number];
9
9