import { DeesElement, property, html, customElement, type TemplateResult, css, state, cssManager } from '@design.estate/dees-element'; import * as appstate from '../../appstate.js'; import * as interfaces from '../../../dist_ts_interfaces/index.js'; import { viewHostCss } from '../shared/css.js'; import { type IStatsTile } from '@design.estate/dees-catalog'; import { appendProtocolConnectionSample, createProtocolConnectionSeries, getLatestHistoryEpochSecond, mergeChartPoints, mergeProtocolConnectionHistory, shouldRestartNetworkActivityTimer, type IChartPoint, type TProtocolChartSeries, } from './network-activity-history.js'; import { NETWORK_ACTIVITY_COLORS, PROTOCOL_CONNECTION_CHART_LINES, PROTOCOL_CONNECTION_LEGEND_STATS, colorNetworkProtocolSeries, createNetworkProtocolDonutData, getNetworkActivityColor, } from './network-activity-visuals.js'; declare global { interface HTMLElementTagNameMap { 'ops-view-network-activity': OpsViewNetworkActivity; } } @customElement('ops-view-network-activity') export class OpsViewNetworkActivity extends DeesElement { /** How far back the realtime charts show */ private static readonly CHART_WINDOW_MS = 10 * 60 * 1000; // 10 minutes /** How often a new data point is added */ private static readonly UPDATE_INTERVAL_MS = 1000; // 1 second /** Derived: max data points the buffer holds */ private static readonly MAX_DATA_POINTS = OpsViewNetworkActivity.CHART_WINDOW_MS / OpsViewNetworkActivity.UPDATE_INTERVAL_MS; @state() accessor statsState = appstate.statsStatePart.getState()!; @state() accessor networkState = appstate.networkStatePart.getState()!; @state() accessor trafficDataIn: IChartPoint[] = []; @state() accessor trafficDataOut: IChartPoint[] = []; @state() accessor frontendConnectionSeries: TProtocolChartSeries = createProtocolConnectionSeries(); @state() accessor backendConnectionSeries: TProtocolChartSeries = createProtocolConnectionSeries(); // Track if we need to update the chart to avoid unnecessary re-renders private lastChartUpdate = 0; private chartUpdateThreshold = OpsViewNetworkActivity.UPDATE_INTERVAL_MS; // Minimum ms between chart updates private trafficUpdateTimer: any = null; private requestsPerSecHistory: number[] = []; // Track requests/sec over time for trend private visibilityHandler: (() => void) | null = null; private lastThroughputHistoryEpochSecond = 0; private lastFrontendHistoryEpochSecond = 0; private lastBackendHistoryEpochSecond = 0; constructor() { super(); this.subscribeToStateParts(); this.initializeTrafficData(); this.updateNetworkData(); this.startTrafficUpdateTimer(); } async connectedCallback() { await super.connectedCallback(); // Pause/resume traffic timer when tab visibility changes this.visibilityHandler = () => { if (document.hidden) { this.stopTrafficUpdateTimer(); } else { void appstate.networkStatePart .dispatchAction(appstate.fetchNetworkStatsAction, null) .catch((error) => { console.error('Failed to refresh network history after visibility resume:', error); }) .finally(() => { if (shouldRestartNetworkActivityTimer(this.isConnected, document.hidden)) { this.startTrafficUpdateTimer(); } }); } }; document.addEventListener('visibilitychange', this.visibilityHandler); // When network view becomes visible, ensure we fetch network data await appstate.networkStatePart.dispatchAction(appstate.fetchNetworkStatsAction, null); } async disconnectedCallback() { await super.disconnectedCallback(); this.stopTrafficUpdateTimer(); if (this.visibilityHandler) { document.removeEventListener('visibilitychange', this.visibilityHandler); this.visibilityHandler = null; } } private subscribeToStateParts() { // Subscribe and track unsubscribe functions const statsUnsubscribe = appstate.statsStatePart.select().subscribe((state) => { this.statsState = state; }); this.rxSubscriptions.push(statsUnsubscribe); const networkUnsubscribe = appstate.networkStatePart.select().subscribe((state) => { this.networkState = state; this.updateNetworkData(); }); this.rxSubscriptions.push(networkUnsubscribe); } private initializeTrafficData() { const now = Date.now(); const { MAX_DATA_POINTS, UPDATE_INTERVAL_MS } = OpsViewNetworkActivity; // Initialize with empty data points for both in and out const emptyData = Array.from({ length: MAX_DATA_POINTS }, (_, i) => { const time = now - ((MAX_DATA_POINTS - 1 - i) * UPDATE_INTERVAL_MS); return { x: new Date(time).toISOString(), y: 0, }; }); this.trafficDataIn = [...emptyData]; this.trafficDataOut = emptyData.map(point => ({ ...point })); } private mergeThroughputHistory(): void { const history = this.networkState.throughputHistory || []; const latestHistoryEpochSecond = getLatestHistoryEpochSecond(history); if (latestHistoryEpochSecond <= this.lastThroughputHistoryEpochSecond) return; const canonicalIn = history.map((point) => ({ x: new Date(point.timestamp).toISOString(), y: Math.round(((point.in * 8) / 1000000) * 10) / 10, })); const canonicalOut = history.map((point) => ({ x: new Date(point.timestamp).toISOString(), y: Math.round(((point.out * 8) / 1000000) * 10) / 10, })); this.trafficDataIn = mergeChartPoints( this.trafficDataIn, canonicalIn, OpsViewNetworkActivity.MAX_DATA_POINTS, ); this.trafficDataOut = mergeChartPoints( this.trafficDataOut, canonicalOut, OpsViewNetworkActivity.MAX_DATA_POINTS, ); this.lastThroughputHistoryEpochSecond = latestHistoryEpochSecond; } private getProtocolCounts( distributionArg: interfaces.data.IProtocolDistribution, ): Record { return { 'HTTP/1.1': distributionArg.h1Active, 'HTTP/2': distributionArg.h2Active, 'HTTP/3': distributionArg.h3Active, 'WebSocket': distributionArg.wsActive, 'Other': distributionArg.otherActive, }; } private mergeProtocolConnectionHistories(): void { const frontendHistory = this.networkState.frontendConnectionHistory || []; const latestFrontendEpochSecond = getLatestHistoryEpochSecond(frontendHistory); if (latestFrontendEpochSecond > this.lastFrontendHistoryEpochSecond) { this.frontendConnectionSeries = mergeProtocolConnectionHistory( this.frontendConnectionSeries, frontendHistory, OpsViewNetworkActivity.MAX_DATA_POINTS, ); this.lastFrontendHistoryEpochSecond = latestFrontendEpochSecond; } const backendHistory = this.networkState.backendConnectionHistory || []; const latestBackendEpochSecond = getLatestHistoryEpochSecond(backendHistory); if (latestBackendEpochSecond > this.lastBackendHistoryEpochSecond) { this.backendConnectionSeries = mergeProtocolConnectionHistory( this.backendConnectionSeries, backendHistory, OpsViewNetworkActivity.MAX_DATA_POINTS, ); this.lastBackendHistoryEpochSecond = latestBackendEpochSecond; } } private addProtocolConnectionDataPoints(timestampArg: string): void { if (this.networkState.frontendProtocols) { this.frontendConnectionSeries = appendProtocolConnectionSample( this.frontendConnectionSeries, timestampArg, this.getProtocolCounts(this.networkState.frontendProtocols), OpsViewNetworkActivity.MAX_DATA_POINTS, ); } if (this.networkState.backendProtocols) { this.backendConnectionSeries = appendProtocolConnectionSample( this.backendConnectionSeries, timestampArg, this.getProtocolCounts(this.networkState.backendProtocols), OpsViewNetworkActivity.MAX_DATA_POINTS, ); } } public static styles = [ cssManager.defaultStyles, viewHostCss, css` .networkContainer { display: flex; flex-direction: column; gap: 24px; container-type: inline-size; } .protocolBadge { display: inline-flex; align-items: center; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: 500; } .protocolBadge.http { background: ${cssManager.bdTheme('#e3f2fd', '#1a2c3a')}; color: ${cssManager.bdTheme('#1976d2', '#5a9fd4')}; } .protocolBadge.https { background: ${cssManager.bdTheme('#e8f5e9', '#1a3a1a')}; color: ${cssManager.bdTheme('#388e3c', '#66bb6a')}; } .protocolBadge.tcp { background: ${cssManager.bdTheme('#fff3e0', '#3a2a1a')}; color: ${cssManager.bdTheme('#f57c00', '#ff9933')}; } .protocolBadge.smtp { background: ${cssManager.bdTheme('#f3e5f5', '#2a1a3a')}; color: ${cssManager.bdTheme('#7b1fa2', '#ba68c8')}; } .protocolBadge.dns { background: ${cssManager.bdTheme('#e0f2f1', '#1a3a3a')}; color: ${cssManager.bdTheme('#00796b', '#4db6ac')}; } .protocolBadge.h1 { background: ${cssManager.bdTheme('#e3f2fd', '#1a2c3a')}; color: ${cssManager.bdTheme('#1976d2', '#5a9fd4')}; } .protocolBadge.h2 { background: ${cssManager.bdTheme('#e8f5e9', '#1a3a1a')}; color: ${cssManager.bdTheme('#388e3c', '#66bb6a')}; } .protocolBadge.h3 { background: ${cssManager.bdTheme('#f3e5f5', '#2a1a3a')}; color: ${cssManager.bdTheme('#7b1fa2', '#ba68c8')}; } .protocolBadge.unknown { background: ${cssManager.bdTheme('#f5f5f5', '#2a2a2a')}; color: ${cssManager.bdTheme('#757575', '#999999')}; } .suppressionBadge { display: inline-flex; align-items: center; padding: 2px 6px; border-radius: 3px; font-size: 11px; font-weight: 500; background: ${cssManager.bdTheme('#fff3e0', '#3a2a1a')}; color: ${cssManager.bdTheme('#f57c00', '#ff9933')}; margin-left: 4px; } .statusBadge { display: inline-flex; align-items: center; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: 500; } .statusBadge.success { background: ${cssManager.bdTheme('#e8f5e9', '#1a3a1a')}; color: ${cssManager.bdTheme('#388e3c', '#66bb6a')}; } .statusBadge.error { background: ${cssManager.bdTheme('#ffebee', '#3a1a1a')}; color: ${cssManager.bdTheme('#d32f2f', '#ff6666')}; } .statusBadge.warning { background: ${cssManager.bdTheme('#fff3e0', '#3a2a1a')}; color: ${cssManager.bdTheme('#f57c00', '#ff9933')}; } .intelligenceBadge { display: inline-flex; align-items: center; padding: 4px 8px; border-radius: 999px; font-size: 12px; font-weight: 500; background: ${cssManager.bdTheme('#eef2ff', '#1e1b4b')}; color: ${cssManager.bdTheme('#4338ca', '#a5b4fc')}; } .protocolChartGrid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; min-width: 0; } .protocolChartColumn { display: flex; flex-direction: column; gap: 16px; min-width: 0; } .protocolChartColumn > dees-chart-donut, .protocolChartColumn > dees-chart-area { width: 100%; max-width: 100%; min-width: 0; } @container (max-width: 800px) { .protocolChartGrid { grid-template-columns: minmax(0, 1fr); } } `, ]; public render() { return html` Network Activity
${this.renderNetworkStats()} `${val} Mbit/s`} > ${this.renderProtocolCharts()} ${this.renderTopIPs()} ${this.renderTopASNs()} ${this.renderTopIPsByBandwidth()} ${this.renderDomainActivity()} ${this.renderBackendProtocols()}
`; } private formatNumber(num: number): string { if (num >= 1000000) { return (num / 1000000).toFixed(1) + 'M'; } else if (num >= 1000) { return (num / 1000).toFixed(1) + 'K'; } return num.toFixed(0); } private formatBytes(bytes: number): string { const units = ['B', 'KB', 'MB', 'GB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(1)} ${units[unitIndex]}`; } private formatBitsPerSecond(bytesPerSecond: number): string { const bitsPerSecond = bytesPerSecond * 8; // Convert bytes to bits const units = ['bit/s', 'kbit/s', 'Mbit/s', 'Gbit/s']; let size = bitsPerSecond; let unitIndex = 0; while (size >= 1000 && unitIndex < units.length - 1) { size /= 1000; // Use 1000 for bits (not 1024) unitIndex++; } return `${size.toFixed(1)} ${units[unitIndex]}`; } private formatOptional(value: unknown): string { if (value === null || value === undefined || value === '') return '-'; return String(value); } private formatDateTime(timestamp?: number | null): string { return timestamp ? new Date(timestamp).toLocaleString() : '-'; } private getIpIntelligence(ip: string): interfaces.data.IIpIntelligenceRecord | undefined { return this.networkState.ipIntelligence?.find((record) => record.ipAddress === ip); } private getIpOrganization(record?: interfaces.data.IIpIntelligenceRecord): string { return record?.asnOrg || record?.registrantOrg || ''; } private getIpIntelligenceColumns(ip: string): Record { const record = this.getIpIntelligence(ip); const organization = this.getIpOrganization(record); return { 'Intelligence': record ? html`${this.formatOptional(organization || record.countryCode || 'Known')}` : html`Enriching...`, 'ASN': record?.asn ? `AS${record.asn}` : '-', 'Organization': this.formatOptional(organization), 'Country': this.formatOptional(record?.countryCode || record?.country), 'Network Range': this.formatOptional(record?.networkRange), 'Last Seen': this.formatDateTime(record?.lastSeenAt), }; } private getIpDataActions() { return [ { name: 'Refresh Intelligence', iconName: 'lucide:refresh-cw', type: ['inRow', 'contextmenu'] as any, actionFunc: async (actionData: any) => { const ip = actionData.item.ip; await appstate.securityPolicyStatePart.dispatchAction(appstate.refreshIpIntelligenceAction, ip); await appstate.networkStatePart.dispatchAction(appstate.fetchNetworkStatsAction, null); }, }, { name: 'Block IP', iconName: 'lucide:shield-ban', type: ['inRow', 'contextmenu'] as any, actionFunc: async (actionData: any) => { await this.createBlockRuleDialog('ip', actionData.item.ip, 'Blocked from Network Activity'); }, }, { name: 'Block Network Range', iconName: 'lucide:network', type: ['contextmenu'] as any, actionRelevancyCheckFunc: (entry: any) => Boolean(this.getIpIntelligence(entry.ip)?.networkRange), actionFunc: async (actionData: any) => { const record = this.getIpIntelligence(actionData.item.ip); await this.createBlockRuleDialog('cidr', record!.networkRange!, 'Blocked network range from Network Activity'); }, }, { name: 'Block ASN', iconName: 'lucide:radio-tower', type: ['contextmenu'] as any, actionRelevancyCheckFunc: (entry: any) => Boolean(this.getIpIntelligence(entry.ip)?.asn), actionFunc: async (actionData: any) => { const record = this.getIpIntelligence(actionData.item.ip); await this.createBlockRuleDialog('asn', String(record!.asn), 'Blocked ASN from Network Activity'); }, }, { name: 'Block Organization', iconName: 'lucide:building-2', type: ['contextmenu'] as any, actionRelevancyCheckFunc: (entry: any) => Boolean(this.getIpOrganization(this.getIpIntelligence(entry.ip))), actionFunc: async (actionData: any) => { const record = this.getIpIntelligence(actionData.item.ip); await this.createBlockRuleDialog('organization', this.getIpOrganization(record), 'Blocked organization from Network Activity'); }, }, { name: 'View Intelligence', iconName: 'lucide:info', type: ['doubleClick', 'contextmenu'] as any, actionRelevancyCheckFunc: (entry: any) => Boolean(this.getIpIntelligence(entry.ip)), actionFunc: async (actionData: any) => { await this.showIpIntelligenceDetails(actionData.item.ip); }, }, ]; } private getAsnDataActions() { return [ { name: 'Block ASN', iconName: 'lucide:radio-tower', type: ['inRow', 'contextmenu'] as any, actionFunc: async (actionData: any) => { await this.createBlockRuleDialog('asn', String(actionData.item.asn), 'Blocked ASN from Network Activity'); }, }, { name: 'Block Organization', iconName: 'lucide:building-2', type: ['contextmenu'] as any, actionRelevancyCheckFunc: (entry: any) => Boolean(entry.organization), actionFunc: async (actionData: any) => { await this.createBlockRuleDialog('organization', actionData.item.organization, 'Blocked organization from Network Activity'); }, }, ]; } private calculateThroughput(): { in: number; out: number } { // Use real throughput data from network state return { in: this.networkState.throughputRate.bytesInPerSecond, out: this.networkState.throughputRate.bytesOutPerSecond, }; } private renderNetworkStats(): TemplateResult { // Use server-side requests/sec from SmartProxy's Rust engine const reqPerSec = this.networkState.requestsPerSecond || 0; const throughput = this.calculateThroughput(); const activeConnections = this.statsState.serverStats?.activeConnections || 0; // Build trend data from pre-computed history (mutated in updateNetworkData, not here) const trendData = [...this.requestsPerSecHistory]; while (trendData.length < 20) { trendData.unshift(0); } const tiles: IStatsTile[] = [ { id: 'connections', title: 'Active Connections', value: activeConnections, type: 'number', icon: 'lucide:Plug', color: NETWORK_ACTIVITY_COLORS['Total Active'], description: `Total: ${this.formatNumber(this.statsState.serverStats?.totalConnections || 0)} connections`, actions: [ { name: 'View Details', iconName: 'fa:magnifyingGlass', action: async () => { }, }, ], }, { id: 'requests', title: 'Requests/sec', value: reqPerSec, type: 'trend', icon: 'lucide:ChartLine', color: NETWORK_ACTIVITY_COLORS['Requests/sec'], trendData: trendData, description: `Total: ${this.formatNumber(this.networkState.requestsTotal || 0)} requests`, }, { id: 'throughputIn', title: 'Throughput In', value: this.formatBitsPerSecond(throughput.in), unit: '', type: 'number', icon: 'lucide:Download', color: NETWORK_ACTIVITY_COLORS.Inbound, description: `Total: ${this.formatBytes(this.networkState.totalBytes?.in || 0)}`, }, { id: 'throughputOut', title: 'Throughput Out', value: this.formatBitsPerSecond(throughput.out), unit: '', type: 'number', icon: 'lucide:Upload', color: NETWORK_ACTIVITY_COLORS.Outbound, description: `Total: ${this.formatBytes(this.networkState.totalBytes?.out || 0)}`, }, ]; return html` `; } private renderProtocolCharts(): TemplateResult { const fp = this.networkState.frontendProtocols; const bp = this.networkState.backendProtocols; const buildDonutData = ( dist: interfaces.data.IProtocolDistribution | null, historySeries: TProtocolChartSeries, ) => dist ? createNetworkProtocolDonutData( this.getProtocolCounts(dist), historySeries.map((series) => series.name), ) : []; const frontendData = buildDonutData(fp, this.frontendConnectionSeries); const backendData = buildDonutData(bp, this.backendConnectionSeries); return html`
0 ? frontendData : [{ name: 'No Traffic', value: 1, color: getNetworkActivityColor('Other') }]} .showLegend=${true} .showLabels=${true} .innerRadiusPercent=${'55%'} .valueFormatter=${(val: number) => `${val} active`} > `${Math.round(val)} active`} >
0 ? backendData : [{ name: 'No Traffic', value: 1, color: getNetworkActivityColor('Other') }]} .showLegend=${true} .showLabels=${true} .innerRadiusPercent=${'55%'} .valueFormatter=${(val: number) => `${val} active`} > `${Math.round(val)} active`} >
`; } private renderTopIPs(): TemplateResult { if (this.networkState.topIPs.length === 0) { return html``; } // Build per-IP bandwidth lookup const bandwidthByIP = new Map(); if (this.networkState.throughputByIP) { for (const entry of this.networkState.throughputByIP) { bandwidthByIP.set(entry.ip, { in: entry.in, out: entry.out }); } } // Calculate total connections across all top IPs const totalConnections = this.networkState.topIPs.reduce((sum, ipData) => sum + ipData.count, 0); return html` { const bw = bandwidthByIP.get(ipData.ip); return { 'IP Address': ipData.ip, 'Connections': ipData.count, 'Bandwidth In': bw ? this.formatBitsPerSecond(bw.in) : '0 bit/s', 'Bandwidth Out': bw ? this.formatBitsPerSecond(bw.out) : '0 bit/s', 'Share': totalConnections > 0 ? ((ipData.count / totalConnections) * 100).toFixed(1) + '%' : '0%', ...this.getIpIntelligenceColumns(ipData.ip), }; }} .dataActions=${this.getIpDataActions()} heading1="Top Connected IPs" heading2="IPs with most active connections, bandwidth, and intelligence" searchable .showColumnFilters=${true} .pagination=${false} dataName="ip" > `; } private renderTopASNs(): TemplateResult { if (!this.networkState.topASNs || this.networkState.topASNs.length === 0) { return html``; } return html` { return { 'ASN': `AS${asnData.asn}`, 'Organization': this.formatOptional(asnData.organization), 'Connections': asnData.activeConnections, 'IPs': asnData.ipCount, 'Bandwidth In': this.formatBitsPerSecond(asnData.bytesInPerSecond), 'Bandwidth Out': this.formatBitsPerSecond(asnData.bytesOutPerSecond), 'Total Bandwidth': this.formatBitsPerSecond(asnData.bytesInPerSecond + asnData.bytesOutPerSecond), 'Country': this.formatOptional(asnData.country), 'Sample IPs': asnData.sampleIps.join(', '), }; }} .dataActions=${this.getAsnDataActions()} heading1="Top Connected ASNs" heading2="Organizations causing the most active connections across observed IPs" searchable .showColumnFilters=${true} .pagination=${false} dataName="ASN" > `; } private renderTopIPsByBandwidth(): TemplateResult { if (!this.networkState.topIPsByBandwidth || this.networkState.topIPsByBandwidth.length === 0) { return html``; } return html` { return { 'IP Address': ipData.ip, 'Bandwidth In': this.formatBitsPerSecond(ipData.bwIn), 'Bandwidth Out': this.formatBitsPerSecond(ipData.bwOut), 'Total Bandwidth': this.formatBitsPerSecond(ipData.bwIn + ipData.bwOut), 'Connections': ipData.count, ...this.getIpIntelligenceColumns(ipData.ip), }; }} .dataActions=${this.getIpDataActions()} heading1="Top IPs by Bandwidth" heading2="IPs with highest throughput and intelligence" searchable .showColumnFilters=${true} .pagination=${false} dataName="ip" > `; } private renderDomainActivity(): TemplateResult { if (!this.networkState.domainActivity || this.networkState.domainActivity.length === 0) { return html``; } return html` { const totalBytesPerMin = (item.bytesInPerSecond + item.bytesOutPerSecond) * 60; return { 'Domain': item.domain, 'Throughput In': this.formatBitsPerSecond(item.bytesInPerSecond), 'Throughput Out': this.formatBitsPerSecond(item.bytesOutPerSecond), 'Transferred / min': this.formatBytes(totalBytesPerMin), 'Connections': item.activeConnections, 'Req/s': item.requestsPerSecond != null ? item.requestsPerSecond.toFixed(1) : '-', 'Req/min': item.requestsLastMinute != null ? item.requestsLastMinute.toFixed(0) : '-', 'Requests': item.requestCount?.toLocaleString() ?? '0', 'Routes': item.routeCount, }; }} heading1="Domain Activity" heading2="Per-domain network activity from request-level metrics" searchable .showColumnFilters=${true} .pagination=${false} dataName="domain" > `; } private renderBackendProtocols(): TemplateResult { const backends = this.networkState.backends; if (!backends || backends.length === 0) { return html``; } return html` { const totalErrors = item.connectErrors + item.handshakeErrors + item.requestErrors; const protocolClass = item.protocol.toLowerCase().replace(/[^a-z0-9]/g, ''); return { 'Backend': item.backend, 'Domain': item.domain || '-', 'Protocol': html` ${item.protocol.toUpperCase()} ${item.h2Suppressed ? html`H2 suppressed` : ''} ${item.h3Suppressed ? html`H3 suppressed` : ''} `, 'Active': item.activeConnections, 'Total': this.formatNumber(item.totalConnections), 'Avg Connect': item.avgConnectTimeMs > 0 ? `${item.avgConnectTimeMs.toFixed(1)}ms` : '-', 'Pool Hit Rate': item.poolHitRate > 0 ? `${(item.poolHitRate * 100).toFixed(1)}%` : '-', 'Errors': totalErrors > 0 ? html`${totalErrors}` : html`0`, 'Cache Age': item.cacheAgeSecs != null ? `${Math.round(item.cacheAgeSecs)}s` : '-', }; }} .dataActions=${[ { name: 'View Details', iconName: 'lucide:info', type: ['inRow', 'doubleClick', 'contextmenu'] as any, actionFunc: async (actionData: any) => { await this.showBackendDetails(actionData.item); } } ]} heading1="Backend Protocols" heading2="Auto-detected backend protocols and connection pool health" searchable .showColumnFilters=${true} .pagination=${false} dataName="backend" > `; } private async showBackendDetails(backend: interfaces.data.IBackendInfo) { const { DeesModal } = await import('@design.estate/dees-catalog'); await DeesModal.createAndShow({ heading: `Backend: ${backend.backend}`, content: html`
`, menuOptions: [ { name: 'Copy Backend Key', iconName: 'lucide:Copy', action: async () => { await navigator.clipboard.writeText(backend.backend); } } ] }); } private getDropdownKey(value: any): string { return typeof value === 'string' ? value : value?.key || ''; } private async createBlockRuleDialog( type: interfaces.data.TSecurityBlockRuleType, value: string, reason: string, ): Promise { const { DeesModal } = await import('@design.estate/dees-catalog'); const typeOptions = [ { key: 'ip', option: 'IP address' }, { key: 'cidr', option: 'CIDR / network range' }, { key: 'asn', option: 'ASN' }, { key: 'organization', option: 'Organization' }, ]; const matchModeOptions = [ { key: 'contains', option: 'Organization contains value' }, { key: 'exact', option: 'Organization exactly matches value' }, ]; await DeesModal.createAndShow({ heading: 'Create Security Block Rule', content: html` option.key === type)} > `, menuOptions: [ { name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => modalArg.destroy() }, { name: 'Create', iconName: 'lucide:shield-ban', action: async (modalArg: any) => { const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form'); if (!form) return; const data = await form.collectFormData(); const selectedType = this.getDropdownKey(data.type) as interfaces.data.TSecurityBlockRuleType; const selectedValue = String(data.value || '').trim(); if (!selectedType || !selectedValue) return; const matchMode = selectedType === 'organization' ? this.getDropdownKey(data.matchMode) as interfaces.data.TSecurityBlockRuleMatchMode : undefined; await appstate.securityPolicyStatePart.dispatchAction(appstate.createSecurityBlockRuleAction, { type: selectedType, value: selectedValue, matchMode, reason: String(data.reason || '').trim() || undefined, enabled: data.enabled !== false, }); await appstate.networkStatePart.dispatchAction(appstate.fetchNetworkStatsAction, null); await modalArg.destroy(); }, }, ], }); } private async showIpIntelligenceDetails(ip: string): Promise { const record = this.getIpIntelligence(ip); if (!record) return; const { DeesModal } = await import('@design.estate/dees-catalog'); await DeesModal.createAndShow({ heading: `IP Intelligence: ${ip}`, content: html`
`, menuOptions: [ { name: 'Copy Abuse Contact', iconName: 'lucide:copy', action: async () => { if (record.abuseContact) await navigator.clipboard.writeText(record.abuseContact); }, }, { name: 'Block IP', iconName: 'lucide:shield-ban', action: async () => { await this.createBlockRuleDialog('ip', record.ipAddress, 'Blocked from IP intelligence details'); }, }, ], }); } private async updateNetworkData() { // Track requests/sec history for the trend sparkline (moved out of render) const reqPerSec = this.networkState.requestsPerSecond || 0; this.requestsPerSecHistory.push(reqPerSec); if (this.requestsPerSecHistory.length > 20) { this.requestsPerSecHistory.shift(); } this.mergeThroughputHistory(); this.mergeProtocolConnectionHistories(); } private startTrafficUpdateTimer() { this.stopTrafficUpdateTimer(); // Clear any existing timer this.trafficUpdateTimer = setInterval(() => { this.addTrafficDataPoint(); }, OpsViewNetworkActivity.UPDATE_INTERVAL_MS); } private addTrafficDataPoint() { const now = Date.now(); // Throttle chart updates to avoid excessive re-renders if (now - this.lastChartUpdate < this.chartUpdateThreshold) { return; } const throughput = this.calculateThroughput(); if (this.networkState.lastUpdated && now - this.networkState.lastUpdated > 3000) { return; } // Convert to Mbps (bytes * 8 / 1,000,000) const throughputInMbps = (throughput.in * 8) / 1000000; const throughputOutMbps = (throughput.out * 8) / 1000000; // Add new data points const timestamp = new Date(now).toISOString(); const newDataPointIn = { x: timestamp, y: Math.round(throughputInMbps * 10) / 10 }; const newDataPointOut = { x: timestamp, y: Math.round(throughputOutMbps * 10) / 10 }; this.trafficDataIn = mergeChartPoints( this.trafficDataIn, [newDataPointIn], OpsViewNetworkActivity.MAX_DATA_POINTS, ); this.trafficDataOut = mergeChartPoints( this.trafficDataOut, [newDataPointOut], OpsViewNetworkActivity.MAX_DATA_POINTS, ); this.addProtocolConnectionDataPoints(timestamp); this.lastChartUpdate = now; } private stopTrafficUpdateTimer() { if (this.trafficUpdateTimer) { clearInterval(this.trafficUpdateTimer); this.trafficUpdateTimer = null; } } }