@serve.zone/dcrouter 13.17.9 → 13.19.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 (43) hide show
  1. package/dist_serve/bundle.js +6 -5
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.d.ts +9 -5
  4. package/dist_ts/classes.dcrouter.js +152 -120
  5. package/dist_ts/config/classes.route-config-manager.d.ts +13 -5
  6. package/dist_ts/config/classes.route-config-manager.js +76 -36
  7. package/dist_ts/db/documents/classes.route.doc.d.ts +2 -0
  8. package/dist_ts/db/documents/classes.route.doc.js +11 -2
  9. package/dist_ts/email/classes.email-domain.manager.d.ts +7 -0
  10. package/dist_ts/email/classes.email-domain.manager.js +118 -55
  11. package/dist_ts/email/classes.smartmta-storage-manager.d.ts +13 -0
  12. package/dist_ts/email/classes.smartmta-storage-manager.js +101 -0
  13. package/dist_ts/email/email-dns-records.d.ts +14 -0
  14. package/dist_ts/email/email-dns-records.js +34 -0
  15. package/dist_ts/email/index.d.ts +2 -0
  16. package/dist_ts/email/index.js +3 -1
  17. package/dist_ts/opsserver/handlers/email-ops.handler.js +6 -15
  18. package/dist_ts/opsserver/handlers/route-management.handler.js +5 -7
  19. package/dist_ts/opsserver/handlers/stats.handler.js +41 -7
  20. package/dist_ts_interfaces/data/route-management.d.ts +2 -0
  21. package/dist_ts_migrations/index.js +25 -1
  22. package/dist_ts_web/00_commitinfo_data.js +1 -1
  23. package/dist_ts_web/appstate.js +13 -4
  24. package/dist_ts_web/elements/network/ops-view-routes.d.ts +2 -0
  25. package/dist_ts_web/elements/network/ops-view-routes.js +44 -21
  26. package/package.json +2 -2
  27. package/readme.md +190 -1543
  28. package/ts/00_commitinfo_data.ts +1 -1
  29. package/ts/classes.dcrouter.ts +190 -138
  30. package/ts/config/classes.route-config-manager.ts +97 -42
  31. package/ts/db/documents/classes.route.doc.ts +7 -0
  32. package/ts/email/classes.email-domain.manager.ts +136 -51
  33. package/ts/email/classes.smartmta-storage-manager.ts +108 -0
  34. package/ts/email/email-dns-records.ts +53 -0
  35. package/ts/email/index.ts +2 -0
  36. package/ts/opsserver/handlers/email-ops.handler.ts +5 -19
  37. package/ts/opsserver/handlers/route-management.handler.ts +4 -6
  38. package/ts/opsserver/handlers/stats.handler.ts +43 -7
  39. package/ts_apiclient/readme.md +69 -195
  40. package/ts_web/00_commitinfo_data.ts +1 -1
  41. package/ts_web/appstate.ts +16 -4
  42. package/ts_web/elements/network/ops-view-routes.ts +47 -29
  43. package/ts_web/readme.md +41 -242
