@serve.zone/dcrouter 13.2.2 → 13.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 (81) hide show
  1. package/dist_serve/bundle.js +1499 -1413
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts_web/00_commitinfo_data.js +1 -1
  4. package/dist_ts_web/appstate.d.ts +1 -0
  5. package/dist_ts_web/appstate.js +14 -38
  6. package/dist_ts_web/elements/access/index.d.ts +1 -0
  7. package/dist_ts_web/elements/access/index.js +2 -0
  8. package/dist_ts_web/elements/{ops-view-apitokens.d.ts → access/ops-view-apitokens.d.ts} +1 -1
  9. package/dist_ts_web/elements/{ops-view-apitokens.js → access/ops-view-apitokens.js} +4 -4
  10. package/dist_ts_web/elements/email/index.d.ts +2 -0
  11. package/dist_ts_web/elements/email/index.js +3 -0
  12. package/dist_ts_web/elements/email/ops-view-email-security.d.ts +14 -0
  13. package/dist_ts_web/elements/email/ops-view-email-security.js +197 -0
  14. package/dist_ts_web/elements/{ops-view-emails.d.ts → email/ops-view-emails.d.ts} +2 -2
  15. package/dist_ts_web/elements/{ops-view-emails.js → email/ops-view-emails.js} +5 -5
  16. package/dist_ts_web/elements/index.d.ts +5 -12
  17. package/dist_ts_web/elements/index.js +6 -13
  18. package/dist_ts_web/elements/network/index.d.ts +7 -0
  19. package/dist_ts_web/elements/network/index.js +8 -0
  20. package/dist_ts_web/elements/{ops-view-network.d.ts → network/ops-view-network-activity.d.ts} +3 -3
  21. package/dist_ts_web/elements/{ops-view-network.js → network/ops-view-network-activity.js} +20 -32
  22. package/dist_ts_web/elements/{ops-view-networktargets.d.ts → network/ops-view-networktargets.d.ts} +1 -1
  23. package/dist_ts_web/elements/{ops-view-networktargets.js → network/ops-view-networktargets.js} +5 -5
  24. package/dist_ts_web/elements/{ops-view-remoteingress.d.ts → network/ops-view-remoteingress.d.ts} +1 -1
  25. package/dist_ts_web/elements/{ops-view-remoteingress.js → network/ops-view-remoteingress.js} +5 -5
  26. package/dist_ts_web/elements/{ops-view-routes.d.ts → network/ops-view-routes.d.ts} +1 -1
  27. package/dist_ts_web/elements/{ops-view-routes.js → network/ops-view-routes.js} +5 -5
  28. package/dist_ts_web/elements/{ops-view-sourceprofiles.d.ts → network/ops-view-sourceprofiles.d.ts} +1 -1
  29. package/dist_ts_web/elements/{ops-view-sourceprofiles.js → network/ops-view-sourceprofiles.js} +5 -5
  30. package/dist_ts_web/elements/{ops-view-targetprofiles.d.ts → network/ops-view-targetprofiles.d.ts} +2 -2
  31. package/dist_ts_web/elements/{ops-view-targetprofiles.js → network/ops-view-targetprofiles.js} +6 -6
  32. package/dist_ts_web/elements/{ops-view-vpn.d.ts → network/ops-view-vpn.d.ts} +2 -2
  33. package/dist_ts_web/elements/{ops-view-vpn.js → network/ops-view-vpn.js} +6 -6
  34. package/dist_ts_web/elements/ops-dashboard.d.ts +8 -2
  35. package/dist_ts_web/elements/ops-dashboard.js +101 -83
  36. package/dist_ts_web/elements/overview/index.d.ts +2 -0
  37. package/dist_ts_web/elements/overview/index.js +3 -0
  38. package/dist_ts_web/elements/{ops-view-config.d.ts → overview/ops-view-config.d.ts} +2 -2
  39. package/dist_ts_web/elements/{ops-view-config.js → overview/ops-view-config.js} +9 -9
  40. package/dist_ts_web/elements/{ops-view-overview.d.ts → overview/ops-view-overview.d.ts} +2 -2
  41. package/dist_ts_web/elements/{ops-view-overview.js → overview/ops-view-overview.js} +4 -4
  42. package/dist_ts_web/elements/security/index.d.ts +3 -0
  43. package/dist_ts_web/elements/security/index.js +4 -0
  44. package/dist_ts_web/elements/security/ops-view-security-authentication.d.ts +13 -0
  45. package/dist_ts_web/elements/security/ops-view-security-authentication.js +157 -0
  46. package/dist_ts_web/elements/security/ops-view-security-blocked.d.ts +15 -0
  47. package/dist_ts_web/elements/security/ops-view-security-blocked.js +153 -0
  48. package/dist_ts_web/elements/security/ops-view-security-overview.d.ts +16 -0
  49. package/dist_ts_web/elements/security/ops-view-security-overview.js +205 -0
  50. package/dist_ts_web/router.d.ts +5 -3
  51. package/dist_ts_web/router.js +75 -17
  52. package/package.json +2 -2
  53. package/ts/00_commitinfo_data.ts +1 -1
  54. package/ts_web/00_commitinfo_data.ts +1 -1
  55. package/ts_web/appstate.ts +15 -42
  56. package/ts_web/elements/access/index.ts +1 -0
  57. package/ts_web/elements/{ops-view-apitokens.ts → access/ops-view-apitokens.ts} +3 -3
  58. package/ts_web/elements/email/index.ts +2 -0
  59. package/ts_web/elements/email/ops-view-email-security.ts +160 -0
  60. package/ts_web/elements/{ops-view-emails.ts → email/ops-view-emails.ts} +4 -4
  61. package/ts_web/elements/index.ts +6 -13
  62. package/ts_web/elements/network/index.ts +7 -0
  63. package/ts_web/elements/{ops-view-network.ts → network/ops-view-network-activity.ts} +43 -55
  64. package/ts_web/elements/{ops-view-networktargets.ts → network/ops-view-networktargets.ts} +4 -4
  65. package/ts_web/elements/{ops-view-remoteingress.ts → network/ops-view-remoteingress.ts} +4 -4
  66. package/ts_web/elements/{ops-view-routes.ts → network/ops-view-routes.ts} +4 -4
  67. package/ts_web/elements/{ops-view-sourceprofiles.ts → network/ops-view-sourceprofiles.ts} +4 -4
  68. package/ts_web/elements/{ops-view-targetprofiles.ts → network/ops-view-targetprofiles.ts} +5 -5
  69. package/ts_web/elements/{ops-view-vpn.ts → network/ops-view-vpn.ts} +5 -5
  70. package/ts_web/elements/ops-dashboard.ts +125 -90
  71. package/ts_web/elements/overview/index.ts +2 -0
  72. package/ts_web/elements/{ops-view-config.ts → overview/ops-view-config.ts} +8 -8
  73. package/ts_web/elements/{ops-view-overview.ts → overview/ops-view-overview.ts} +3 -3
  74. package/ts_web/elements/security/index.ts +3 -0
  75. package/ts_web/elements/security/ops-view-security-authentication.ts +121 -0
  76. package/ts_web/elements/security/ops-view-security-blocked.ts +118 -0
  77. package/ts_web/elements/security/ops-view-security-overview.ts +172 -0
  78. package/ts_web/router.ts +81 -17
  79. package/dist_ts_web/elements/ops-view-security.d.ts +0 -24
  80. package/dist_ts_web/elements/ops-view-security.js +0 -484
  81. package/ts_web/elements/ops-view-security.ts +0 -456
