@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.
Files changed (34) hide show
  1. package/dist_serve/bundle.js +519 -519
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.d.ts +5 -0
  4. package/dist_ts/classes.dcrouter.js +34 -10
  5. package/dist_ts/config/classes.route-config-manager.d.ts +1 -0
  6. package/dist_ts/config/classes.route-config-manager.js +4 -1
  7. package/dist_ts/monitoring/classes.metricsmanager.d.ts +6 -2
  8. package/dist_ts/monitoring/classes.metricsmanager.js +67 -41
  9. package/dist_ts/opsserver/handlers/security.handler.js +11 -5
  10. package/dist_ts/opsserver/handlers/stats.handler.js +2 -1
  11. package/dist_ts/vpn/classes.vpn-manager.d.ts +5 -1
  12. package/dist_ts/vpn/classes.vpn-manager.js +55 -17
  13. package/dist_ts_interfaces/data/stats.d.ts +10 -0
  14. package/dist_ts_web/00_commitinfo_data.js +1 -1
  15. package/dist_ts_web/appstate.js +23 -16
  16. package/dist_ts_web/elements/network/ops-view-network-activity.js +7 -3
  17. package/dist_ts_web/elements/network/ops-view-vpn.d.ts +3 -0
  18. package/dist_ts_web/elements/network/ops-view-vpn.js +44 -16
  19. package/package.json +3 -3
  20. package/readme.md +123 -155
  21. package/ts/00_commitinfo_data.ts +1 -1
  22. package/ts/classes.dcrouter.ts +51 -14
  23. package/ts/config/classes.route-config-manager.ts +6 -0
  24. package/ts/monitoring/classes.metricsmanager.ts +71 -40
  25. package/ts/opsserver/handlers/security.handler.ts +11 -5
  26. package/ts/opsserver/handlers/stats.handler.ts +1 -0
  27. package/ts/readme.md +46 -103
  28. package/ts/vpn/classes.vpn-manager.ts +66 -15
  29. package/ts_apiclient/readme.md +57 -59
  30. package/ts_web/00_commitinfo_data.ts +1 -1
  31. package/ts_web/appstate.ts +23 -18
  32. package/ts_web/elements/network/ops-view-network-activity.ts +6 -2
  33. package/ts_web/elements/network/ops-view-vpn.ts +50 -14
  34. package/ts_web/readme.md +27 -47
@@ -1,8 +1,6 @@
1
1
  # @serve.zone/dcrouter-apiclient
2
2
 
3
- Typed, object-oriented API client for operating a running dcrouter instance. 🔧
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
- Or import through the main package:
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', 'password');
30
+ await client.login('admin', 'admin');
33
31
 
34
- const { routes } = await client.routes.list();
35
- console.log(routes.map((route) => `${route.origin}:${route.name}`));
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 client stores the returned identity for later requests |
49
- | API token | Pass `apiToken` into the constructor for token-based automation |
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
- - API routes can be created, updated, deleted, and toggled.
84
- - System routes can be listed and toggled, but not edited or deleted.
85
- - A system route is any route whose `origin !== 'api'`.
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
- for (const route of routes) {
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 route = await client.routes.build()
81
+ const newRoute = await client.routes.build()
101
82
  .setName('internal-app')
102
83
  .setMatch({
103
- ports: 80,
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 route.toggle(false);
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
- ## Example: Certificates and Stats
102
+ ## Token and Remote Ingress Example
117
103
 
118
104
  ```typescript
119
- const { certificates, summary } = await client.certificates.list();
120
- console.log(summary.valid, summary.failed);
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 health = await client.stats.getHealth();
123
- const recentLogs = await client.logs.getRecent({ level: 'error', limit: 20 });
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 embed the dashboard.
130
- - It does not replace the request interfaces package if you only need raw types.
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, `@serve.zone/dcrouter-web` for the dashboard bundle/components, and `@serve.zone/dcrouter-interfaces` for raw API contracts.
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 [license](../license) file.
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
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '13.20.0',
6
+ version: '13.21.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -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: connectionsResponse.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', // Only fetch network if on network view
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=${'backend'}
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 appstate.vpnStatePart.dispatchAction(appstate.fetchVpnAction, null);
63
- // Ensure target profiles are loaded for autocomplete candidates
64
- await appstate.targetProfilesStatePart.dispatchAction(appstate.fetchTargetProfilesAction, null);
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?.length
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 profileState = appstate.targetProfilesStatePart.getState();
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(ids?: string[]): string[] | undefined {
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
- return labelsById.get(id) || id;
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 UI package for dcrouter's operations dashboard. 🖥️
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 Is In Here
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
- - 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
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
- ## Route Management UX
16
+ ## View Map
33
17
 
34
- The web UI reflects dcrouter's current route ownership model:
35
-
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
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
- The frontend uses TypedRequest and shared interfaces from `@serve.zone/dcrouter-interfaces`.
44
-
45
- State actions in `appstate.ts` fetch and mutate:
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
- The browser bundle is built from this package and served by the main dcrouter package.
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 bundle
40
+ pnpm run build
61
41
  pnpm run watch
62
42
  ```
63
43
 
64
- The generated bundle is written into `dist_serve/` by the main build pipeline.
44
+ The built dashboard assets are emitted into `dist_serve/` by the workspace build pipeline.
65
45
 
66
- ## When To Use This Package
46
+ ## What This Package Is For
67
47
 
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.
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 [license](../license) file.
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