feat(ops/monitoring): add in-memory log buffer, metrics time-series and ops UI integration

This commit is contained in:
2026-02-19 17:23:43 +00:00
parent dc6ce341bd
commit eacddc7ce1
14 changed files with 482 additions and 128 deletions

View File

@@ -20,6 +20,15 @@ export class OpsViewLogs extends DeesElement {
filters: {},
};
@state()
accessor filterLevel: string | undefined;
@state()
accessor filterCategory: string | undefined;
@state()
accessor filterLimit: number = 100;
constructor() {
super();
const subscription = appstate.logStatePart
@@ -174,29 +183,43 @@ export class OpsViewLogs extends DeesElement {
`;
}
async connectedCallback() {
super.connectedCallback();
// Auto-fetch logs when the view mounts
this.fetchLogs();
}
private async fetchLogs() {
const filters = this.getActiveFilters();
await appstate.logStatePart.dispatchAction(appstate.fetchRecentLogsAction, {
limit: filters.limit || 100,
level: filters.level as 'debug' | 'info' | 'warn' | 'error' | undefined,
category: filters.category as 'smtp' | 'dns' | 'security' | 'system' | 'email' | undefined,
limit: this.filterLimit,
level: this.filterLevel as 'debug' | 'info' | 'warn' | 'error' | undefined,
category: this.filterCategory as 'smtp' | 'dns' | 'security' | 'system' | 'email' | undefined,
});
}
private updateFilter(type: string, value: string) {
if (value === 'all') {
value = undefined;
const resolved = value === 'all' ? undefined : value;
switch (type) {
case 'level':
this.filterLevel = resolved;
break;
case 'category':
this.filterCategory = resolved;
break;
case 'limit':
this.filterLimit = resolved ? parseInt(resolved, 10) : 100;
break;
}
// Update filters then fetch logs
this.fetchLogs();
}
private getActiveFilters() {
return {
level: this.logState.filters.level?.[0],
category: this.logState.filters.category?.[0],
limit: 100,
level: this.filterLevel,
category: this.filterCategory,
limit: this.filterLimit,
};
}

View File

@@ -26,14 +26,36 @@ export class OpsViewOverview extends DeesElement {
error: null,
};
@state()
accessor logState: appstate.ILogState = {
recentLogs: [],
isStreaming: false,
filters: {},
};
constructor() {
super();
const subscription = appstate.statsStatePart
const statsSub = appstate.statsStatePart
.select((stateArg) => stateArg)
.subscribe((statsState) => {
this.statsState = statsState;
});
this.rxSubscriptions.push(subscription);
this.rxSubscriptions.push(statsSub);
const logSub = appstate.logStatePart
.select((stateArg) => stateArg)
.subscribe((logState) => {
this.logState = logState;
});
this.rxSubscriptions.push(logSub);
}
async connectedCallback() {
super.connectedCallback();
// Ensure logs are fetched for the overview charts
if (this.logState.recentLogs.length === 0) {
appstate.logStatePart.dispatchAction(appstate.fetchRecentLogsAction, { limit: 100 });
}
}
public static styles = [
@@ -96,10 +118,24 @@ export class OpsViewOverview extends DeesElement {
${this.renderDnsStats()}
<div class="chartGrid">
<dees-chart-area .label=${'Email Traffic (24h)'} .data=${[]}></dees-chart-area>
<dees-chart-area .label=${'DNS Queries (24h)'} .data=${[]}></dees-chart-area>
<dees-chart-log .label=${'Recent Events'} .data=${[]}></dees-chart-log>
<dees-chart-log .label=${'Security Alerts'} .data=${[]}></dees-chart-log>
<dees-chart-area
.label=${'Email Traffic (24h)'}
.series=${this.getEmailTrafficSeries()}
.yAxisFormatter=${(val: number) => `${val}`}
></dees-chart-area>
<dees-chart-area
.label=${'DNS Queries (24h)'}
.series=${this.getDnsQuerySeries()}
.yAxisFormatter=${(val: number) => `${val}`}
></dees-chart-area>
<dees-chart-log
.label=${'Recent Events'}
.logEntries=${this.getRecentEventEntries()}
></dees-chart-log>
<dees-chart-log
.label=${'Security Alerts'}
.logEntries=${this.getSecurityAlertEntries()}
></dees-chart-log>
</div>
`}
`;
@@ -337,4 +373,42 @@ export class OpsViewOverview extends DeesElement {
<dees-statsgrid .tiles=${tiles}></dees-statsgrid>
`;
}
// --- Chart data helpers ---
private getRecentEventEntries(): Array<{ timestamp: string; level: 'debug' | 'info' | 'warn' | 'error' | 'success'; message: string; source?: string }> {
return this.logState.recentLogs.map((log) => ({
timestamp: new Date(log.timestamp).toISOString(),
level: log.level as 'debug' | 'info' | 'warn' | 'error',
message: log.message,
source: log.category,
}));
}
private getSecurityAlertEntries(): Array<{ timestamp: string; level: 'debug' | 'info' | 'warn' | 'error' | 'success'; message: string; source?: string }> {
const events: any[] = this.statsState.securityMetrics?.recentEvents || [];
return events.map((evt: any) => ({
timestamp: new Date(evt.timestamp).toISOString(),
level: evt.level === 'critical' || evt.level === 'error' ? 'error' as const : evt.level === 'warn' ? 'warn' as const : 'info' as const,
message: evt.message,
source: evt.type,
}));
}
private getEmailTrafficSeries(): Array<{ name: string; color: string; data: Array<{ x: number; y: number }> }> {
const ts = this.statsState.emailStats?.timeSeries;
if (!ts) return [];
return [
{ name: 'Sent', color: '#22c55e', data: (ts.sent || []).map((p: any) => ({ x: p.timestamp, y: p.value })) },
{ name: 'Received', color: '#3b82f6', data: (ts.received || []).map((p: any) => ({ x: p.timestamp, y: p.value })) },
];
}
private getDnsQuerySeries(): Array<{ name: string; color: string; data: Array<{ x: number; y: number }> }> {
const ts = this.statsState.dnsStats?.timeSeries;
if (!ts) return [];
return [
{ name: 'Queries', color: '#8b5cf6', data: (ts.queries || []).map((p: any) => ({ x: p.timestamp, y: p.value })) },
];
}
}

