@serve.zone/dcrouter 16.1.0 → 16.2.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.
- package/deno.json +1 -1
- package/dist_serve/bundle.js +11786 -7664
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/config/classes.route-config-manager.d.ts +24 -4
- package/dist_ts/config/classes.route-config-manager.js +195 -34
- package/dist_ts/config/helpers.http-redirects.js +21 -12
- package/dist_ts/config/helpers.special-forwards.d.ts +14 -0
- package/dist_ts/config/helpers.special-forwards.js +85 -0
- package/dist_ts/opsserver/handlers/gatewayclient.handler.d.ts +1 -0
- package/dist_ts/opsserver/handlers/gatewayclient.handler.js +56 -9
- package/dist_ts/opsserver/handlers/route-management.handler.js +36 -1
- package/dist_ts_interfaces/data/route-management.d.ts +19 -1
- package/dist_ts_interfaces/data/route-management.js +5 -1
- package/dist_ts_interfaces/requests/route-management.d.ts +50 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate/routes.d.ts +10 -0
- package/dist_ts_web/appstate/routes.js +67 -1
- package/dist_ts_web/elements/network/ops-view-redirects.d.ts +1 -0
- package/dist_ts_web/elements/network/ops-view-redirects.js +12 -4
- package/dist_ts_web/elements/network/ops-view-routes.d.ts +20 -1
- package/dist_ts_web/elements/network/ops-view-routes.js +182 -52
- package/dist_ts_web/elements/network/ops-view-special-forwards.d.ts +12 -0
- package/dist_ts_web/elements/network/ops-view-special-forwards.js +375 -0
- package/dist_ts_web/elements/ops-dashboard.js +1 -3
- package/dist_ts_web/router.js +10 -2
- package/package.json +7 -7
- package/readme.md +7 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/config/classes.route-config-manager.ts +271 -32
- package/ts/config/helpers.http-redirects.ts +24 -16
- package/ts/config/helpers.special-forwards.ts +108 -0
- package/ts/opsserver/handlers/gatewayclient.handler.ts +55 -8
- package/ts/opsserver/handlers/route-management.handler.ts +59 -0
- package/ts_apiclient/readme.md +2 -2
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate/routes.ts +84 -0
- package/ts_web/elements/network/ops-view-redirects.ts +5 -1
- package/ts_web/elements/network/ops-view-routes.ts +175 -31
- package/ts_web/elements/network/ops-view-special-forwards.ts +352 -0
- package/ts_web/elements/ops-dashboard.ts +0 -2
- package/ts_web/readme.md +9 -0
- package/ts_web/router.ts +10 -1
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DeesElement,
|
|
3
|
+
css,
|
|
4
|
+
cssManager,
|
|
5
|
+
customElement,
|
|
6
|
+
html,
|
|
7
|
+
state,
|
|
8
|
+
type TemplateResult,
|
|
9
|
+
} from '@design.estate/dees-element';
|
|
10
|
+
import * as appstate from '../../appstate.js';
|
|
11
|
+
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
|
12
|
+
import { viewHostCss } from '../shared/css.js';
|
|
13
|
+
|
|
14
|
+
const managedRouteKind = 'letsencrypt-http01-forward' as const;
|
|
15
|
+
|
|
16
|
+
function getDropdownKey(value: unknown): string {
|
|
17
|
+
return typeof value === 'string'
|
|
18
|
+
? value
|
|
19
|
+
: value && typeof value === 'object' && 'key' in value
|
|
20
|
+
? String((value as { key: unknown }).key)
|
|
21
|
+
: '';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getDomains(route: interfaces.data.IMergedRoute): string[] {
|
|
25
|
+
const domains = route.route.match.domains;
|
|
26
|
+
if (!domains) return [];
|
|
27
|
+
return Array.isArray(domains) ? domains : [domains];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getTarget(route: interfaces.data.IMergedRoute): { host: string; port: number } | undefined {
|
|
31
|
+
const target = route.route.action.targets?.[0];
|
|
32
|
+
if (!target || typeof target.port !== 'number') return undefined;
|
|
33
|
+
const host = typeof target.host === 'string'
|
|
34
|
+
? target.host
|
|
35
|
+
: Array.isArray(target.host)
|
|
36
|
+
? target.host[0]
|
|
37
|
+
: undefined;
|
|
38
|
+
return host ? { host, port: target.port } : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@customElement('ops-view-special-forwards')
|
|
42
|
+
export class OpsViewSpecialForwards extends DeesElement {
|
|
43
|
+
@state()
|
|
44
|
+
accessor routeState: appstate.IRouteManagementState = appstate.routeManagementStatePart.getState()!;
|
|
45
|
+
|
|
46
|
+
@state()
|
|
47
|
+
accessor profilesTargetsState: appstate.IProfilesTargetsState =
|
|
48
|
+
appstate.profilesTargetsStatePart.getState()!;
|
|
49
|
+
|
|
50
|
+
constructor() {
|
|
51
|
+
super();
|
|
52
|
+
this.rxSubscriptions.push(
|
|
53
|
+
appstate.routeManagementStatePart.select().subscribe((routeState) => {
|
|
54
|
+
this.routeState = routeState;
|
|
55
|
+
}),
|
|
56
|
+
appstate.profilesTargetsStatePart.select().subscribe((profilesTargetsState) => {
|
|
57
|
+
this.profilesTargetsState = profilesTargetsState;
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public static styles = [
|
|
63
|
+
cssManager.defaultStyles,
|
|
64
|
+
viewHostCss,
|
|
65
|
+
css`
|
|
66
|
+
:host {
|
|
67
|
+
display: block;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.specialForwardsContainer {
|
|
71
|
+
display: flex;
|
|
72
|
+
flex-direction: column;
|
|
73
|
+
gap: 20px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.explanation {
|
|
77
|
+
padding: 14px 16px;
|
|
78
|
+
border: 1px solid var(--dees-color-border-default);
|
|
79
|
+
border-radius: 8px;
|
|
80
|
+
color: var(--dees-color-text-secondary);
|
|
81
|
+
font-size: 14px;
|
|
82
|
+
line-height: 1.5;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.explanation strong {
|
|
86
|
+
color: var(--dees-color-text-primary);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.error {
|
|
90
|
+
padding: 12px 16px;
|
|
91
|
+
border: 1px solid #ef4444;
|
|
92
|
+
border-radius: 8px;
|
|
93
|
+
color: #ef4444;
|
|
94
|
+
}
|
|
95
|
+
`,
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
public render(): TemplateResult {
|
|
99
|
+
const forwards = this.routeState.mergedRoutes.filter((route) => {
|
|
100
|
+
return route.metadata?.managedRouteKind === managedRouteKind
|
|
101
|
+
&& route.metadata.ownerType !== 'gatewayClient';
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return html`
|
|
105
|
+
<div class="specialForwardsContainer">
|
|
106
|
+
<div class="explanation">
|
|
107
|
+
<strong>Let's Encrypt HTTP-01 Forward</strong> exposes only
|
|
108
|
+
<code>/.well-known/acme-challenge/*</code> on public port 80. DCRouter manages the
|
|
109
|
+
path, priority, protocol, and PUBLIC source profile; you choose domains and a target.
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
${this.routeState.error
|
|
113
|
+
? html`<div class="error">${this.routeState.error}</div>`
|
|
114
|
+
: ''}
|
|
115
|
+
|
|
116
|
+
<dees-table
|
|
117
|
+
.heading1=${'Special Forwards'}
|
|
118
|
+
.heading2=${'Server-managed routes for protocol-specific forwarding'}
|
|
119
|
+
.data=${forwards}
|
|
120
|
+
.showColumnFilters=${true}
|
|
121
|
+
.displayFunction=${(route: interfaces.data.IMergedRoute) => {
|
|
122
|
+
const target = getTarget(route);
|
|
123
|
+
return {
|
|
124
|
+
Status: route.enabled ? 'Enabled' : 'Disabled',
|
|
125
|
+
Type: 'Let\'s Encrypt HTTP-01',
|
|
126
|
+
Name: route.route.name || route.id,
|
|
127
|
+
Domains: getDomains(route).join(', '),
|
|
128
|
+
Path: route.route.match.path || '-',
|
|
129
|
+
Target: route.metadata?.networkTargetName
|
|
130
|
+
|| (target ? `${target.host}:${target.port}` : '-'),
|
|
131
|
+
'Remote Ingress': route.route.remoteIngress?.enabled ? 'Enabled' : 'Disabled',
|
|
132
|
+
};
|
|
133
|
+
}}
|
|
134
|
+
.dataActions=${[
|
|
135
|
+
{
|
|
136
|
+
name: 'Add HTTP-01 Forward',
|
|
137
|
+
iconName: 'lucide:plus',
|
|
138
|
+
type: ['header' as const],
|
|
139
|
+
actionFunc: async () => this.showForwardDialog(),
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'Refresh',
|
|
143
|
+
iconName: 'lucide:refreshCw',
|
|
144
|
+
type: ['header' as const],
|
|
145
|
+
actionFunc: async () => this.refreshData(),
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: 'Edit',
|
|
149
|
+
iconName: 'lucide:pencil',
|
|
150
|
+
type: ['inRow', 'contextmenu'] as any,
|
|
151
|
+
actionFunc: async (actionData: any) => {
|
|
152
|
+
await this.showForwardDialog(actionData.item as interfaces.data.IMergedRoute);
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'Enable / Disable',
|
|
157
|
+
iconName: 'lucide:power',
|
|
158
|
+
type: ['inRow', 'contextmenu'] as any,
|
|
159
|
+
actionFunc: async (actionData: any) => {
|
|
160
|
+
const route = actionData.item as interfaces.data.IMergedRoute;
|
|
161
|
+
await appstate.routeManagementStatePart.dispatchAction(
|
|
162
|
+
appstate.toggleRouteAction,
|
|
163
|
+
{ id: route.id, enabled: !route.enabled },
|
|
164
|
+
);
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: 'Delete',
|
|
169
|
+
iconName: 'lucide:trash2',
|
|
170
|
+
type: ['inRow', 'contextmenu'] as any,
|
|
171
|
+
actionFunc: async (actionData: any) => {
|
|
172
|
+
await this.confirmDelete(actionData.item as interfaces.data.IMergedRoute);
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
]}
|
|
176
|
+
></dees-table>
|
|
177
|
+
</div>
|
|
178
|
+
`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private async refreshData(): Promise<void> {
|
|
182
|
+
await Promise.all([
|
|
183
|
+
appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null),
|
|
184
|
+
appstate.profilesTargetsStatePart.dispatchAction(
|
|
185
|
+
appstate.fetchProfilesAndTargetsAction,
|
|
186
|
+
null,
|
|
187
|
+
),
|
|
188
|
+
]);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private async showForwardDialog(existing?: interfaces.data.IMergedRoute): Promise<void> {
|
|
192
|
+
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
193
|
+
const target = existing ? getTarget(existing) : undefined;
|
|
194
|
+
const targetOptions = [
|
|
195
|
+
{ key: '', option: '(none — inline target)' },
|
|
196
|
+
...this.profilesTargetsState.targets.map((networkTarget) => ({
|
|
197
|
+
key: networkTarget.id,
|
|
198
|
+
option: `${networkTarget.name} (${
|
|
199
|
+
Array.isArray(networkTarget.host) ? networkTarget.host.join(', ') : networkTarget.host
|
|
200
|
+
}:${networkTarget.port})`,
|
|
201
|
+
})),
|
|
202
|
+
];
|
|
203
|
+
const selectedTarget = targetOptions.find((option) => {
|
|
204
|
+
return option.key === (existing?.metadata?.networkTargetRef || '');
|
|
205
|
+
}) || targetOptions[0];
|
|
206
|
+
|
|
207
|
+
await DeesModal.createAndShow({
|
|
208
|
+
heading: existing
|
|
209
|
+
? `Edit HTTP-01 Forward: ${existing.route.name || existing.id}`
|
|
210
|
+
: 'Add Let\'s Encrypt HTTP-01 Forward',
|
|
211
|
+
content: html`
|
|
212
|
+
<dees-form>
|
|
213
|
+
<dees-input-text
|
|
214
|
+
.key=${'name'}
|
|
215
|
+
.label=${'Name'}
|
|
216
|
+
.value=${existing?.route.name || ''}
|
|
217
|
+
.required=${true}
|
|
218
|
+
></dees-input-text>
|
|
219
|
+
<dees-input-list
|
|
220
|
+
.key=${'domains'}
|
|
221
|
+
.label=${'Domains'}
|
|
222
|
+
.description=${'Exact public hostnames; HTTP-01 does not support wildcards'}
|
|
223
|
+
.placeholder=${'Add domain...'}
|
|
224
|
+
.value=${existing ? getDomains(existing) : []}
|
|
225
|
+
></dees-input-list>
|
|
226
|
+
<dees-input-dropdown
|
|
227
|
+
.key=${'networkTargetRef'}
|
|
228
|
+
.label=${'Network Target'}
|
|
229
|
+
.options=${targetOptions}
|
|
230
|
+
.selectedOption=${selectedTarget}
|
|
231
|
+
></dees-input-dropdown>
|
|
232
|
+
<dees-input-text
|
|
233
|
+
.key=${'targetHost'}
|
|
234
|
+
.label=${'Inline Target Host'}
|
|
235
|
+
.description=${'Used only when no Network Target is selected'}
|
|
236
|
+
.value=${target?.host || ''}
|
|
237
|
+
></dees-input-text>
|
|
238
|
+
<dees-input-text
|
|
239
|
+
.key=${'targetPort'}
|
|
240
|
+
.label=${'Inline Target Port'}
|
|
241
|
+
.description=${'Usually port 80 on the ACME responder'}
|
|
242
|
+
.value=${target ? String(target.port) : '80'}
|
|
243
|
+
></dees-input-text>
|
|
244
|
+
<dees-input-checkbox
|
|
245
|
+
.key=${'remoteIngressEnabled'}
|
|
246
|
+
.label=${'Enable Remote Ingress'}
|
|
247
|
+
.value=${existing?.route.remoteIngress?.enabled === true}
|
|
248
|
+
></dees-input-checkbox>
|
|
249
|
+
<dees-input-list
|
|
250
|
+
.key=${'remoteIngressEdgeFilter'}
|
|
251
|
+
.label=${'Remote Ingress Edge Filter'}
|
|
252
|
+
.description=${'Optional edge IDs or tags; leave empty for all edges'}
|
|
253
|
+
.placeholder=${'Add edge ID or tag...'}
|
|
254
|
+
.value=${existing?.route.remoteIngress?.edgeFilter || []}
|
|
255
|
+
></dees-input-list>
|
|
256
|
+
</dees-form>
|
|
257
|
+
`,
|
|
258
|
+
menuOptions: [
|
|
259
|
+
{
|
|
260
|
+
name: 'Cancel',
|
|
261
|
+
iconName: 'lucide:x',
|
|
262
|
+
action: async (modalArg: any) => modalArg.destroy(),
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: existing ? 'Save' : 'Create',
|
|
266
|
+
iconName: existing ? 'lucide:check' : 'lucide:plus',
|
|
267
|
+
action: async (modalArg: any) => {
|
|
268
|
+
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
|
269
|
+
if (!form) return;
|
|
270
|
+
const data = await form.collectFormData();
|
|
271
|
+
const name = String(data.name || '').trim();
|
|
272
|
+
const domains = Array.isArray(data.domains)
|
|
273
|
+
? data.domains.map((domain: unknown) => String(domain).trim()).filter(Boolean)
|
|
274
|
+
: [];
|
|
275
|
+
const networkTargetRef = getDropdownKey(data.networkTargetRef).trim();
|
|
276
|
+
const targetHost = String(data.targetHost || '').trim();
|
|
277
|
+
const targetPort = Number.parseInt(String(data.targetPort || ''), 10);
|
|
278
|
+
if (!name || domains.length === 0) {
|
|
279
|
+
alert('Name and at least one domain are required.');
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (
|
|
283
|
+
!networkTargetRef
|
|
284
|
+
&& (!targetHost || !Number.isInteger(targetPort) || targetPort < 1 || targetPort > 65535)
|
|
285
|
+
) {
|
|
286
|
+
alert('A valid inline target host and port are required.');
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const edgeFilter = Array.isArray(data.remoteIngressEdgeFilter)
|
|
291
|
+
? data.remoteIngressEdgeFilter.map((entry: unknown) => String(entry).trim()).filter(Boolean)
|
|
292
|
+
: [];
|
|
293
|
+
const config: interfaces.data.ILetsEncryptHttp01ForwardConfig = {
|
|
294
|
+
name,
|
|
295
|
+
domains,
|
|
296
|
+
...(networkTargetRef
|
|
297
|
+
? { networkTargetRef }
|
|
298
|
+
: { target: { host: targetHost, port: targetPort } }),
|
|
299
|
+
...(data.remoteIngressEnabled
|
|
300
|
+
? {
|
|
301
|
+
remoteIngress: {
|
|
302
|
+
enabled: true,
|
|
303
|
+
...(edgeFilter.length ? { edgeFilter } : {}),
|
|
304
|
+
},
|
|
305
|
+
}
|
|
306
|
+
: {}),
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
if (existing) {
|
|
310
|
+
await appstate.routeManagementStatePart.dispatchAction(
|
|
311
|
+
appstate.updateLetsEncryptHttp01ForwardAction,
|
|
312
|
+
{ id: existing.id, config, enabled: existing.enabled },
|
|
313
|
+
);
|
|
314
|
+
} else {
|
|
315
|
+
await appstate.routeManagementStatePart.dispatchAction(
|
|
316
|
+
appstate.createLetsEncryptHttp01ForwardAction,
|
|
317
|
+
{ config },
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
await modalArg.destroy();
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private async confirmDelete(route: interfaces.data.IMergedRoute): Promise<void> {
|
|
328
|
+
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
329
|
+
await DeesModal.createAndShow({
|
|
330
|
+
heading: `Delete Special Forward: ${route.route.name || route.id}`,
|
|
331
|
+
content: html`<p>This removes the managed HTTP-01 route for ${getDomains(route).join(', ')}.</p>`,
|
|
332
|
+
menuOptions: [
|
|
333
|
+
{
|
|
334
|
+
name: 'Cancel',
|
|
335
|
+
iconName: 'lucide:x',
|
|
336
|
+
action: async (modalArg: any) => modalArg.destroy(),
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
name: 'Delete',
|
|
340
|
+
iconName: 'lucide:trash2',
|
|
341
|
+
action: async (modalArg: any) => {
|
|
342
|
+
await appstate.routeManagementStatePart.dispatchAction(
|
|
343
|
+
appstate.deleteLetsEncryptHttp01ForwardAction,
|
|
344
|
+
route.id,
|
|
345
|
+
);
|
|
346
|
+
await modalArg.destroy();
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
],
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
@@ -23,7 +23,6 @@ import { OpsViewConfig } from './overview/ops-view-config.js';
|
|
|
23
23
|
// Network group
|
|
24
24
|
import { OpsViewNetworkActivity } from './network/ops-view-network-activity.js';
|
|
25
25
|
import { OpsViewRoutes } from './network/ops-view-routes.js';
|
|
26
|
-
import { OpsViewRedirects } from './network/ops-view-redirects.js';
|
|
27
26
|
import { OpsViewSourceProfiles } from './network/ops-view-sourceprofiles.js';
|
|
28
27
|
import { OpsViewNetworkTargets } from './network/ops-view-networktargets.js';
|
|
29
28
|
import { OpsViewTargetProfiles } from './network/ops-view-targetprofiles.js';
|
|
@@ -101,7 +100,6 @@ export class OpsDashboard extends DeesElement {
|
|
|
101
100
|
subViews: [
|
|
102
101
|
{ slug: 'activity', name: 'Network Activity', iconName: 'lucide:activity', element: OpsViewNetworkActivity },
|
|
103
102
|
{ slug: 'routes', name: 'Routes', iconName: 'lucide:route', element: OpsViewRoutes },
|
|
104
|
-
{ slug: 'redirects', name: 'Redirects', iconName: 'lucide:CornerDownRight', element: OpsViewRedirects },
|
|
105
103
|
{ slug: 'sourceprofiles', name: 'Source Profiles', iconName: 'lucide:shieldCheck', element: OpsViewSourceProfiles },
|
|
106
104
|
{ slug: 'networktargets', name: 'Network Targets', iconName: 'lucide:server', element: OpsViewNetworkTargets },
|
|
107
105
|
{ slug: 'targetprofiles', name: 'Target Profiles', iconName: 'lucide:target', element: OpsViewTargetProfiles },
|
package/ts_web/readme.md
CHANGED
|
@@ -27,6 +27,15 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|
|
27
27
|
| `security` | `overview`, `blocked`, `authentication` |
|
|
28
28
|
| `domains` | `providers`, `domains`, `dns`, `certificates` |
|
|
29
29
|
|
|
30
|
+
## Routes Navigation
|
|
31
|
+
|
|
32
|
+
The `/network/routes` view uses two levels of tabs:
|
|
33
|
+
|
|
34
|
+
- primary sections: **Standard Routes**, **Special Forwards**, and **Redirects**
|
|
35
|
+
- ownership filters within Standard Routes: **All**, **Operator**, **Gateway Client**, and **System**
|
|
36
|
+
|
|
37
|
+
Special Forwards create server-managed Let's Encrypt HTTP-01 routes for exact domains and a selected inline or Network Target backend. Redirects remain a derived runtime view. Tab state is stored in the `tab` and `owner` query parameters so deep links and browser history remain stable; the legacy `/network/redirects` URL is normalized to `/network/routes?tab=redirects`.
|
|
38
|
+
|
|
30
39
|
## Runtime Communication
|
|
31
40
|
|
|
32
41
|
The dashboard talks to the dcrouter OpsServer through:
|
package/ts_web/router.ts
CHANGED
|
@@ -9,7 +9,7 @@ const flatViews = ['logs'] as const;
|
|
|
9
9
|
// Tabbed views and their valid subviews
|
|
10
10
|
const subviewMap: Record<string, readonly string[]> = {
|
|
11
11
|
overview: ['stats', 'configuration'] as const,
|
|
12
|
-
network: ['activity', 'routes', '
|
|
12
|
+
network: ['activity', 'routes', 'sourceprofiles', 'networktargets', 'targetprofiles', 'remoteingress', 'vpn'] as const,
|
|
13
13
|
email: ['log', 'security', 'domains'] as const,
|
|
14
14
|
access: ['gatewayclients', 'apitokens', 'users'] as const,
|
|
15
15
|
security: ['overview', 'blocked', 'authentication'] as const,
|
|
@@ -102,6 +102,15 @@ class AppRouter {
|
|
|
102
102
|
private handleInitialRoute(): void {
|
|
103
103
|
const path = window.location.pathname;
|
|
104
104
|
|
|
105
|
+
if (path === '/network/redirects') {
|
|
106
|
+
const url = new URL(window.location.href);
|
|
107
|
+
url.pathname = '/network/routes';
|
|
108
|
+
url.searchParams.set('tab', 'redirects');
|
|
109
|
+
window.history.replaceState(window.history.state, '', url);
|
|
110
|
+
this.updateViewState('network', 'routes');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
105
114
|
if (!path || path === '/') {
|
|
106
115
|
this.router.pushUrl('/overview');
|
|
107
116
|
return;
|