@serve.zone/dcrouter 6.3.0 → 6.4.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.
Files changed (55) hide show
  1. package/dist_serve/bundle.js +559 -452
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.d.ts +22 -0
  4. package/dist_ts/classes.dcrouter.js +42 -1
  5. package/dist_ts/index.d.ts +1 -0
  6. package/dist_ts/index.js +3 -1
  7. package/dist_ts/opsserver/classes.opsserver.d.ts +1 -0
  8. package/dist_ts/opsserver/classes.opsserver.js +3 -1
  9. package/dist_ts/opsserver/handlers/index.d.ts +1 -0
  10. package/dist_ts/opsserver/handlers/index.js +2 -1
  11. package/dist_ts/opsserver/handlers/remoteingress.handler.d.ts +8 -0
  12. package/dist_ts/opsserver/handlers/remoteingress.handler.js +107 -0
  13. package/dist_ts/plugins.d.ts +2 -1
  14. package/dist_ts/plugins.js +3 -2
  15. package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +56 -0
  16. package/dist_ts/remoteingress/classes.remoteingress-manager.js +133 -0
  17. package/dist_ts/remoteingress/classes.tunnel-manager.d.ts +45 -0
  18. package/dist_ts/remoteingress/classes.tunnel-manager.js +107 -0
  19. package/dist_ts/remoteingress/index.d.ts +2 -0
  20. package/dist_ts/remoteingress/index.js +3 -0
  21. package/dist_ts_interfaces/data/index.d.ts +1 -0
  22. package/dist_ts_interfaces/data/index.js +2 -1
  23. package/dist_ts_interfaces/data/remoteingress.d.ts +24 -0
  24. package/dist_ts_interfaces/data/remoteingress.js +2 -0
  25. package/dist_ts_interfaces/requests/index.d.ts +1 -0
  26. package/dist_ts_interfaces/requests/index.js +2 -1
  27. package/dist_ts_interfaces/requests/remoteingress.d.ts +89 -0
  28. package/dist_ts_interfaces/requests/remoteingress.js +3 -0
  29. package/dist_ts_web/00_commitinfo_data.js +1 -1
  30. package/dist_ts_web/appstate.d.ts +19 -0
  31. package/dist_ts_web/appstate.js +124 -2
  32. package/dist_ts_web/elements/index.d.ts +1 -0
  33. package/dist_ts_web/elements/index.js +2 -1
  34. package/dist_ts_web/elements/ops-dashboard.js +6 -1
  35. package/dist_ts_web/elements/ops-view-remoteingress.d.ts +20 -0
  36. package/dist_ts_web/elements/ops-view-remoteingress.js +317 -0
  37. package/dist_ts_web/router.d.ts +1 -1
  38. package/dist_ts_web/router.js +2 -2
  39. package/package.json +3 -2
  40. package/ts/00_commitinfo_data.ts +1 -1
  41. package/ts/classes.dcrouter.ts +66 -0
  42. package/ts/index.ts +3 -0
  43. package/ts/opsserver/classes.opsserver.ts +2 -0
  44. package/ts/opsserver/handlers/index.ts +2 -1
  45. package/ts/opsserver/handlers/remoteingress.handler.ts +163 -0
  46. package/ts/plugins.ts +3 -1
  47. package/ts/remoteingress/classes.remoteingress-manager.ts +160 -0
  48. package/ts/remoteingress/classes.tunnel-manager.ts +126 -0
  49. package/ts/remoteingress/index.ts +2 -0
  50. package/ts_web/00_commitinfo_data.ts +1 -1
  51. package/ts_web/appstate.ts +180 -1
  52. package/ts_web/elements/index.ts +1 -0
  53. package/ts_web/elements/ops-dashboard.ts +5 -0
  54. package/ts_web/elements/ops-view-remoteingress.ts +290 -0
  55. package/ts_web/router.ts +1 -1
@@ -20,6 +20,7 @@ import { OpsViewLogs } from './ops-view-logs.js';
20
20
  import { OpsViewConfig } from './ops-view-config.js';
21
21
  import { OpsViewSecurity } from './ops-view-security.js';
22
22
  import { OpsViewCertificates } from './ops-view-certificates.js';
