@serve.zone/dcrouter 6.2.4 → 6.4.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 (60) 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 +26 -0
  4. package/dist_ts/classes.dcrouter.js +49 -5
  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/paths.d.ts +19 -9
  14. package/dist_ts/paths.js +30 -28
  15. package/dist_ts/plugins.d.ts +2 -1
  16. package/dist_ts/plugins.js +3 -2
  17. package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +56 -0
  18. package/dist_ts/remoteingress/classes.remoteingress-manager.js +133 -0
  19. package/dist_ts/remoteingress/classes.tunnel-manager.d.ts +45 -0
  20. package/dist_ts/remoteingress/classes.tunnel-manager.js +107 -0
  21. package/dist_ts/remoteingress/index.d.ts +2 -0
  22. package/dist_ts/remoteingress/index.js +3 -0
  23. package/dist_ts/security/classes.contentscanner.js +1 -2
  24. package/dist_ts_interfaces/data/index.d.ts +1 -0
  25. package/dist_ts_interfaces/data/index.js +2 -1
  26. package/dist_ts_interfaces/data/remoteingress.d.ts +24 -0
  27. package/dist_ts_interfaces/data/remoteingress.js +2 -0
  28. package/dist_ts_interfaces/requests/index.d.ts +1 -0
  29. package/dist_ts_interfaces/requests/index.js +2 -1
  30. package/dist_ts_interfaces/requests/remoteingress.d.ts +89 -0
  31. package/dist_ts_interfaces/requests/remoteingress.js +3 -0
  32. package/dist_ts_web/00_commitinfo_data.js +1 -1
  33. package/dist_ts_web/appstate.d.ts +19 -0
  34. package/dist_ts_web/appstate.js +124 -2
  35. package/dist_ts_web/elements/index.d.ts +1 -0
  36. package/dist_ts_web/elements/index.js +2 -1
  37. package/dist_ts_web/elements/ops-dashboard.js +6 -1
  38. package/dist_ts_web/elements/ops-view-remoteingress.d.ts +20 -0
  39. package/dist_ts_web/elements/ops-view-remoteingress.js +317 -0
  40. package/dist_ts_web/router.d.ts +1 -1
  41. package/dist_ts_web/router.js +2 -2
  42. package/package.json +2 -1
  43. package/ts/00_commitinfo_data.ts +1 -1
  44. package/ts/classes.dcrouter.ts +79 -6
  45. package/ts/index.ts +3 -0
  46. package/ts/opsserver/classes.opsserver.ts +2 -0
  47. package/ts/opsserver/handlers/index.ts +2 -1
  48. package/ts/opsserver/handlers/remoteingress.handler.ts +163 -0
  49. package/ts/paths.ts +32 -31
  50. package/ts/plugins.ts +3 -1
  51. package/ts/remoteingress/classes.remoteingress-manager.ts +160 -0
  52. package/ts/remoteingress/classes.tunnel-manager.ts +126 -0
  53. package/ts/remoteingress/index.ts +2 -0
  54. package/ts/security/classes.contentscanner.ts +0 -1
  55. package/ts_web/00_commitinfo_data.ts +1 -1
  56. package/ts_web/appstate.ts +180 -1
  57. package/ts_web/elements/index.ts +1 -0
  58. package/ts_web/elements/ops-dashboard.ts +5 -0
  59. package/ts_web/elements/ops-view-remoteingress.ts +290 -0
  60. package/ts_web/router.ts +1 -1
@@ -116,7 +116,7 @@ export const configStatePart = await appState.getStatePart<IConfigState>(
116
116
  // Determine initial view from URL path
117
117
  const getInitialView = (): string => {
118
118
  const path = typeof window !== 'undefined' ? window.location.pathname : '/';
119
- const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates'];
119
+ const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates', 'remoteingress'];
120
120
  const segments = path.split('/').filter(Boolean);
121
121
  const view = segments[0];
122
122
  return validViews.includes(view) ? view : 'overview';
@@ -192,6 +192,34 @@ export const certificateStatePart = await appState.getStatePart<ICertificateStat
192
192
  'soft'
193
193
  );
194
194
 
195
+ // ============================================================================
196
+ // Remote Ingress State
197
+ // ============================================================================
198
+
199
+ export interface IRemoteIngressState {
200
+ edges: interfaces.data.IRemoteIngress[];
201
+ statuses: interfaces.data.IRemoteIngressStatus[];
202
+ selectedEdgeId: string | null;
203
+ newEdgeSecret: string | null;
204
+ isLoading: boolean;
205
+ error: string | null;
206
+ lastUpdated: number;
207
+ }
208
+
209
+ export const remoteIngressStatePart = await appState.getStatePart<IRemoteIngressState>(
210
+ 'remoteIngress',
211
+ {
212
+ edges: [],
213
+ statuses: [],
214
+ selectedEdgeId: null,
215
+ newEdgeSecret: null,
216
+ isLoading: false,
217
+ error: null,
218
+ lastUpdated: 0,
219
+ },
220
+ 'soft'
221
+ );
222
+
195
223
  // Actions for state management
196
224
  interface IActionContext {
197
225
  identity: interfaces.data.IIdentity | null;
@@ -378,6 +406,13 @@ export const setActiveViewAction = uiStatePart.createAction<string>(async (state
378
406
  }, 100);
379
407
  }
380
408
 
409
+ // If switching to remoteingress view, ensure we fetch edge data
410
+ if (viewName === 'remoteingress' && currentState.activeView !== 'remoteingress') {
411
+ setTimeout(() => {
412
+ remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
413
+ }, 100);
414
+ }
415
+
381
416
  return {
382
417
  ...currentState,
383
418
  activeView: viewName,
@@ -745,6 +780,150 @@ export const reprovisionCertificateAction = certificateStatePart.createAction<st
745
780
  }
746
781
  );
