feat(swdash): Add live speedtest progress UI to service worker dashboard
This commit is contained in:
@@ -1,5 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-12-04 - 7.1.0 - feat(swdash)
|
||||
Add live speedtest progress UI to service worker dashboard
|
||||
|
||||
- Introduce reactive speedtest state (phase, progress, elapsed) in sw-dash-overview component
|
||||
- Start a progress interval to animate overall test progress and estimate phases (latency, download, upload)
|
||||
- Dispatch 'speedtest-complete' event and show a brief complete state before resetting UI
|
||||
- Add helper methods for phase labels and elapsed time formatting
|
||||
- Add CSS for progress bar, shimmer animation and phase pulse to sw-dash-styles
|
||||
|
||||
## 2025-12-04 - 7.0.0 - BREAKING CHANGE(serviceworker)
|
||||
Move serviceworker speedtest to time-based chunked transfers and update dashboard/server contract
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@api.global/typedserver',
|
||||
version: '7.0.0',
|
||||
version: '7.1.0',
|
||||
description: 'A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.'
|
||||
}
|
||||
|
||||
@@ -70,15 +70,46 @@ export class SwDashOverview extends LitElement {
|
||||
|
||||
@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,
|
||||
@@ -87,11 +118,36 @@ export class SwDashOverview extends LitElement {
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error('Speedtest failed:', err);
|
||||
this.speedtestPhase = 'idle';
|
||||
} finally {
|
||||
this.speedtestRunning = false;
|
||||
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">Loading metrics...</div>`;
|
||||
@@ -166,11 +222,23 @@ export class SwDashOverview extends LitElement {
|
||||
<span class="online-dot ${m.speedtest.isOnline ? 'online' : 'offline'}"></span>
|
||||
<span class="value ${m.speedtest.isOnline ? 'success' : 'error'}">${m.speedtest.isOnline ? 'Online' : 'Offline'}</span>
|
||||
</div>
|
||||
<div class="row"><span class="label">Download:</span><span class="value">${m.speedtest.lastDownloadSpeedMbps.toFixed(2)} Mbps</span></div>
|
||||
<div class="speed-bar"><div class="speed-fill" style="width: ${Math.min(m.speedtest.lastDownloadSpeedMbps, 100)}%"></div></div>
|
||||
<div class="row"><span class="label">Upload:</span><span class="value">${m.speedtest.lastUploadSpeedMbps.toFixed(2)} Mbps</span></div>
|
||||
<div class="speed-bar"><div class="speed-fill" style="width: ${Math.min(m.speedtest.lastUploadSpeedMbps, 100)}%"></div></div>
|
||||
<div class="row"><span class="label">Latency:</span><span class="value">${m.speedtest.lastLatencyMs.toFixed(0)} ms</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="row"><span class="label">Download:</span><span class="value">${m.speedtest.lastDownloadSpeedMbps.toFixed(2)} Mbps</span></div>
|
||||
<div class="speed-bar"><div class="speed-fill" style="width: ${Math.min(m.speedtest.lastDownloadSpeedMbps, 100)}%"></div></div>
|
||||
<div class="row"><span class="label">Upload:</span><span class="value">${m.speedtest.lastUploadSpeedMbps.toFixed(2)} Mbps</span></div>
|
||||
<div class="speed-bar"><div class="speed-fill" style="width: ${Math.min(m.speedtest.lastUploadSpeedMbps, 100)}%"></div></div>
|
||||
<div class="row"><span class="label">Latency:</span><span class="value">${m.speedtest.lastLatencyMs.toFixed(0)} ms</span></div>
|
||||
`}
|
||||
<div class="btn-row">
|
||||
<button class="btn" ?disabled="${this.speedtestRunning}" @click="${this.runSpeedtest}">
|
||||
${this.speedtestRunning ? 'Testing...' : 'Run Test'}
|
||||
|
||||
@@ -424,4 +424,71 @@ export const speedtestStyles: CSSResult = css`
|
||||
background: var(--sw-gauge-good);
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
/* Speedtest progress indicator */
|
||||
.speedtest-progress {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.progress-phase {
|
||||
color: var(--sw-text-cyan);
|
||||
font-weight: bold;
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
.progress-time {
|
||||
color: var(--sw-text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 20px;
|
||||
background: var(--sw-bg-input);
|
||||
border: 1px solid var(--sw-border);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--sw-gauge-good), var(--sw-text-cyan));
|
||||
transition: width 0.1s linear;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-fill::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
.progress-fill.complete {
|
||||
background: var(--sw-text-primary);
|
||||
}
|
||||
|
||||
.progress-fill.complete::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(100%); }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.6; }
|
||||
}
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user