@@ -2150,7 +2150,7 @@ export const updateRouteAction = routeManagementStatePart.createAction<{
2150
2150
  interfaces.requests.IReq_UpdateRoute
2151
2151
  >('/typedrequest', 'updateRoute');
2152
2152
 
2153
- await request.fire({
2153
+ const response = await request.fire({
2154
2154
  identity: context.identity!,
2155
2155
  id: dataArg.id,
2156
2156
  route: dataArg.route,
@@ -2158,6 +2158,10 @@ export const updateRouteAction = routeManagementStatePart.createAction<{
2158
2158
  metadata: dataArg.metadata,
2159
2159
  });
2160
2160
 
2161
+ if (!response.success) {
2162
+ throw new Error(response.message || 'Failed to update route');
2163
+ }
2164
+
2161
2165
  return await actionContext!.dispatch(fetchMergedRoutesAction, null);
2162
2166
  } catch (error: unknown) {
2163
2167
  return {
@@ -2177,11 +2181,15 @@ export const deleteRouteAction = routeManagementStatePart.createAction<string>(
2177
2181
  interfaces.requests.IReq_DeleteRoute
2178
2182
  >('/typedrequest', 'deleteRoute');
2179
2183
 
2180
- await request.fire({
2184
+ const response = await request.fire({
2181
2185
  identity: context.identity!,
2182
2186
  id: routeId,
2183
2187
  });
2184
2188
 
2189
+ if (!response.success) {
2190
+ throw new Error(response.message || 'Failed to delete route');
2191
+ }
2192
+
2185
2193
  return await actionContext!.dispatch(fetchMergedRoutesAction, null);
2186
2194
  } catch (error: unknown) {
2187
2195
  return {
@@ -2204,12 +2212,16 @@ export const toggleRouteAction = routeManagementStatePart.createAction<{
2204
2212
  interfaces.requests.IReq_ToggleRoute
2205
2213
  >('/typedrequest', 'toggleRoute');
2206
2214
 
2207
- await request.fire({
2215
+ const response = await request.fire({
2208
2216
  identity: context.identity!,
2209
2217
  id: dataArg.id,
2210
2218
  enabled: dataArg.enabled,
2211
2219
  });
2212
2220
 
2221
+ if (!response.success) {
2222
+ throw new Error(response.message || 'Failed to toggle route');
2223
+ }
2224
+
2213
2225
  return await actionContext!.dispatch(fetchMergedRoutesAction, null);
2214
2226
  } catch (error: unknown) {
2215
2227
  return {
@@ -2765,4 +2777,4 @@ startAutoRefresh();
2765
2777
  // Connect TypedSocket if already logged in (e.g., persistent session)
2766
2778
  if (loginStatePart.getState()!.isLoggedIn) {
2767
2779
  connectSocket();
2768
- }
2780
+ }
@@ -272,15 +272,13 @@ export class OpsViewRoutes extends DeesElement {
272
272
  const clickedRoute = e.detail;
273
273
  if (!clickedRoute) return;
274
274
 
275
- // Find the corresponding merged route
276
- const merged = this.routeState.mergedRoutes.find(
277
- (mr) => mr.route.name === clickedRoute.name,
278
- );
275
+ const merged = this.findMergedRoute(clickedRoute);
279
276
  if (!merged) return;
280
277
 
281
278
  const { DeesModal } = await import('@design.estate/dees-catalog');
282
279
 
283
280
  const meta = merged.metadata;
281
+ const isSystemManaged = this.isSystemManagedRoute(merged);
284
282
  await DeesModal.createAndShow({
285
283
  heading: `Route: ${merged.route.name}`,
286
284
  content: html`
@@ -288,6 +286,7 @@ export class OpsViewRoutes extends DeesElement {
288
286
  <p>Origin: <strong style="color: #0af;">${merged.origin}</strong></p>
289
287
  <p>Status: <strong>${merged.enabled ? 'Enabled' : 'Disabled'}</strong></p>
290
288
  <p>ID: <code style="color: #888;">${merged.id}</code></p>
289
+ ${isSystemManaged ? html`<p>This route is system-managed. Change its source config to modify it directly.</p>` : ''}
291
290
  ${meta?.sourceProfileName ? html`<p>Source Profile: <strong style="color: #a78bfa;">${meta.sourceProfileName}</strong></p>` : ''}
292
291
  ${meta?.networkTargetName ? html`<p>Network Target: <strong style="color: #a78bfa;">${meta.networkTargetName}</strong></p>` : ''}
293
292
  </div>
@@ -304,25 +303,29 @@ export class OpsViewRoutes extends DeesElement {
304
303
  await modalArg.destroy();
305
304
  },
306
305
  },
307
- {
308
- name: 'Edit',
309
- iconName: 'lucide:pencil',
310
- action: async (modalArg: any) => {
311
- await modalArg.destroy();
312
- this.showEditRouteDialog(merged);
313
- },
314
- },
315
- {
316
- name: 'Delete',
317
- iconName: 'lucide:trash-2',
318
- action: async (modalArg: any) => {
319
- await appstate.routeManagementStatePart.dispatchAction(
320
- appstate.deleteRouteAction,
321
- merged.id,
322
- );
323
- await modalArg.destroy();
324
- },
325
- },
306
+ ...(!isSystemManaged
307
+ ? [
308
+ {
309
+ name: 'Edit',
310
+ iconName: 'lucide:pencil',
311
+ action: async (modalArg: any) => {
312
+ await modalArg.destroy();
313
+ this.showEditRouteDialog(merged);
314
+ },
315
+ },
316
+ {
317
+ name: 'Delete',
318
+ iconName: 'lucide:trash-2',
319
+ action: async (modalArg: any) => {
320
+ await appstate.routeManagementStatePart.dispatchAction(
321
+ appstate.deleteRouteAction,
322
+ merged.id,
323
+ );
324
+ await modalArg.destroy();
325
+ },
326
+ },
327
+ ]
328
+ : []),
326
329
  {
327
330
  name: 'Close',
328
331
  iconName: 'lucide:x',
@@ -336,10 +339,9 @@ export class OpsViewRoutes extends DeesElement {
336
339
  const clickedRoute = e.detail;
337
340
  if (!clickedRoute) return;
338
341
 
339
- const merged = this.routeState.mergedRoutes.find(
340
- (mr) => mr.route.name === clickedRoute.name,
341
- );
342
+ const merged = this.findMergedRoute(clickedRoute);
342
343
  if (!merged) return;
344
+ if (this.isSystemManagedRoute(merged)) return;
343
345
 
344
346
  this.showEditRouteDialog(merged);
345
347
  }
@@ -348,10 +350,9 @@ export class OpsViewRoutes extends DeesElement {
348
350
  const clickedRoute = e.detail;
349
351
  if (!clickedRoute) return;
350
352
 
351
- const merged = this.routeState.mergedRoutes.find(
352
- (mr) => mr.route.name === clickedRoute.name,
353
- );
353
+ const merged = this.findMergedRoute(clickedRoute);
354
354
  if (!merged) return;
355
+ if (this.isSystemManagedRoute(merged)) return;
355
356
 
356
357
  const { DeesModal } = await import('@design.estate/dees-catalog');
357
358
  await DeesModal.createAndShow({
@@ -675,6 +676,23 @@ export class OpsViewRoutes extends DeesElement {
675
676
  appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
676
677
  }
677
678
 
679
+ private findMergedRoute(clickedRoute: { id?: string; name?: string }): interfaces.data.IMergedRoute | undefined {
680
+ if (clickedRoute.id) {
681
+ const routeById = this.routeState.mergedRoutes.find((mr) => mr.id === clickedRoute.id);
682
+ if (routeById) return routeById;
683
+ }
684
+
685
+ if (clickedRoute.name) {
686
+ return this.routeState.mergedRoutes.find((mr) => mr.route.name === clickedRoute.name);
687
+ }
688
+
689
+ return undefined;
690
+ }
691
+
692
+ private isSystemManagedRoute(merged: interfaces.data.IMergedRoute): boolean {
693
+ return merged.origin !== 'api';
694
+ }
695
+
678
696
  async firstUpdated() {
679
697
  await appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
680
698
 
package/ts_web/readme.md CHANGED
@@ -1,273 +1,72 @@
1
1
  # @serve.zone/dcrouter-web
2
2
 
3
- Web-based Operations Dashboard for DcRouter. 🖥️
3
+ Browser UI package for dcrouter's operations dashboard. 🖥️
4
4
 
5
- A modern, reactive web application for monitoring and managing your DcRouter instance in real-time. Built with web components using [@design.estate/dees-element](https://code.foss.global/design.estate/dees-element) and [@design.estate/dees-catalog](https://code.foss.global/design.estate/dees-catalog).
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
- ## Features
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
- ts_web/
106
- ├── index.ts # Entry point renders <ops-dashboard>
107
- ├── appstate.ts # State management (all state parts + actions)
108
- ├── router.ts # Client-side routing (AppRouter)
109
- ├── plugins.ts # Dependency imports
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
- ### State Management
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
- ### Client-Side Routing
22
+ The dashboard currently includes views for:
202
23
 
203
- ```
204
- /overview → Overview dashboard
205
- /network Network monitoring
206
- /emails → Email management
207
- /emails/queued → Queued emails
208
- /emails/sent → Sent emails
209
- /emails/failed → Failed emails
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
- URL state is synchronized with the UI — bookmarking and deep linking fully supported.
32
+ ## Route Management UX
221
33
 
222
- ## Development
34
+ The web UI reflects dcrouter's current route ownership model:
223
35
 
224
- ### Running Locally
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
- Start DcRouter with OpsServer enabled:
41
+ ## How It Talks To dcrouter
227
42
 
228
- ```typescript
229
- import { DcRouter } from '@serve.zone/dcrouter';
43
+ The frontend uses TypedRequest and shared interfaces from `@serve.zone/dcrouter-interfaces`.
230
44
 
231
- const router = new DcRouter({
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
- await router.start();
237
- // Dashboard at http://localhost:3000
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
- ### Building
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 output to `./dist_serve/bundle.js` and served by the OpsServer.
251
-
252
- ### Adding a New View
64
+ The generated bundle is written into `dist_serve/` by the main build pipeline.
253
65
 
254
- 1. Create a view component in `elements/`:
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
- 2. Add it to the dashboard tabs in `ops-dashboard.ts`
269
- 3. Add the route in `router.ts`
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