@@ -11,22 +11,45 @@ import {
11
11
  state,
12
12
  type TemplateResult
13
13
  } from '@design.estate/dees-element';
14
+ import type { IView } from '@design.estate/dees-catalog';
14
15
 
15
- // Import view components
16
- import { OpsViewOverview } from './ops-view-overview.js';
17
- import { OpsViewNetwork } from './ops-view-network.js';
18
- import { OpsViewEmails } from './ops-view-emails.js';
16
+ // Top-level / flat views
19
17
  import { OpsViewLogs } from './ops-view-logs.js';
20
- import { OpsViewConfig } from './ops-view-config.js';
21
- import { OpsViewRoutes } from './ops-view-routes.js';
22
- import { OpsViewApiTokens } from './ops-view-apitokens.js';
23
- import { OpsViewSecurity } from './ops-view-security.js';
24
18
  import { OpsViewCertificates } from './ops-view-certificates.js';
25
- import { OpsViewRemoteIngress } from './ops-view-remoteingress.js';
26
- import { OpsViewVpn } from './ops-view-vpn.js';
27
- import { OpsViewSourceProfiles } from './ops-view-sourceprofiles.js';
28
- import { OpsViewNetworkTargets } from './ops-view-networktargets.js';
29
- import { OpsViewTargetProfiles } from './ops-view-targetprofiles.js';
19
+
20
+ // Overview group
21
+ import { OpsViewOverview } from './overview/ops-view-overview.js';
22
+ import { OpsViewConfig } from './overview/ops-view-config.js';
23
+
24
+ // Network group
25
+ import { OpsViewNetworkActivity } from './network/ops-view-network-activity.js';
26
+ import { OpsViewRoutes } from './network/ops-view-routes.js';
27
+ import { OpsViewSourceProfiles } from './network/ops-view-sourceprofiles.js';
28
+ import { OpsViewNetworkTargets } from './network/ops-view-networktargets.js';
29
+ import { OpsViewTargetProfiles } from './network/ops-view-targetprofiles.js';
30
+ import { OpsViewRemoteIngress } from './network/ops-view-remoteingress.js';
31
+ import { OpsViewVpn } from './network/ops-view-vpn.js';
32
+
33
+ // Email group
34
+ import { OpsViewEmails } from './email/ops-view-emails.js';
35
+ import { OpsViewEmailSecurity } from './email/ops-view-email-security.js';
36
+
37
+ // Access group
38
+ import { OpsViewApiTokens } from './access/ops-view-apitokens.js';
39
+
40
+ // Security group
41
+ import { OpsViewSecurityOverview } from './security/ops-view-security-overview.js';
42
+ import { OpsViewSecurityBlocked } from './security/ops-view-security-blocked.js';
43
+ import { OpsViewSecurityAuthentication } from './security/ops-view-security-authentication.js';
44
+
45
+ /**
46
+ * Extended IView with explicit URL slug. Without an explicit `slug`, the URL
47
+ * slug is derived from `name.toLowerCase().replace(/\s+/g, '')`.
48
+ */
49
+ interface ITabbedView extends IView {
50
+ slug?: string;
51
+ subViews?: ITabbedView[];
52
+ }
30
53
 
