Add bandwidth statistics tab with live chart

- Add new Statistics tab between Downloads and Settings
- Implement real-time bandwidth chart using Canvas (60s history)
- Add session overview with 8 stats cards (speed, downloaded, files, packages, etc.)
- Add provider statistics with progress bars
- Add getSessionStats IPC endpoint
- Support dark/light theme in chart rendering
This commit is contained in:
Sucukdeluxe
2026-03-01 15:56:57 +01:00
parent f5d7ee4d1a
commit a0cdac87e8
11 changed files with 597 additions and 3 deletions
+5
View File
@@ -5,6 +5,7 @@ import {
AppSettings,
DuplicatePolicy,
ParsedPackageInput,
SessionStats,
StartConflictEntry,
StartConflictResolutionResult,
UiSnapshot,
@@ -225,6 +226,10 @@ export class AppController {
return this.manager.importQueue(json);
}
public getSessionStats(): SessionStats {
return this.manager.getSessionStats();
}
public shutdown(): void {
abortActiveUpdateDownload();
this.manager.prepareForShutdown();
+75
View File
@@ -4737,4 +4737,79 @@ export class DownloadManager extends EventEmitter {
this.persistNow();
this.emitState();
}
public getSessionStats(): import("../shared/types").SessionStats {
const now = nowMs();
this.pruneSpeedEvents(now);
const bandwidthSamples: import("../shared/types").BandwidthSample[] = [];
for (let i = this.speedEventsHead; i < this.speedEvents.length; i += 1) {
const event = this.speedEvents[i];
if (event) {
bandwidthSamples.push({
timestamp: event.at,
speedBps: event.bytes * 3
});
}
}
const paused = this.session.running && this.session.paused;
const currentSpeedBps = paused ? 0 : this.speedBytesLastWindow / 3;
let totalBytes = 0;
let maxSpeed = 0;
for (let i = this.speedEventsHead; i < this.speedEvents.length; i += 1) {
const event = this.speedEvents[i];
if (event) {
totalBytes += event.bytes;
const speed = event.bytes * 3;
if (speed > maxSpeed) {
maxSpeed = speed;
}
}
}
const sessionDurationSeconds = this.session.runStartedAt > 0
? Math.max(0, Math.floor((now - this.session.runStartedAt) / 1000))
: 0;
const averageSpeedBps = sessionDurationSeconds > 0
? Math.floor(this.session.totalDownloadedBytes / sessionDurationSeconds)
: 0;
let totalDownloads = 0;
let completedDownloads = 0;
let failedDownloads = 0;
let activeDownloads = 0;
let queuedDownloads = 0;
for (const item of Object.values(this.session.items)) {
totalDownloads += 1;
if (item.status === "completed") {
completedDownloads += 1;
} else if (item.status === "failed") {
failedDownloads += 1;
} else if (item.status === "downloading" || item.status === "validating") {
activeDownloads += 1;
} else if (item.status === "queued" || item.status === "reconnect_wait") {
queuedDownloads += 1;
}
}
return {
bandwidth: {
samples: bandwidthSamples.slice(-120),
currentSpeedBps: Math.floor(currentSpeedBps),
averageSpeedBps,
maxSpeedBps: Math.floor(maxSpeed),
totalBytesSession: this.session.totalDownloadedBytes,
sessionDurationSeconds
},
totalDownloads,
completedDownloads,
failedDownloads,
activeDownloads,
queuedDownloads
};
}
}
+1
View File
@@ -343,6 +343,7 @@ function registerIpcHandlers(): void {
const result = mainWindow ? await dialog.showOpenDialog(mainWindow, options) : await dialog.showOpenDialog(options);
return result.canceled ? [] : result.filePaths;
});
ipcMain.handle(IPC_CHANNELS.GET_SESSION_STATS, () => controller.getSessionStats());
controller.onState = (snapshot) => {
if (!mainWindow || mainWindow.isDestroyed()) {