Prepare v2.0.74 sidepanel reliability release

Restore and persist the package-based link collector with progressive metadata, bounded high-volume updates, safe hydration, visible selection, and complete localization.

Harden download controls, snapshot ordering, history pagination, statistics recovery, settings saves, backup imports, notification persistence, and Windows storage races with regression coverage.
This commit is contained in:
Sucukdeluxe
2026-08-26 20:35:06 +02:00
parent bdcf9e3754
commit 2ea494cd5c
69 changed files with 7660 additions and 883 deletions
+103 -25
View File
@@ -90,8 +90,10 @@ import {
loadStatisticsLedger,
normalizeStatisticsLedger,
projectStatisticsLedger,
rebaseStatisticsProviderSeedBaseline,
saveStatisticsLedger,
seedStatisticsDayProviderBytes
seedStatisticsDayProviderBytes,
suppressStatisticsProviderSeedForDay
} from "./statistics-ledger";
import { finalizePackageResult, projectPackageFailureCategory } from "./package-telemetry";
import type { NotificationEvent } from "./notification-outbox";
@@ -1910,9 +1912,10 @@ export class DownloadManager extends EventEmitter {
this.settingsSnapshotCacheAt = 0;
}
private lastEmittedItemHashes = new Map<string, string>();
private lastEmittedPackageHashes = new Map<string, string>();
private firstEmitDone = false;
private lastEmittedItemHashes = new Map<string, string>();
private lastEmittedPackageHashes = new Map<string, string>();
private snapshotRevision = 0;
private firstEmitDone = false;
private lastFullEmitAt = 0;
private static readonly FULL_RESYNC_INTERVAL_MS = 30000;
@@ -2787,7 +2790,20 @@ export class DownloadManager extends EventEmitter {
const elapsed = this.session.runStartedAt > 0 ? (now - this.session.runStartedAt) / 1000 : 0;
const rate = doneItems > 0 && elapsed > 0 ? doneItems / elapsed : 0;
const remaining = totalItems - doneItems;
const eta = remaining > 0 && rate > 0 ? remaining / rate : -1;
const eta = remaining > 0 && rate > 0 ? remaining / rate : -1;
let runRemainingBytes = 0;
let runRemainingUnknownItems = 0;
if (this.session.running) {
for (const itemId of this.runItemIds) {
const item = this.session.items[itemId];
if (!item || isFinishedStatus(item.status)) continue;
if (item.totalBytes && item.totalBytes > 0) {
runRemainingBytes += Math.max(0, item.totalBytes - Math.max(0, item.downloadedBytes));
} else {
runRemainingUnknownItems += 1;
}
}
}
const reconnectMs = Math.max(0, this.session.reconnectUntil - now);
@@ -2805,6 +2821,7 @@ export class DownloadManager extends EventEmitter {
: null;
return {
snapshotRevision: ++this.snapshotRevision,
rotationEvents: getRecentRotationEvents(40),
accountRuntime: createAccountRuntimeEntries(rendererState.accounts, Object.values(snapshotSession.items), now),
settings: rendererState.settings,
@@ -2827,8 +2844,10 @@ export class DownloadManager extends EventEmitter {
for (const [pid, bytes] of this.speedBytesPerPackage) {
out[pid] = Math.floor(bytes / SPEED_WINDOW_SECONDS);
}
return out;
})()
return out;
})(),
runRemainingBytes,
runRemainingUnknownItems
};
}
@@ -3036,7 +3055,7 @@ export class DownloadManager extends EventEmitter {
return Object.values(this.session.packages).some((pkg) => pathKey(pkg.outputDir) === key);
}
public resetSessionStats(): void {
public resetSessionStats(): void {
const now = nowMs();
this.session.totalDownloadedBytes = 0;
this.sessionDownloadedBytes = 0;
@@ -3053,24 +3072,77 @@ export class DownloadManager extends EventEmitter {
this.summary = null;
this.invalidateStatsCache();
this.persistSoon();
this.emitState(true);
}
public resetDownloadStats(): void {
this.settings.totalDownloadedAllTime = 0;
this.settings.totalCompletedFilesAllTime = 0;
this.settings.providerTotalUsageBytes = {};
this.settings.debridLinkApiKeyTotalUsageBytes = {};
this.statisticsLedger = createStatisticsLedger();
this.rollingAccountStatistics.reset(this.statisticsLedger);
this.emitState(true);
}
public rebaseStatisticsProviderDailyUsage(provider: DebridProvider, providerUsageBytes: number): void {
const rebasedAt = nowMs();
const rebased = rebaseStatisticsProviderSeedBaseline(
this.statisticsLedger,
provider,
providerUsageBytes,
rebasedAt
);
saveStatisticsLedger(this.storagePaths.statisticsFile, rebased);
this.statisticsLedger = rebased;
this.rollingAccountStatistics.reset(rebased, rebasedAt);
this.statisticsDirty = false;
this.statisticsUrgent = false;
saveStatisticsLedger(this.storagePaths.statisticsFile, this.statisticsLedger);
this.lastSettingsPersistAt = nowMs();
saveSettings(this.storagePaths, this.settings);
this.invalidateStatsCache();
this.emitState(true);
}
this.invalidateStatsCache();
this.emitState(true);
}
public resetDownloadStats(): void {
const resetAt = nowMs();
const previousSettings = JSON.parse(JSON.stringify(this.settings)) as AppSettings;
const previousLedger = normalizeStatisticsLedger(this.statisticsLedger, resetAt);
const nextSettings = JSON.parse(JSON.stringify(previousSettings)) as AppSettings;
const currentUsageDay = getProviderUsageDayKey(resetAt);
if (nextSettings.providerDailyUsageDay !== currentUsageDay) {
nextSettings.providerDailyUsageDay = currentUsageDay;
nextSettings.providerDailyUsageBytes = {};
nextSettings.debridLinkApiKeyDailyUsageBytes = {};
nextSettings.megaDebridAccountDailyUsageBytes = {};
nextSettings.realDebridAccountDailyUsageBytes = {};
}
nextSettings.totalDownloadedAllTime = 0;
nextSettings.totalCompletedFilesAllTime = 0;
nextSettings.providerTotalUsageBytes = {};
nextSettings.debridLinkApiKeyTotalUsageBytes = {};
const nextLedger = suppressStatisticsProviderSeedForDay(
createStatisticsLedger(resetAt),
resetAt,
nextSettings.providerDailyUsageBytes
);
saveStatisticsLedger(this.storagePaths.statisticsFile, nextLedger);
try {
saveSettings(this.storagePaths, nextSettings);
} catch (error) {
const rollbackErrors: string[] = [];
try {
saveStatisticsLedger(this.storagePaths.statisticsFile, previousLedger);
} catch (rollbackError) {
rollbackErrors.push(`Statistik: ${compactErrorText(rollbackError)}`);
}
try {
saveSettings(this.storagePaths, previousSettings);
} catch (rollbackError) {
rollbackErrors.push(`Einstellungen: ${compactErrorText(rollbackError)}`);
}
if (rollbackErrors.length > 0) {
logger.error(`Statistik-Reset-Rollback fehlgeschlagen: ${rollbackErrors.join(" | ")}`);
}
throw error;
}
Object.assign(this.settings, nextSettings);
this.statisticsLedger = nextLedger;
this.rollingAccountStatistics.reset(nextLedger, resetAt);
this.statisticsDirty = false;
this.statisticsUrgent = false;
this.lastSettingsPersistAt = resetAt;
this.invalidateStatsCache();
this.emitState(true);
}
public renamePackage(packageId: string, newName: string): void {
const pkg = this.session.packages[packageId];
@@ -7062,9 +7134,15 @@ export class DownloadManager extends EventEmitter {
} catch (error) {
this.statisticsDirty = true;
logger.warn(`Statistik konnte nicht gespeichert werden: ${compactErrorText(error)}`);
try {
saveSettings(this.storagePaths, this.settings);
this.lastSettingsPersistAt = now;
} catch (settingsError) {
logger.warn(`Statistik-Fallback konnte nicht gespeichert werden: ${compactErrorText(settingsError)}`);
}
}
}
}
}
public persistNowSync(): void {
this.clearPersistTimer();