31
54
  @customElement('ops-dashboard')
32
55
  export class OpsDashboard extends DeesElement {
@@ -37,6 +60,7 @@ export class OpsDashboard extends DeesElement {
37
60
 
38
61
  @state() accessor uiState: appstate.IUiState = {
39
62
  activeView: 'overview',
63
+ activeSubview: null,
40
64
  sidebarCollapsed: false,
41
65
  autoRefresh: true,
42
66
  refreshInterval: 1000,
@@ -49,27 +73,36 @@ export class OpsDashboard extends DeesElement {
49
73
  error: null,
50
74
  };
51
75
 
52
- // Store viewTabs as a property to maintain object references
53
- private viewTabs = [
76
+ // Store viewTabs as a property to maintain object references (used for === selectedView identity)
77
+ private viewTabs: ITabbedView[] = [
54
78
  {
55
79
  name: 'Overview',
56
80
  iconName: 'lucide:layoutDashboard',
57
- element: OpsViewOverview,
58
- },
59
- {
60
- name: 'Configuration',
61
- iconName: 'lucide:settings',
62
- element: OpsViewConfig,
81
+ subViews: [
82
+ { slug: 'stats', name: 'Stats', iconName: 'lucide:activity', element: OpsViewOverview },
83
+ { slug: 'configuration', name: 'Configuration', iconName: 'lucide:settings', element: OpsViewConfig },
84
+ ],
63
85
  },
64
86
  {
65
87
  name: 'Network',
66
88
  iconName: 'lucide:network',
67
- element: OpsViewNetwork,
89
+ subViews: [
90
+ { slug: 'activity', name: 'Network Activity', iconName: 'lucide:activity', element: OpsViewNetworkActivity },
91
+ { slug: 'routes', name: 'Routes', iconName: 'lucide:route', element: OpsViewRoutes },
92
+ { slug: 'sourceprofiles', name: 'Source Profiles', iconName: 'lucide:shieldCheck', element: OpsViewSourceProfiles },
93
+ { slug: 'networktargets', name: 'Network Targets', iconName: 'lucide:server', element: OpsViewNetworkTargets },
94
+ { slug: 'targetprofiles', name: 'Target Profiles', iconName: 'lucide:target', element: OpsViewTargetProfiles },
95
+ { slug: 'remoteingress', name: 'Remote Ingress', iconName: 'lucide:globe', element: OpsViewRemoteIngress },
96
+ { slug: 'vpn', name: 'VPN', iconName: 'lucide:shield', element: OpsViewVpn },
97
+ ],
68
98
  },
69
99
  {
70
- name: 'Emails',
100
+ name: 'Email',
71
101
  iconName: 'lucide:mail',
72
- element: OpsViewEmails,
102
+ subViews: [
103
+ { slug: 'log', name: 'Email Log', iconName: 'lucide:scrollText', element: OpsViewEmails },
104
+ { slug: 'security', name: 'Email Security', iconName: 'lucide:shieldCheck', element: OpsViewEmailSecurity },
105
+ ],
73
106
  },
74
107
  {
75
108
  name: 'Logs',
@@ -77,52 +110,48 @@ export class OpsDashboard extends DeesElement {
77
110
  element: OpsViewLogs,
78
111
  },
79
112
  {
80
- name: 'Routes',
81
- iconName: 'lucide:route',
82
- element: OpsViewRoutes,
83
- },
84
- {
85
- name: 'SourceProfiles',
86
- iconName: 'lucide:shieldCheck',
87
- element: OpsViewSourceProfiles,
88
- },
89
- {
90
- name: 'NetworkTargets',
91
- iconName: 'lucide:server',
92
- element: OpsViewNetworkTargets,
93
- },
94
- {
95
- name: 'TargetProfiles',
96
- iconName: 'lucide:target',
97
- element: OpsViewTargetProfiles,
98
- },
99
- {
100
- name: 'ApiTokens',
101
- iconName: 'lucide:key',
102
- element: OpsViewApiTokens,
113
+ name: 'Access',
114
+ iconName: 'lucide:keyRound',
115
+ subViews: [
116
+ { slug: 'apitokens', name: 'API Tokens', iconName: 'lucide:key', element: OpsViewApiTokens },
117
+ ],
103
118
  },
104
119
  {
105
120
  name: 'Security',
106
121
  iconName: 'lucide:shield',
107
- element: OpsViewSecurity,
122
+ subViews: [
123
+ { slug: 'overview', name: 'Overview', iconName: 'lucide:eye', element: OpsViewSecurityOverview },
124
+ { slug: 'blocked', name: 'Blocked IPs', iconName: 'lucide:shieldBan', element: OpsViewSecurityBlocked },
125
+ { slug: 'authentication', name: 'Authentication', iconName: 'lucide:lock', element: OpsViewSecurityAuthentication },
126
+ ],
108
127
  },
109
128
  {
110
129
  name: 'Certificates',
111
130
  iconName: 'lucide:badgeCheck',
112
131
  element: OpsViewCertificates,
113
132
  },
114
- {
115
- name: 'RemoteIngress',
116
- iconName: 'lucide:globe',
117
- element: OpsViewRemoteIngress,
118
- },
119
- {
120
- name: 'VPN',
121
- iconName: 'lucide:shield',
122
- element: OpsViewVpn,
123
- },
124
133
  ];
125
134
 
135
+ /** URL slug for a view (explicit `slug` field, or lowercased name with spaces stripped). */
136
+ private slugFor(view: ITabbedView): string {
137
+ return view.slug ?? view.name.toLowerCase().replace(/\s+/g, '');
138
+ }
139
+
140
+ /** Find the parent group of a subview, or undefined for top-level views. */
141
+ private findParent(view: ITabbedView): ITabbedView | undefined {
142
+ return this.viewTabs.find((v) => v.subViews?.includes(view));
143
+ }
144
+
145
+ /** Look up a view (or subview) by its URL slug pair. */
146
+ private findViewBySlug(viewSlug: string, subSlug: string | null): ITabbedView | undefined {
147
+ const top = this.viewTabs.find((v) => this.slugFor(v) === viewSlug);
148
+ if (!top) return undefined;
149
+ if (subSlug && top.subViews) {
150
+ return top.subViews.find((sv) => this.slugFor(sv) === subSlug) ?? top;
151
+ }
152
+ return top;
153
+ }
154
+
126
155
  private get globalMessages() {
127
156
  const messages: Array<{ id: string; type: string; message: string; dismissible?: boolean }> = [];
128
157
  const config = this.configState.config;
@@ -138,17 +167,19 @@ export class OpsDashboard extends DeesElement {
138
167
  }
139
168
 
140
169
  /**
141
- * Get the current view tab based on the UI state's activeView.
170
+ * Get the current view tab based on the UI state's activeView/activeSubview.
142
171
  * Used to pass the correct selectedView to dees-simple-appdash on initial render.
143
172
  */
144
- private get currentViewTab() {
145
- return this.viewTabs.find(t => t.name.toLowerCase() === this.uiState.activeView) || this.viewTabs[0];
173
+ private get currentViewTab(): ITabbedView {
174
+ return (
175
+ this.findViewBySlug(this.uiState.activeView, this.uiState.activeSubview) ?? this.viewTabs[0]
176
+ );
146
177
  }
147
178
 
148
179
  constructor() {
149
180
  super();
150
181
  document.title = 'DCRouter OpsServer';
151
-
182
+
152
183
  // Subscribe to login state
153
184
  const loginSubscription = appstate.loginStatePart
154
185
  .select((stateArg) => stateArg)
@@ -161,7 +192,7 @@ export class OpsDashboard extends DeesElement {
161
192
  }
162
193
  });
163
194
  this.rxSubscriptions.push(loginSubscription);
164
-
195
+
165
196
  // Subscribe to config state (for global warnings)
166
197
  const configSubscription = appstate.configStatePart
167
198
  .select((stateArg) => stateArg)
@@ -176,38 +207,27 @@ export class OpsDashboard extends DeesElement {
176
207
  .subscribe((uiState) => {
177
208
  this.uiState = uiState;
178
209
  // Sync appdash view when state changes (e.g., from URL navigation)
179
- this.syncAppdashView(uiState.activeView);
210
+ this.syncAppdashView(uiState.activeView, uiState.activeSubview);
180
211
  });
181
212
  this.rxSubscriptions.push(uiSubscription);
182
213
  }
183
214
 
184
215
  /**
185
216
  * Sync the dees-simple-appdash view selection with the current state.
186
- * This is needed when the URL changes and we need to update the UI.
217
+ * This is needed when the URL changes externally (back/forward, deep link).
187
218
  */
188
- private syncAppdashView(viewName: string): void {
219
+ private syncAppdashView(viewSlug: string, subviewSlug: string | null): void {
189
220
  const appDash = this.shadowRoot?.querySelector('dees-simple-appdash') as any;
190
221
  if (!appDash) return;
191
222
 
192
- const targetTab = this.viewTabs.find(t => t.name.toLowerCase() === viewName);
193
- if (!targetTab) return;
223
+ const targetView = this.findViewBySlug(viewSlug, subviewSlug);
224
+ if (!targetView) return;
194
225
 
195
- // Check if we need to switch (avoid unnecessary updates)
196
- if (appDash.selectedView === targetTab) return;
226
+ if (appDash.selectedView === targetView) return;
197
227
 
198
- // Update the selected view programmatically
199
- appDash.selectedView = targetTab;
200
-
201
- // Update the displayed content
202
- const content = appDash.shadowRoot?.querySelector('.appcontent');
203
- if (content) {
204
- if (appDash.currentView) {
205
- appDash.currentView.remove();
206
- }
207
- const view = new targetTab.element();
208
- content.appendChild(view);
209
- appDash.currentView = view;
210
- }
228
+ // Use loadView to update both selectedView and the mounted element.
229
+ // It will dispatch view-select; our handler skips when state already matches.
230
+ appDash.loadView(targetView);
211
231
  }
212
232
 
213
233
  public static styles = [
@@ -249,7 +269,7 @@ export class OpsDashboard extends DeesElement {
249
269
  public async firstUpdated() {
250
270
  const simpleLogin = this.shadowRoot!.querySelector('dees-simple-login') as any;
251
271
  simpleLogin.addEventListener('login', (e: Event) => {
252
- // Handle logout event
272
+ // Handle login event
253
273
  const detail = (e as CustomEvent).detail;
254
274
  this.login(detail.data.username, detail.data.password);
255
275
  });
@@ -258,9 +278,24 @@ export class OpsDashboard extends DeesElement {
258
278
  const appDash = this.shadowRoot!.querySelector('dees-simple-appdash');
259
279
  if (appDash) {
260
280
  appDash.addEventListener('view-select', (e: Event) => {
261
- const viewName = (e as CustomEvent).detail.view.name.toLowerCase();
262
- // Use router for navigation instead of direct state update
263
- appRouter.navigateToView(viewName);
281
+ const view = (e as CustomEvent).detail.view as ITabbedView;
282
+ const parent = this.findParent(view);
283
+ const currentState = appstate.uiStatePart.getState();
284
+ if (parent) {
285
+ const parentSlug = this.slugFor(parent);
286
+ const subSlug = this.slugFor(view);
287
+ // Skip if already on this exact subview — preserves URL on initial mount
288
+ if (currentState?.activeView === parentSlug && currentState?.activeSubview === subSlug) {
289
+ return;
290
+ }
291
+ appRouter.navigateToView(parentSlug, subSlug);
292
+ } else {
293
+ const slug = this.slugFor(view);
294
+ if (currentState?.activeView === slug && !currentState?.activeSubview) {
295
+ return;
296
+ }
297
+ appRouter.navigateToView(slug);
298
+ }
264
299
  });
265
300
 
266
301
  // Handle logout event
@@ -306,12 +341,12 @@ export class OpsDashboard extends DeesElement {
306
341
  const simpleLogin = this.shadowRoot!.querySelector('dees-simple-login') as any;
307
342
  const form = simpleLogin.shadowRoot!.querySelector('dees-form') as any;
308
343
  form.setStatus('pending', 'Logging in...');
309
-
344
+
310
345
  const state = await appstate.loginStatePart.dispatchAction(appstate.loginAction, {
311
346
  username,
312
347
  password,
313
348
  });
314
-
349
+
315
350
  if (state.identity) {
316
351
  console.log('Login successful');
317
352
  this.loginState = state;
@@ -325,4 +360,4 @@ export class OpsDashboard extends DeesElement {
325
360
  form!.reset();
326
361
  }
327
362
  }
328
- }
363
+ }
@@ -0,0 +1,2 @@
1
+ export * from './ops-view-overview.js';
2
+ export * from './ops-view-config.js';
@@ -1,7 +1,7 @@
1
- import * as plugins from '../plugins.js';
2
- import * as shared from './shared/index.js';
3
- import * as appstate from '../appstate.js';
4
- import { appRouter } from '../router.js';
1
+ import * as plugins from '../../plugins.js';
2
+ import * as shared from '../shared/index.js';
3
+ import * as appstate from '../../appstate.js';
4
+ import { appRouter } from '../../router.js';
5
5
 
6
6
  import {
7
7
  DeesElement,
@@ -86,7 +86,7 @@ export class OpsViewConfig extends DeesElement {
86
86
  infoText="This view displays the current running configuration. DcRouter is configured through code or remote management."
87
87
  @navigate=${(e: CustomEvent) => {
88
88
  if (e.detail?.view) {
89
- appRouter.navigateToView(e.detail.view);
89
+ appRouter.navigateToView(e.detail.view, e.detail.subview);
90
90
  }
91
91
  }}
92
92
  >
@@ -149,7 +149,7 @@ export class OpsViewConfig extends DeesElement {
149
149
  }
150
150
 
151
151
  const actions: IConfigSectionAction[] = [
152
- { label: 'View Routes', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'routes' } },
152
+ { label: 'View Routes', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'network', subview: 'routes' } },
153
153
  ];
154
154
 
155
155
  return html`
@@ -181,7 +181,7 @@ export class OpsViewConfig extends DeesElement {
181
181
  }
182
182
 
183
183
  const actions: IConfigSectionAction[] = [
184
- { label: 'View Emails', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'emails' } },
184
+ { label: 'View Emails', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'email', subview: 'log' } },
185
185
  ];
186
186
 
187
187
  return html`
@@ -305,7 +305,7 @@ export class OpsViewConfig extends DeesElement {
305
305
  ];
306
306
 
307
307
  const actions: IConfigSectionAction[] = [
308
- { label: 'View Remote Ingress', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'remoteingress' } },
308
+ { label: 'View Remote Ingress', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'network', subview: 'remoteingress' } },
309
309
  ];
310
310
 
311
311
  return html`
@@ -1,6 +1,6 @@
1
- import * as plugins from '../plugins.js';
2
- import * as shared from './shared/index.js';
3
- import * as appstate from '../appstate.js';
1
+ import * as plugins from '../../plugins.js';
2
+ import * as shared from '../shared/index.js';
3
+ import * as appstate from '../../appstate.js';
4
4
 
5
5
  import {
6
6
  DeesElement,
@@ -0,0 +1,3 @@
1
+ export * from './ops-view-security-overview.js';
2
+ export * from './ops-view-security-blocked.js';
3
+ export * from './ops-view-security-authentication.js';
@@ -0,0 +1,121 @@
1
+ import * as appstate from '../../appstate.js';
2
+ import { viewHostCss } from '../shared/css.js';
3
+
4
+ import {
5
+ DeesElement,
6
+ customElement,
7
+ html,
8
+ state,
9
+ css,
10
+ cssManager,
11
+ type TemplateResult,
12
+ } from '@design.estate/dees-element';
13
+ import { type IStatsTile } from '@design.estate/dees-catalog';
14
+
15
+ declare global {
16
+ interface HTMLElementTagNameMap {
17
+ 'ops-view-security-authentication': OpsViewSecurityAuthentication;
18
+ }
19
+ }
20
+
21
+ @customElement('ops-view-security-authentication')
22
+ export class OpsViewSecurityAuthentication extends DeesElement {
23
+ @state()
24
+ accessor statsState: appstate.IStatsState = appstate.statsStatePart.getState()!;
25
+
26
+ constructor() {
27
+ super();
28
+ const sub = appstate.statsStatePart
29
+ .select((s) => s)
30
+ .subscribe((s) => {
31
+ this.statsState = s;
32
+ });
33
+ this.rxSubscriptions.push(sub);
34
+ }
35
+
36
+ public static styles = [
37
+ cssManager.defaultStyles,
38
+ viewHostCss,
39
+ css`
40
+ h2 {
41
+ margin: 32px 0 16px 0;
42
+ font-size: 24px;
43
+ font-weight: 600;
44
+ color: ${cssManager.bdTheme('#333', '#ccc')};
45
+ }
46
+ dees-statsgrid {
47
+ margin-bottom: 32px;
48
+ }
49
+ `,
50
+ ];
51
+
52
+ public render(): TemplateResult {
53
+ const metrics = this.statsState.securityMetrics;
54
+
55
+ if (!metrics) {
56
+ return html`
57
+ <div class="loadingMessage">
58
+ <p>Loading security metrics...</p>
59
+ </div>
60
+ `;
61
+ }
62
+
63
+ // Derive auth events from recentEvents
64
+ const allEvents: any[] = metrics.recentEvents || [];
65
+ const authEvents = allEvents.filter((evt: any) => evt.type === 'authentication');
66
+ const successfulLogins = authEvents.filter((evt: any) => evt.success === true).length;
67
+
68
+ const tiles: IStatsTile[] = [
69
+ {
70
+ id: 'authFailures',
71
+ title: 'Authentication Failures',
72
+ value: metrics.authenticationFailures,
73
+ type: 'number',
74
+ icon: 'lucide:LockOpen',
75
+ color: metrics.authenticationFailures > 10 ? '#ef4444' : '#f59e0b',
76
+ description: 'Failed authentication attempts today',
77
+ },
78
+ {
79
+ id: 'successfulLogins',
80
+ title: 'Successful Logins',
81
+ value: successfulLogins,
82
+ type: 'number',
83
+ icon: 'lucide:Lock',
84
+ color: '#22c55e',
85
+ description: 'Successful logins today',
86
+ },
87
+ ];
88
+
89
+ // Map auth events to login history table data
90
+ const loginHistory = authEvents.map((evt: any) => ({
91
+ timestamp: evt.timestamp,
92
+ username: evt.details?.username || 'unknown',
93
+ ipAddress: evt.ipAddress || 'unknown',
94
+ success: evt.success ?? false,
95
+ reason: evt.success ? '' : evt.message || 'Authentication failed',
96
+ }));
97
+
98
+ return html`
99
+ <dees-heading level="hr">Authentication</dees-heading>
100
+
101
+ <dees-statsgrid
102
+ .tiles=${tiles}
103
+ .minTileWidth=${200}
104
+ ></dees-statsgrid>
105
+
106
+ <h2>Recent Login Attempts</h2>
107
+ <dees-table
108
+ .heading1=${'Login History'}
109
+ .heading2=${'Recent authentication attempts'}
110
+ .data=${loginHistory}
111
+ .displayFunction=${(item) => ({
112
+ 'Time': new Date(item.timestamp).toLocaleString(),
113
+ 'Username': item.username,
114
+ 'IP Address': item.ipAddress,
115
+ 'Status': item.success ? 'Success' : 'Failed',
116
+ 'Reason': item.reason || '-',
117
+ })}
118
+ ></dees-table>
119
+ `;
120
+ }
121
+ }
@@ -0,0 +1,118 @@
1
+ import * as appstate from '../../appstate.js';
2
+ import { viewHostCss } from '../shared/css.js';
3
+
4
+ import {
5
+ DeesElement,
6
+ customElement,
7
+ html,
8
+ state,
9
+ css,
10
+ cssManager,
11
+ type TemplateResult,
12
+ } from '@design.estate/dees-element';
13
+ import { type IStatsTile } from '@design.estate/dees-catalog';
14
+
15
+ declare global {
16
+ interface HTMLElementTagNameMap {
17
+ 'ops-view-security-blocked': OpsViewSecurityBlocked;
18
+ }
19
+ }
20
+
21
+ @customElement('ops-view-security-blocked')
22
+ export class OpsViewSecurityBlocked extends DeesElement {
23
+ @state()
24
+ accessor statsState: appstate.IStatsState = appstate.statsStatePart.getState()!;
25
+
26
+ constructor() {
27
+ super();
28
+ const sub = appstate.statsStatePart
29
+ .select((s) => s)
30
+ .subscribe((s) => {
31
+ this.statsState = s;
32
+ });
33
+ this.rxSubscriptions.push(sub);
34
+ }
35
+
36
+ public static styles = [
37
+ cssManager.defaultStyles,
38
+ viewHostCss,
39
+ css`
40
+ dees-statsgrid {
41
+ margin-bottom: 32px;
42
+ }
43
+ `,
44
+ ];
45
+
46
+ public render(): TemplateResult {
47
+ const metrics = this.statsState.securityMetrics;
48
+
49
+ if (!metrics) {
50
+ return html`
51
+ <div class="loadingMessage">
52
+ <p>Loading security metrics...</p>
53
+ </div>
54
+ `;
55
+ }
56
+
57
+ const blockedIPs: string[] = metrics.blockedIPs || [];
58
+
59
+ const tiles: IStatsTile[] = [
60
+ {
61
+ id: 'totalBlocked',
62
+ title: 'Blocked IPs',
63
+ value: blockedIPs.length,
64
+ type: 'number',
65
+ icon: 'lucide:ShieldBan',
66
+ color: blockedIPs.length > 0 ? '#ef4444' : '#22c55e',
67
+ description: 'Currently blocked addresses',
68
+ },
69
+ ];
70
+
71
+ return html`
72
+ <dees-heading level="hr">Blocked IPs</dees-heading>
73
+
74
+ <dees-statsgrid
75
+ .tiles=${tiles}
76
+ .minTileWidth=${200}
77
+ ></dees-statsgrid>
78
+
79
+ <dees-table
80
+ .heading1=${'Blocked IP Addresses'}
81
+ .heading2=${'IPs blocked due to suspicious activity'}
82
+ .data=${blockedIPs.map((ip) => ({ ip }))}
83
+ .displayFunction=${(item) => ({
84
+ 'IP Address': item.ip,
85
+ 'Reason': 'Suspicious activity',
86
+ })}
87
+ .dataActions=${[
88
+ {
89
+ name: 'Unblock',
90
+ iconName: 'lucide:shield-off',
91
+ type: ['contextmenu' as const],
92
+ actionFunc: async (item) => {
93
+ await this.unblockIP(item.ip);
94
+ },
95
+ },
96
+ {
97
+ name: 'Clear All',
98
+ iconName: 'lucide:trash-2',
99
+ type: ['header' as const],
100
+ actionFunc: async () => {
101
+ await this.clearBlockedIPs();
102
+ },
103
+ },
104
+ ]}
105
+ ></dees-table>
106
+ `;
107
+ }
108
+
109
+ private async clearBlockedIPs() {
110
+ // SmartProxy manages IP blocking — not yet exposed via API
111
+ alert('Clearing blocked IPs is not yet supported from the UI.');
112
+ }
113
+
114
+ private async unblockIP(ip: string) {
115
+ // SmartProxy manages IP blocking — not yet exposed via API
116
+ alert(`Unblocking IP ${ip} is not yet supported from the UI.`);
117
+ }
118
+ }