@serve.zone/dcrouter 13.18.0 → 13.19.1
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 +532 -531
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +2 -0
- package/dist_ts/classes.dcrouter.js +50 -39
- package/dist_ts/config/classes.route-config-manager.d.ts +14 -5
- package/dist_ts/config/classes.route-config-manager.js +121 -44
- package/dist_ts/db/documents/classes.route.doc.d.ts +2 -0
- package/dist_ts/db/documents/classes.route.doc.js +11 -2
- package/dist_ts/email/classes.email-domain.manager.js +9 -28
- package/dist_ts/email/email-dns-records.d.ts +14 -0
- package/dist_ts/email/email-dns-records.js +34 -0
- package/dist_ts/email/index.d.ts +1 -0
- package/dist_ts/email/index.js +2 -1
- package/dist_ts/opsserver/handlers/route-management.handler.js +5 -7
- package/dist_ts_interfaces/data/route-management.d.ts +2 -0
- package/dist_ts_migrations/index.js +25 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.js +13 -4
- package/dist_ts_web/elements/network/ops-view-routes.d.ts +2 -0
- package/dist_ts_web/elements/network/ops-view-routes.js +124 -36
- package/package.json +2 -3
- package/readme.md +190 -1543
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +61 -47
- package/ts/config/classes.route-config-manager.ts +148 -50
- package/ts/db/documents/classes.route.doc.ts +7 -0
- package/ts/email/classes.email-domain.manager.ts +8 -28
- package/ts/email/email-dns-records.ts +53 -0
- package/ts/email/index.ts +1 -0
- package/ts/opsserver/handlers/route-management.handler.ts +4 -6
- package/ts_apiclient/readme.md +69 -195
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +16 -4
- package/ts_web/elements/network/ops-view-routes.ts +136 -44
- package/ts_web/readme.md +41 -242
|
@@ -15,16 +15,70 @@ import {
|
|
|
15
15
|
|
|
16
16
|
// TLS dropdown options shared by create and edit dialogs
|
|
17
17
|
const tlsModeOptions = [
|
|
18
|
-
{ key: 'none', option: '(none —
|
|
19
|
-
{ key: 'passthrough', option: 'Passthrough' },
|
|
20
|
-
{ key: 'terminate', option: 'Terminate' },
|
|
21
|
-
{ key: 'terminate-and-reencrypt', option: 'Terminate & Re-encrypt' },
|
|
18
|
+
{ key: 'none', option: '(none — plain TCP/HTTP, use for SSH)' },
|
|
19
|
+
{ key: 'passthrough', option: 'Passthrough (TLS only)' },
|
|
20
|
+
{ key: 'terminate', option: 'Terminate TLS' },
|
|
21
|
+
{ key: 'terminate-and-reencrypt', option: 'Terminate & Re-encrypt TLS' },
|
|
22
22
|
];
|
|
23
23
|
const tlsCertOptions = [
|
|
24
24
|
{ key: 'auto', option: 'Auto (ACME/Let\'s Encrypt)' },
|
|
25
25
|
{ key: 'custom', option: 'Custom certificate' },
|
|
26
26
|
];
|
|
27
27
|
|
|
28
|
+
function getDropdownKey(value: any): string {
|
|
29
|
+
return typeof value === 'string' ? value : value?.key || '';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseTargetPort(value: any): number | undefined {
|
|
33
|
+
const parsed = typeof value === 'number'
|
|
34
|
+
? value
|
|
35
|
+
: typeof value === 'string'
|
|
36
|
+
? parseInt(value.trim(), 10)
|
|
37
|
+
: Number.NaN;
|
|
38
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
return parsed;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getRouteTargetInputs(formEl: any) {
|
|
45
|
+
const textInputs = Array.from(formEl.querySelectorAll('dees-input-text')) as any[];
|
|
46
|
+
return {
|
|
47
|
+
hostInput: textInputs.find((input) => input.key === 'targetHost'),
|
|
48
|
+
portInput: textInputs.find((input) => input.key === 'targetPort'),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function setupTargetInputState(formEl: any) {
|
|
53
|
+
const updateState = async () => {
|
|
54
|
+
const data = await formEl.collectFormData();
|
|
55
|
+
const usesNetworkTarget = !!getDropdownKey(data.networkTargetRef);
|
|
56
|
+
const { hostInput, portInput } = getRouteTargetInputs(formEl);
|
|
57
|
+
const hostDescription = usesNetworkTarget
|
|
58
|
+
? 'Controlled by the selected network target'
|
|
59
|
+
: 'Used when no network target is selected';
|
|
60
|
+
const portDescription = usesNetworkTarget
|
|
61
|
+
? 'Controlled by the selected network target'
|
|
62
|
+
: 'Used when no network target is selected';
|
|
63
|
+
|
|
64
|
+
if (hostInput) {
|
|
65
|
+
hostInput.disabled = usesNetworkTarget;
|
|
66
|
+
hostInput.required = !usesNetworkTarget;
|
|
67
|
+
hostInput.description = hostDescription;
|
|
68
|
+
}
|
|
69
|
+
if (portInput) {
|
|
70
|
+
portInput.disabled = usesNetworkTarget;
|
|
71
|
+
portInput.required = !usesNetworkTarget;
|
|
72
|
+
portInput.description = portDescription;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await formEl.updateRequiredStatus?.();
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
formEl.changeSubject.subscribe(() => updateState());
|
|
79
|
+
updateState();
|
|
80
|
+
}
|
|
81
|
+
|
|
28
82
|
/**
|
|
29
83
|
* Toggle TLS form field visibility based on selected TLS mode and certificate type.
|
|
30
84
|
*/
|
|
@@ -272,15 +326,13 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
272
326
|
const clickedRoute = e.detail;
|
|
273
327
|
if (!clickedRoute) return;
|
|
274
328
|
|
|
275
|
-
|
|
276
|
-
const merged = this.routeState.mergedRoutes.find(
|
|
277
|
-
(mr) => mr.route.name === clickedRoute.name,
|
|
278
|
-
);
|
|
329
|
+
const merged = this.findMergedRoute(clickedRoute);
|
|
279
330
|
if (!merged) return;
|
|
280
331
|
|
|
281
332
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
282
333
|
|
|
283
334
|
const meta = merged.metadata;
|
|
335
|
+
const isSystemManaged = this.isSystemManagedRoute(merged);
|
|
284
336
|
await DeesModal.createAndShow({
|
|
285
337
|
heading: `Route: ${merged.route.name}`,
|
|
286
338
|
content: html`
|
|
@@ -288,6 +340,7 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
288
340
|
<p>Origin: <strong style="color: #0af;">${merged.origin}</strong></p>
|
|
289
341
|
<p>Status: <strong>${merged.enabled ? 'Enabled' : 'Disabled'}</strong></p>
|
|
290
342
|
<p>ID: <code style="color: #888;">${merged.id}</code></p>
|
|
343
|
+
${isSystemManaged ? html`<p>This route is system-managed. Change its source config to modify it directly.</p>` : ''}
|
|
291
344
|
${meta?.sourceProfileName ? html`<p>Source Profile: <strong style="color: #a78bfa;">${meta.sourceProfileName}</strong></p>` : ''}
|
|
292
345
|
${meta?.networkTargetName ? html`<p>Network Target: <strong style="color: #a78bfa;">${meta.networkTargetName}</strong></p>` : ''}
|
|
293
346
|
</div>
|
|
@@ -304,25 +357,29 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
304
357
|
await modalArg.destroy();
|
|
305
358
|
},
|
|
306
359
|
},
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
360
|
+
...(!isSystemManaged
|
|
361
|
+
? [
|
|
362
|
+
{
|
|
363
|
+
name: 'Edit',
|
|
364
|
+
iconName: 'lucide:pencil',
|
|
365
|
+
action: async (modalArg: any) => {
|
|
366
|
+
await modalArg.destroy();
|
|
367
|
+
this.showEditRouteDialog(merged);
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
name: 'Delete',
|
|
372
|
+
iconName: 'lucide:trash-2',
|
|
373
|
+
action: async (modalArg: any) => {
|
|
374
|
+
await appstate.routeManagementStatePart.dispatchAction(
|
|
375
|
+
appstate.deleteRouteAction,
|
|
376
|
+
merged.id,
|
|
377
|
+
);
|
|
378
|
+
await modalArg.destroy();
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
]
|
|
382
|
+
: []),
|
|
326
383
|
{
|
|
327
384
|
name: 'Close',
|
|
328
385
|
iconName: 'lucide:x',
|
|
@@ -336,10 +393,9 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
336
393
|
const clickedRoute = e.detail;
|
|
337
394
|
if (!clickedRoute) return;
|
|
338
395
|
|
|
339
|
-
const merged = this.
|
|
340
|
-
(mr) => mr.route.name === clickedRoute.name,
|
|
341
|
-
);
|
|
396
|
+
const merged = this.findMergedRoute(clickedRoute);
|
|
342
397
|
if (!merged) return;
|
|
398
|
+
if (this.isSystemManagedRoute(merged)) return;
|
|
343
399
|
|
|
344
400
|
this.showEditRouteDialog(merged);
|
|
345
401
|
}
|
|
@@ -348,10 +404,9 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
348
404
|
const clickedRoute = e.detail;
|
|
349
405
|
if (!clickedRoute) return;
|
|
350
406
|
|
|
351
|
-
const merged = this.
|
|
352
|
-
(mr) => mr.route.name === clickedRoute.name,
|
|
353
|
-
);
|
|
407
|
+
const merged = this.findMergedRoute(clickedRoute);
|
|
354
408
|
if (!merged) return;
|
|
409
|
+
if (this.isSystemManagedRoute(merged)) return;
|
|
355
410
|
|
|
356
411
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
357
412
|
await DeesModal.createAndShow({
|
|
@@ -469,6 +524,16 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
469
524
|
: [];
|
|
470
525
|
const priority = formData.priority ? parseInt(formData.priority, 10) : undefined;
|
|
471
526
|
|
|
527
|
+
const profileKey = getDropdownKey(formData.sourceProfileRef);
|
|
528
|
+
const targetKey = getDropdownKey(formData.networkTargetRef);
|
|
529
|
+
const targetPort = parseTargetPort(formData.targetPort)
|
|
530
|
+
?? (targetKey ? parseTargetPort(currentTargetPort) ?? ports[0] : undefined);
|
|
531
|
+
|
|
532
|
+
if (targetPort === undefined) {
|
|
533
|
+
alert('Target Port must be a valid port number when no network target is selected.');
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
472
537
|
const updatedRoute: any = {
|
|
473
538
|
name: formData.name,
|
|
474
539
|
match: {
|
|
@@ -479,8 +544,8 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
479
544
|
type: 'forward',
|
|
480
545
|
targets: [
|
|
481
546
|
{
|
|
482
|
-
host: formData.targetHost || 'localhost',
|
|
483
|
-
port:
|
|
547
|
+
host: formData.targetHost || currentTargetHost || 'localhost',
|
|
548
|
+
port: targetPort,
|
|
484
549
|
},
|
|
485
550
|
],
|
|
486
551
|
},
|
|
@@ -507,15 +572,17 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
507
572
|
}
|
|
508
573
|
|
|
509
574
|
const metadata: any = {};
|
|
510
|
-
const profileRefValue = formData.sourceProfileRef as any;
|
|
511
|
-
const profileKey = typeof profileRefValue === 'string' ? profileRefValue : profileRefValue?.key;
|
|
512
575
|
if (profileKey) {
|
|
513
576
|
metadata.sourceProfileRef = profileKey;
|
|
577
|
+
} else if (merged.metadata?.sourceProfileRef) {
|
|
578
|
+
metadata.sourceProfileRef = '';
|
|
579
|
+
metadata.sourceProfileName = '';
|
|
514
580
|
}
|
|
515
|
-
const targetRefValue = formData.networkTargetRef as any;
|
|
516
|
-
const targetKey = typeof targetRefValue === 'string' ? targetRefValue : targetRefValue?.key;
|
|
517
581
|
if (targetKey) {
|
|
518
582
|
metadata.networkTargetRef = targetKey;
|
|
583
|
+
} else if (merged.metadata?.networkTargetRef) {
|
|
584
|
+
metadata.networkTargetRef = '';
|
|
585
|
+
metadata.networkTargetName = '';
|
|
519
586
|
}
|
|
520
587
|
|
|
521
588
|
await appstate.routeManagementStatePart.dispatchAction(
|
|
@@ -536,6 +603,7 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
536
603
|
if (editForm) {
|
|
537
604
|
await editForm.updateComplete;
|
|
538
605
|
setupTlsVisibility(editForm);
|
|
606
|
+
setupTargetInputState(editForm);
|
|
539
607
|
}
|
|
540
608
|
}
|
|
541
609
|
|
|
@@ -603,6 +671,16 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
603
671
|
: [];
|
|
604
672
|
const priority = formData.priority ? parseInt(formData.priority, 10) : undefined;
|
|
605
673
|
|
|
674
|
+
const profileKey = getDropdownKey(formData.sourceProfileRef);
|
|
675
|
+
const targetKey = getDropdownKey(formData.networkTargetRef);
|
|
676
|
+
const targetPort = parseTargetPort(formData.targetPort)
|
|
677
|
+
?? (targetKey ? ports[0] : undefined);
|
|
678
|
+
|
|
679
|
+
if (targetPort === undefined) {
|
|
680
|
+
alert('Target Port must be a valid port number when no network target is selected.');
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
|
|
606
684
|
const route: any = {
|
|
607
685
|
name: formData.name,
|
|
608
686
|
match: {
|
|
@@ -614,7 +692,7 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
614
692
|
targets: [
|
|
615
693
|
{
|
|
616
694
|
host: formData.targetHost || 'localhost',
|
|
617
|
-
port:
|
|
695
|
+
port: targetPort,
|
|
618
696
|
},
|
|
619
697
|
],
|
|
620
698
|
},
|
|
@@ -640,13 +718,9 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
640
718
|
|
|
641
719
|
// Build metadata if profile/target selected
|
|
642
720
|
const metadata: any = {};
|
|
643
|
-
const profileRefValue = formData.sourceProfileRef as any;
|
|
644
|
-
const profileKey = typeof profileRefValue === 'string' ? profileRefValue : profileRefValue?.key;
|
|
645
721
|
if (profileKey) {
|
|
646
722
|
metadata.sourceProfileRef = profileKey;
|
|
647
723
|
}
|
|
648
|
-
const targetRefValue = formData.networkTargetRef as any;
|
|
649
|
-
const targetKey = typeof targetRefValue === 'string' ? targetRefValue : targetRefValue?.key;
|
|
650
724
|
if (targetKey) {
|
|
651
725
|
metadata.networkTargetRef = targetKey;
|
|
652
726
|
}
|
|
@@ -668,6 +742,7 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
668
742
|
if (createForm) {
|
|
669
743
|
await createForm.updateComplete;
|
|
670
744
|
setupTlsVisibility(createForm);
|
|
745
|
+
setupTargetInputState(createForm);
|
|
671
746
|
}
|
|
672
747
|
}
|
|
673
748
|
|
|
@@ -675,6 +750,23 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
675
750
|
appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
|
|
676
751
|
}
|
|
677
752
|
|
|
753
|
+
private findMergedRoute(clickedRoute: { id?: string; name?: string }): interfaces.data.IMergedRoute | undefined {
|
|
754
|
+
if (clickedRoute.id) {
|
|
755
|
+
const routeById = this.routeState.mergedRoutes.find((mr) => mr.id === clickedRoute.id);
|
|
756
|
+
if (routeById) return routeById;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (clickedRoute.name) {
|
|
760
|
+
return this.routeState.mergedRoutes.find((mr) => mr.route.name === clickedRoute.name);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
return undefined;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
private isSystemManagedRoute(merged: interfaces.data.IMergedRoute): boolean {
|
|
767
|
+
return merged.origin !== 'api';
|
|
768
|
+
}
|
|
769
|
+
|
|
678
770
|
async firstUpdated() {
|
|
679
771
|
await appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
|
|
680
772
|
|
package/ts_web/readme.md
CHANGED
|
@@ -1,273 +1,72 @@
|
|
|
1
1
|
# @serve.zone/dcrouter-web
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Browser UI package for dcrouter's operations dashboard. 🖥️
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
This package contains the browser entrypoint, app state, router, and web components that power the Ops dashboard served by dcrouter.
|
|
6
6
|
|
|
7
7
|
## Issue Reporting and Security
|
|
8
8
|
|
|
9
9
|
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
10
|
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
### 🔐 Secure Authentication
|
|
14
|
-
- JWT-based login with persistent sessions (IndexedDB)
|
|
15
|
-
- Automatic session expiry detection and cleanup
|
|
16
|
-
- Secure username/password authentication
|
|
17
|
-
|
|
18
|
-
### 📊 Overview Dashboard
|
|
19
|
-
- Real-time server statistics (CPU, memory, uptime)
|
|
20
|
-
- Active connection counts and email throughput
|
|
21
|
-
- DNS query metrics and RADIUS session tracking
|
|
22
|
-
- Auto-refreshing with configurable intervals
|
|
23
|
-
|
|
24
|
-
### 🌐 Network View
|
|
25
|
-
- Active connection monitoring with real-time data from SmartProxy
|
|
26
|
-
- Top connected IPs with connection counts and percentages
|
|
27
|
-
- Throughput rates (inbound/outbound in kbit/s, Mbit/s, Gbit/s)
|
|
28
|
-
- Traffic chart with selectable time ranges
|
|
29
|
-
|
|
30
|
-
### 📧 Email Management
|
|
31
|
-
- **Queued** — Emails pending delivery with queue position
|
|
32
|
-
- **Sent** — Successfully delivered emails with timestamps
|
|
33
|
-
- **Failed** — Failed emails with resend capability
|
|
34
|
-
- **Security** — Security incidents from email processing
|
|
35
|
-
- Bounce record management and suppression list controls
|
|
36
|
-
|
|
37
|
-
### 🔐 Certificate Management
|
|
38
|
-
- Domain-centric certificate overview with status indicators
|
|
39
|
-
- Certificate source tracking (ACME, provision function, static)
|
|
40
|
-
- Expiry date monitoring and alerts
|
|
41
|
-
- Per-domain backoff status for failed provisions
|
|
42
|
-
- One-click reprovisioning per domain
|
|
43
|
-
- Certificate import, export, and deletion
|
|
44
|
-
|
|
45
|
-
### 🌍 Remote Ingress Management
|
|
46
|
-
- Edge node registration with name, ports, and tags
|
|
47
|
-
- Real-time connection status (connected/disconnected/disabled)
|
|
48
|
-
- Public IP and active tunnel count per edge
|
|
49
|
-
- Auto-derived port display with manual/derived breakdown
|
|
50
|
-
- **Connection token generation** — one-click "Copy Token" for easy edge provisioning
|
|
51
|
-
- Enable/disable, edit, secret regeneration, and delete actions
|
|
52
|
-
|
|
53
|
-
### 🔐 VPN Management
|
|
54
|
-
- VPN server status with forwarding mode, subnet, and WireGuard port
|
|
55
|
-
- Client registration table with create, enable/disable, and delete actions
|
|
56
|
-
- WireGuard config download, clipboard copy, and **QR code display** on client creation
|
|
57
|
-
- QR code export for existing clients — scan with WireGuard mobile app (iOS/Android)
|
|
58
|
-
- Per-client telemetry (bytes sent/received, keepalives)
|
|
59
|
-
- Server public key display for manual client configuration
|
|
60
|
-
|
|
61
|
-
### 📜 Log Viewer
|
|
62
|
-
- Real-time log streaming
|
|
63
|
-
- Filter by log level (error, warning, info, debug)
|
|
64
|
-
- Search and time-range selection
|
|
65
|
-
|
|
66
|
-
### 🛣️ Route & API Token Management
|
|
67
|
-
- Programmatic route CRUD with enable/disable and override controls
|
|
68
|
-
- API token creation, revocation, and scope management
|
|
69
|
-
- Routes tab and API Tokens tab in unified view
|
|
70
|
-
|
|
71
|
-
### 🛡️ Security Profiles & Network Targets
|
|
72
|
-
- Create, edit, and delete reusable security profiles (IP allow/block lists, rate limits, max connections)
|
|
73
|
-
- Create, edit, and delete reusable network targets (host:port destinations)
|
|
74
|
-
- In-row and context menu actions for quick editing
|
|
75
|
-
- Changes propagate automatically to all referencing routes
|
|
76
|
-
|
|
77
|
-
### ⚙️ Configuration
|
|
78
|
-
- Read-only display of current system configuration
|
|
79
|
-
- Status badges for boolean values (enabled/disabled)
|
|
80
|
-
- Array values displayed as pills with counts
|
|
81
|
-
- Section icons and formatted byte/time values
|
|
82
|
-
|
|
83
|
-
### 🛡️ Security Dashboard
|
|
84
|
-
- IP reputation monitoring
|
|
85
|
-
- Rate limit status across domains
|
|
86
|
-
- Blocked connection tracking
|
|
87
|
-
- Security event timeline
|
|
88
|
-
|
|
89
|
-
## Architecture
|
|
90
|
-
|
|
91
|
-
### Technology Stack
|
|
92
|
-
|
|
93
|
-
| Layer | Package | Purpose |
|
|
94
|
-
|-------|---------|---------|
|
|
95
|
-
| **Components** | `@design.estate/dees-element` | Web component framework (lit-element based) |
|
|
96
|
-
| **UI Kit** | `@design.estate/dees-catalog` | Pre-built components (tables, charts, forms, app shell) |
|
|
97
|
-
| **State** | `@push.rocks/smartstate` | Reactive state management with persistent/soft modes |
|
|
98
|
-
| **Routing** | Client-side router | URL-synchronized view navigation |
|
|
99
|
-
| **API** | `@api.global/typedrequest` | Type-safe communication with OpsServer |
|
|
100
|
-
| **Types** | `@serve.zone/dcrouter-interfaces` | Shared TypedRequest interface definitions |
|
|
101
|
-
|
|
102
|
-
### Component Structure
|
|
11
|
+
## What Is In Here
|
|
103
12
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
└── elements/
|
|
111
|
-
├── ops-dashboard.ts # Main app shell
|
|
112
|
-
├── ops-view-overview.ts # Overview statistics
|
|
113
|
-
├── ops-view-network.ts # Network monitoring
|
|
114
|
-
├── ops-view-emails.ts # Email queue management
|
|
115
|
-
├── ops-view-certificates.ts # Certificate overview & reprovisioning
|
|
116
|
-
├── ops-view-remoteingress.ts # Remote ingress edge management
|
|
117
|
-
├── ops-view-vpn.ts # VPN client management
|
|
118
|
-
├── ops-view-logs.ts # Log viewer
|
|
119
|
-
├── ops-view-routes.ts # Route & API token management
|
|
120
|
-
├── ops-view-config.ts # Configuration display
|
|
121
|
-
├── ops-view-security.ts # Security dashboard
|
|
122
|
-
└── shared/
|
|
123
|
-
├── css.ts # Shared styles
|
|
124
|
-
└── ops-sectionheading.ts # Section heading component
|
|
125
|
-
```
|
|
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 |
|
|
126
19
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
The app uses `@push.rocks/smartstate` v2.3+ with multiple state parts, scheduled actions with `autoPause: 'visibility'`, and batched updates:
|
|
130
|
-
|
|
131
|
-
| State Part | Mode | Description |
|
|
132
|
-
|-----------|------|-------------|
|
|
133
|
-
| `loginStatePart` | Persistent (IndexedDB) | JWT identity and login status |
|
|
134
|
-
| `statsStatePart` | Soft (memory) | Server, email, DNS, security, RADIUS, VPN metrics |
|
|
135
|
-
| `configStatePart` | Soft | Current system configuration |
|
|
136
|
-
| `uiStatePart` | Soft | Active view, sidebar, auto-refresh, theme |
|
|
137
|
-
| `logStatePart` | Soft | Recent logs, streaming status, filters |
|
|
138
|
-
| `networkStatePart` | Soft | Connections, IPs, throughput rates |
|
|
139
|
-
| `emailOpsStatePart` | Soft | Email queues, bounces, suppression list |
|
|
140
|
-
| `certificateStatePart` | Soft | Certificate list, summary, loading state |
|
|
141
|
-
| `remoteIngressStatePart` | Soft | Edge list, statuses, new edge secret |
|
|
142
|
-
| `vpnStatePart` | Soft | VPN clients, server status, new client config |
|
|
143
|
-
|
|
144
|
-
### Tab Visibility Optimization
|
|
145
|
-
|
|
146
|
-
The dashboard automatically pauses all background activity when the browser tab is hidden and resumes when visible:
|
|
147
|
-
|
|
148
|
-
- **Auto-refresh polling** uses `createScheduledAction` with `autoPause: 'visibility'` — stops HTTP requests while the tab is sleeping
|
|
149
|
-
- **In-flight guard** prevents concurrent refresh requests from piling up
|
|
150
|
-
- **WebSocket connection** disconnects when hidden and reconnects when visible, preventing log entry accumulation
|
|
151
|
-
- **Network traffic timer** pauses chart updates when the tab is backgrounded
|
|
152
|
-
- **Log entry batching** — incoming WebSocket log pushes are buffered and flushed once per animation frame to avoid per-entry re-renders
|
|
153
|
-
|
|
154
|
-
### Actions
|
|
155
|
-
|
|
156
|
-
```typescript
|
|
157
|
-
// Authentication
|
|
158
|
-
loginAction(username, password) // JWT login
|
|
159
|
-
logoutAction() // Clear session
|
|
160
|
-
|
|
161
|
-
// Data fetching (auto-refresh compatible)
|
|
162
|
-
fetchAllStatsAction() // Server + email + DNS + security stats
|
|
163
|
-
fetchConfigurationAction() // System configuration
|
|
164
|
-
fetchRecentLogsAction() // Log entries
|
|
165
|
-
fetchNetworkStatsAction() // Connection + throughput data
|
|
166
|
-
|
|
167
|
-
// Email operations
|
|
168
|
-
fetchQueuedEmailsAction() // Pending emails
|
|
169
|
-
fetchSentEmailsAction() // Delivered emails
|
|
170
|
-
fetchFailedEmailsAction() // Failed emails
|
|
171
|
-
fetchSecurityIncidentsAction() // Security events
|
|
172
|
-
fetchBounceRecordsAction() // Bounce records
|
|
173
|
-
resendEmailAction(emailId) // Re-queue failed email
|
|
174
|
-
removeFromSuppressionAction(email) // Remove from suppression list
|
|
175
|
-
|
|
176
|
-
// Certificates
|
|
177
|
-
fetchCertificateOverviewAction() // All certificates with summary
|
|
178
|
-
reprovisionCertificateAction(domain) // Reprovision a certificate
|
|
179
|
-
deleteCertificateAction(domain) // Delete a certificate
|
|
180
|
-
importCertificateAction(cert) // Import a certificate
|
|
181
|
-
fetchCertificateExport(domain) // Export (standalone function)
|
|
182
|
-
|
|
183
|
-
// Remote Ingress
|
|
184
|
-
fetchRemoteIngressAction() // Edges + statuses
|
|
185
|
-
createRemoteIngressAction(data) // Create new edge
|
|
186
|
-
updateRemoteIngressAction(data) // Update edge settings
|
|
187
|
-
deleteRemoteIngressAction(id) // Remove edge
|
|
188
|
-
regenerateRemoteIngressSecretAction(id) // New secret
|
|
189
|
-
toggleRemoteIngressAction(id, enabled) // Enable/disable
|
|
190
|
-
clearNewEdgeSecretAction() // Dismiss secret banner
|
|
191
|
-
fetchConnectionToken(edgeId) // Get connection token (standalone function)
|
|
192
|
-
|
|
193
|
-
// VPN
|
|
194
|
-
fetchVpnAction() // Clients + server status
|
|
195
|
-
createVpnClientAction(data) // Create new VPN client
|
|
196
|
-
deleteVpnClientAction(clientId) // Remove VPN client
|
|
197
|
-
toggleVpnClientAction(id, enabled) // Enable/disable
|
|
198
|
-
clearNewClientConfigAction() // Dismiss config banner
|
|
199
|
-
```
|
|
20
|
+
## Main Views
|
|
200
21
|
|
|
201
|
-
|
|
22
|
+
The dashboard currently includes views for:
|
|
202
23
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
/emails/security → Security incidents
|
|
211
|
-
/certificates → Certificate management
|
|
212
|
-
/remoteingress → Remote ingress edge management
|
|
213
|
-
/vpn → VPN client management
|
|
214
|
-
/routes → Route & API token management
|
|
215
|
-
/logs → Log viewer
|
|
216
|
-
/configuration → System configuration
|
|
217
|
-
/security → Security dashboard
|
|
218
|
-
```
|
|
24
|
+
- overview and configuration
|
|
25
|
+
- network activity and route management
|
|
26
|
+
- source profiles, target profiles, and network targets
|
|
27
|
+
- email activity and email domains
|
|
28
|
+
- DNS providers, domains, DNS records, and certificates
|
|
29
|
+
- API tokens and users
|
|
30
|
+
- VPN, remote ingress, logs, and security views
|
|
219
31
|
|
|
220
|
-
|
|
32
|
+
## Route Management UX
|
|
221
33
|
|
|
222
|
-
|
|
34
|
+
The web UI reflects dcrouter's current route ownership model:
|
|
223
35
|
|
|
224
|
-
|
|
36
|
+
- system routes are shown separately from user routes
|
|
37
|
+
- system routes are visible and toggleable
|
|
38
|
+
- system routes are not directly editable or deletable
|
|
39
|
+
- API routes are fully managed through the route-management forms
|
|
225
40
|
|
|
226
|
-
|
|
41
|
+
## How It Talks To dcrouter
|
|
227
42
|
|
|
228
|
-
|
|
229
|
-
import { DcRouter } from '@serve.zone/dcrouter';
|
|
43
|
+
The frontend uses TypedRequest and shared interfaces from `@serve.zone/dcrouter-interfaces`.
|
|
230
44
|
|
|
231
|
-
|
|
232
|
-
// OpsServer starts automatically on port 3000
|
|
233
|
-
smartProxyConfig: { routes: [/* your routes */] }
|
|
234
|
-
});
|
|
45
|
+
State actions in `appstate.ts` fetch and mutate:
|
|
235
46
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
|
239
54
|
|
|
240
|
-
|
|
55
|
+
## Development Notes
|
|
56
|
+
|
|
57
|
+
The browser bundle is built from this package and served by the main dcrouter package.
|
|
241
58
|
|
|
242
59
|
```bash
|
|
243
|
-
# Build the bundle
|
|
244
60
|
pnpm run bundle
|
|
245
|
-
|
|
246
|
-
# Watch for development (auto-rebuild + restart)
|
|
247
61
|
pnpm run watch
|
|
248
62
|
```
|
|
249
63
|
|
|
250
|
-
The bundle is
|
|
251
|
-
|
|
252
|
-
### Adding a New View
|
|
64
|
+
The generated bundle is written into `dist_serve/` by the main build pipeline.
|
|
253
65
|
|
|
254
|
-
|
|
255
|
-
```typescript
|
|
256
|
-
import { DeesElement, customElement, html, css } from '@design.estate/dees-element';
|
|
257
|
-
|
|
258
|
-
@customElement('ops-view-myview')
|
|
259
|
-
export class OpsViewMyView extends DeesElement {
|
|
260
|
-
public static styles = [css`:host { display: block; padding: 24px; }`];
|
|
261
|
-
|
|
262
|
-
public render() {
|
|
263
|
-
return html`<ops-sectionheading>My View</ops-sectionheading>`;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
```
|
|
66
|
+
## When To Use This Package
|
|
267
67
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
4. Add any state management in `appstate.ts`
|
|
68
|
+
- Use it if you want the dashboard frontend as a package/module boundary.
|
|
69
|
+
- Use the main `@serve.zone/dcrouter` package if you want the server that actually serves this UI.
|
|
271
70
|
|
|
272
71
|
## License and Legal Information
|
|
273
72
|
|