23
+ import { OpsViewRemoteIngress } from './ops-view-remoteingress.js';
23
24
 
24
25
  @customElement('ops-dashboard')
25
26
  export class OpsDashboard extends DeesElement {
@@ -66,6 +67,10 @@ export class OpsDashboard extends DeesElement {
66
67
  name: 'Certificates',
67
68
  element: OpsViewCertificates,
68
69
  },
70
+ {
71
+ name: 'RemoteIngress',
72
+ element: OpsViewRemoteIngress,
73
+ },
69
74
  ];
70
75
 
71
76
  /**
@@ -0,0 +1,290 @@
1
+ import {
2
+ DeesElement,
3
+ html,
4
+ customElement,
5
+ type TemplateResult,
6
+ css,
7
+ state,
8
+ cssManager,
9
+ } from '@design.estate/dees-element';
10
+ import * as appstate from '../appstate.js';
11
+ import * as interfaces from '../../dist_ts_interfaces/index.js';
12
+ import { viewHostCss } from './shared/css.js';
13
+ import { type IStatsTile } from '@design.estate/dees-catalog';
14
+
15
+ declare global {
16
+ interface HTMLElementTagNameMap {
17
+ 'ops-view-remoteingress': OpsViewRemoteIngress;
18
+ }
19
+ }
20
+
21
+ @customElement('ops-view-remoteingress')
22
+ export class OpsViewRemoteIngress extends DeesElement {
23
+ @state()
24
+ accessor riState: appstate.IRemoteIngressState = appstate.remoteIngressStatePart.getState();
25
+
26
+ constructor() {
27
+ super();
28
+ const sub = appstate.remoteIngressStatePart.state.subscribe((newState) => {
29
+ this.riState = newState;
30
+ });
31
+ this.rxSubscriptions.push(sub);
32
+ }
33
+
34
+ async connectedCallback() {
35
+ await super.connectedCallback();
36
+ await appstate.remoteIngressStatePart.dispatchAction(appstate.fetchRemoteIngressAction, null);
37
+ }
38
+
39
+ public static styles = [
40
+ cssManager.defaultStyles,
41
+ viewHostCss,
42
+ css`
43
+ .remoteIngressContainer {
44
+ display: flex;
45
+ flex-direction: column;
46
+ gap: 24px;
47
+ }
48
+
49
+ .statusBadge {
50
+ display: inline-flex;
51
+ align-items: center;
52
+ padding: 3px 10px;
53
+ border-radius: 12px;
54
+ font-size: 12px;
55
+ font-weight: 600;
56
+ letter-spacing: 0.02em;
57
+ text-transform: uppercase;
58
+ }
59
+
60
+ .statusBadge.connected {
61
+ background: ${cssManager.bdTheme('#dcfce7', '#14532d')};
62
+ color: ${cssManager.bdTheme('#166534', '#4ade80')};
63
+ }
64
+
65
+ .statusBadge.disconnected {
66
+ background: ${cssManager.bdTheme('#fef2f2', '#450a0a')};
67
+ color: ${cssManager.bdTheme('#991b1b', '#f87171')};
68
+ }
69
+
70
+ .statusBadge.disabled {
71
+ background: ${cssManager.bdTheme('#f3f4f6', '#374151')};
72
+ color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
73
+ }
74
+
75
+ .secretDialog {
76
+ padding: 16px;
77
+ background: ${cssManager.bdTheme('#fffbeb', '#1c1917')};
78
+ border: 1px solid ${cssManager.bdTheme('#fbbf24', '#92400e')};
79
+ border-radius: 8px;
80
+ margin-bottom: 16px;
81
+ }
82
+
83
+ .secretDialog code {
84
+ display: block;
85
+ padding: 8px 12px;
86
+ background: ${cssManager.bdTheme('#1f2937', '#111827')};
87
+ color: #10b981;
88
+ border-radius: 4px;
89
+ font-family: monospace;
90
+ font-size: 13px;
91
+ word-break: break-all;
92
+ margin: 8px 0;
93
+ user-select: all;
94
+ }
95
+
96
+ .secretDialog .warning {
97
+ font-size: 12px;
98
+ color: ${cssManager.bdTheme('#92400e', '#fbbf24')};
99
+ margin-top: 8px;
100
+ }
101
+
102
+ .portsDisplay {
103
+ display: flex;
104
+ gap: 4px;
105
+ flex-wrap: wrap;
106
+ }
107
+
108
+ .portBadge {
109
+ display: inline-flex;
110
+ padding: 2px 8px;
111
+ border-radius: 4px;
112
+ font-size: 12px;
113
+ font-weight: 500;
114
+ background: ${cssManager.bdTheme('#eff6ff', '#172554')};
115
+ color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};
116
+ }
117
+ `,
118
+ ];
119
+
120
+ render(): TemplateResult {
121
+ const totalEdges = this.riState.edges.length;
122
+ const connectedEdges = this.riState.statuses.filter(s => s.connected).length;
123
+ const disconnectedEdges = totalEdges - connectedEdges;
124
+ const activeTunnels = this.riState.statuses.reduce((sum, s) => sum + s.activeTunnels, 0);
125
+
126
+ const statsTiles: IStatsTile[] = [
127
+ {
128
+ id: 'totalEdges',
129
+ title: 'Total Edges',
130
+ type: 'number',
131
+ value: totalEdges,
132
+ icon: 'lucide:server',
133
+ description: 'Registered edge nodes',
134
+ color: '#3b82f6',
135
+ },
136
+ {
137
+ id: 'connectedEdges',
138
+ title: 'Connected',
139
+ type: 'number',
140
+ value: connectedEdges,
141
+ icon: 'lucide:link',
142
+ description: 'Currently connected edges',
143
+ color: '#10b981',
144
+ },
145
+ {
146
+ id: 'disconnectedEdges',
147
+ title: 'Disconnected',
148
+ type: 'number',
149
+ value: disconnectedEdges,
150
+ icon: 'lucide:unlink',
151
+ description: 'Offline edge nodes',
152
+ color: disconnectedEdges > 0 ? '#ef4444' : '#6b7280',
153
+ },
154
+ {
155
+ id: 'activeTunnels',
156
+ title: 'Active Tunnels',
157
+ type: 'number',
158
+ value: activeTunnels,
159
+ icon: 'lucide:cable',
160
+ description: 'Active client connections',
161
+ color: '#8b5cf6',
162
+ },
163
+ ];
164
+
165
+ return html`
166
+ <ops-sectionheading>Remote Ingress</ops-sectionheading>
167
+
168
+ ${this.riState.newEdgeSecret ? html`
169
+ <div class="secretDialog">
170
+ <strong>Edge Secret (copy now - shown only once):</strong>
171
+ <code>${this.riState.newEdgeSecret}</code>
172
+ <div class="warning">This secret will not be shown again. Save it securely.</div>
173
+ <dees-button
174
+ @click=${() => appstate.remoteIngressStatePart.dispatchAction(appstate.clearNewEdgeSecretAction, null)}
175
+ >Dismiss</dees-button>
176
+ </div>
177
+ ` : ''}
178
+
179
+ <div class="remoteIngressContainer">
180
+ <dees-statsgrid .tiles=${statsTiles}></dees-statsgrid>
181
+
182
+ <dees-table
183
+ .heading1=${'Edge Nodes'}
184
+ .heading2=${'Manage remote ingress edge registrations'}
185
+ .data=${this.riState.edges}
186
+ .displayFunction=${(edge: interfaces.data.IRemoteIngress) => ({
187
+ name: edge.name,
188
+ status: this.getEdgeStatusHtml(edge),
189
+ publicIp: this.getEdgePublicIp(edge.id),
190
+ ports: this.getPortsHtml(edge.listenPorts),
191
+ tunnels: this.getEdgeTunnelCount(edge.id),
192
+ lastHeartbeat: this.getLastHeartbeat(edge.id),
193
+ })}
194
+ .dataActions=${[
195
+ {
196
+ name: 'Regenerate Secret',
197
+ iconName: 'lucide:key',
198
+ action: async (edge: interfaces.data.IRemoteIngress) => {
199
+ await appstate.remoteIngressStatePart.dispatchAction(
200
+ appstate.regenerateRemoteIngressSecretAction,
201
+ edge.id,
202
+ );
203
+ },
204
+ },
205
+ {
206
+ name: 'Delete',
207
+ iconName: 'lucide:trash2',
208
+ action: async (edge: interfaces.data.IRemoteIngress) => {
209
+ await appstate.remoteIngressStatePart.dispatchAction(
210
+ appstate.deleteRemoteIngressAction,
211
+ edge.id,
212
+ );
213
+ },
214
+ },
215
+ ]}
216
+ .createNewAction=${async () => {
217
+ const { DeesModal } = await import('@design.estate/dees-catalog');
218
+ const result = await DeesModal.createAndShow({
219
+ heading: 'Create Edge Node',
220
+ content: html`
221
+ <dees-form>
222
+ <dees-input-text .key=${'name'} .label=${'Name'} .required=${true}></dees-input-text>
223
+ <dees-input-text .key=${'listenPorts'} .label=${'Listen Ports (comma-separated)'} .required=${true} .value=${'443,25'}></dees-input-text>
224
+ <dees-input-text .key=${'tags'} .label=${'Tags (comma-separated, optional)'}></dees-input-text>
225
+ </dees-form>
226
+ `,
227
+ menuOptions: [],
228
+ });
229
+ if (result) {
230
+ const formData = result as any;
231
+ const ports = (formData.name ? formData.listenPorts : '443')
232
+ .split(',')
233
+ .map((p: string) => parseInt(p.trim(), 10))
234
+ .filter((p: number) => !isNaN(p));
235
+ const tags = formData.tags
236
+ ? formData.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
237
+ : undefined;
238
+ await appstate.remoteIngressStatePart.dispatchAction(
239
+ appstate.createRemoteIngressAction,
240
+ {
241
+ name: formData.name,
242
+ listenPorts: ports,
243
+ tags,
244
+ },
245
+ );
246
+ }
247
+ }}
248
+ ></dees-table>
249
+ </div>
250
+ `;
251
+ }
252
+
253
+ private getEdgeStatus(edgeId: string): interfaces.data.IRemoteIngressStatus | undefined {
254
+ return this.riState.statuses.find(s => s.edgeId === edgeId);
255
+ }
256
+
257
+ private getEdgeStatusHtml(edge: interfaces.data.IRemoteIngress): TemplateResult {
258
+ if (!edge.enabled) {
259
+ return html`<span class="statusBadge disabled">Disabled</span>`;
260
+ }
261
+ const status = this.getEdgeStatus(edge.id);
262
+ if (status?.connected) {
263
+ return html`<span class="statusBadge connected">Connected</span>`;
264
+ }
265
+ return html`<span class="statusBadge disconnected">Disconnected</span>`;
266
+ }
267
+
268
+ private getEdgePublicIp(edgeId: string): string {
269
+ const status = this.getEdgeStatus(edgeId);
270
+ return status?.publicIp || '-';
271
+ }
272
+
273
+ private getPortsHtml(ports: number[]): TemplateResult {
274
+ return html`<div class="portsDisplay">${ports.map(p => html`<span class="portBadge">${p}</span>`)}</div>`;
275
+ }
276
+
277
+ private getEdgeTunnelCount(edgeId: string): number {
278
+ const status = this.getEdgeStatus(edgeId);
279
+ return status?.activeTunnels || 0;
280
+ }
281
+
282
+ private getLastHeartbeat(edgeId: string): string {
283
+ const status = this.getEdgeStatus(edgeId);
284
+ if (!status?.lastHeartbeat) return '-';
285
+ const ago = Date.now() - status.lastHeartbeat;
286
+ if (ago < 60000) return `${Math.floor(ago / 1000)}s ago`;
287
+ if (ago < 3600000) return `${Math.floor(ago / 60000)}m ago`;
288
+ return `${Math.floor(ago / 3600000)}h ago`;
289
+ }
290
+ }
package/ts_web/router.ts CHANGED
@@ -3,7 +3,7 @@ import * as appstate from './appstate.js';
3
3
 
4
4
  const SmartRouter = plugins.domtools.plugins.smartrouter.SmartRouter;
5
5
 
6
- export const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates'] as const;
6
+ export const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates', 'remoteingress'] as const;
7
7
  export const validEmailFolders = ['queued', 'sent', 'failed', 'security'] as const;
8
8
 
9
9
  export type TValidView = typeof validViews[number];