import { DeesElement, property, html, customElement, type TemplateResult, css, state, cssManager } from '@design.estate/dees-element'; import * as appstate from '../appstate.js'; import { viewHostCss } from './shared/css.js'; import { type IStatsTile } from '@design.estate/dees-catalog'; declare global { interface HTMLElementTagNameMap { 'ops-view-network': OpsViewNetwork; } } interface INetworkRequest { id: string; timestamp: number; method: string; url: string; hostname: string; port: number; protocol: 'http' | 'https' | 'tcp' | 'udp'; statusCode?: number; duration: number; bytesIn: number; bytesOut: number; remoteIp: string; route?: string; } @customElement('ops-view-network') export class OpsViewNetwork extends DeesElement { @state() private statsState = appstate.statsStatePart.getState(); @state() private networkState = appstate.networkStatePart.getState(); @state() private networkRequests: INetworkRequest[] = []; @state() private trafficDataIn: Array<{ x: string | number; y: number }> = []; @state() private trafficDataOut: Array<{ x: string | number; y: number }> = []; private lastTrafficUpdateTime = 0; private trafficUpdateInterval = 1000; // Update every 1 second private requestCountHistory = new Map(); // Track requests per time bucket private trafficUpdateTimer: any = null; private requestsPerSecHistory: number[] = []; // Track requests/sec over time for trend // Removed byte tracking - now using real-time data from SmartProxy constructor() { super(); this.subscribeToStateParts(); this.initializeTrafficData(); this.updateNetworkData(); this.startTrafficUpdateTimer(); } async disconnectedCallback() { await super.disconnectedCallback(); this.stopTrafficUpdateTimer(); } private subscribeToStateParts() { appstate.statsStatePart.state.subscribe((state) => { this.statsState = state; this.updateNetworkData(); }); appstate.networkStatePart.state.subscribe((state) => { this.networkState = state; this.updateNetworkData(); }); } private initializeTrafficData() { const now = Date.now(); // Fixed 5 minute time range const range = 5 * 60 * 1000; // 5 minutes const bucketSize = range / 60; // 60 data points // Initialize with empty data points for both in and out const emptyData = Array.from({ length: 60 }, (_, i) => { const time = now - ((59 - i) * bucketSize); return { x: new Date(time).toISOString(), y: 0, }; }); this.trafficDataIn = [...emptyData]; this.trafficDataOut = emptyData.map(point => ({ ...point })); this.lastTrafficUpdateTime = now; } public static styles = [ cssManager.defaultStyles, viewHostCss, css` :host { display: block; padding: 24px; } .networkContainer { display: flex; flex-direction: column; gap: 24px; } dees-statsgrid { margin-bottom: 24px; } dees-chart-area { margin-bottom: 24px; } .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')}; } .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')}; } `, ]; public render() { return html` Network Activity ${this.renderNetworkStats()} `${val} Mbit/s`} .tooltipFormatter=${(point: any) => { const mbps = point.y || 0; const seriesName = point.series?.name || 'Throughput'; const timestamp = new Date(point.x).toLocaleTimeString(); return ` ${timestamp} ${seriesName}: ${mbps.toFixed(2)} Mbit/s `; }} > ${this.renderTopIPs()} ({ Time: new Date(req.timestamp).toLocaleTimeString(), Protocol: html`${req.protocol.toUpperCase()}`, Method: req.method, 'Host:Port': `${req.hostname}:${req.port}`, Path: this.truncateUrl(req.url), Status: this.renderStatus(req.statusCode), Duration: `${req.duration}ms`, 'In/Out': `${this.formatBytes(req.bytesIn)} / ${this.formatBytes(req.bytesOut)}`, 'Remote IP': req.remoteIp, })} .dataActions=${[ { name: 'View Details', iconName: 'magnifyingGlass', type: ['inRow', 'doubleClick', 'contextmenu'], actionFunc: async (actionData) => { await this.showRequestDetails(actionData.item); } } ]} heading1="Recent Network Activity" heading2="Recent network requests" searchable .pagination=${true} .paginationSize=${50} dataName="request" > `; } private async showRequestDetails(request: INetworkRequest) { const { DeesModal } = await import('@design.estate/dees-catalog'); await DeesModal.createAndShow({ heading: 'Request Details', content: html` `, menuOptions: [ { name: 'Copy Request ID', iconName: 'copy', action: async () => { await navigator.clipboard.writeText(request.id); console.log('Request ID copied to clipboard'); } } ] }); } private renderStatus(statusCode?: number): TemplateResult { if (!statusCode) { return html`N/A`; } const statusClass = statusCode >= 200 && statusCode < 300 ? 'success' : statusCode >= 400 ? 'error' : 'warning'; return html`${statusCode}`; } private truncateUrl(url: string, maxLength = 50): string { if (url.length <= maxLength) return url; return url.substring(0, maxLength - 3) + '...'; } 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 calculateRequestsPerSecond(): number { // Calculate from actual request data in the last minute const oneMinuteAgo = Date.now() - 60000; const recentRequests = this.networkRequests.filter(req => req.timestamp >= oneMinuteAgo); const reqPerSec = Math.round(recentRequests.length / 60); // Track history for trend (keep last 20 values) this.requestsPerSecHistory.push(reqPerSec); if (this.requestsPerSecHistory.length > 20) { this.requestsPerSecHistory.shift(); } return reqPerSec; } 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 { const reqPerSec = this.calculateRequestsPerSecond(); const throughput = this.calculateThroughput(); const activeConnections = this.statsState.serverStats?.activeConnections || 0; // Use request count history for the requests/sec trend const trendData = [...this.requestsPerSecHistory]; // If we don't have enough data, pad with zeros while (trendData.length < 20) { trendData.unshift(0); } const tiles: IStatsTile[] = [ { id: 'connections', title: 'Active Connections', value: activeConnections, type: 'number', icon: 'plug', color: activeConnections > 100 ? '#f59e0b' : '#22c55e', description: `Total: ${this.statsState.serverStats?.totalConnections || 0}`, actions: [ { name: 'View Details', iconName: 'magnifyingGlass', action: async () => { }, }, ], }, { id: 'requests', title: 'Requests/sec', value: reqPerSec, type: 'trend', icon: 'chartLine', color: '#3b82f6', trendData: trendData, description: `Average over last minute`, }, { id: 'throughputIn', title: 'Throughput In', value: this.formatBitsPerSecond(throughput.in), unit: '', type: 'number', icon: 'download', color: '#22c55e', }, { id: 'throughputOut', title: 'Throughput Out', value: this.formatBitsPerSecond(throughput.out), unit: '', type: 'number', icon: 'upload', color: '#8b5cf6', }, ]; return html` { console.log('Export feature coming soon'); }, }, ]} > `; } private renderTopIPs(): TemplateResult { if (this.networkState.topIPs.length === 0) { return html``; } // Calculate total connections across all top IPs const totalConnections = this.networkState.topIPs.reduce((sum, ipData) => sum + ipData.count, 0); return html` ({ 'IP Address': ipData.ip, 'Connections': ipData.count, 'Percentage': totalConnections > 0 ? ((ipData.count / totalConnections) * 100).toFixed(1) + '%' : '0%', })} heading1="Top Connected IPs" heading2="IPs with most active connections" .pagination=${false} dataName="ip" > `; } private async updateNetworkData() { // Convert connection data to network requests format if (this.networkState.connections.length > 0) { this.networkRequests = this.networkState.connections.map((conn, index) => ({ id: conn.id, timestamp: conn.startTime, method: 'GET', // Default method for proxy connections url: '/', hostname: conn.remoteAddress, port: conn.protocol === 'https' ? 443 : 80, protocol: conn.protocol === 'https' || conn.protocol === 'http' ? conn.protocol : 'tcp', statusCode: conn.state === 'connected' ? 200 : undefined, duration: Date.now() - conn.startTime, bytesIn: conn.bytesReceived, bytesOut: conn.bytesSent, remoteIp: conn.remoteAddress, route: 'proxy', })); } else { this.networkRequests = []; } // Generate traffic data based on request history this.updateTrafficData(); } private updateTrafficData() { const now = Date.now(); // Fixed 5 minute time range const range = 5 * 60 * 1000; // 5 minutes const bucketSize = range / 60; // 60 data points // 60 data points // Check if enough time has passed to add a new data point const timeSinceLastUpdate = now - this.lastTrafficUpdateTime; const shouldAddNewPoint = timeSinceLastUpdate >= this.trafficUpdateInterval; console.log('UpdateTrafficData called:', { networkRequestsCount: this.networkRequests.length, timeSinceLastUpdate, shouldAddNewPoint, currentDataPoints: this.trafficDataIn.length }); if (!shouldAddNewPoint && this.trafficDataIn.length > 0) { // Not enough time has passed, don't update return; } // Use real-time throughput data from SmartProxy (same as throughput tiles) const throughput = this.calculateThroughput(); // Convert to Mbps (bytes * 8 / 1,000,000) const throughputInMbps = (throughput.in * 8) / 1000000; const throughputOutMbps = (throughput.out * 8) / 1000000; console.log('Throughput calculation:', { bytesInPerSecond: throughput.in, bytesOutPerSecond: throughput.out, throughputInMbps, throughputOutMbps, throughputTileValue: `${this.formatBitsPerSecond(throughput.in)} IN, ${this.formatBitsPerSecond(throughput.out)} OUT` }); if (this.trafficDataIn.length === 0) { // Initialize if empty this.initializeTrafficData(); } else { // Add new data points for both in and out const timestamp = new Date(now).toISOString(); const newDataPointIn = { x: timestamp, y: Math.round(throughputInMbps * 10) / 10 // Round to 1 decimal place }; const newDataPointOut = { x: timestamp, y: Math.round(throughputOutMbps * 10) / 10 // Round to 1 decimal place }; // Create new arrays with existing data plus new points const newTrafficDataIn = [...this.trafficDataIn, newDataPointIn]; const newTrafficDataOut = [...this.trafficDataOut, newDataPointOut]; // Keep only the last 60 points if (newTrafficDataIn.length > 60) { newTrafficDataIn.shift(); // Remove oldest point newTrafficDataOut.shift(); } this.trafficDataIn = newTrafficDataIn; this.trafficDataOut = newTrafficDataOut; this.lastTrafficUpdateTime = now; console.log('Added new traffic data points:', { timestamp: timestamp, throughputInMbps: newDataPointIn.y, throughputOutMbps: newDataPointOut.y, totalPoints: this.trafficDataIn.length }); } } private startTrafficUpdateTimer() { this.stopTrafficUpdateTimer(); // Clear any existing timer this.trafficUpdateTimer = setInterval(() => { this.updateTrafficData(); }, 1000); // Check every second, but only update when interval has passed } private stopTrafficUpdateTimer() { if (this.trafficUpdateTimer) { clearInterval(this.trafficUpdateTimer); this.trafficUpdateTimer = null; } } }