@serve.zone/dcrouter 18.1.0 → 18.1.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 (47) hide show
  1. package/deno.json +1 -1
  2. package/dist_serve/bundle.js +8 -8
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/classes.dcrouter.d.ts +4 -1
  5. package/dist_ts/classes.dcrouter.js +30 -6
  6. package/dist_ts/db/documents/classes.authentication-event.doc.d.ts +25 -0
  7. package/dist_ts/db/documents/classes.authentication-event.doc.js +273 -0
  8. package/dist_ts/db/documents/classes.email-traffic-bucket.doc.d.ts +25 -0
  9. package/dist_ts/db/documents/classes.email-traffic-bucket.doc.js +200 -0
  10. package/dist_ts/db/documents/index.d.ts +2 -0
  11. package/dist_ts/db/documents/index.js +3 -1
  12. package/dist_ts/email/classes.accepted-email-spool.d.ts +1 -0
  13. package/dist_ts/email/classes.accepted-email-spool.js +9 -1
  14. package/dist_ts/monitoring/classes.metricsmanager.d.ts +19 -0
  15. package/dist_ts/monitoring/classes.metricsmanager.js +240 -44
  16. package/dist_ts/opsserver/handlers/admin.handler.d.ts +1 -0
  17. package/dist_ts/opsserver/handlers/admin.handler.js +61 -14
  18. package/dist_ts/opsserver/handlers/security.handler.js +21 -3
  19. package/dist_ts/opsserver/handlers/stats.handler.js +27 -10
  20. package/dist_ts/security/classes.authentication-event-manager.d.ts +35 -0
  21. package/dist_ts/security/classes.authentication-event-manager.js +185 -0
  22. package/dist_ts/security/index.d.ts +1 -0
  23. package/dist_ts/security/index.js +2 -1
  24. package/dist_ts_interfaces/data/stats.d.ts +16 -0
  25. package/dist_ts_migrations/index.js +74 -15
  26. package/dist_ts_web/00_commitinfo_data.js +1 -1
  27. package/dist_ts_web/elements/overview/ops-view-overview.js +3 -2
  28. package/dist_ts_web/elements/security/ops-view-security-authentication.js +20 -13
  29. package/dist_ts_web/elements/security/ops-view-security-overview.js +8 -11
  30. package/package.json +1 -1
  31. package/readme.md +8 -0
  32. package/ts/00_commitinfo_data.ts +1 -1
  33. package/ts/classes.dcrouter.ts +31 -5
  34. package/ts/db/documents/classes.authentication-event.doc.ts +248 -0
  35. package/ts/db/documents/classes.email-traffic-bucket.doc.ts +161 -0
  36. package/ts/db/documents/index.ts +2 -0
  37. package/ts/email/classes.accepted-email-spool.ts +8 -0
  38. package/ts/monitoring/classes.metricsmanager.ts +287 -50
  39. package/ts/opsserver/handlers/admin.handler.ts +74 -15
  40. package/ts/opsserver/handlers/security.handler.ts +23 -2
  41. package/ts/opsserver/handlers/stats.handler.ts +29 -9
  42. package/ts/security/classes.authentication-event-manager.ts +221 -0
  43. package/ts/security/index.ts +1 -0
  44. package/ts_web/00_commitinfo_data.ts +1 -1
  45. package/ts_web/elements/overview/ops-view-overview.ts +2 -1
  46. package/ts_web/elements/security/ops-view-security-authentication.ts +20 -13
  47. package/ts_web/elements/security/ops-view-security-overview.ts +7 -12
