import { LitElement, html, css, property, state, customElement } from './plugins.js'; import type { CSSResult, TemplateResult } from './plugins.js'; import { sharedStyles, panelStyles, gaugeStyles, buttonStyles, speedtestStyles } from './sw-dash-styles.js'; import { SwDashTable } from './sw-dash-table.js'; export interface IMetricsData { cache: { hits: number; misses: number; errors: number; bytesServedFromCache: number; bytesFetched: number; averageResponseTime: number; }; network: { totalRequests: number; successfulRequests: number; failedRequests: number; timeouts: number; averageLatency: number; totalBytesTransferred: number; }; update: { totalChecks: number; successfulChecks: number; failedChecks: number; updatesFound: number; updatesApplied: number; lastCheckTimestamp: number; lastUpdateTimestamp: number; }; connection: { connectedClients: number; totalConnectionAttempts: number; successfulConnections: number; failedConnections: number; }; speedtest: { lastDownloadSpeedMbps: number; lastUploadSpeedMbps: number; lastLatencyMs: number; lastTestTimestamp: number; testCount: number; isOnline: boolean; }; startTime: number; uptime: number; cacheHitRate: number; networkSuccessRate: number; resourceCount: number; } /** * Overview panel component with metrics gauges and stats */ @customElement('sw-dash-overview') export class SwDashOverview extends LitElement { public static styles: CSSResult[] = [ sharedStyles, panelStyles, gaugeStyles, buttonStyles, speedtestStyles, css` :host { display: block; } .panel-content { padding: var(--space-4); } .section-divider { margin-top: var(--space-4); padding-top: var(--space-4); border-top: 1px solid var(--border-muted); } ` ]; @property({ type: Object }) accessor metrics: IMetricsData | null = null; @state() accessor speedtestRunning = false; @state() accessor speedtestPhase: 'idle' | 'latency' | 'download' | 'upload' | 'complete' = 'idle'; @state() accessor speedtestProgress = 0; @state() accessor speedtestElapsed = 0; // Speedtest timing constants (must match service worker) private static readonly TEST_DURATION_MS = 5000; // 5 seconds per test private progressInterval: number | null = null; private async runSpeedtest(): Promise { if (this.speedtestRunning) return; this.speedtestRunning = true; this.speedtestPhase = 'latency'; this.speedtestProgress = 0; this.speedtestElapsed = 0; // Start progress animation (total ~10.5s: latency ~0.5s + 5s download + 5s upload) const totalEstimatedMs = 10500; const startTime = Date.now(); this.progressInterval = window.setInterval(() => { this.speedtestElapsed = Date.now() - startTime; this.speedtestProgress = Math.min(100, (this.speedtestElapsed / totalEstimatedMs) * 100); // Estimate phase based on elapsed time if (this.speedtestElapsed < 500) { this.speedtestPhase = 'latency'; } else if (this.speedtestElapsed < 5500) { this.speedtestPhase = 'download'; } else { this.speedtestPhase = 'upload'; } }, 100); try { const response = await fetch('/sw-dash/speedtest'); const result = await response.json(); this.speedtestPhase = 'complete'; this.speedtestProgress = 100; // Dispatch event to parent to update metrics this.dispatchEvent(new CustomEvent('speedtest-complete', { detail: result, bubbles: true, composed: true })); } catch (err) { console.error('Speedtest failed:', err); this.speedtestPhase = 'idle'; } finally { if (this.progressInterval) { window.clearInterval(this.progressInterval); this.progressInterval = null; } // Keep showing complete state briefly, then reset setTimeout(() => { this.speedtestRunning = false; this.speedtestPhase = 'idle'; this.speedtestProgress = 0; }, 1500); } } private getPhaseLabel(): string { switch (this.speedtestPhase) { case 'latency': return 'Testing latency'; case 'download': return 'Download test'; case 'upload': return 'Upload test'; case 'complete': return 'Complete'; default: return ''; } } private formatElapsed(): string { const seconds = Math.floor(this.speedtestElapsed / 1000); return `${seconds}s`; } public render(): TemplateResult { if (!this.metrics) { return html`
Loading metrics...
`; } const m = this.metrics; const gaugeClass = SwDashTable.getGaugeClass; return html`
Cache
Hit Rate ${m.cacheHitRate}%
Hits${SwDashTable.formatNumber(m.cache.hits)}
Misses${SwDashTable.formatNumber(m.cache.misses)}
Errors${SwDashTable.formatNumber(m.cache.errors)}
From Cache${SwDashTable.formatBytes(m.cache.bytesServedFromCache)}
Fetched${SwDashTable.formatBytes(m.cache.bytesFetched)}
Resources${m.resourceCount}
Network
Success Rate ${m.networkSuccessRate}%
Total Requests${SwDashTable.formatNumber(m.network.totalRequests)}
Successful${SwDashTable.formatNumber(m.network.successfulRequests)}
Failed${SwDashTable.formatNumber(m.network.failedRequests)}
Timeouts${SwDashTable.formatNumber(m.network.timeouts)}
Avg Latency${m.network.averageLatency}ms
Transferred${SwDashTable.formatBytes(m.network.totalBytesTransferred)}
Updates
Total Checks${SwDashTable.formatNumber(m.update.totalChecks)}
Successful${SwDashTable.formatNumber(m.update.successfulChecks)}
Failed${SwDashTable.formatNumber(m.update.failedChecks)}
Updates Found${SwDashTable.formatNumber(m.update.updatesFound)}
Updates Applied${SwDashTable.formatNumber(m.update.updatesApplied)}
Last Check${SwDashTable.formatTimestamp(m.update.lastCheckTimestamp)}
Connections
Active Clients${SwDashTable.formatNumber(m.connection.connectedClients)}
Total Attempts${SwDashTable.formatNumber(m.connection.totalConnectionAttempts)}
Successful${SwDashTable.formatNumber(m.connection.successfulConnections)}
Failed${SwDashTable.formatNumber(m.connection.failedConnections)}
Started${SwDashTable.formatTimestamp(m.startTime)}
Speedtest
${m.speedtest.isOnline ? 'Online' : 'Offline'}
${this.speedtestRunning ? html`
${this.getPhaseLabel()} ${this.formatElapsed()}
` : html`
${m.speedtest.lastDownloadSpeedMbps.toFixed(1)}
Mbps
Download
${m.speedtest.lastUploadSpeedMbps.toFixed(1)}
Mbps
Upload
${m.speedtest.lastLatencyMs.toFixed(0)}
ms
Latency
`}
`; } }