fix(statistics): preserve private account history safely
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -85,6 +85,7 @@ import {
|
||||
addStatisticsOutcomeInPlace,
|
||||
createStatisticsLedger,
|
||||
loadStatisticsLedger,
|
||||
normalizeStatisticsLedger,
|
||||
projectStatisticsLedger,
|
||||
saveStatisticsLedger,
|
||||
seedStatisticsDayProviderBytes
|
||||
@@ -2652,6 +2653,10 @@ export class DownloadManager extends EventEmitter {
|
||||
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;
|
||||
|
||||
@@ -165,8 +165,22 @@ export function buildRedactedSettingsPayload(settings: AppSettings): Record<stri
|
||||
}
|
||||
|
||||
export function buildStatsPayload(snapshot: UiSnapshot): Record<string, unknown> {
|
||||
const rolling24Hours = snapshot.stats.rolling24Hours;
|
||||
return {
|
||||
session: snapshot.stats,
|
||||
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,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
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> = {}): AppSettings {
|
||||
@@ -17,6 +18,14 @@ 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", () => {
|
||||
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");
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user