View File

@@ -249,7 +249,14 @@ export class OpsViewSecurity extends DeesElement {
private renderOverview(metrics: any) {
const threatLevel = this.calculateThreatLevel(metrics);
const threatScore = this.getThreatScore(metrics);
// Derive active sessions from recent successful auth events (last hour)
const allEvents: any[] = metrics.recentEvents || [];
const oneHourAgo = Date.now() - 3600000;
const recentAuthSuccesses = allEvents.filter(
(evt: any) => evt.type === 'authentication' && evt.success === true && evt.timestamp >= oneHourAgo
).length;
const tiles: IStatsTile[] = [
{
id: 'threatLevel',
@@ -271,7 +278,7 @@ export class OpsViewSecurity extends DeesElement {
{
id: 'blockedThreats',
title: 'Blocked Threats',
value: metrics.blockedIPs.length + metrics.spamDetected,
value: (metrics.blockedIPs?.length || 0) + metrics.spamDetected,
type: 'number',
icon: 'lucide:ShieldCheck',
color: '#ef4444',
@@ -280,11 +287,11 @@ export class OpsViewSecurity extends DeesElement {
{
id: 'activeSessions',
title: 'Active Sessions',
value: 0,
value: recentAuthSuccesses,
type: 'number',
icon: 'lucide:Users',
color: '#22c55e',
description: 'Current authenticated sessions',
description: 'Authenticated in last hour',
},
{
id: 'authFailures',
@@ -349,6 +356,11 @@ export class OpsViewSecurity extends DeesElement {
}
private renderAuthentication(metrics: any) {
// Derive auth events from recentEvents
const allEvents: any[] = metrics.recentEvents || [];
const authEvents = allEvents.filter((evt: any) => evt.type === 'authentication');
const successfulLogins = authEvents.filter((evt: any) => evt.success === true).length;
const tiles: IStatsTile[] = [
{
id: 'authFailures',
@@ -362,7 +374,7 @@ export class OpsViewSecurity extends DeesElement {
{
id: 'successfulLogins',
title: 'Successful Logins',
value: 0,
value: successfulLogins,
type: 'number',
icon: 'lucide:Lock',
color: '#22c55e',
@@ -370,6 +382,15 @@ export class OpsViewSecurity extends DeesElement {
},
];
// Map auth events to login history table data
const loginHistory = authEvents.map((evt: any) => ({
timestamp: evt.timestamp,
username: evt.details?.username || 'unknown',
ipAddress: evt.ipAddress || 'unknown',
success: evt.success ?? false,
reason: evt.success ? '' : evt.message || 'Authentication failed',
}));
return html`
<dees-statsgrid
.tiles=${tiles}
@@ -380,7 +401,7 @@ export class OpsViewSecurity extends DeesElement {
<dees-table
.heading1=${'Login History'}
.heading2=${'Recent authentication attempts'}
.data=${[]}
.data=${loginHistory}
.displayFunction=${(item) => ({
'Time': new Date(item.timestamp).toLocaleString(),
'Username': item.username,
@@ -483,48 +504,38 @@ export class OpsViewSecurity extends DeesElement {
private getThreatScore(metrics: any): number {
// Simple scoring algorithm
let score = 100;
score -= metrics.blockedIPs.length * 2;
score -= metrics.authenticationFailures * 1;
score -= metrics.spamDetected * 0.5;
score -= metrics.malwareDetected * 3;
score -= metrics.phishingDetected * 3;
score -= metrics.suspiciousActivities * 2;
const blockedCount = Array.isArray(metrics.blockedIPs) ? metrics.blockedIPs.length : (metrics.blockedIPs || 0);
score -= blockedCount * 2;
score -= (metrics.authenticationFailures || 0) * 1;
score -= (metrics.spamDetected || 0) * 0.5;
score -= (metrics.malwareDetected || 0) * 3;
score -= (metrics.phishingDetected || 0) * 3;
score -= (metrics.suspiciousActivities || 0) * 2;
return Math.max(0, Math.min(100, Math.round(score)));
}
private getSecurityEvents(metrics: any): any[] {
// Mock data - in real implementation, this would come from the server
return [
{
timestamp: Date.now() - 1000 * 60 * 5,
event: 'Multiple failed login attempts',
severity: 'warning',
details: 'IP: 192.168.1.100',
},
{
timestamp: Date.now() - 1000 * 60 * 15,
event: 'SPF check failed',
severity: 'medium',
details: 'Domain: example.com',
},
{
timestamp: Date.now() - 1000 * 60 * 30,
event: 'IP blocked due to spam',
severity: 'high',
details: 'IP: 10.0.0.1',
},
];
const events: any[] = metrics.recentEvents || [];
return events.map((evt: any) => ({
timestamp: evt.timestamp,
event: evt.message,
severity: evt.level === 'critical' ? 'critical' : evt.level === 'error' ? 'high' : evt.level === 'warn' ? 'warning' : 'info',
details: evt.ipAddress ? `IP: ${evt.ipAddress}` : evt.domain ? `Domain: ${evt.domain}` : evt.type,
}));
}
private async clearBlockedIPs() {
console.log('Clear blocked IPs');
// SmartProxy manages IP blocking — not yet exposed via API
alert('Clearing blocked IPs is not yet supported from the UI.');
}
private async unblockIP(ip: string) {
console.log('Unblock IP:', ip);
// SmartProxy manages IP blocking — not yet exposed via API
alert(`Unblocking IP ${ip} is not yet supported from the UI.`);
}
private async saveEmailSecuritySettings() {
console.log('Save email security settings');
// Config is read-only from the UI for now
alert('Email security settings are read-only. Update the dcrouter configuration file to change these settings.');
}
}