@serve.zone/dcrouter 13.20.0 → 13.21.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/dist_serve/bundle.js +519 -519
- 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 +4 -1
- package/dist_ts/monitoring/classes.metricsmanager.d.ts +6 -2
- package/dist_ts/monitoring/classes.metricsmanager.js +67 -41
- package/dist_ts/opsserver/handlers/security.handler.js +11 -5
- package/dist_ts/opsserver/handlers/stats.handler.js +2 -1
- package/dist_ts/vpn/classes.vpn-manager.d.ts +5 -1
- package/dist_ts/vpn/classes.vpn-manager.js +55 -17
- package/dist_ts_interfaces/data/stats.d.ts +10 -0
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.js +23 -16
- package/dist_ts_web/elements/network/ops-view-network-activity.js +7 -3
- 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 +3 -3
- 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 +6 -0
- package/ts/monitoring/classes.metricsmanager.ts +71 -40
- package/ts/opsserver/handlers/security.handler.ts +11 -5
- package/ts/opsserver/handlers/stats.handler.ts +1 -0
- 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/appstate.ts +23 -18
- package/ts_web/elements/network/ops-view-network-activity.ts +6 -2
- package/ts_web/elements/network/ops-view-vpn.ts +50 -14
- package/ts_web/readme.md +27 -47
package/ts_apiclient/readme.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# @serve.zone/dcrouter-apiclient
|
|
2
2
|
|
|
3
|
-
Typed, object-oriented
|
|
4
|
-
|
|
5
|
-
Use this package when you want a clean TypeScript client instead of manually firing TypedRequest calls. It wraps the OpsServer API in resource managers and resource classes such as routes, certificates, tokens, edges, emails, stats, logs, config, and RADIUS.
|
|
3
|
+
Typed, object-oriented client for operating a running dcrouter instance. It wraps the OpsServer `/typedrequest` API in managers and resource classes so your scripts can work with routes, certificates, tokens, remote ingress edges, emails, stats, config, logs, and RADIUS without hand-rolling requests.
|
|
6
4
|
|
|
7
5
|
## Issue Reporting and Security
|
|
8
6
|
|
|
@@ -14,7 +12,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|
|
14
12
|
pnpm add @serve.zone/dcrouter-apiclient
|
|
15
13
|
```
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
You can also import the same client through the main package subpath:
|
|
18
16
|
|
|
19
17
|
```typescript
|
|
20
18
|
import { DcRouterApiClient } from '@serve.zone/dcrouter/apiclient';
|
|
@@ -29,24 +27,40 @@ const client = new DcRouterApiClient({
|
|
|
29
27
|
baseUrl: 'https://dcrouter.example.com',
|
|
30
28
|
});
|
|
31
29
|
|
|
32
|
-
await client.login('admin', '
|
|
30
|
+
await client.login('admin', 'admin');
|
|
33
31
|
|
|
34
|
-
const { routes } = await client.routes.list();
|
|
35
|
-
console.log(routes.
|
|
32
|
+
const { routes, warnings } = await client.routes.list();
|
|
33
|
+
console.log('route count', routes.length, 'warnings', warnings.length);
|
|
36
34
|
|
|
37
|
-
await client.routes.build()
|
|
35
|
+
const route = await client.routes.build()
|
|
38
36
|
.setName('api-gateway')
|
|
39
37
|
.setMatch({ ports: 443, domains: ['api.example.com'] })
|
|
40
38
|
.setAction({ type: 'forward', targets: [{ host: '127.0.0.1', port: 8080 }] })
|
|
41
39
|
.save();
|
|
40
|
+
|
|
41
|
+
await route.toggle(false);
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
## What the Client Gives You
|
|
45
|
+
|
|
46
|
+
| Manager | Purpose |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `client.routes` | List merged routes, create API routes, toggle routes |
|
|
49
|
+
| `client.certificates` | Inspect certificates and run certificate operations |
|
|
50
|
+
| `client.apiTokens` | Create, list, toggle, roll, and revoke API tokens |
|
|
51
|
+
| `client.remoteIngress` | Manage edge registrations, statuses, and connection tokens |
|
|
52
|
+
| `client.emails` | Inspect email items and trigger resend flows |
|
|
53
|
+
| `client.stats` | Health, statistics, and operational summaries |
|
|
54
|
+
| `client.config` | Read the current configuration view |
|
|
55
|
+
| `client.logs` | Read recent logs and log-related data |
|
|
56
|
+
| `client.radius` | Manage RADIUS clients, VLANs, and sessions |
|
|
57
|
+
|
|
44
58
|
## Authentication Modes
|
|
45
59
|
|
|
46
60
|
| Mode | How it works |
|
|
47
61
|
| --- | --- |
|
|
48
|
-
| Admin login | Call `login(username, password)` and the
|
|
49
|
-
| API token | Pass `apiToken`
|
|
62
|
+
| Admin login | Call `login(username, password)` and the returned identity is stored on the client |
|
|
63
|
+
| API token | Pass `apiToken` in the constructor and it is injected into requests automatically |
|
|
50
64
|
|
|
51
65
|
```typescript
|
|
52
66
|
const client = new DcRouterApiClient({
|
|
@@ -55,52 +69,19 @@ const client = new DcRouterApiClient({
|
|
|
55
69
|
});
|
|
56
70
|
```
|
|
57
71
|
|
|
58
|
-
## Main Managers
|
|
59
|
-
|
|
60
|
-
| Manager | Purpose |
|
|
61
|
-
| --- | --- |
|
|
62
|
-
| `client.routes` | List routes and create API-managed routes |
|
|
63
|
-
| `client.certificates` | Inspect and operate on certificate records |
|
|
64
|
-
| `client.apiTokens` | Create, list, toggle, roll, revoke API tokens |
|
|
65
|
-
| `client.remoteIngress` | Manage registered remote ingress edges |
|
|
66
|
-
| `client.stats` | Read operational metrics and health data |
|
|
67
|
-
| `client.config` | Read current configuration view |
|
|
68
|
-
| `client.logs` | Read recent logs or stream them |
|
|
69
|
-
| `client.emails` | List emails and trigger resend flows |
|
|
70
|
-
| `client.radius` | Operate on RADIUS clients, VLANs, sessions, and accounting |
|
|
71
|
-
|
|
72
|
-
## Route Behavior
|
|
73
|
-
|
|
74
|
-
Routes are returned as `Route` instances with:
|
|
75
|
-
|
|
76
|
-
- `id`
|
|
77
|
-
- `name`
|
|
78
|
-
- `enabled`
|
|
79
|
-
- `origin`
|
|
80
|
-
|
|
81
72
|
Important behavior:
|
|
82
73
|
|
|
83
|
-
-
|
|
84
|
-
-
|
|
85
|
-
-
|
|
86
|
-
|
|
87
|
-
```typescript
|
|
88
|
-
const { routes } = await client.routes.list();
|
|
74
|
+
- `baseUrl` is normalized, and the client automatically calls `${baseUrl}/typedrequest`
|
|
75
|
+
- `buildRequestPayload()` injects the current identity and optional API token for you
|
|
76
|
+
- system routes can be toggled, but only API routes are meant for edit and delete flows
|
|
89
77
|
|
|
90
|
-
|
|
91
|
-
if (route.origin !== 'api') {
|
|
92
|
-
await route.toggle(false);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
## Builder Example
|
|
78
|
+
## Route Builder Example
|
|
98
79
|
|
|
99
80
|
```typescript
|
|
100
|
-
const
|
|
81
|
+
const newRoute = await client.routes.build()
|
|
101
82
|
.setName('internal-app')
|
|
102
83
|
.setMatch({
|
|
103
|
-
ports:
|
|
84
|
+
ports: 443,
|
|
104
85
|
domains: ['internal.example.com'],
|
|
105
86
|
})
|
|
106
87
|
.setAction({
|
|
@@ -110,30 +91,47 @@ const route = await client.routes.build()
|
|
|
110
91
|
.setEnabled(true)
|
|
111
92
|
.save();
|
|
112
93
|
|
|
113
|
-
await
|
|
94
|
+
await newRoute.update({
|
|
95
|
+
action: {
|
|
96
|
+
type: 'forward',
|
|
97
|
+
targets: [{ host: '127.0.0.1', port: 3001 }],
|
|
98
|
+
},
|
|
99
|
+
});
|
|
114
100
|
```
|
|
115
101
|
|
|
116
|
-
##
|
|
102
|
+
## Token and Remote Ingress Example
|
|
117
103
|
|
|
118
104
|
```typescript
|
|
119
|
-
const
|
|
120
|
-
|
|
105
|
+
const token = await client.apiTokens.build()
|
|
106
|
+
.setName('ci-token')
|
|
107
|
+
.setScopes(['routes:read', 'routes:write'])
|
|
108
|
+
.setExpiresInDays(30)
|
|
109
|
+
.save();
|
|
110
|
+
|
|
111
|
+
console.log('copy this once:', token.tokenValue);
|
|
112
|
+
|
|
113
|
+
const edge = await client.remoteIngress.build()
|
|
114
|
+
.setName('edge-eu-1')
|
|
115
|
+
.setListenPorts([80, 443])
|
|
116
|
+
.setAutoDerivePorts(true)
|
|
117
|
+
.setTags(['production', 'eu'])
|
|
118
|
+
.save();
|
|
121
119
|
|
|
122
|
-
const
|
|
123
|
-
|
|
120
|
+
const connectionToken = await edge.getConnectionToken();
|
|
121
|
+
console.log(connectionToken);
|
|
124
122
|
```
|
|
125
123
|
|
|
126
124
|
## What This Package Does Not Do
|
|
127
125
|
|
|
128
126
|
- It does not start dcrouter.
|
|
129
|
-
- It does not
|
|
130
|
-
- It does not replace the
|
|
127
|
+
- It does not bundle the dashboard.
|
|
128
|
+
- It does not replace the raw interfaces package when you want low-level TypedRequest contracts.
|
|
131
129
|
|
|
132
|
-
Use `@serve.zone/dcrouter` to run the server
|
|
130
|
+
Use `@serve.zone/dcrouter` to run the server and `@serve.zone/dcrouter-interfaces` for the shared request/data types.
|
|
133
131
|
|
|
134
132
|
## License and Legal Information
|
|
135
133
|
|
|
136
|
-
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [
|
|
134
|
+
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../license) file.
|
|
137
135
|
|
|
138
136
|
**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.
|
|
139
137
|
|
package/ts_web/appstate.ts
CHANGED
|
@@ -512,15 +512,6 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
|
|
512
512
|
if (!context.identity) return currentState;
|
|
513
513
|
|
|
514
514
|
try {
|
|
515
|
-
// Fetch active connections using the existing endpoint
|
|
516
|
-
const connectionsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
517
|
-
interfaces.requests.IReq_GetActiveConnections
|
|
518
|
-
>('/typedrequest', 'getActiveConnections');
|
|
519
|
-
|
|
520
|
-
const connectionsResponse = await connectionsRequest.fire({
|
|
521
|
-
identity: context.identity,
|
|
522
|
-
});
|
|
523
|
-
|
|
524
515
|
// Get network stats for throughput and IP data
|
|
525
516
|
const networkStatsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
526
517
|
interfaces.requests.IReq_GetNetworkStats
|
|
@@ -533,22 +524,35 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
|
|
533
524
|
// Use the connections data for the connection list
|
|
534
525
|
// and network stats for throughput and IP analytics
|
|
535
526
|
const connectionsByIP: { [ip: string]: number } = {};
|
|
527
|
+
const throughputByIP = new Map<string, { in: number; out: number }>();
|
|
528
|
+
for (const item of networkStatsResponse.throughputByIP || []) {
|
|
529
|
+
throughputByIP.set(item.ip, { in: item.in, out: item.out });
|
|
530
|
+
}
|
|
536
531
|
|
|
537
532
|
// Build connectionsByIP from network stats if available
|
|
538
533
|
if (networkStatsResponse.connectionsByIP && Array.isArray(networkStatsResponse.connectionsByIP)) {
|
|
539
534
|
networkStatsResponse.connectionsByIP.forEach((item: { ip: string; count: number }) => {
|
|
540
535
|
connectionsByIP[item.ip] = item.count;
|
|
541
536
|
});
|
|
542
|
-
} else {
|
|
543
|
-
// Fallback: calculate from connections
|
|
544
|
-
connectionsResponse.connections.forEach(conn => {
|
|
545
|
-
const ip = conn.remoteAddress;
|
|
546
|
-
connectionsByIP[ip] = (connectionsByIP[ip] || 0) + 1;
|
|
547
|
-
});
|
|
548
537
|
}
|
|
549
538
|
|
|
539
|
+
const connections: interfaces.data.IConnectionInfo[] = Object.entries(connectionsByIP).map(([ip, count]) => {
|
|
540
|
+
const tp = throughputByIP.get(ip);
|
|
541
|
+
return {
|
|
542
|
+
id: `ip-${ip}`,
|
|
543
|
+
remoteAddress: ip,
|
|
544
|
+
localAddress: 'server',
|
|
545
|
+
startTime: 0,
|
|
546
|
+
protocol: 'https',
|
|
547
|
+
state: 'connected',
|
|
548
|
+
bytesReceived: tp?.in || 0,
|
|
549
|
+
bytesSent: tp?.out || 0,
|
|
550
|
+
connectionCount: count,
|
|
551
|
+
};
|
|
552
|
+
});
|
|
553
|
+
|
|
550
554
|
return {
|
|
551
|
-
connections
|
|
555
|
+
connections,
|
|
552
556
|
connectionsByIP,
|
|
553
557
|
throughputRate: networkStatsResponse.throughputRate || { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
|
554
558
|
totalBytes: networkStatsResponse.totalDataTransferred
|
|
@@ -2589,7 +2593,7 @@ async function dispatchCombinedRefreshActionInner() {
|
|
|
2589
2593
|
email: true,
|
|
2590
2594
|
dns: true,
|
|
2591
2595
|
security: true,
|
|
2592
|
-
network: currentView === 'network'
|
|
2596
|
+
network: currentView === 'network' && currentSubview === 'activity',
|
|
2593
2597
|
radius: true,
|
|
2594
2598
|
vpn: true,
|
|
2595
2599
|
},
|
|
@@ -2617,7 +2621,7 @@ async function dispatchCombinedRefreshActionInner() {
|
|
|
2617
2621
|
|
|
2618
2622
|
// Build connectionsByIP from connectionDetails (now populated with real per-IP data)
|
|
2619
2623
|
network.connectionDetails.forEach(conn => {
|
|
2620
|
-
connectionsByIP[conn.remoteAddress] = (connectionsByIP[conn.remoteAddress] || 0) + 1;
|
|
2624
|
+
connectionsByIP[conn.remoteAddress] = (connectionsByIP[conn.remoteAddress] || 0) + (conn.connectionCount || 1);
|
|
2621
2625
|
});
|
|
2622
2626
|
|
|
2623
2627
|
// Build connections from connectionDetails (real per-IP aggregates)
|
|
@@ -2630,6 +2634,7 @@ async function dispatchCombinedRefreshActionInner() {
|
|
|
2630
2634
|
state: conn.state as any,
|
|
2631
2635
|
bytesReceived: conn.bytesIn,
|
|
2632
2636
|
bytesSent: conn.bytesOut,
|
|
2637
|
+
connectionCount: conn.connectionCount,
|
|
2633
2638
|
}));
|
|
2634
2639
|
|
|
2635
2640
|
networkStatePart.setState({
|
|
@@ -79,7 +79,6 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|
|
79
79
|
// Subscribe and track unsubscribe functions
|
|
80
80
|
const statsUnsubscribe = appstate.statsStatePart.select().subscribe((state) => {
|
|
81
81
|
this.statsState = state;
|
|
82
|
-
this.updateNetworkData();
|
|
83
82
|
});
|
|
84
83
|
this.rxSubscriptions.push(statsUnsubscribe);
|
|
85
84
|
|
|
@@ -560,6 +559,8 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|
|
560
559
|
'Throughput Out': this.formatBitsPerSecond(item.bytesOutPerSecond),
|
|
561
560
|
'Transferred / min': this.formatBytes(totalBytesPerMin),
|
|
562
561
|
'Connections': item.activeConnections,
|
|
562
|
+
'Req/s': item.requestsPerSecond != null ? item.requestsPerSecond.toFixed(1) : '-',
|
|
563
|
+
'Req/min': item.requestsLastMinute != null ? item.requestsLastMinute.toFixed(0) : '-',
|
|
563
564
|
'Requests': item.requestCount?.toLocaleString() ?? '0',
|
|
564
565
|
'Routes': item.routeCount,
|
|
565
566
|
};
|
|
@@ -583,7 +584,7 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|
|
583
584
|
return html`
|
|
584
585
|
<dees-table
|
|
585
586
|
.data=${backends}
|
|
586
|
-
.rowKey=${'
|
|
587
|
+
.rowKey=${'id'}
|
|
587
588
|
.highlightUpdates=${'flash'}
|
|
588
589
|
.displayFunction=${(item: interfaces.data.IBackendInfo) => {
|
|
589
590
|
const totalErrors = item.connectErrors + item.handshakeErrors + item.requestErrors;
|
|
@@ -707,6 +708,9 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|
|
707
708
|
}
|
|
708
709
|
|
|
709
710
|
const throughput = this.calculateThroughput();
|
|
711
|
+
if (this.networkState.lastUpdated && now - this.networkState.lastUpdated > 3000) {
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
710
714
|
|
|
711
715
|
// Convert to Mbps (bytes * 8 / 1,000,000)
|
|
712
716
|
const throughputInMbps = (throughput.in * 8) / 1000000;
|
|
@@ -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
|
|