fix(statistics): preserve private account history safely
This commit is contained in:
@@ -1092,7 +1092,7 @@ export class AppController {
|
|||||||
exportedAt: new Date().toISOString(),
|
exportedAt: new Date().toISOString(),
|
||||||
session: this.manager.getSession(),
|
session: this.manager.getSession(),
|
||||||
history: loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode, this.historyLimits()),
|
history: loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode, this.historyLimits()),
|
||||||
statistics: this.manager.getStats().statistics!,
|
statistics: this.manager.getStatisticsLedgerForBackup(),
|
||||||
remoteDiagnostics
|
remoteDiagnostics
|
||||||
});
|
});
|
||||||
const encrypted = encryptBackup(JSON.stringify(payloadObj), passphrase);
|
const encrypted = encryptBackup(JSON.stringify(payloadObj), passphrase);
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ import {
|
|||||||
addStatisticsOutcomeInPlace,
|
addStatisticsOutcomeInPlace,
|
||||||
createStatisticsLedger,
|
createStatisticsLedger,
|
||||||
loadStatisticsLedger,
|
loadStatisticsLedger,
|
||||||
|
normalizeStatisticsLedger,
|
||||||
projectStatisticsLedger,
|
projectStatisticsLedger,
|
||||||
saveStatisticsLedger,
|
saveStatisticsLedger,
|
||||||
seedStatisticsDayProviderBytes
|
seedStatisticsDayProviderBytes
|
||||||
@@ -2624,7 +2625,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public getStats(now = nowMs()): DownloadStats {
|
public getStats(now = nowMs()): DownloadStats {
|
||||||
const itemCount = this.itemCount;
|
const itemCount = this.itemCount;
|
||||||
if (this.statsCache && this.session.running && itemCount >= 500 && now - this.statsCacheAt < 1500) {
|
if (this.statsCache && this.session.running && itemCount >= 500 && now - this.statsCacheAt < 1500) {
|
||||||
return this.statsCache;
|
return this.statsCache;
|
||||||
@@ -2649,8 +2650,12 @@ export class DownloadManager extends EventEmitter {
|
|||||||
};
|
};
|
||||||
this.statsCache = stats;
|
this.statsCache = stats;
|
||||||
this.statsCacheAt = now;
|
this.statsCacheAt = now;
|
||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getStatisticsLedgerForBackup(now = nowMs()): StatisticsLedger {
|
||||||
|
return normalizeStatisticsLedger(this.statisticsLedger, now);
|
||||||
|
}
|
||||||
|
|
||||||
public getLiveTotalRuntimeMs(now = nowMs()): number {
|
public getLiveTotalRuntimeMs(now = nowMs()): number {
|
||||||
return Math.max(0, this.runtimePersistedTotalMs + Math.max(0, now - this.runtimePersistedAt));
|
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 recordedAt = nowMs();
|
||||||
const effectiveProvider = resolveMegaDebridProvider(this.settings, provider) || provider;
|
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);
|
addStatisticsBytesInPlace(this.statisticsLedger, effectiveProvider, byteDelta, recordedAt);
|
||||||
this.rollingAccountStatistics.record(
|
this.rollingAccountStatistics.record(
|
||||||
effectiveProvider,
|
effectiveProvider,
|
||||||
byteDelta,
|
byteDelta,
|
||||||
providerAccountId,
|
providerAccountId,
|
||||||
providerAccountLabel,
|
statisticsAccountLabel,
|
||||||
recordedAt
|
recordedAt
|
||||||
);
|
);
|
||||||
this.statisticsDirty = true;
|
this.statisticsDirty = true;
|
||||||
|
|||||||
@@ -164,9 +164,23 @@ export function buildRedactedSettingsPayload(settings: AppSettings): Record<stri
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildStatsPayload(snapshot: UiSnapshot): Record<string, unknown> {
|
export function buildStatsPayload(snapshot: UiSnapshot): Record<string, unknown> {
|
||||||
return {
|
const rolling24Hours = snapshot.stats.rolling24Hours;
|
||||||
session: snapshot.stats,
|
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: {
|
totals: {
|
||||||
totalPackages: Object.keys(snapshot.session.packages).length,
|
totalPackages: Object.keys(snapshot.session.packages).length,
|
||||||
totalItems: Object.keys(snapshot.session.items).length,
|
totalItems: Object.keys(snapshot.session.items).length,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
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";
|
import type { AppSettings, SessionState, HistoryEntry, StatisticsLedger } from "../src/shared/types";
|
||||||
|
|
||||||
function settings(overrides: Partial<AppSettings> = {}): AppSettings {
|
function settings(overrides: Partial<AppSettings> = {}): 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 };
|
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)", () => {
|
it("omits session AND history when backupIncludeDownloads is false (default)", () => {
|
||||||
const p = buildBackupPayload({ ...baseInput, settings: { backupIncludeDownloads: false } as AppSettings });
|
const p = buildBackupPayload({ ...baseInput, settings: { backupIncludeDownloads: false } as AppSettings });
|
||||||
expect(p.kind).toBe("settings-only");
|
expect(p.kind).toBe("settings-only");
|
||||||
|
|||||||
@@ -13362,7 +13362,33 @@ describe("download manager", () => {
|
|||||||
realDebridApiTokens: serializeRealDebridApiAccounts([
|
realDebridApiTokens: serializeRealDebridApiAccounts([
|
||||||
{ id: "rda_one", token: "token-one" },
|
{ id: "rda_one", token: "token-one" },
|
||||||
{ id: "rda_two", token: "token-two" }
|
{ 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 manager = new DownloadManager(settings, emptySession(), createStoragePaths(path.join(root, "state")));
|
||||||
const internal = manager as unknown as {
|
const internal = manager as unknown as {
|
||||||
@@ -13381,11 +13407,12 @@ describe("download manager", () => {
|
|||||||
|
|
||||||
expect(stats.rolling24Hours).toMatchObject({ downloadedBytes: 125 });
|
expect(stats.rolling24Hours).toMatchObject({ downloadedBytes: 125 });
|
||||||
expect(stats.rolling24Hours?.accounts.map((account) => [account.id, account.label, account.bytes])).toEqual([
|
expect(stats.rolling24Hours?.accounts.map((account) => [account.id, account.label, account.bytes])).toEqual([
|
||||||
["rda_two", "Secondary", 75],
|
["rda_two", "Backup", 75],
|
||||||
["rda_one", "Primary", 50]
|
["rda_one", "xSucukDE", 50]
|
||||||
]);
|
]);
|
||||||
expect(stats.statistics?.minutes).toEqual([]);
|
expect(stats.statistics?.minutes).toEqual([]);
|
||||||
expect(internal.statisticsLedger.minutes).toHaveLength(1);
|
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", () => {
|
it("keeps rolling traffic on session reset and clears it on total reset", () => {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { defaultSettings } from "../src/main/constants";
|
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 { serializeRealDebridApiAccounts } from "../src/shared/real-debrid-accounts";
|
||||||
|
import { createVisualFixture } from "./visual/fixtures";
|
||||||
|
|
||||||
describe("Real-Debrid support summary", () => {
|
describe("Real-Debrid support summary", () => {
|
||||||
it("reports pool counts without exposing account IDs or credentials", () => {
|
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("rdw_private");
|
||||||
expect(serialized).not.toContain("secret-");
|
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