747
782
 
783
+ // ============================================================================
784
+ // Remote Ingress Actions
785
+ // ============================================================================
786
+
787
+ export const fetchRemoteIngressAction = remoteIngressStatePart.createAction(async (statePartArg) => {
788
+ const context = getActionContext();
789
+ const currentState = statePartArg.getState();
790
+
791
+ try {
792
+ const edgesRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
793
+ interfaces.requests.IReq_GetRemoteIngresses
794
+ >('/typedrequest', 'getRemoteIngresses');
795
+
796
+ const statusRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
797
+ interfaces.requests.IReq_GetRemoteIngressStatus
798
+ >('/typedrequest', 'getRemoteIngressStatus');
799
+
800
+ const [edgesResponse, statusResponse] = await Promise.all([
801
+ edgesRequest.fire({ identity: context.identity }),
802
+ statusRequest.fire({ identity: context.identity }),
803
+ ]);
804
+
805
+ return {
806
+ ...currentState,
807
+ edges: edgesResponse.edges,
808
+ statuses: statusResponse.statuses,
809
+ isLoading: false,
810
+ error: null,
811
+ lastUpdated: Date.now(),
812
+ };
813
+ } catch (error) {
814
+ return {
815
+ ...currentState,
816
+ isLoading: false,
817
+ error: error instanceof Error ? error.message : 'Failed to fetch remote ingress data',
818
+ };
819
+ }
820
+ });
821
+
822
+ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
823
+ name: string;
824
+ listenPorts: number[];
825
+ tags?: string[];
826
+ }>(async (statePartArg, dataArg) => {
827
+ const context = getActionContext();
828
+ const currentState = statePartArg.getState();
829
+
830
+ try {
831
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
832
+ interfaces.requests.IReq_CreateRemoteIngress
833
+ >('/typedrequest', 'createRemoteIngress');
834
+
835
+ const response = await request.fire({
836
+ identity: context.identity,
837
+ name: dataArg.name,
838
+ listenPorts: dataArg.listenPorts,
839
+ tags: dataArg.tags,
840
+ });
841
+
842
+ if (response.success) {
843
+ // Refresh the list and store the new secret for display
844
+ await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
845
+ return {
846
+ ...statePartArg.getState(),
847
+ newEdgeSecret: response.edge.secret,
848
+ };
849
+ }
850
+
851
+ return currentState;
852
+ } catch (error) {
853
+ return {
854
+ ...currentState,
855
+ error: error instanceof Error ? error.message : 'Failed to create edge',
856
+ };
857
+ }
858
+ });
859
+
860
+ export const deleteRemoteIngressAction = remoteIngressStatePart.createAction<string>(
861
+ async (statePartArg, edgeId) => {
862
+ const context = getActionContext();
863
+ const currentState = statePartArg.getState();
864
+
865
+ try {
866
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
867
+ interfaces.requests.IReq_DeleteRemoteIngress
868
+ >('/typedrequest', 'deleteRemoteIngress');
869
+
870
+ await request.fire({
871
+ identity: context.identity,
872
+ id: edgeId,
873
+ });
874
+
875
+ await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
876
+ return statePartArg.getState();
877
+ } catch (error) {
878
+ return {
879
+ ...currentState,
880
+ error: error instanceof Error ? error.message : 'Failed to delete edge',
881
+ };
882
+ }
883
+ }
884
+ );
885
+
886
+ export const regenerateRemoteIngressSecretAction = remoteIngressStatePart.createAction<string>(
887
+ async (statePartArg, edgeId) => {
888
+ const context = getActionContext();
889
+ const currentState = statePartArg.getState();
890
+
891
+ try {
892
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
893
+ interfaces.requests.IReq_RegenerateRemoteIngressSecret
894
+ >('/typedrequest', 'regenerateRemoteIngressSecret');
895
+
896
+ const response = await request.fire({
897
+ identity: context.identity,
898
+ id: edgeId,
899
+ });
900
+
901
+ if (response.success) {
902
+ return {
903
+ ...currentState,
904
+ newEdgeSecret: response.secret,
905
+ };
906
+ }
907
+
908
+ return currentState;
909
+ } catch (error) {
910
+ return {
911
+ ...currentState,
912
+ error: error instanceof Error ? error.message : 'Failed to regenerate secret',
913
+ };
914
+ }
915
+ }
916
+ );
917
+
918
+ export const clearNewEdgeSecretAction = remoteIngressStatePart.createAction(
919
+ async (statePartArg) => {
920
+ return {
921
+ ...statePartArg.getState(),
922
+ newEdgeSecret: null,
923
+ };
924
+ }
925
+ );
926
+
748
927
  // Combined refresh action for efficient polling
749
928
  async function dispatchCombinedRefreshAction() {
750
929
  const context = getActionContext();
@@ -6,4 +6,5 @@ export * from './ops-view-logs.js';
6
6
  export * from './ops-view-config.js';
7
7
  export * from './ops-view-security.js';
8
8
  export * from './ops-view-certificates.js';
9
+ export * from './ops-view-remoteingress.js';
9
10
  export * from './shared/index.js';
@@ -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];