290 lines
12 KiB
TypeScript
290 lines
12 KiB
TypeScript
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<void> {
|
|
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`<div class="panel"><div class="panel-content">Loading metrics...</div></div>`;
|
|
}
|
|
|
|
const m = this.metrics;
|
|
const gaugeClass = SwDashTable.getGaugeClass;
|
|
|
|
return html`
|
|
<div class="grid">
|
|
<!-- Cache Panel -->
|
|
<div class="panel">
|
|
<div class="panel-title">Cache</div>
|
|
<div class="panel-content">
|
|
<div class="gauge">
|
|
<div class="gauge-header">
|
|
<span class="gauge-label">Hit Rate</span>
|
|
<span class="gauge-value">${m.cacheHitRate}%</span>
|
|
</div>
|
|
<div class="gauge-bar">
|
|
<div class="gauge-fill ${gaugeClass(m.cacheHitRate)}" style="width: ${m.cacheHitRate}%"></div>
|
|
</div>
|
|
</div>
|
|
<div class="row"><span class="label">Hits</span><span class="value success">${SwDashTable.formatNumber(m.cache.hits)}</span></div>
|
|
<div class="row"><span class="label">Misses</span><span class="value warning">${SwDashTable.formatNumber(m.cache.misses)}</span></div>
|
|
<div class="row"><span class="label">Errors</span><span class="value ${m.cache.errors > 0 ? 'error' : ''}">${SwDashTable.formatNumber(m.cache.errors)}</span></div>
|
|
<div class="row"><span class="label">From Cache</span><span class="value">${SwDashTable.formatBytes(m.cache.bytesServedFromCache)}</span></div>
|
|
<div class="row"><span class="label">Fetched</span><span class="value">${SwDashTable.formatBytes(m.cache.bytesFetched)}</span></div>
|
|
<div class="row"><span class="label">Resources</span><span class="value">${m.resourceCount}</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Network Panel -->
|
|
<div class="panel">
|
|
<div class="panel-title">Network</div>
|
|
<div class="panel-content">
|
|
<div class="gauge">
|
|
<div class="gauge-header">
|
|
<span class="gauge-label">Success Rate</span>
|
|
<span class="gauge-value">${m.networkSuccessRate}%</span>
|
|
</div>
|
|
<div class="gauge-bar">
|
|
<div class="gauge-fill ${gaugeClass(m.networkSuccessRate)}" style="width: ${m.networkSuccessRate}%"></div>
|
|
</div>
|
|
</div>
|
|
<div class="row"><span class="label">Total Requests</span><span class="value">${SwDashTable.formatNumber(m.network.totalRequests)}</span></div>
|
|
<div class="row"><span class="label">Successful</span><span class="value success">${SwDashTable.formatNumber(m.network.successfulRequests)}</span></div>
|
|
<div class="row"><span class="label">Failed</span><span class="value ${m.network.failedRequests > 0 ? 'error' : ''}">${SwDashTable.formatNumber(m.network.failedRequests)}</span></div>
|
|
<div class="row"><span class="label">Timeouts</span><span class="value ${m.network.timeouts > 0 ? 'warning' : ''}">${SwDashTable.formatNumber(m.network.timeouts)}</span></div>
|
|
<div class="row"><span class="label">Avg Latency</span><span class="value">${m.network.averageLatency}ms</span></div>
|
|
<div class="row"><span class="label">Transferred</span><span class="value">${SwDashTable.formatBytes(m.network.totalBytesTransferred)}</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Updates Panel -->
|
|
<div class="panel">
|
|
<div class="panel-title">Updates</div>
|
|
<div class="panel-content">
|
|
<div class="row"><span class="label">Total Checks</span><span class="value">${SwDashTable.formatNumber(m.update.totalChecks)}</span></div>
|
|
<div class="row"><span class="label">Successful</span><span class="value success">${SwDashTable.formatNumber(m.update.successfulChecks)}</span></div>
|
|
<div class="row"><span class="label">Failed</span><span class="value ${m.update.failedChecks > 0 ? 'error' : ''}">${SwDashTable.formatNumber(m.update.failedChecks)}</span></div>
|
|
<div class="row"><span class="label">Updates Found</span><span class="value">${SwDashTable.formatNumber(m.update.updatesFound)}</span></div>
|
|
<div class="row"><span class="label">Updates Applied</span><span class="value success">${SwDashTable.formatNumber(m.update.updatesApplied)}</span></div>
|
|
<div class="row"><span class="label">Last Check</span><span class="value">${SwDashTable.formatTimestamp(m.update.lastCheckTimestamp)}</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Connections Panel -->
|
|
<div class="panel">
|
|
<div class="panel-title">Connections</div>
|
|
<div class="panel-content">
|
|
<div class="row"><span class="label">Active Clients</span><span class="value success">${SwDashTable.formatNumber(m.connection.connectedClients)}</span></div>
|
|
<div class="row"><span class="label">Total Attempts</span><span class="value">${SwDashTable.formatNumber(m.connection.totalConnectionAttempts)}</span></div>
|
|
<div class="row"><span class="label">Successful</span><span class="value success">${SwDashTable.formatNumber(m.connection.successfulConnections)}</span></div>
|
|
<div class="row"><span class="label">Failed</span><span class="value ${m.connection.failedConnections > 0 ? 'error' : ''}">${SwDashTable.formatNumber(m.connection.failedConnections)}</span></div>
|
|
<div class="section-divider">
|
|
<div class="row"><span class="label">Started</span><span class="value">${SwDashTable.formatTimestamp(m.startTime)}</span></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Speedtest Panel -->
|
|
<div class="panel">
|
|
<div class="panel-title">Speedtest</div>
|
|
<div class="panel-content">
|
|
<div class="online-indicator ${m.speedtest.isOnline ? 'online' : 'offline'}">
|
|
<span class="online-dot"></span>
|
|
<span>${m.speedtest.isOnline ? 'Online' : 'Offline'}</span>
|
|
</div>
|
|
${this.speedtestRunning ? html`
|
|
<div class="speedtest-progress">
|
|
<div class="progress-header">
|
|
<span class="progress-phase">${this.getPhaseLabel()}</span>
|
|
<span class="progress-time">${this.formatElapsed()}</span>
|
|
</div>
|
|
<div class="progress-bar">
|
|
<div class="progress-fill ${this.speedtestPhase === 'complete' ? 'complete' : ''}" style="width: ${this.speedtestProgress}%"></div>
|
|
</div>
|
|
</div>
|
|
` : html`
|
|
<div class="speedtest-results">
|
|
<div class="speedtest-metric">
|
|
<div class="speedtest-value">${m.speedtest.lastDownloadSpeedMbps.toFixed(1)}</div>
|
|
<div class="speedtest-unit">Mbps</div>
|
|
<div class="speedtest-label">Download</div>
|
|
</div>
|
|
<div class="speedtest-metric">
|
|
<div class="speedtest-value">${m.speedtest.lastUploadSpeedMbps.toFixed(1)}</div>
|
|
<div class="speedtest-unit">Mbps</div>
|
|
<div class="speedtest-label">Upload</div>
|
|
</div>
|
|
<div class="speedtest-metric">
|
|
<div class="speedtest-value">${m.speedtest.lastLatencyMs.toFixed(0)}</div>
|
|
<div class="speedtest-unit">ms</div>
|
|
<div class="speedtest-label">Latency</div>
|
|
</div>
|
|
</div>
|
|
`}
|
|
<div class="btn-row">
|
|
<button class="btn btn-secondary" ?disabled="${this.speedtestRunning}" @click="${this.runSpeedtest}">
|
|
${this.speedtestRunning ? 'Testing...' : 'Run Test'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|