Add app runtime statistics

This commit is contained in:
Sucukdeluxe 2026-03-09 04:59:00 +01:00
parent 971f669bb6
commit 1f9a26e4b0
12 changed files with 194 additions and 8 deletions

View File

@ -76,6 +76,7 @@ export class AppController {
private onStateHandler: ((snapshot: UiSnapshot) => void) | null = null; private onStateHandler: ((snapshot: UiSnapshot) => void) | null = null;
private autoResumePending = false; private autoResumePending = false;
private runtimeStatsTimer: NodeJS.Timeout | null = null;
public constructor() { public constructor() {
configureLogger(this.storagePaths.baseDir); configureLogger(this.storagePaths.baseDir);
@ -114,6 +115,11 @@ export class AppController {
runtimeDir: this.storagePaths.baseDir runtimeDir: this.storagePaths.baseDir
}); });
startDebugServer(this.manager, this.storagePaths.baseDir); startDebugServer(this.manager, this.storagePaths.baseDir);
this.runtimeStatsTimer = setInterval(() => {
this.manager.persistRuntimeStats();
this.settings = this.manager.getSettings();
}, 60_000);
this.runtimeStatsTimer.unref?.();
if (this.settings.autoResumeOnStart) { if (this.settings.autoResumeOnStart) {
const snapshot = this.manager.getSnapshot(); const snapshot = this.manager.getSnapshot();
@ -237,6 +243,7 @@ export class AppController {
const liveSettings = this.manager.getSettings(); const liveSettings = this.manager.getSettings();
nextSettings.totalDownloadedAllTime = Math.max(nextSettings.totalDownloadedAllTime || 0, liveSettings.totalDownloadedAllTime || 0); nextSettings.totalDownloadedAllTime = Math.max(nextSettings.totalDownloadedAllTime || 0, liveSettings.totalDownloadedAllTime || 0);
nextSettings.totalCompletedFilesAllTime = Math.max(nextSettings.totalCompletedFilesAllTime || 0, liveSettings.totalCompletedFilesAllTime || 0); nextSettings.totalCompletedFilesAllTime = Math.max(nextSettings.totalCompletedFilesAllTime || 0, liveSettings.totalCompletedFilesAllTime || 0);
nextSettings.totalRuntimeAllTimeMs = Math.max(nextSettings.totalRuntimeAllTimeMs || 0, this.manager.getLiveTotalRuntimeMs());
nextSettings.providerDailyUsageDay = liveSettings.providerDailyUsageDay; nextSettings.providerDailyUsageDay = liveSettings.providerDailyUsageDay;
nextSettings.providerDailyUsageBytes = { ...(liveSettings.providerDailyUsageBytes || {}) }; nextSettings.providerDailyUsageBytes = { ...(liveSettings.providerDailyUsageBytes || {}) };
nextSettings.providerTotalUsageBytes = { ...(liveSettings.providerTotalUsageBytes || {}) }; nextSettings.providerTotalUsageBytes = { ...(liveSettings.providerTotalUsageBytes || {}) };
@ -639,6 +646,10 @@ export class AppController {
} }
public shutdown(): void { public shutdown(): void {
if (this.runtimeStatsTimer) {
clearInterval(this.runtimeStatsTimer);
this.runtimeStatsTimer = null;
}
stopDebugServer(); stopDebugServer();
abortActiveUpdateDownload(); abortActiveUpdateDownload();
this.manager.prepareForShutdown(); this.manager.prepareForShutdown();

View File

@ -104,6 +104,7 @@ export function defaultSettings(): AppSettings {
confirmDeleteSelection: true, confirmDeleteSelection: true,
totalDownloadedAllTime: 0, totalDownloadedAllTime: 0,
totalCompletedFilesAllTime: 0, totalCompletedFilesAllTime: 0,
totalRuntimeAllTimeMs: 0,
bandwidthSchedules: [], bandwidthSchedules: [],
columnOrder: ["name", "size", "progress", "hoster", "account", "prio", "status", "speed"], columnOrder: ["name", "size", "progress", "hoster", "account", "prio", "status", "speed"],
extractCpuPriority: "high", extractCpuPriority: "high",

View File

@ -681,7 +681,8 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
...buildStatsPayload(snapshot), ...buildStatsPayload(snapshot),
allTime: { allTime: {
totalDownloadedAllTime: settings.totalDownloadedAllTime, totalDownloadedAllTime: settings.totalDownloadedAllTime,
totalCompletedFilesAllTime: settings.totalCompletedFilesAllTime totalCompletedFilesAllTime: settings.totalCompletedFilesAllTime,
totalRuntimeAllTimeMs: settings.totalRuntimeAllTimeMs
} }
}); });
return; return;

View File

@ -1254,6 +1254,9 @@ export class DownloadManager extends EventEmitter {
private lastPersistAt = 0; private lastPersistAt = 0;
private lastSettingsPersistAt = 0; private lastSettingsPersistAt = 0;
private appSessionStartedAt = 0;
private runtimePersistedTotalMs = 0;
private runtimePersistedAt = 0;
private cleanupQueue: Promise<void> = Promise.resolve(); private cleanupQueue: Promise<void> = Promise.resolve();
@ -1332,6 +1335,10 @@ export class DownloadManager extends EventEmitter {
public constructor(settings: AppSettings, session: SessionState, storagePaths: StoragePaths, options: DownloadManagerOptions = {}) { public constructor(settings: AppSettings, session: SessionState, storagePaths: StoragePaths, options: DownloadManagerOptions = {}) {
super(); super();
this.settings = settings; this.settings = settings;
const startedAt = nowMs();
this.appSessionStartedAt = startedAt;
this.runtimePersistedTotalMs = Math.max(0, Number(settings.totalRuntimeAllTimeMs || 0));
this.runtimePersistedAt = startedAt;
this.session = cloneSession(session); this.session = cloneSession(session);
this.itemCount = Object.keys(this.session.items).length; this.itemCount = Object.keys(this.session.items).length;
this.storagePaths = storagePaths; this.storagePaths = storagePaths;
@ -1600,7 +1607,11 @@ export class DownloadManager extends EventEmitter {
const previous = this.settings; const previous = this.settings;
next.totalDownloadedAllTime = Math.max(next.totalDownloadedAllTime || 0, this.settings.totalDownloadedAllTime || 0); next.totalDownloadedAllTime = Math.max(next.totalDownloadedAllTime || 0, this.settings.totalDownloadedAllTime || 0);
next.totalCompletedFilesAllTime = Math.max(next.totalCompletedFilesAllTime || 0, this.settings.totalCompletedFilesAllTime || 0); next.totalCompletedFilesAllTime = Math.max(next.totalCompletedFilesAllTime || 0, this.settings.totalCompletedFilesAllTime || 0);
const now = nowMs();
next.totalRuntimeAllTimeMs = Math.max(next.totalRuntimeAllTimeMs || 0, this.getLiveTotalRuntimeMs(now));
this.settings = next; this.settings = next;
this.runtimePersistedTotalMs = this.settings.totalRuntimeAllTimeMs || 0;
this.runtimePersistedAt = now;
this.ensureProviderDailyUsageFresh(nowMs()); this.ensureProviderDailyUsageFresh(nowMs());
this.debridService.setSettings(next); this.debridService.setSettings(next);
this.allDebridHostInfoCache.clear(); this.allDebridHostInfoCache.clear();
@ -1749,13 +1760,53 @@ export class DownloadManager extends EventEmitter {
totalFilesSession: this.sessionCompletedFiles, totalFilesSession: this.sessionCompletedFiles,
totalFilesAllTime: this.settings.totalCompletedFilesAllTime, totalFilesAllTime: this.settings.totalCompletedFilesAllTime,
totalPackages: this.session.packageOrder.length, totalPackages: this.session.packageOrder.length,
sessionStartedAt: this.session.runStartedAt sessionStartedAt: this.session.runStartedAt,
appSessionStartedAt: this.appSessionStartedAt,
sessionRuntimeMs: this.getAppSessionRuntimeMs(now),
totalRuntimeMs: this.getLiveTotalRuntimeMs(now),
runtimeMeasuredAt: now
}; };
this.statsCache = stats; this.statsCache = stats;
this.statsCacheAt = now; this.statsCacheAt = now;
return stats; return stats;
} }
public getLiveTotalRuntimeMs(now = nowMs()): number {
return Math.max(0, this.runtimePersistedTotalMs + Math.max(0, now - this.runtimePersistedAt));
}
private getAppSessionRuntimeMs(now = nowMs()): number {
return this.appSessionStartedAt > 0 ? Math.max(0, now - this.appSessionStartedAt) : 0;
}
private foldRuntimeIntoSettings(now = nowMs()): boolean {
const totalRuntimeMs = this.getLiveTotalRuntimeMs(now);
if (!Number.isFinite(totalRuntimeMs) || totalRuntimeMs <= (this.settings.totalRuntimeAllTimeMs || 0)) {
return false;
}
this.settings.totalRuntimeAllTimeMs = totalRuntimeMs;
this.runtimePersistedTotalMs = totalRuntimeMs;
this.runtimePersistedAt = now;
this.invalidateStatsCache();
return true;
}
public persistRuntimeStats(sync = false): void {
if (this.blockAllPersistence) {
return;
}
const now = nowMs();
if (!this.foldRuntimeIntoSettings(now)) {
return;
}
this.lastSettingsPersistAt = now;
if (sync) {
saveSettings(this.storagePaths, this.settings);
return;
}
void saveSettingsAsync(this.storagePaths, this.settings).catch((err) => logger.warn(`saveSettingsAsync Fehler: ${compactErrorText(err as Error)}`));
}
private invalidateStatsCache(): void { private invalidateStatsCache(): void {
this.statsCache = null; this.statsCache = null;
this.statsCacheAt = 0; this.statsCacheAt = 0;
@ -4293,6 +4344,7 @@ export class DownloadManager extends EventEmitter {
const pkgCount = Object.keys(this.session.packages).length; const pkgCount = Object.keys(this.session.packages).length;
const itemCount = Object.keys(this.session.items).length; const itemCount = Object.keys(this.session.items).length;
logger.info(`Shutdown-Save: ${pkgCount} Pakete, ${itemCount} Items`); logger.info(`Shutdown-Save: ${pkgCount} Pakete, ${itemCount} Items`);
this.foldRuntimeIntoSettings(nowMs());
saveSession(this.storagePaths, this.session); saveSession(this.storagePaths, this.session);
saveSettings(this.storagePaths, this.settings); saveSettings(this.storagePaths, this.settings);
} else { } else {
@ -4614,6 +4666,7 @@ export class DownloadManager extends EventEmitter {
this.lastPersistAt = now; this.lastPersistAt = now;
void saveSessionAsync(this.storagePaths, this.session).catch((err) => logger.warn(`saveSessionAsync Fehler: ${compactErrorText(err)}`)); void saveSessionAsync(this.storagePaths, this.session).catch((err) => logger.warn(`saveSessionAsync Fehler: ${compactErrorText(err)}`));
if (now - this.lastSettingsPersistAt >= 30000) { if (now - this.lastSettingsPersistAt >= 30000) {
this.foldRuntimeIntoSettings(now);
this.lastSettingsPersistAt = now; this.lastSettingsPersistAt = now;
void saveSettingsAsync(this.storagePaths, this.settings).catch((err) => logger.warn(`saveSettingsAsync Fehler: ${compactErrorText(err as Error)}`)); void saveSettingsAsync(this.storagePaths, this.settings).catch((err) => logger.warn(`saveSettingsAsync Fehler: ${compactErrorText(err as Error)}`));
} }

View File

@ -384,6 +384,7 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
confirmDeleteSelection: settings.confirmDeleteSelection !== undefined ? Boolean(settings.confirmDeleteSelection) : defaults.confirmDeleteSelection, confirmDeleteSelection: settings.confirmDeleteSelection !== undefined ? Boolean(settings.confirmDeleteSelection) : defaults.confirmDeleteSelection,
totalDownloadedAllTime: typeof settings.totalDownloadedAllTime === "number" && settings.totalDownloadedAllTime >= 0 ? settings.totalDownloadedAllTime : defaults.totalDownloadedAllTime, totalDownloadedAllTime: typeof settings.totalDownloadedAllTime === "number" && settings.totalDownloadedAllTime >= 0 ? settings.totalDownloadedAllTime : defaults.totalDownloadedAllTime,
totalCompletedFilesAllTime: typeof settings.totalCompletedFilesAllTime === "number" && settings.totalCompletedFilesAllTime >= 0 ? settings.totalCompletedFilesAllTime : defaults.totalCompletedFilesAllTime, totalCompletedFilesAllTime: typeof settings.totalCompletedFilesAllTime === "number" && settings.totalCompletedFilesAllTime >= 0 ? settings.totalCompletedFilesAllTime : defaults.totalCompletedFilesAllTime,
totalRuntimeAllTimeMs: typeof settings.totalRuntimeAllTimeMs === "number" && settings.totalRuntimeAllTimeMs >= 0 ? settings.totalRuntimeAllTimeMs : defaults.totalRuntimeAllTimeMs,
theme: VALID_THEMES.has(settings.theme) ? settings.theme : defaults.theme, theme: VALID_THEMES.has(settings.theme) ? settings.theme : defaults.theme,
bandwidthSchedules: normalizeBandwidthSchedules(settings.bandwidthSchedules), bandwidthSchedules: normalizeBandwidthSchedules(settings.bandwidthSchedules),
columnOrder: normalizeColumnOrder(settings.columnOrder), columnOrder: normalizeColumnOrder(settings.columnOrder),

View File

@ -89,7 +89,8 @@ export function buildSupportBundle(manager: DownloadManager, baseDir: string): B
...buildStatsPayload(snapshot), ...buildStatsPayload(snapshot),
allTime: { allTime: {
totalDownloadedAllTime: settings.totalDownloadedAllTime, totalDownloadedAllTime: settings.totalDownloadedAllTime,
totalCompletedFilesAllTime: settings.totalCompletedFilesAllTime totalCompletedFilesAllTime: settings.totalCompletedFilesAllTime,
totalRuntimeAllTimeMs: settings.totalRuntimeAllTimeMs
} }
}); });
addJson(zip, "overview/debug-setup.json", debugSetup); addJson(zip, "overview/debug-setup.json", debugSetup);

View File

@ -134,6 +134,7 @@ export function buildRedactedSettingsPayload(settings: AppSettings): Record<stri
statistics: { statistics: {
totalDownloadedAllTime: settings.totalDownloadedAllTime, totalDownloadedAllTime: settings.totalDownloadedAllTime,
totalCompletedFilesAllTime: settings.totalCompletedFilesAllTime, totalCompletedFilesAllTime: settings.totalCompletedFilesAllTime,
totalRuntimeAllTimeMs: settings.totalRuntimeAllTimeMs,
providerDailyLimitBytes: settings.providerDailyLimitBytes, providerDailyLimitBytes: settings.providerDailyLimitBytes,
providerDailyUsageBytes: settings.providerDailyUsageBytes, providerDailyUsageBytes: settings.providerDailyUsageBytes,
providerTotalUsageBytes: settings.providerTotalUsageBytes, providerTotalUsageBytes: settings.providerTotalUsageBytes,

View File

@ -764,7 +764,11 @@ const emptyStats = (): DownloadStats => ({
totalFilesSession: 0, totalFilesSession: 0,
totalFilesAllTime: 0, totalFilesAllTime: 0,
totalPackages: 0, totalPackages: 0,
sessionStartedAt: 0 sessionStartedAt: 0,
appSessionStartedAt: 0,
sessionRuntimeMs: 0,
totalRuntimeMs: 0,
runtimeMeasuredAt: 0
}); });
const emptySnapshot = (): UiSnapshot => ({ const emptySnapshot = (): UiSnapshot => ({
@ -783,7 +787,7 @@ const emptySnapshot = (): UiSnapshot => ({
updateRepo: "", autoUpdateCheck: true, clipboardWatch: false, minimizeToTray: false, updateRepo: "", autoUpdateCheck: true, clipboardWatch: false, minimizeToTray: false,
theme: "dark", collapseNewPackages: true, autoSortPackagesByProgress: true, autoSkipExtracted: false, confirmDeleteSelection: true, theme: "dark", collapseNewPackages: true, autoSortPackagesByProgress: true, autoSkipExtracted: false, confirmDeleteSelection: true,
accountListShowDetailedDebridLinkKeys: false, accountListShowDetailedDebridLinkKeys: false,
bandwidthSchedules: [], totalDownloadedAllTime: 0, totalCompletedFilesAllTime: 0, bandwidthSchedules: [], totalDownloadedAllTime: 0, totalCompletedFilesAllTime: 0, totalRuntimeAllTimeMs: 0,
columnOrder: ["name", "size", "progress", "hoster", "account", "prio", "status", "speed"], columnOrder: ["name", "size", "progress", "hoster", "account", "prio", "status", "speed"],
autoExtractWhenStopped: true, autoExtractWhenStopped: true,
disabledProviders: [], disabledProviders: [],
@ -928,6 +932,55 @@ function humanSize(bytes: number): string {
return `${(bytes / (1024 * 1024 * 1024 * 1024)).toFixed(3)} TB`; return `${(bytes / (1024 * 1024 * 1024 * 1024)).toFixed(3)} TB`;
} }
function formatRuntimeDuration(durationMs: number): string {
const totalSeconds = Math.max(0, Math.floor((durationMs || 0) / 1000));
const minuteSeconds = 60;
const hourSeconds = 60 * minuteSeconds;
const daySeconds = 24 * hourSeconds;
const weekSeconds = 7 * daySeconds;
const monthSeconds = 30 * daySeconds;
const formatUnit = (value: number, singular: string, plural: string, padTo = 0): string => {
const normalized = Math.max(0, Math.floor(value));
const text = padTo > 0 ? String(normalized).padStart(padTo, "0") : String(normalized);
return `${text} ${normalized === 1 ? singular : plural}`;
};
if (totalSeconds < hourSeconds) {
const minutes = Math.floor(totalSeconds / minuteSeconds);
const seconds = totalSeconds % minuteSeconds;
return `${formatUnit(minutes, "Minute", "Minuten")}, ${formatUnit(seconds, "Sekunde", "Sekunden", 2)}`;
}
if (totalSeconds < daySeconds) {
const hours = Math.floor(totalSeconds / hourSeconds);
const minutes = Math.floor((totalSeconds % hourSeconds) / minuteSeconds);
return `${formatUnit(hours, "Stunde", "Stunden")}, ${formatUnit(minutes, "Minute", "Minuten", 2)}`;
}
if (totalSeconds < weekSeconds) {
const days = Math.floor(totalSeconds / daySeconds);
const hours = Math.floor((totalSeconds % daySeconds) / hourSeconds);
const minutes = Math.floor((totalSeconds % hourSeconds) / minuteSeconds);
return `${formatUnit(days, "Tag", "Tage")}, ${formatUnit(hours, "Stunde", "Stunden")}, ${formatUnit(minutes, "Minute", "Minuten")}`;
}
if (totalSeconds < monthSeconds) {
const weeks = Math.floor(totalSeconds / weekSeconds);
const days = Math.floor((totalSeconds % weekSeconds) / daySeconds);
const hours = Math.floor((totalSeconds % daySeconds) / hourSeconds);
const minutes = Math.floor((totalSeconds % hourSeconds) / minuteSeconds);
return `${formatUnit(weeks, "Woche", "Wochen")}, ${formatUnit(days, "Tag", "Tage")}, ${formatUnit(hours, "Stunde", "Stunden")}, ${formatUnit(minutes, "Minute", "Minuten")}`;
}
const months = Math.floor(totalSeconds / monthSeconds);
const weeks = Math.floor((totalSeconds % monthSeconds) / weekSeconds);
const days = Math.floor((totalSeconds % weekSeconds) / daySeconds);
const hours = Math.floor((totalSeconds % daySeconds) / hourSeconds);
const minutes = Math.floor((totalSeconds % hourSeconds) / minuteSeconds);
return `${formatUnit(months, "Monat", "Monate")}, ${formatUnit(weeks, "Woche", "Wochen")}, ${formatUnit(days, "Tag", "Tage")}, ${formatUnit(hours, "Stunde", "Stunden")}, ${formatUnit(minutes, "Minute", "Minuten")}`;
}
function formatAllDebridSourceLabel(source: AllDebridHostInfo["source"]): string { function formatAllDebridSourceLabel(source: AllDebridHostInfo["source"]): string {
return source === "web" ? "Web-Login" : "API-Key"; return source === "web" ? "Web-Login" : "API-Key";
} }
@ -1317,6 +1370,7 @@ export function App(): ReactElement {
const [schedulePickerOpen, setSchedulePickerOpen] = useState(false); const [schedulePickerOpen, setSchedulePickerOpen] = useState(false);
const [scheduleTimeInput, setScheduleTimeInput] = useState(""); const [scheduleTimeInput, setScheduleTimeInput] = useState("");
const [scheduleCountdown, setScheduleCountdown] = useState(""); const [scheduleCountdown, setScheduleCountdown] = useState("");
const [runtimeNow, setRuntimeNow] = useState(() => Date.now());
const settingsDirtyRef = useRef(false); const settingsDirtyRef = useRef(false);
const settingsDraftRevisionRef = useRef(0); const settingsDraftRevisionRef = useRef(0);
const panelDirtyRevisionRef = useRef(0); const panelDirtyRevisionRef = useRef(0);
@ -1535,6 +1589,11 @@ export function App(): ReactElement {
return () => clearInterval(timer); return () => clearInterval(timer);
}, [snapshot.settings.scheduledStartEpochMs]); }, [snapshot.settings.scheduledStartEpochMs]);
useEffect(() => {
const timer = setInterval(() => setRuntimeNow(Date.now()), 1000);
return () => clearInterval(timer);
}, []);
const showToast = useCallback((message: string, timeoutMs = 2200): void => { const showToast = useCallback((message: string, timeoutMs = 2200): void => {
setStatusToast(message); setStatusToast(message);
if (toastTimerRef.current) { clearTimeout(toastTimerRef.current); } if (toastTimerRef.current) { clearTimeout(toastTimerRef.current); }
@ -3716,6 +3775,12 @@ export function App(): ReactElement {
return Object.entries(stats); return Object.entries(stats);
}, [snapshot.session.items]); }, [snapshot.session.items]);
const runtimeOffsetMs = snapshot.stats.runtimeMeasuredAt > 0
? Math.max(0, runtimeNow - snapshot.stats.runtimeMeasuredAt)
: 0;
const liveSessionRuntimeMs = Math.max(0, (snapshot.stats.sessionRuntimeMs || 0) + runtimeOffsetMs);
const liveTotalRuntimeMs = Math.max(0, (snapshot.stats.totalRuntimeMs || 0) + runtimeOffsetMs);
return ( return (
<div <div
className={`app-shell${dragOver ? " drag-over" : ""}`} className={`app-shell${dragOver ? " drag-over" : ""}`}
@ -4370,6 +4435,14 @@ export function App(): ReactElement {
<span className="stat-label">Heruntergeladen (Gesamt)</span> <span className="stat-label">Heruntergeladen (Gesamt)</span>
<span className="stat-value">{humanSize(snapshot.stats.totalDownloadedAllTime)}</span> <span className="stat-value">{humanSize(snapshot.stats.totalDownloadedAllTime)}</span>
</div> </div>
<div className="stat-item">
<span className="stat-label">Laufzeit (Session)</span>
<span className="stat-value">{formatRuntimeDuration(liveSessionRuntimeMs)}</span>
</div>
<div className="stat-item">
<span className="stat-label">Laufzeit (Gesamt)</span>
<span className="stat-value">{formatRuntimeDuration(liveTotalRuntimeMs)}</span>
</div>
<div className="stat-item"> <div className="stat-item">
<span className="stat-label">Fertige Dateien (Gesamt)</span> <span className="stat-label">Fertige Dateien (Gesamt)</span>
<span className="stat-value">{snapshot.stats.totalFilesAllTime}</span> <span className="stat-value">{snapshot.stats.totalFilesAllTime}</span>

View File

@ -45,6 +45,10 @@ export interface DownloadStats {
totalFilesAllTime: number; totalFilesAllTime: number;
totalPackages: number; totalPackages: number;
sessionStartedAt: number; sessionStartedAt: number;
appSessionStartedAt: number;
sessionRuntimeMs: number;
totalRuntimeMs: number;
runtimeMeasuredAt: number;
} }
export interface AppSettings { export interface AppSettings {
@ -110,6 +114,7 @@ export interface AppSettings {
confirmDeleteSelection: boolean; confirmDeleteSelection: boolean;
totalDownloadedAllTime: number; totalDownloadedAllTime: number;
totalCompletedFilesAllTime: number; totalCompletedFilesAllTime: number;
totalRuntimeAllTimeMs: number;
bandwidthSchedules: BandwidthScheduleEntry[]; bandwidthSchedules: BandwidthScheduleEntry[];
columnOrder: string[]; columnOrder: string[];
extractCpuPriority: ExtractCpuPriority; extractCpuPriority: ExtractCpuPriority;

View File

@ -173,7 +173,11 @@ function buildSnapshot(baseDir: string): UiSnapshot {
totalFilesSession: 0, totalFilesSession: 0,
totalFilesAllTime: 0, totalFilesAllTime: 0,
totalPackages: 1, totalPackages: 1,
sessionStartedAt: Date.now() - 30_000 sessionStartedAt: Date.now() - 30_000,
appSessionStartedAt: Date.now() - 60_000,
sessionRuntimeMs: 60_000,
totalRuntimeMs: 3 * 60_000,
runtimeMeasuredAt: Date.now()
}, },
speedText: "8.0 MB/s", speedText: "8.0 MB/s",
etaText: "ETA: 00:25", etaText: "ETA: 00:25",
@ -209,7 +213,8 @@ async function createFixture() {
debridLinkApiKeys, debridLinkApiKeys,
debridLinkDisabledKeyIds: debridLinkKeyIds[1] ? [debridLinkKeyIds[1]] : [], debridLinkDisabledKeyIds: debridLinkKeyIds[1] ? [debridLinkKeyIds[1]] : [],
totalDownloadedAllTime: 128 * 1024 * 1024, totalDownloadedAllTime: 128 * 1024 * 1024,
totalCompletedFilesAllTime: 12 totalCompletedFilesAllTime: 12,
totalRuntimeAllTimeMs: 5 * 60_000
}); });
saveHistory(storagePaths, [ saveHistory(storagePaths, [
{ {

View File

@ -6709,6 +6709,36 @@ describe("download manager", () => {
expect(fs.existsSync(originalExtractedPath)).toBe(false); expect(fs.existsSync(originalExtractedPath)).toBe(false);
}); });
it("tracks app runtime for session and all-time statistics", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
tempDirs.push(root);
const stateDir = path.join(root, "state");
const storagePaths = createStoragePaths(stateDir);
const manager = new DownloadManager(
{
...defaultSettings(),
token: "rd-token",
outputDir: path.join(root, "downloads"),
extractDir: path.join(root, "extract"),
totalRuntimeAllTimeMs: 2 * 60 * 60 * 1000
},
emptySession(),
storagePaths
);
await new Promise((resolve) => setTimeout(resolve, 120));
const stats = manager.getStats();
expect(stats.sessionRuntimeMs).toBeGreaterThanOrEqual(100);
expect(stats.totalRuntimeMs).toBeGreaterThanOrEqual(2 * 60 * 60 * 1000 + 100);
expect(stats.runtimeMeasuredAt).toBeGreaterThan(0);
manager.persistRuntimeStats(true);
const savedSettings = JSON.parse(fs.readFileSync(storagePaths.configFile, "utf8")) as { totalRuntimeAllTimeMs?: number };
expect(savedSettings.totalRuntimeAllTimeMs || 0).toBeGreaterThanOrEqual(2 * 60 * 60 * 1000 + 100);
}, 10000);
it("writes auto-rename details into rename and item logs", async () => { it("writes auto-rename details into rename and item logs", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-")); const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
tempDirs.push(root); tempDirs.push(root);

View File

@ -115,7 +115,11 @@ function buildSnapshot(): UiSnapshot {
totalFilesSession: 0, totalFilesSession: 0,
totalFilesAllTime: 0, totalFilesAllTime: 0,
totalPackages: 2, totalPackages: 2,
sessionStartedAt: 0 sessionStartedAt: 0,
appSessionStartedAt: 0,
sessionRuntimeMs: 0,
totalRuntimeMs: 0,
runtimeMeasuredAt: 0
}, },
speedText: "", speedText: "",
etaText: "", etaText: "",