@serve.zone/dcrouter 13.19.1 → 13.20.2
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/dist_serve/bundle.js +700 -690
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +5 -0
- package/dist_ts/classes.dcrouter.js +34 -10
- package/dist_ts/config/classes.route-config-manager.d.ts +1 -0
- package/dist_ts/config/classes.route-config-manager.js +12 -2
- package/dist_ts/vpn/classes.vpn-manager.d.ts +5 -1
- package/dist_ts/vpn/classes.vpn-manager.js +55 -17
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/network/ops-view-routes.js +70 -10
- package/dist_ts_web/elements/network/ops-view-vpn.d.ts +3 -0
- package/dist_ts_web/elements/network/ops-view-vpn.js +44 -16
- package/package.json +1 -1
- package/readme.md +123 -155
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +51 -14
- package/ts/config/classes.route-config-manager.ts +16 -1
- package/ts/readme.md +46 -103
- package/ts/vpn/classes.vpn-manager.ts +66 -15
- package/ts_apiclient/readme.md +57 -59
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/network/ops-view-routes.ts +71 -8
- package/ts_web/elements/network/ops-view-vpn.ts +50 -14
- package/ts_web/readme.md +27 -47
|
@@ -43,22 +43,28 @@ function parseTargetPort(value: any): number | undefined {
|
|
|
43
43
|
|
|
44
44
|
function getRouteTargetInputs(formEl: any) {
|
|
45
45
|
const textInputs = Array.from(formEl.querySelectorAll('dees-input-text')) as any[];
|
|
46
|
+
const checkboxInputs = Array.from(formEl.querySelectorAll('dees-input-checkbox')) as any[];
|
|
46
47
|
return {
|
|
47
48
|
hostInput: textInputs.find((input) => input.key === 'targetHost'),
|
|
48
49
|
portInput: textInputs.find((input) => input.key === 'targetPort'),
|
|
50
|
+
preservePortInput: checkboxInputs.find((input) => input.key === 'preserveMatchPort'),
|
|
49
51
|
};
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
function setupTargetInputState(formEl: any) {
|
|
53
55
|
const updateState = async () => {
|
|
54
56
|
const data = await formEl.collectFormData();
|
|
57
|
+
const contentEl = formEl.closest('.content') || formEl.parentElement;
|
|
55
58
|
const usesNetworkTarget = !!getDropdownKey(data.networkTargetRef);
|
|
56
|
-
const
|
|
59
|
+
const preserveMatchPort = !usesNetworkTarget && Boolean(data.preserveMatchPort);
|
|
60
|
+
const { hostInput, portInput, preservePortInput } = getRouteTargetInputs(formEl);
|
|
57
61
|
const hostDescription = usesNetworkTarget
|
|
58
62
|
? 'Controlled by the selected network target'
|
|
59
63
|
: 'Used when no network target is selected';
|
|
60
64
|
const portDescription = usesNetworkTarget
|
|
61
65
|
? 'Controlled by the selected network target'
|
|
66
|
+
: preserveMatchPort
|
|
67
|
+
? 'Forwarded to the backend on the same port the client matched'
|
|
62
68
|
: 'Used when no network target is selected';
|
|
63
69
|
|
|
64
70
|
if (hostInput) {
|
|
@@ -67,10 +73,24 @@ function setupTargetInputState(formEl: any) {
|
|
|
67
73
|
hostInput.description = hostDescription;
|
|
68
74
|
}
|
|
69
75
|
if (portInput) {
|
|
70
|
-
portInput.disabled = usesNetworkTarget;
|
|
71
|
-
portInput.required = !usesNetworkTarget;
|
|
76
|
+
portInput.disabled = usesNetworkTarget || preserveMatchPort;
|
|
77
|
+
portInput.required = !usesNetworkTarget && !preserveMatchPort;
|
|
72
78
|
portInput.description = portDescription;
|
|
73
79
|
}
|
|
80
|
+
if (preservePortInput) {
|
|
81
|
+
preservePortInput.disabled = usesNetworkTarget;
|
|
82
|
+
preservePortInput.description = usesNetworkTarget
|
|
83
|
+
? 'Unavailable when a network target is selected'
|
|
84
|
+
: 'Forward to the backend using the same port that matched this route';
|
|
85
|
+
if (usesNetworkTarget) {
|
|
86
|
+
preservePortInput.value = false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const remoteIngressGroup = contentEl?.querySelector('.remoteIngressGroup') as HTMLElement | null;
|
|
91
|
+
if (remoteIngressGroup) {
|
|
92
|
+
remoteIngressGroup.style.display = Boolean(data.remoteIngressEnabled) ? 'flex' : 'none';
|
|
93
|
+
}
|
|
74
94
|
|
|
75
95
|
await formEl.updateRequiredStatus?.();
|
|
76
96
|
};
|
|
@@ -465,10 +485,13 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
465
485
|
? (Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains])
|
|
466
486
|
: [];
|
|
467
487
|
const firstTarget = route.action.targets?.[0];
|
|
488
|
+
const currentPreserveMatchPort = firstTarget?.port === 'preserve';
|
|
468
489
|
const currentTargetHost = firstTarget
|
|
469
490
|
? (Array.isArray(firstTarget.host) ? firstTarget.host[0] : firstTarget.host)
|
|
470
491
|
: '';
|
|
471
|
-
const currentTargetPort = firstTarget?.port
|
|
492
|
+
const currentTargetPort = typeof firstTarget?.port === 'number' ? String(firstTarget.port) : '';
|
|
493
|
+
const currentRemoteIngressEnabled = route.remoteIngress?.enabled === true;
|
|
494
|
+
const currentEdgeFilter = route.remoteIngress?.edgeFilter || [];
|
|
472
495
|
|
|
473
496
|
// Compute current TLS state for pre-population
|
|
474
497
|
const currentTls = (route.action as any).tls;
|
|
@@ -493,6 +516,11 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
493
516
|
<dees-input-dropdown .key=${'networkTargetRef'} .label=${'Network Target'} .options=${targetOptions} .selectedOption=${targetOptions.find((o) => o.key === (merged.metadata?.networkTargetRef || '')) || null}></dees-input-dropdown>
|
|
494
517
|
<dees-input-text .key=${'targetHost'} .label=${'Target Host'} .description=${'Used when no network target is selected'} .value=${currentTargetHost}></dees-input-text>
|
|
495
518
|
<dees-input-text .key=${'targetPort'} .label=${'Target Port'} .description=${'Used when no network target is selected'} .value=${currentTargetPort}></dees-input-text>
|
|
519
|
+
<dees-input-checkbox .key=${'preserveMatchPort'} .label=${'Preserve incoming port'} .value=${currentPreserveMatchPort}></dees-input-checkbox>
|
|
520
|
+
<dees-input-checkbox .key=${'remoteIngressEnabled'} .label=${'Enable Remote Ingress'} .value=${currentRemoteIngressEnabled}></dees-input-checkbox>
|
|
521
|
+
<div class="remoteIngressGroup" style="display: ${currentRemoteIngressEnabled ? 'flex' : 'none'}; flex-direction: column; gap: 16px;">
|
|
522
|
+
<dees-input-list .key=${'remoteIngressEdgeFilter'} .label=${'Edge Filter'} .description=${'Optional edge IDs or tags. Leave empty to allow all edges.'} .placeholder=${'Add edge ID or tag...'} .value=${currentEdgeFilter}></dees-input-list>
|
|
523
|
+
</div>
|
|
496
524
|
<dees-input-dropdown .key=${'tlsMode'} .label=${'TLS Mode'} .options=${tlsModeOptions} .selectedOption=${tlsModeOptions.find((o) => o.key === currentTlsMode) || tlsModeOptions[0]}></dees-input-dropdown>
|
|
497
525
|
<div class="tlsCertificateGroup" style="display: ${needsCert ? 'flex' : 'none'}; flex-direction: column; gap: 16px;">
|
|
498
526
|
<dees-input-dropdown .key=${'tlsCertificate'} .label=${'Certificate'} .options=${tlsCertOptions} .selectedOption=${tlsCertOptions.find((o) => o.key === currentTlsCert) || tlsCertOptions[0]}></dees-input-dropdown>
|
|
@@ -526,14 +554,22 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
526
554
|
|
|
527
555
|
const profileKey = getDropdownKey(formData.sourceProfileRef);
|
|
528
556
|
const targetKey = getDropdownKey(formData.networkTargetRef);
|
|
529
|
-
const
|
|
530
|
-
|
|
557
|
+
const preserveMatchPort = !targetKey && Boolean(formData.preserveMatchPort);
|
|
558
|
+
const targetPort = preserveMatchPort
|
|
559
|
+
? 'preserve'
|
|
560
|
+
: parseTargetPort(formData.targetPort)
|
|
561
|
+
?? (targetKey ? parseTargetPort(currentTargetPort) ?? ports[0] : undefined);
|
|
531
562
|
|
|
532
563
|
if (targetPort === undefined) {
|
|
533
564
|
alert('Target Port must be a valid port number when no network target is selected.');
|
|
534
565
|
return;
|
|
535
566
|
}
|
|
536
567
|
|
|
568
|
+
const remoteIngressEnabled = Boolean(formData.remoteIngressEnabled);
|
|
569
|
+
const remoteIngressEdgeFilter: string[] = Array.isArray(formData.remoteIngressEdgeFilter)
|
|
570
|
+
? formData.remoteIngressEdgeFilter.filter(Boolean)
|
|
571
|
+
: [];
|
|
572
|
+
|
|
537
573
|
const updatedRoute: any = {
|
|
538
574
|
name: formData.name,
|
|
539
575
|
match: {
|
|
@@ -549,6 +585,12 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
549
585
|
},
|
|
550
586
|
],
|
|
551
587
|
},
|
|
588
|
+
remoteIngress: remoteIngressEnabled
|
|
589
|
+
? {
|
|
590
|
+
enabled: true,
|
|
591
|
+
...(remoteIngressEdgeFilter.length > 0 ? { edgeFilter: remoteIngressEdgeFilter } : {}),
|
|
592
|
+
}
|
|
593
|
+
: null,
|
|
552
594
|
...(priority != null && !isNaN(priority) ? { priority } : {}),
|
|
553
595
|
};
|
|
554
596
|
|
|
@@ -640,6 +682,11 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
640
682
|
<dees-input-dropdown .key=${'networkTargetRef'} .label=${'Network Target'} .options=${targetOptions}></dees-input-dropdown>
|
|
641
683
|
<dees-input-text .key=${'targetHost'} .label=${'Target Host'} .description=${'Used when no network target is selected'} .value=${'localhost'}></dees-input-text>
|
|
642
684
|
<dees-input-text .key=${'targetPort'} .label=${'Target Port'} .description=${'Used when no network target is selected'}></dees-input-text>
|
|
685
|
+
<dees-input-checkbox .key=${'preserveMatchPort'} .label=${'Preserve incoming port'} .value=${false}></dees-input-checkbox>
|
|
686
|
+
<dees-input-checkbox .key=${'remoteIngressEnabled'} .label=${'Enable Remote Ingress'} .value=${false}></dees-input-checkbox>
|
|
687
|
+
<div class="remoteIngressGroup" style="display: none; flex-direction: column; gap: 16px;">
|
|
688
|
+
<dees-input-list .key=${'remoteIngressEdgeFilter'} .label=${'Edge Filter'} .description=${'Optional edge IDs or tags. Leave empty to allow all edges.'} .placeholder=${'Add edge ID or tag...'}></dees-input-list>
|
|
689
|
+
</div>
|
|
643
690
|
<dees-input-dropdown .key=${'tlsMode'} .label=${'TLS Mode'} .options=${tlsModeOptions} .selectedOption=${tlsModeOptions[0]}></dees-input-dropdown>
|
|
644
691
|
<div class="tlsCertificateGroup" style="display: none; flex-direction: column; gap: 16px;">
|
|
645
692
|
<dees-input-dropdown .key=${'tlsCertificate'} .label=${'Certificate'} .options=${tlsCertOptions} .selectedOption=${tlsCertOptions[0]}></dees-input-dropdown>
|
|
@@ -673,14 +720,22 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
673
720
|
|
|
674
721
|
const profileKey = getDropdownKey(formData.sourceProfileRef);
|
|
675
722
|
const targetKey = getDropdownKey(formData.networkTargetRef);
|
|
676
|
-
const
|
|
677
|
-
|
|
723
|
+
const preserveMatchPort = !targetKey && Boolean(formData.preserveMatchPort);
|
|
724
|
+
const targetPort = preserveMatchPort
|
|
725
|
+
? 'preserve'
|
|
726
|
+
: parseTargetPort(formData.targetPort)
|
|
727
|
+
?? (targetKey ? ports[0] : undefined);
|
|
678
728
|
|
|
679
729
|
if (targetPort === undefined) {
|
|
680
730
|
alert('Target Port must be a valid port number when no network target is selected.');
|
|
681
731
|
return;
|
|
682
732
|
}
|
|
683
733
|
|
|
734
|
+
const remoteIngressEnabled = Boolean(formData.remoteIngressEnabled);
|
|
735
|
+
const remoteIngressEdgeFilter: string[] = Array.isArray(formData.remoteIngressEdgeFilter)
|
|
736
|
+
? formData.remoteIngressEdgeFilter.filter(Boolean)
|
|
737
|
+
: [];
|
|
738
|
+
|
|
684
739
|
const route: any = {
|
|
685
740
|
name: formData.name,
|
|
686
741
|
match: {
|
|
@@ -696,6 +751,14 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
696
751
|
},
|
|
697
752
|
],
|
|
698
753
|
},
|
|
754
|
+
...(remoteIngressEnabled
|
|
755
|
+
? {
|
|
756
|
+
remoteIngress: {
|
|
757
|
+
enabled: true,
|
|
758
|
+
...(remoteIngressEdgeFilter.length > 0 ? { edgeFilter: remoteIngressEdgeFilter } : {}),
|
|
759
|
+
},
|
|
760
|
+
}
|
|
761
|
+
: {}),
|
|
699
762
|
...(priority != null && !isNaN(priority) ? { priority } : {}),
|
|
700
763
|
};
|
|
701
764
|
|
|
@@ -49,19 +49,28 @@ export class OpsViewVpn extends DeesElement {
|
|
|
49
49
|
@state()
|
|
50
50
|
accessor vpnState: appstate.IVpnState = appstate.vpnStatePart.getState()!;
|
|
51
51
|
|
|
52
|
+
@state()
|
|
53
|
+
accessor targetProfilesState: appstate.ITargetProfilesState = appstate.targetProfilesStatePart.getState()!;
|
|
54
|
+
|
|
52
55
|
constructor() {
|
|
53
56
|
super();
|
|
54
57
|
const sub = appstate.vpnStatePart.select().subscribe((newState) => {
|
|
55
58
|
this.vpnState = newState;
|
|
56
59
|
});
|
|
57
60
|
this.rxSubscriptions.push(sub);
|
|
61
|
+
|
|
62
|
+
const targetProfilesSub = appstate.targetProfilesStatePart.select().subscribe((newState) => {
|
|
63
|
+
this.targetProfilesState = newState;
|
|
64
|
+
});
|
|
65
|
+
this.rxSubscriptions.push(targetProfilesSub);
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
async connectedCallback() {
|
|
61
69
|
await super.connectedCallback();
|
|
62
|
-
await
|
|
63
|
-
|
|
64
|
-
|
|
70
|
+
await Promise.all([
|
|
71
|
+
appstate.vpnStatePart.dispatchAction(appstate.fetchVpnAction, null),
|
|
72
|
+
appstate.targetProfilesStatePart.dispatchAction(appstate.fetchTargetProfilesAction, null),
|
|
73
|
+
]);
|
|
65
74
|
}
|
|
66
75
|
|
|
67
76
|
public static styles = [
|
|
@@ -330,13 +339,7 @@ export class OpsViewVpn extends DeesElement {
|
|
|
330
339
|
'Status': statusHtml,
|
|
331
340
|
'Routing': routingHtml,
|
|
332
341
|
'VPN IP': client.assignedIp || '-',
|
|
333
|
-
'Target Profiles': client.targetProfileIds
|
|
334
|
-
? html`${client.targetProfileIds.map(id => {
|
|
335
|
-
const profileState = appstate.targetProfilesStatePart.getState();
|
|
336
|
-
const profile = profileState?.profiles.find(p => p.id === id);
|
|
337
|
-
return html`<span class="tagBadge">${profile?.name || id}</span>`;
|
|
338
|
-
})}`
|
|
339
|
-
: '-',
|
|
342
|
+
'Target Profiles': this.renderTargetProfileBadges(client.targetProfileIds),
|
|
340
343
|
'Description': client.description || '-',
|
|
341
344
|
'Created': new Date(client.createdAt).toLocaleDateString(),
|
|
342
345
|
};
|
|
@@ -347,6 +350,7 @@ export class OpsViewVpn extends DeesElement {
|
|
|
347
350
|
iconName: 'lucide:plus',
|
|
348
351
|
type: ['header'],
|
|
349
352
|
actionFunc: async () => {
|
|
353
|
+
await this.ensureTargetProfilesLoaded();
|
|
350
354
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
351
355
|
const profileCandidates = this.getTargetProfileCandidates();
|
|
352
356
|
const createModal = await DeesModal.createAndShow({
|
|
@@ -647,6 +651,7 @@ export class OpsViewVpn extends DeesElement {
|
|
|
647
651
|
type: ['contextmenu', 'inRow'],
|
|
648
652
|
actionFunc: async (actionData: any) => {
|
|
649
653
|
const client = actionData.item as interfaces.data.IVpnClient;
|
|
654
|
+
await this.ensureTargetProfilesLoaded();
|
|
650
655
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
651
656
|
const currentDescription = client.description ?? '';
|
|
652
657
|
const currentTargetProfileNames = this.resolveProfileIdsToLabels(client.targetProfileIds) || [];
|
|
@@ -810,12 +815,28 @@ export class OpsViewVpn extends DeesElement {
|
|
|
810
815
|
`;
|
|
811
816
|
}
|
|
812
817
|
|
|
818
|
+
private async ensureTargetProfilesLoaded(): Promise<void> {
|
|
819
|
+
await appstate.targetProfilesStatePart.dispatchAction(appstate.fetchTargetProfilesAction, null);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
private renderTargetProfileBadges(ids?: string[]): TemplateResult | string {
|
|
823
|
+
const labels = this.resolveProfileIdsToLabels(ids, {
|
|
824
|
+
pendingLabel: 'Loading profile...',
|
|
825
|
+
missingLabel: (id) => `Unknown profile (${id})`,
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
if (!labels?.length) {
|
|
829
|
+
return '-';
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
return html`${labels.map((label) => html`<span class="tagBadge">${label}</span>`)}`;
|
|
833
|
+
}
|
|
834
|
+
|
|
813
835
|
/**
|
|
814
836
|
* Build stable profile labels for list inputs.
|
|
815
837
|
*/
|
|
816
838
|
private getTargetProfileChoices() {
|
|
817
|
-
const
|
|
818
|
-
const profiles = profileState?.profiles || [];
|
|
839
|
+
const profiles = this.targetProfilesState.profiles || [];
|
|
819
840
|
const nameCounts = new Map<string, number>();
|
|
820
841
|
|
|
821
842
|
for (const profile of profiles) {
|
|
@@ -837,12 +858,27 @@ export class OpsViewVpn extends DeesElement {
|
|
|
837
858
|
/**
|
|
838
859
|
* Convert profile IDs to form labels (for populating edit form values).
|
|
839
860
|
*/
|
|
840
|
-
private resolveProfileIdsToLabels(
|
|
861
|
+
private resolveProfileIdsToLabels(
|
|
862
|
+
ids?: string[],
|
|
863
|
+
options: {
|
|
864
|
+
pendingLabel?: string;
|
|
865
|
+
missingLabel?: (id: string) => string;
|
|
866
|
+
} = {},
|
|
867
|
+
): string[] | undefined {
|
|
841
868
|
if (!ids?.length) return undefined;
|
|
842
869
|
const choices = this.getTargetProfileChoices();
|
|
843
870
|
const labelsById = new Map(choices.map((profile) => [profile.id, profile.label]));
|
|
844
871
|
return ids.map((id) => {
|
|
845
|
-
|
|
872
|
+
const label = labelsById.get(id);
|
|
873
|
+
if (label) {
|
|
874
|
+
return label;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
if (this.targetProfilesState.lastUpdated === 0 && !this.targetProfilesState.error) {
|
|
878
|
+
return options.pendingLabel || 'Loading profile...';
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
return options.missingLabel?.(id) || id;
|
|
846
882
|
});
|
|
847
883
|
}
|
|
848
884
|
|
package/ts_web/readme.md
CHANGED
|
@@ -1,76 +1,56 @@
|
|
|
1
1
|
# @serve.zone/dcrouter-web
|
|
2
2
|
|
|
3
|
-
Browser
|
|
4
|
-
|
|
5
|
-
This package contains the browser entrypoint, app state, router, and web components that power the Ops dashboard served by dcrouter.
|
|
3
|
+
Browser-side frontend for the dcrouter Ops dashboard. This folder is the SPA entrypoint, router, app state, and web-component UI rendered by OpsServer.
|
|
6
4
|
|
|
7
5
|
## Issue Reporting and Security
|
|
8
6
|
|
|
9
7
|
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
|
10
8
|
|
|
11
|
-
## What
|
|
12
|
-
|
|
13
|
-
| Path | Purpose |
|
|
14
|
-
| --- | --- |
|
|
15
|
-
| `index.ts` | Browser entrypoint that initializes routing and renders `<ops-dashboard>` |
|
|
16
|
-
| `appstate.ts` | Central reactive state and action definitions |
|
|
17
|
-
| `router.ts` | URL-based dashboard routing |
|
|
18
|
-
| `elements/` | Dashboard views and reusable UI pieces |
|
|
19
|
-
|
|
20
|
-
## Main Views
|
|
21
|
-
|
|
22
|
-
The dashboard currently includes views for:
|
|
9
|
+
## What It Boots
|
|
23
10
|
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
- DNS providers, domains, DNS records, and certificates
|
|
29
|
-
- API tokens and users
|
|
30
|
-
- VPN, remote ingress, logs, and security views
|
|
11
|
+
- `index.ts` initializes the app router and renders `<ops-dashboard>` into `document.body`
|
|
12
|
+
- `router.ts` defines top-level dashboard routes and subviews
|
|
13
|
+
- `appstate.ts` holds reactive state, TypedRequest actions, and TypedSocket log streaming
|
|
14
|
+
- `elements/` contains the dashboard shell and feature views
|
|
31
15
|
|
|
32
|
-
##
|
|
16
|
+
## View Map
|
|
33
17
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
18
|
+
| Top-level view | Subviews |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| `overview` | `stats`, `configuration` |
|
|
21
|
+
| `network` | `activity`, `routes`, `sourceprofiles`, `networktargets`, `targetprofiles`, `remoteingress`, `vpn` |
|
|
22
|
+
| `email` | `log`, `security`, `domains` |
|
|
23
|
+
| `access` | `apitokens`, `users` |
|
|
24
|
+
| `security` | `overview`, `blocked`, `authentication` |
|
|
25
|
+
| `domains` | `providers`, `domains`, `dns`, `certificates` |
|
|
26
|
+
| `logs` | flat view |
|
|
40
27
|
|
|
41
28
|
## How It Talks To dcrouter
|
|
42
29
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
- stats and health
|
|
48
|
-
- logs
|
|
49
|
-
- routes and tokens
|
|
50
|
-
- certificates and ACME config
|
|
51
|
-
- DNS providers, domains, and records
|
|
52
|
-
- email domains and email operations
|
|
53
|
-
- VPN, remote ingress, and RADIUS data
|
|
30
|
+
- TypedRequest for the main API surface
|
|
31
|
+
- shared request and data contracts from `@serve.zone/dcrouter-interfaces`
|
|
32
|
+
- TypedSocket for real-time log streaming
|
|
33
|
+
- QR code generation for VPN client UX
|
|
54
34
|
|
|
55
35
|
## Development Notes
|
|
56
36
|
|
|
57
|
-
|
|
37
|
+
This package is the frontend module boundary, but it is built and served as part of the main workspace.
|
|
58
38
|
|
|
59
39
|
```bash
|
|
60
|
-
pnpm run
|
|
40
|
+
pnpm run build
|
|
61
41
|
pnpm run watch
|
|
62
42
|
```
|
|
63
43
|
|
|
64
|
-
The
|
|
44
|
+
The built dashboard assets are emitted into `dist_serve/` by the workspace build pipeline.
|
|
65
45
|
|
|
66
|
-
##
|
|
46
|
+
## What This Package Is For
|
|
67
47
|
|
|
68
|
-
- Use it
|
|
69
|
-
- Use
|
|
48
|
+
- Use it when you want the dashboard frontend as its own published module boundary.
|
|
49
|
+
- Use `@serve.zone/dcrouter` when you want the server that actually hosts this UI and the backend API.
|
|
70
50
|
|
|
71
51
|
## License and Legal Information
|
|
72
52
|
|
|
73
|
-
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [
|
|
53
|
+
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../license) file.
|
|
74
54
|
|
|
75
55
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
|
76
56
|
|