@@ -66,10 +66,10 @@ export class StatsHandler {
66
66
  stats: {
67
67
  sent: stats.sentToday,
68
68
  received: stats.receivedToday,
69
- bounced: Math.floor(stats.sentToday * stats.bounceRate / 100),
69
+ bounced: stats.bouncedToday,
70
70
  queued: stats.queueSize,
71
- failed: 0,
72
- averageDeliveryTime: 0,
71
+ failed: stats.failedToday,
72
+ averageDeliveryTime: stats.averageDeliveryTime,
73
73
  deliveryRate: stats.deliveryRate,
74
74
  bounceRate: stats.bounceRate,
75
75
  },
@@ -219,10 +219,10 @@ export class StatsHandler {
219
219
  metrics.email = {
220
220
  sent: stats.sentToday,
221
221
  received: stats.receivedToday,
222
- bounced: Math.floor(stats.sentToday * stats.bounceRate / 100),
222
+ bounced: stats.bouncedToday,
223
223
  queued: stats.queueSize,
224
- failed: 0,
225
- averageDeliveryTime: 0,
224
+ failed: stats.failedToday,
225
+ averageDeliveryTime: stats.averageDeliveryTime,
226
226
  deliveryRate: stats.deliveryRate,
227
227
  bounceRate: stats.bounceRate,
228
228
  timeSeries,
@@ -256,7 +256,15 @@ export class StatsHandler {
256
256
 
257
257
  if (sections.security && this.opsServerRef.dcRouterRef.metricsManager) {
258
258
  promises.push(
259
- this.opsServerRef.dcRouterRef.metricsManager.getSecurityStats().then(stats => {
259
+ Promise.all([
260
+ this.opsServerRef.dcRouterRef.metricsManager.getSecurityStats(),
261
+ this.opsServerRef.dcRouterRef.authenticationEventManager
262
+ ? this.opsServerRef.dcRouterRef.authenticationEventManager.getWindowStats()
263
+ : Promise.resolve({
264
+ windowStart: Date.now() - 86_400_000,
265
+ successes: 0, failures: 0, events: [],
266
+ }),
267
+ ]).then(([stats, authentication]) => {
260
268
  // Get recent events from the SecurityLogger singleton
261
269
  const securityLogger = SecurityLogger.getInstance();
262
270
  const recentEvents = securityLogger.getRecentEvents(50).map((evt) => ({
@@ -276,7 +284,10 @@ export class StatsHandler {
276
284
  spamDetected: stats.spamDetected,
277
285
  malwareDetected: stats.malwareDetected,
278
286
  phishingDetected: stats.phishingDetected,
279
- authenticationFailures: stats.authFailures,
287
+ authenticationFailures: authentication.failures,
288
+ authenticationSuccesses: authentication.successes,
289
+ authenticationWindowStart: authentication.windowStart,
290
+ authenticationEvents: authentication.events,
280
291
  suspiciousActivities: stats.totalThreatsBlocked,
281
292
  recentEvents,
282
293
  };
@@ -463,9 +474,12 @@ export class StatsHandler {
463
474
  sentToday: number;
464
475
  receivedToday: number;
465
476
  bounceRate: number;
477
+ failedToday: number;
478
+ bouncedToday: number;
466
479
  deliveryRate: number;
467
480
  queueSize: number;
468
481
  domainBreakdown?: { [domain: string]: interfaces.data.IEmailStats };
482
+ averageDeliveryTime: number;
469
483
  }> {
470
484
  // Get metrics from MetricsManager if available
471
485
  if (this.opsServerRef.dcRouterRef.metricsManager) {
@@ -474,8 +488,11 @@ export class StatsHandler {
474
488
  sentToday: emailStats.sentToday,
475
489
  receivedToday: emailStats.receivedToday,
476
490
  bounceRate: emailStats.bounceRate,
491
+ failedToday: emailStats.failedToday,
492
+ bouncedToday: emailStats.bouncedToday,
477
493
  deliveryRate: emailStats.deliveryRate,
478
494
  queueSize: emailStats.queueSize,
495
+ averageDeliveryTime: emailStats.averageDeliveryTime,
479
496
  };
480
497
  }
481
498
 
@@ -484,8 +501,11 @@ export class StatsHandler {
484
501
  sentToday: 0,
485
502
  receivedToday: 0,
486
503
  bounceRate: 0,
487
- deliveryRate: 100,
504
+ deliveryRate: 1,
505
+ failedToday: 0,
506
+ bouncedToday: 0,
488
507
  queueSize: 0,
508
+ averageDeliveryTime: 0,
489
509
  };
490
510
  }
491
511
 
@@ -0,0 +1,221 @@
1
+ import * as plugins from '../plugins.js';
2
+ import { logger } from '../logger.js';
3
+ import type { DcRouter } from '../classes.dcrouter.js';
4
+ import { AuthenticationEventDoc } from '../db/documents/classes.authentication-event.doc.js';
5
+ import type {
6
+ IAuthenticationEvent,
7
+ TAuthenticationFailureReason,
8
+ TAuthenticationSource,
9
+ TResolvedAuthenticationSource,
10
+ } from '../../ts_interfaces/data/stats.js';
11
+ import {
12
+ SecurityEventType,
13
+ SecurityLogLevel,
14
+ SecurityLogger,
15
+ } from './classes.securitylogger.js';
16
+
17
+ const MAX_IN_MEMORY_EVENTS = 10_000;
18
+ const MAX_PERSIST_BATCH_SIZE = 500;
19
+
20
+ export interface IAuthenticationWindowStats {
21
+ windowStart: number;
22
+ successes: number;
23
+ failures: number;
24
+ events: IAuthenticationEvent[];
25
+ }
26
+
27
+ export class AuthenticationEventManager {
28
+ private pendingEvents = new Map<string, IAuthenticationEvent>();
29
+ private recentEvents = new Map<string, IAuthenticationEvent>();
30
+ private flushInFlight?: Promise<void>;
31
+ private pruneInFlight?: Promise<number>;
32
+ private windowStatsInFlight = new Map<string, Promise<IAuthenticationWindowStats>>();
33
+
34
+ constructor(private dcRouterRef: DcRouter) {}
35
+
36
+ private canPersist(): boolean {
37
+ return this.dcRouterRef.options.dbConfig?.enabled !== false
38
+ && Boolean(this.dcRouterRef.dcRouterDb?.isReady());
39
+ }
40
+
41
+ private rememberEvent(eventArg: IAuthenticationEvent): void {
42
+ this.pendingEvents.set(eventArg.id, eventArg);
43
+ this.recentEvents.set(eventArg.id, eventArg);
44
+ while (this.pendingEvents.size > MAX_IN_MEMORY_EVENTS) {
45
+ const oldestId = this.pendingEvents.keys().next().value as string | undefined;
46
+ if (!oldestId) break;
47
+ this.pendingEvents.delete(oldestId);
48
+ logger.log('error', 'Authentication event persistence buffer reached its limit; oldest event dropped');
49
+ }
50
+ while (this.recentEvents.size > MAX_IN_MEMORY_EVENTS) {
51
+ const oldestId = this.recentEvents.keys().next().value as string | undefined;
52
+ if (!oldestId) break;
53
+ this.recentEvents.delete(oldestId);
54
+ }
55
+ }
56
+
57
+ private async flushBufferedUnlocked(): Promise<void> {
58
+ if (!this.canPersist() || this.pendingEvents.size === 0) return;
59
+ const events = [...this.pendingEvents.values()];
60
+ for (let offset = 0; offset < events.length; offset += MAX_PERSIST_BATCH_SIZE) {
61
+ const batch = events.slice(offset, offset + MAX_PERSIST_BATCH_SIZE);
62
+ await AuthenticationEventDoc.upsertMany(batch);
63
+ for (const event of batch) {
64
+ if (this.pendingEvents.get(event.id) === event) {
65
+ this.pendingEvents.delete(event.id);
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ public async recordAttempt(optionsArg: {
72
+ username: string;
73
+ userId?: string;
74
+ success: boolean;
75
+ requestedAuthSource: TAuthenticationSource;
76
+ resolvedAuthSource?: TResolvedAuthenticationSource;
77
+ failureReason?: TAuthenticationFailureReason;
78
+ }): Promise<IAuthenticationEvent> {
79
+ const event: IAuthenticationEvent = {
80
+ id: `auth-${plugins.uuid.v4()}`,
81
+ timestamp: Date.now(),
82
+ username: String(optionsArg.username || '').trim().toLowerCase() || 'unknown',
83
+ success: optionsArg.success,
84
+ requestedAuthSource: optionsArg.requestedAuthSource,
85
+ ...(optionsArg.userId ? { userId: optionsArg.userId } : {}),
86
+ ...(optionsArg.resolvedAuthSource
87
+ ? { resolvedAuthSource: optionsArg.resolvedAuthSource }
88
+ : {}),
89
+ ...(optionsArg.failureReason ? { failureReason: optionsArg.failureReason } : {}),
90
+ };
91
+ // Buffer before any asynchronous persistence so authentication cannot be
92
+ // held behind a slow database and the bounded drop policy remains effective.
93
+ this.rememberEvent(event);
94
+ try {
95
+ SecurityLogger.getInstance().logEvent({
96
+ level: event.success ? SecurityLogLevel.INFO : SecurityLogLevel.WARN,
97
+ type: SecurityEventType.AUTHENTICATION,
98
+ message: event.success ? 'Authentication succeeded' : 'Authentication failed',
99
+ userId: event.userId,
100
+ success: event.success,
101
+ details: { authenticationEvent: event },
102
+ });
103
+ } catch (error) {
104
+ logger.log('warn', `Unable to mirror authentication event to the security log: ${(error as Error).message}`);
105
+ }
106
+ return event;
107
+ }
108
+
109
+ public flushBuffered(): Promise<void> {
110
+ if (this.flushInFlight) return this.flushInFlight;
111
+ const run = this.flushBufferedUnlocked();
112
+ this.flushInFlight = run;
113
+ run.then(
114
+ () => { if (this.flushInFlight === run) this.flushInFlight = undefined; },
115
+ () => { if (this.flushInFlight === run) this.flushInFlight = undefined; },
116
+ );
117
+ return run;
118
+ }
119
+
120
+ public pruneBefore(cutoffArg: number): Promise<number> {
121
+ if (this.pruneInFlight) return this.pruneInFlight;
122
+ const run = this.canPersist()
123
+ ? AuthenticationEventDoc.pruneBefore(cutoffArg)
124
+ : Promise.resolve(0);
125
+ this.pruneInFlight = run;
126
+ run.then(
127
+ () => { if (this.pruneInFlight === run) this.pruneInFlight = undefined; },
128
+ () => { if (this.pruneInFlight === run) this.pruneInFlight = undefined; },
129
+ );
130
+ return run;
131
+ }
132
+
133
+ private getInMemoryWindow(cutoffArg: number, limitArg: number): IAuthenticationWindowStats {
134
+ const events = [...this.recentEvents.values()]
135
+ .filter((event) => event.timestamp >= cutoffArg)
136
+ .sort((a, b) => b.timestamp - a.timestamp || b.id.localeCompare(a.id));
137
+ return {
138
+ windowStart: cutoffArg,
139
+ successes: events.filter((event) => event.success).length,
140
+ failures: events.filter((event) => !event.success).length,
141
+ events: events.slice(0, limitArg),
142
+ };
143
+ }
144
+
145
+ private async getWindowStatsUnlocked(
146
+ windowMsArg = 24 * 60 * 60 * 1000,
147
+ limitArg = 100,
148
+ ): Promise<IAuthenticationWindowStats> {
149
+ const cutoff = Date.now() - windowMsArg;
150
+ if (!this.canPersist()) {
151
+ return this.getInMemoryWindow(cutoff, limitArg);
152
+ }
153
+
154
+ try {
155
+ await this.flushBuffered();
156
+ } catch (error) {
157
+ logger.log('warn', `Unable to flush authentication event buffer: ${(error as Error).message}`);
158
+ }
159
+
160
+ try {
161
+ const durable = await AuthenticationEventDoc.getWindowSummary(cutoff, limitArg);
162
+ const buffered = [...this.pendingEvents.values()]
163
+ .filter((event) => event.timestamp >= cutoff);
164
+ const existingIds = new Set<string>();
165
+ const bufferedIds = buffered.map((event) => event.id);
166
+ for (let offset = 0; offset < bufferedIds.length; offset += MAX_PERSIST_BATCH_SIZE) {
167
+ const existingBatch = await AuthenticationEventDoc.findExistingIds(
168
+ bufferedIds.slice(offset, offset + MAX_PERSIST_BATCH_SIZE),
169
+ );
170
+ existingBatch.forEach((id) => existingIds.add(id));
171
+ }
172
+ const unpersisted = buffered.filter((event) => !existingIds.has(event.id));
173
+ const mergedById = new Map<string, IAuthenticationEvent>();
174
+ for (const event of durable.events) mergedById.set(event.id, event);
175
+ for (const event of unpersisted) {
176
+ if (!mergedById.has(event.id)) mergedById.set(event.id, event);
177
+ }
178
+ const mergedEvents = [...mergedById.values()]
179
+ .sort((a, b) => b.timestamp - a.timestamp || b.id.localeCompare(a.id))
180
+ .slice(0, limitArg);
181
+ return {
182
+ windowStart: cutoff,
183
+ successes: durable.successes + unpersisted.filter((event) => event.success).length,
184
+ failures: durable.failures + unpersisted.filter((event) => !event.success).length,
185
+ events: mergedEvents,
186
+ };
187
+ } catch (error) {
188
+ logger.log('warn', `Unable to query durable authentication events: ${(error as Error).message}`);
189
+ return this.getInMemoryWindow(cutoff, limitArg);
190
+ }
191
+ }
192
+
193
+ public getWindowStats(
194
+ windowMsArg = 24 * 60 * 60 * 1000,
195
+ limitArg = 100,
196
+ ): Promise<IAuthenticationWindowStats> {
197
+ if (!Number.isSafeInteger(windowMsArg) || windowMsArg <= 0) {
198
+ return Promise.reject(new Error('Authentication window must be a positive integer'));
199
+ }
200
+ if (!Number.isSafeInteger(limitArg) || limitArg <= 0 || limitArg > 500) {
201
+ return Promise.reject(new Error('Authentication event limit must be from 1 to 500'));
202
+ }
203
+ const key = `${windowMsArg}:${limitArg}`;
204
+ const existing = this.windowStatsInFlight.get(key);
205
+ if (existing) return existing;
206
+ const run = this.getWindowStatsUnlocked(windowMsArg, limitArg);
207
+ this.windowStatsInFlight.set(key, run);
208
+ run.then(
209
+ () => { if (this.windowStatsInFlight.get(key) === run) this.windowStatsInFlight.delete(key); },
210
+ () => { if (this.windowStatsInFlight.get(key) === run) this.windowStatsInFlight.delete(key); },
211
+ );
212
+ return run;
213
+ }
214
+
215
+ /** Test-only reset for the DcRouter-owned in-process buffers. */
216
+ public async resetForTests(): Promise<void> {
217
+ await this.flushInFlight?.catch(() => undefined);
218
+ this.pendingEvents.clear();
219
+ this.recentEvents.clear();
220
+ }
221
+ }
@@ -26,3 +26,4 @@ export {
26
26
  type IRemoteIngressFirewallSnapshot,
27
27
  } from './classes.security-policy-manager.js';
28
28
  export * from './classes.route-policy-augmenter.js';
29
+ export * from './classes.authentication-event-manager.js';
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '18.1.0',
6
+ version: '18.1.1',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -119,7 +119,7 @@ export class OpsViewOverview extends DeesElement {
119
119
  <dees-heading level="hr">Activity Charts</dees-heading>
120
120
  <div class="chartGrid">
121
121
  <dees-chart-area
122
- .label=${'Email Traffic (24h)'}
122
+ .label=${'Email Traffic — Last 24 Hours'}
123
123
  .series=${this.getEmailTrafficSeries()}
124
124
  .realtimeMode=${true}
125
125
  .rollingWindow=${86400000}
@@ -506,6 +506,7 @@ export class OpsViewOverview extends DeesElement {
506
506
  return [
507
507
  { name: 'Sent', color: '#22c55e', data: (ts.sent || []).map((p: any) => ({ x: p.timestamp, y: p.value })) },
508
508
  { name: 'Received', color: '#3b82f6', data: (ts.received || []).map((p: any) => ({ x: p.timestamp, y: p.value })) },
509
+ { name: 'Failed', color: '#ef4444', data: (ts.failed || []).map((p: any) => ({ x: p.timestamp, y: p.value })) },
509
510
  ];
510
511
  }
511
512
 
@@ -60,10 +60,8 @@ export class OpsViewSecurityAuthentication extends DeesElement {
60
60
  `;
61
61
  }
62
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;
63
+ const authEvents = metrics.authenticationEvents || [];
64
+ const successfulLogins = metrics.authenticationSuccesses || 0;
67
65
 
68
66
  const tiles: IStatsTile[] = [
69
67
  {
@@ -73,7 +71,7 @@ export class OpsViewSecurityAuthentication extends DeesElement {
73
71
  type: 'number',
74
72
  icon: 'lucide:LockOpen',
75
73
  color: metrics.authenticationFailures > 10 ? '#ef4444' : '#f59e0b',
76
- description: 'Failed authentication attempts today',
74
+ description: 'Failed authentication attempts in the last 24 hours',
77
75
  },
78
76
  {
79
77
  id: 'successfulLogins',
@@ -82,17 +80,26 @@ export class OpsViewSecurityAuthentication extends DeesElement {
82
80
  type: 'number',
83
81
  icon: 'lucide:Lock',
84
82
  color: '#22c55e',
85
- description: 'Successful logins today',
83
+ description: 'Successful logins in the last 24 hours',
86
84
  },
87
85
  ];
88
86
 
89
- // Map auth events to login history table data
90
- const loginHistory = authEvents.map((evt: any) => ({
87
+ const failureLabels: Record<string, string> = {
88
+ invalidCredentials: 'Invalid credentials',
89
+ serviceUnavailable: 'Authentication service unavailable',
90
+ identityIssuanceFailed: 'Session identity could not be issued',
91
+ internalError: 'Internal authentication error',
92
+ };
93
+ const loginHistory = authEvents.map((evt) => ({
91
94
  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',
95
+ username: evt.username,
96
+ source: evt.resolvedAuthSource
97
+ ? evt.requestedAuthSource === 'auto'
98
+ ? `Auto → ${evt.resolvedAuthSource}`
99
+ : evt.resolvedAuthSource
100
+ : evt.requestedAuthSource,
101
+ success: evt.success,
102
+ reason: evt.success ? '' : failureLabels[evt.failureReason || ''] || 'Authentication failed',
96
103
  }));
97
104
 
98
105
  return html`
@@ -111,7 +118,7 @@ export class OpsViewSecurityAuthentication extends DeesElement {
111
118
  .displayFunction=${(item) => ({
112
119
  'Time': new Date(item.timestamp).toLocaleString(),
113
120
  'Username': item.username,
114
- 'IP Address': item.ipAddress,
121
+ 'Source': item.source,
115
122
  'Status': item.success ? 'Success' : 'Failed',
116
123
  'Reason': item.reason || '-',
117
124
  })}
@@ -63,12 +63,7 @@ export class OpsViewSecurityOverview extends DeesElement {
63
63
  const threatLevel = this.calculateThreatLevel(metrics);
64
64
  const threatScore = this.getThreatScore(metrics);
65
65
 
66
- // Derive active sessions from recent successful auth events (last hour)
67
- const allEvents: any[] = metrics.recentEvents || [];
68
- const oneHourAgo = Date.now() - 3600000;
69
- const recentAuthSuccesses = allEvents.filter(
70
- (evt: any) => evt.type === 'authentication' && evt.success === true && evt.timestamp >= oneHourAgo
71
- ).length;
66
+ const successfulLogins = metrics.authenticationSuccesses || 0;
72
67
 
73
68
  const tiles: IStatsTile[] = [
74
69
  {
@@ -95,16 +90,16 @@ export class OpsViewSecurityOverview extends DeesElement {
95
90
  type: 'number',
96
91
  icon: 'lucide:ShieldCheck',
97
92
  color: '#ef4444',
98
- description: 'Total threats blocked today',
93
+ description: 'Threats observed in the last 24 hours',
99
94
  },
100
95
  {
101
- id: 'activeSessions',
102
- title: 'Active Sessions',
103
- value: recentAuthSuccesses,
96
+ id: 'successfulLogins',
97
+ title: 'Successful Logins',
98
+ value: successfulLogins,
104
99
  type: 'number',
105
100
  icon: 'lucide:Users',
106
101
  color: '#22c55e',
107
- description: 'Authenticated in last hour',
102
+ description: 'Successful logins in the last 24 hours',
108
103
  },
109
104
  {
110
105
  id: 'authFailures',
@@ -113,7 +108,7 @@ export class OpsViewSecurityOverview extends DeesElement {
113
108
  type: 'number',
114
109
  icon: 'lucide:LockOpen',
115
110
  color: metrics.authenticationFailures > 10 ? '#ef4444' : '#f59e0b',
116
- description: 'Failed login attempts today',
111
+ description: 'Failed login attempts in the last 24 hours',
117
112
  },
118
113
  ];
119
114