From dc3d9dfc100a1f5633ca7efd085703d1395ef502 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Fri, 21 Aug 2026 08:37:04 +0200 Subject: [PATCH] fix(statistics): preserve private account history safely --- src/main/app-controller.ts | 2 +- src/main/download-manager.ts | 17 +++++++++++++---- src/main/support-data.ts | 20 +++++++++++++++++--- tests/backup-payload.test.ts | 13 +++++++++++-- tests/download-manager.test.ts | 33 ++++++++++++++++++++++++++++++--- tests/support-data.test.ts | 26 +++++++++++++++++++++++++- 6 files changed, 97 insertions(+), 14 deletions(-) diff --git a/src/main/app-controller.ts b/src/main/app-controller.ts index 48dec31..e4d3b84 100644 --- a/src/main/app-controller.ts +++ b/src/main/app-controller.ts @@ -1092,7 +1092,7 @@ export class AppController { exportedAt: new Date().toISOString(), session: this.manager.getSession(), history: loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode, this.historyLimits()), - statistics: this.manager.getStats().statistics!, + statistics: this.manager.getStatisticsLedgerForBackup(), remoteDiagnostics }); const encrypted = encryptBackup(JSON.stringify(payloadObj), passphrase); diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 8124409..dbc88be 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -85,6 +85,7 @@ import { addStatisticsOutcomeInPlace, createStatisticsLedger, loadStatisticsLedger, + normalizeStatisticsLedger, projectStatisticsLedger, saveStatisticsLedger, seedStatisticsDayProviderBytes @@ -2624,7 +2625,7 @@ export class DownloadManager extends EventEmitter { }; } - public getStats(now = nowMs()): DownloadStats { + public getStats(now = nowMs()): DownloadStats { const itemCount = this.itemCount; if (this.statsCache && this.session.running && itemCount >= 500 && now - this.statsCacheAt < 1500) { return this.statsCache; @@ -2649,8 +2650,12 @@ export class DownloadManager extends EventEmitter { }; this.statsCache = stats; this.statsCacheAt = now; - return stats; - } + return stats; + } + + public getStatisticsLedgerForBackup(now = nowMs()): StatisticsLedger { + return normalizeStatisticsLedger(this.statisticsLedger, now); + } public getLiveTotalRuntimeMs(now = nowMs()): number { return Math.max(0, this.runtimePersistedTotalMs + Math.max(0, now - this.runtimePersistedAt)); @@ -8234,12 +8239,16 @@ export class DownloadManager extends EventEmitter { } const recordedAt = nowMs(); const effectiveProvider = resolveMegaDebridProvider(this.settings, provider) || provider; + const accountStatus = providerAccountId ? this.settings.debridAccountStatuses[providerAccountId] : undefined; + const statisticsAccountLabel = accountStatus?.username?.trim() + || accountStatus?.email?.trim() + || providerAccountLabel; addStatisticsBytesInPlace(this.statisticsLedger, effectiveProvider, byteDelta, recordedAt); this.rollingAccountStatistics.record( effectiveProvider, byteDelta, providerAccountId, - providerAccountLabel, + statisticsAccountLabel, recordedAt ); this.statisticsDirty = true; diff --git a/src/main/support-data.ts b/src/main/support-data.ts index c699983..2678cac 100644 --- a/src/main/support-data.ts +++ b/src/main/support-data.ts @@ -164,9 +164,23 @@ export function buildRedactedSettingsPayload(settings: AppSettings): Record { - return { - session: snapshot.stats, +export function buildStatsPayload(snapshot: UiSnapshot): Record { + const rolling24Hours = snapshot.stats.rolling24Hours; + return { + session: { + ...snapshot.stats, + ...(rolling24Hours ? { + rolling24Hours: { + from: rolling24Hours.from, + to: rolling24Hours.to, + downloadedBytes: rolling24Hours.downloadedBytes, + accounts: rolling24Hours.accounts.map((account) => ({ + provider: account.provider, + bytes: account.bytes + })) + } + } : {}) + }, totals: { totalPackages: Object.keys(snapshot.session.packages).length, totalItems: Object.keys(snapshot.session.items).length, diff --git a/tests/backup-payload.test.ts b/tests/backup-payload.test.ts index 5185779..d78ef52 100644 --- a/tests/backup-payload.test.ts +++ b/tests/backup-payload.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; -import { buildBackupPayload, planBackupImport } from "../src/main/backup-payload"; +import { buildBackupPayload, planBackupImport } from "../src/main/backup-payload"; +import { readFileSync } from "node:fs"; import type { AppSettings, SessionState, HistoryEntry, StatisticsLedger } from "../src/shared/types"; function settings(overrides: Partial = {}): AppSettings { @@ -16,7 +17,15 @@ const statistics: StatisticsLedger = { version: 2, startedAt: 1, days: [], minut const baseInput = { appVersion: "1.7.183", exportedAt: "2026-06-07T00:00:00Z", session, history, statistics }; -describe("buildBackupPayload — default is settings-only", () => { +describe("buildBackupPayload — default is settings-only", () => { + it("exports the full statistics ledger instead of the renderer projection", () => { + const source = readFileSync(new URL("../src/main/app-controller.ts", import.meta.url), "utf8"); + const exportBlock = source.slice(source.indexOf("public exportBackup"), source.indexOf("public async exportOnlineBackup")); + + expect(exportBlock).toContain("getStatisticsLedgerForBackup()"); + expect(exportBlock).not.toContain("getStats().statistics"); + }); + it("omits session AND history when backupIncludeDownloads is false (default)", () => { const p = buildBackupPayload({ ...baseInput, settings: { backupIncludeDownloads: false } as AppSettings }); expect(p.kind).toBe("settings-only"); diff --git a/tests/download-manager.test.ts b/tests/download-manager.test.ts index f4115c4..df60337 100644 --- a/tests/download-manager.test.ts +++ b/tests/download-manager.test.ts @@ -13362,7 +13362,33 @@ describe("download manager", () => { realDebridApiTokens: serializeRealDebridApiAccounts([ { id: "rda_one", token: "token-one" }, { id: "rda_two", token: "token-two" } - ]) + ]), + debridAccountStatuses: { + rda_one: { + accountId: "rda_one", + provider: "realdebrid" as const, + label: "Real-Debrid API 1", + maskedLogin: "Geschützter API-Token", + valid: true, + isPremium: true, + premiumUntilMs: null, + username: "xSucukDE", + message: "Premium aktiv", + checkedAt: Date.now() + }, + rda_two: { + accountId: "rda_two", + provider: "realdebrid" as const, + label: "Real-Debrid API 2", + maskedLogin: "Geschützter API-Token", + valid: true, + isPremium: true, + premiumUntilMs: null, + username: "Backup", + message: "Premium aktiv", + checkedAt: Date.now() + } + } }; const manager = new DownloadManager(settings, emptySession(), createStoragePaths(path.join(root, "state"))); const internal = manager as unknown as { @@ -13381,11 +13407,12 @@ describe("download manager", () => { expect(stats.rolling24Hours).toMatchObject({ downloadedBytes: 125 }); expect(stats.rolling24Hours?.accounts.map((account) => [account.id, account.label, account.bytes])).toEqual([ - ["rda_two", "Secondary", 75], - ["rda_one", "Primary", 50] + ["rda_two", "Backup", 75], + ["rda_one", "xSucukDE", 50] ]); expect(stats.statistics?.minutes).toEqual([]); expect(internal.statisticsLedger.minutes).toHaveLength(1); + expect(manager.getStatisticsLedgerForBackup().minutes).toHaveLength(1); }); it("keeps rolling traffic on session reset and clears it on total reset", () => { diff --git a/tests/support-data.test.ts b/tests/support-data.test.ts index 126f57f..bd580ae 100644 --- a/tests/support-data.test.ts +++ b/tests/support-data.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it } from "vitest"; import { defaultSettings } from "../src/main/constants"; -import { buildAccountSummary } from "../src/main/support-data"; +import { buildAccountSummary, buildStatsPayload } from "../src/main/support-data"; import { serializeRealDebridApiAccounts } from "../src/shared/real-debrid-accounts"; +import { createVisualFixture } from "./visual/fixtures"; describe("Real-Debrid support summary", () => { it("reports pool counts without exposing account IDs or credentials", () => { @@ -29,4 +30,27 @@ describe("Real-Debrid support summary", () => { expect(serialized).not.toContain("rdw_private"); expect(serialized).not.toContain("secret-"); }); + + it("removes rolling account IDs and labels from support statistics", () => { + const snapshot = structuredClone(createVisualFixture("empty").snapshot); + snapshot.stats.rolling24Hours = { + from: 1, + to: 2, + downloadedBytes: 4_096, + accounts: [{ + id: "rdw_private_account", + provider: "realdebrid", + label: "private-user@example.test", + bytes: 4_096 + }] + }; + + const payload = buildStatsPayload(snapshot); + const serialized = JSON.stringify(payload); + + expect(serialized).not.toContain("rdw_private_account"); + expect(serialized).not.toContain("private-user@example.test"); + expect(serialized).toContain("realdebrid"); + expect(serialized).toContain("4096"); + }); });