From 2d9fbb07ea0363b232c7826ce274eddc0ae57eaa Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Tue, 24 Mar 2026 10:15:28 +0100 Subject: [PATCH] Revert daily-log and queue-scope changes back to v1.7.112 state Remove daily-log module entirely (caused UI freezes due to sync I/O even after async rewrite). Revert queue-scope stop() change (was for a different project). All source files now match v1.7.112. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/main/app-controller.ts | 849 ++++++++++++++++++------------------- src/main/daily-log.ts | 235 ---------- src/main/rename-log.ts | 9 +- 3 files changed, 428 insertions(+), 665 deletions(-) delete mode 100644 src/main/daily-log.ts diff --git a/src/main/app-controller.ts b/src/main/app-controller.ts index a5f6c47..e1763b5 100644 --- a/src/main/app-controller.ts +++ b/src/main/app-controller.ts @@ -24,27 +24,26 @@ import { APP_VERSION } from "./constants"; import { DownloadManager } from "./download-manager"; import { fetchAllDebridHostInfo, fetchDebridLinkHostLimits } from "./debrid"; import { parseCollectorInput } from "./link-parser"; -import { configureLogger, getLogFilePath, logger } from "./logger"; -import { AllDebridWebFallback } from "./all-debrid-web"; -import { BestDebridWebFallback } from "./bestdebrid-web"; -import { RealDebridWebFallback } from "./realdebrid-web"; -import { getItemLogPath, initItemLogs, shutdownItemLogs } from "./item-log"; -import { getPackageLogPath, initPackageLogs, shutdownPackageLogs } from "./package-log"; -import { initSessionLog, getSessionLogPath, shutdownSessionLog } from "./session-log"; +import { configureLogger, getLogFilePath, logger } from "./logger"; +import { AllDebridWebFallback } from "./all-debrid-web"; +import { BestDebridWebFallback } from "./bestdebrid-web"; +import { RealDebridWebFallback } from "./realdebrid-web"; +import { getItemLogPath, initItemLogs, shutdownItemLogs } from "./item-log"; +import { getPackageLogPath, initPackageLogs, shutdownPackageLogs } from "./package-log"; +import { initSessionLog, getSessionLogPath, shutdownSessionLog } from "./session-log"; import { MegaWebFallback } from "./mega-web-fallback"; -import { addHistoryEntry, addHistoryEntryForRetention, cancelPendingAsyncSaves, clearHistory, createStoragePaths, loadHistoryForRetention, loadSession, loadSettings, normalizeHistoryEntry, normalizeLoadedSession, normalizeLoadedSessionTransientFields, normalizeSettings, removeHistoryEntry, resetHistoryForRetention, saveHistory, saveSession, saveSettings } from "./storage"; -import { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } from "./update"; -import { rotateDebugToken, startDebugServer, stopDebugServer } from "./debug-server"; -import { encryptBackup, decryptBackup } from "./backup-crypto"; -import { getAuditLogPath, initAuditLog, logAuditEvent, shutdownAuditLog } from "./audit-log"; -import { getDebugSetupCheck } from "./debug-setup"; -import { buildLinkExportSelection, serializeLinkExportText } from "./link-export"; -import { getRenameLogPath, initRenameLog, shutdownRenameLog } from "./rename-log"; -import { initDailyLog, shutdownDailyLog } from "./daily-log"; -import { buildAccountSummary, diffAccountSummary } from "./support-data"; -import { buildSupportBundle, getSupportBundleDefaultFileName } from "./support-bundle"; -import { getTraceConfig, getTraceLogPath, initTraceLog, logTraceEvent, setTraceEnabled, shutdownTraceLog } from "./trace-log"; -import type { DebugSetupCheckResult, SupportTraceConfig } from "../shared/types"; +import { addHistoryEntry, addHistoryEntryForRetention, cancelPendingAsyncSaves, clearHistory, createStoragePaths, loadHistoryForRetention, loadSession, loadSettings, normalizeHistoryEntry, normalizeLoadedSession, normalizeLoadedSessionTransientFields, normalizeSettings, removeHistoryEntry, resetHistoryForRetention, saveHistory, saveSession, saveSettings } from "./storage"; +import { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } from "./update"; +import { rotateDebugToken, startDebugServer, stopDebugServer } from "./debug-server"; +import { encryptBackup, decryptBackup } from "./backup-crypto"; +import { getAuditLogPath, initAuditLog, logAuditEvent, shutdownAuditLog } from "./audit-log"; +import { getDebugSetupCheck } from "./debug-setup"; +import { buildLinkExportSelection, serializeLinkExportText } from "./link-export"; +import { getRenameLogPath, initRenameLog, shutdownRenameLog } from "./rename-log"; +import { buildAccountSummary, diffAccountSummary } from "./support-data"; +import { buildSupportBundle, getSupportBundleDefaultFileName } from "./support-bundle"; +import { getTraceConfig, getTraceLogPath, initTraceLog, logTraceEvent, setTraceEnabled, shutdownTraceLog } from "./trace-log"; +import type { DebugSetupCheckResult, SupportTraceConfig } from "../shared/types"; function sanitizeSettingsPatch(partial: Partial): Partial { const entries = Object.entries(partial || {}).filter(([, value]) => value !== undefined); @@ -74,23 +73,22 @@ export class AppController { private storagePaths = createStoragePaths(path.join(app.getPath("userData"), "runtime")); - private onStateHandler: ((snapshot: UiSnapshot) => void) | null = null; + private onStateHandler: ((snapshot: UiSnapshot) => void) | null = null; + + private autoResumePending = false; + private runtimeStatsTimer: NodeJS.Timeout | null = null; - private autoResumePending = false; - private runtimeStatsTimer: NodeJS.Timeout | null = null; - - public constructor() { - configureLogger(this.storagePaths.baseDir); - initSessionLog(this.storagePaths.baseDir); - initPackageLogs(this.storagePaths.baseDir); - initItemLogs(this.storagePaths.baseDir); - initAuditLog(this.storagePaths.baseDir); - initRenameLog(this.storagePaths.baseDir); - initDailyLog(this.storagePaths.baseDir); - initTraceLog(this.storagePaths.baseDir); - this.settings = loadSettings(this.storagePaths); - resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode); - const session = loadSession(this.storagePaths); + public constructor() { + configureLogger(this.storagePaths.baseDir); + initSessionLog(this.storagePaths.baseDir); + initPackageLogs(this.storagePaths.baseDir); + initItemLogs(this.storagePaths.baseDir); + initAuditLog(this.storagePaths.baseDir); + initRenameLog(this.storagePaths.baseDir); + initTraceLog(this.storagePaths.baseDir); + this.settings = loadSettings(this.storagePaths); + resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode); + const session = loadSession(this.storagePaths); this.megaWebFallback = new MegaWebFallback(() => ({ login: this.settings.megaLogin, password: this.settings.megaPassword @@ -100,31 +98,31 @@ export class AppController { this.bestDebridWebFallback = new BestDebridWebFallback(() => this.settings.rememberToken); this.manager = new DownloadManager(this.settings, session, this.storagePaths, { megaWebUnrestrict: (link: string, signal?: AbortSignal) => this.megaWebFallback.unrestrict(link, signal), - allDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.allDebridWebFallback.unrestrict(link, signal), - realDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.realDebridWebFallback.unrestrict(link, signal), - bestDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.bestDebridWebFallback.unrestrict(link, signal), - invalidateMegaSession: () => this.megaWebFallback.invalidateSession(), - onHistoryEntry: (entry: HistoryEntry) => { - addHistoryEntryForRetention(this.storagePaths, this.settings.historyRetentionMode, entry); - } - }); + allDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.allDebridWebFallback.unrestrict(link, signal), + realDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.realDebridWebFallback.unrestrict(link, signal), + bestDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.bestDebridWebFallback.unrestrict(link, signal), + invalidateMegaSession: () => this.megaWebFallback.invalidateSession(), + onHistoryEntry: (entry: HistoryEntry) => { + addHistoryEntryForRetention(this.storagePaths, this.settings.historyRetentionMode, entry); + } + }); this.manager.on("state", (snapshot: UiSnapshot) => { this.onStateHandler?.(snapshot); - }); - logger.info(`App gestartet v${APP_VERSION}`); - logger.info(`Log-Datei: ${getLogFilePath()}`); - logAuditEvent("INFO", "App gestartet", { - appVersion: APP_VERSION, - runtimeDir: 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) { + }); + logger.info(`App gestartet v${APP_VERSION}`); + logger.info(`Log-Datei: ${getLogFilePath()}`); + logAuditEvent("INFO", "App gestartet", { + appVersion: APP_VERSION, + runtimeDir: 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) { const snapshot = this.manager.getSnapshot(); const hasPending = Object.values(snapshot.session.items).some((item) => item.status === "queued" || item.status === "reconnect_wait"); if (hasPending) { @@ -189,46 +187,46 @@ export class AppController { return APP_VERSION; } - public getSettings(): AppSettings { - return this.settings; - } - - public getAuditLogPath(): string | null { - return getAuditLogPath(); - } - - public getRenameLogPath(): string | null { - return getRenameLogPath(); - } - - public getTraceLogPath(): string | null { - return getTraceLogPath(); - } - - public getTraceConfig(): SupportTraceConfig { - return getTraceConfig(); - } - - public rotateDebugToken(): { path: string; token: string } { - const rotated = rotateDebugToken(this.storagePaths.baseDir); - this.audit("WARN", "Debug-Token rotiert", { path: rotated.path }); - return rotated; - } - - public getDebugSetupCheck(): DebugSetupCheckResult { - return getDebugSetupCheck(this.storagePaths.baseDir); - } - - private audit(level: "INFO" | "WARN" | "ERROR", message: string, fields?: Record): void { - logAuditEvent(level, message, fields); - logTraceEvent(level, "audit", message, fields); - } - - public setTraceEnabled(enabled: boolean, note = "", durationMs?: number): SupportTraceConfig { - const next = setTraceEnabled(enabled, note, durationMs); - this.audit("INFO", enabled ? "Support-Trace aktiviert" : "Support-Trace deaktiviert", { note }); - return next; - } + public getSettings(): AppSettings { + return this.settings; + } + + public getAuditLogPath(): string | null { + return getAuditLogPath(); + } + + public getRenameLogPath(): string | null { + return getRenameLogPath(); + } + + public getTraceLogPath(): string | null { + return getTraceLogPath(); + } + + public getTraceConfig(): SupportTraceConfig { + return getTraceConfig(); + } + + public rotateDebugToken(): { path: string; token: string } { + const rotated = rotateDebugToken(this.storagePaths.baseDir); + this.audit("WARN", "Debug-Token rotiert", { path: rotated.path }); + return rotated; + } + + public getDebugSetupCheck(): DebugSetupCheckResult { + return getDebugSetupCheck(this.storagePaths.baseDir); + } + + private audit(level: "INFO" | "WARN" | "ERROR", message: string, fields?: Record): void { + logAuditEvent(level, message, fields); + logTraceEvent(level, "audit", message, fields); + } + + public setTraceEnabled(enabled: boolean, note = "", durationMs?: number): SupportTraceConfig { + const next = setTraceEnabled(enabled, note, durationMs); + this.audit("INFO", enabled ? "Support-Trace aktiviert" : "Support-Trace deaktiviert", { note }); + return next; + } public updateSettings(partial: Partial): AppSettings { const sanitizedPatch = sanitizeSettingsPatch(partial); @@ -242,32 +240,32 @@ export class AppController { return previousSettings; } - // Preserve the live all-time counters from the download manager - const liveSettings = this.manager.getSettings(); - nextSettings.totalDownloadedAllTime = Math.max(nextSettings.totalDownloadedAllTime || 0, liveSettings.totalDownloadedAllTime || 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.providerDailyUsageBytes = { ...(liveSettings.providerDailyUsageBytes || {}) }; - nextSettings.providerTotalUsageBytes = { ...(liveSettings.providerTotalUsageBytes || {}) }; - nextSettings.debridLinkApiKeyDailyUsageBytes = Object.fromEntries( - Object.entries(liveSettings.debridLinkApiKeyDailyUsageBytes || {}).filter(([keyId]) => getDebridLinkApiKeyIds(nextSettings.debridLinkApiKeys).includes(keyId)) - ); - nextSettings.debridLinkApiKeyTotalUsageBytes = Object.fromEntries( - Object.entries(liveSettings.debridLinkApiKeyTotalUsageBytes || {}).filter(([keyId]) => getDebridLinkApiKeyIds(nextSettings.debridLinkApiKeys).includes(keyId)) - ); - const retentionChanged = previousSettings.historyRetentionMode !== nextSettings.historyRetentionMode; - this.settings = nextSettings; - if (retentionChanged) { - resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode); - } - saveSettings(this.storagePaths, this.settings); - this.manager.setSettings(this.settings); - this.audit("INFO", "Einstellungen aktualisiert", { - changedKeys: Object.keys(sanitizedPatch), - accountChanges: diffAccountSummary(previousSettings, this.settings) - }); - if (previousSettings.rememberToken && !this.settings.rememberToken) { + // Preserve the live all-time counters from the download manager + const liveSettings = this.manager.getSettings(); + nextSettings.totalDownloadedAllTime = Math.max(nextSettings.totalDownloadedAllTime || 0, liveSettings.totalDownloadedAllTime || 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.providerDailyUsageBytes = { ...(liveSettings.providerDailyUsageBytes || {}) }; + nextSettings.providerTotalUsageBytes = { ...(liveSettings.providerTotalUsageBytes || {}) }; + nextSettings.debridLinkApiKeyDailyUsageBytes = Object.fromEntries( + Object.entries(liveSettings.debridLinkApiKeyDailyUsageBytes || {}).filter(([keyId]) => getDebridLinkApiKeyIds(nextSettings.debridLinkApiKeys).includes(keyId)) + ); + nextSettings.debridLinkApiKeyTotalUsageBytes = Object.fromEntries( + Object.entries(liveSettings.debridLinkApiKeyTotalUsageBytes || {}).filter(([keyId]) => getDebridLinkApiKeyIds(nextSettings.debridLinkApiKeys).includes(keyId)) + ); + const retentionChanged = previousSettings.historyRetentionMode !== nextSettings.historyRetentionMode; + this.settings = nextSettings; + if (retentionChanged) { + resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode); + } + saveSettings(this.storagePaths, this.settings); + this.manager.setSettings(this.settings); + this.audit("INFO", "Einstellungen aktualisiert", { + changedKeys: Object.keys(sanitizedPatch), + accountChanges: diffAccountSummary(previousSettings, this.settings) + }); + if (previousSettings.rememberToken && !this.settings.rememberToken) { void this.realDebridWebFallback.clearSessions().catch((error) => { logger.warn(`Real-Debrid Web-Session konnte nicht gelöscht werden: ${String(error)}`); }); @@ -287,12 +285,12 @@ export class AppController { ...liveSettings, ...resetProviderDailyUsage(liveSettings, provider) }); - this.settings = nextSettings; - saveSettings(this.storagePaths, this.settings); - this.manager.setSettings(this.settings); - this.audit("INFO", "Provider-Tagesnutzung zurückgesetzt", { provider }); - return this.settings; - } + this.settings = nextSettings; + saveSettings(this.storagePaths, this.settings); + this.manager.setSettings(this.settings); + this.audit("INFO", "Provider-Tagesnutzung zurückgesetzt", { provider }); + return this.settings; + } public resetDebridLinkApiKeyDailyUsage(keyId: string): AppSettings { const liveSettings = this.manager.getSettings(); @@ -300,31 +298,31 @@ export class AppController { ...liveSettings, ...resetDebridLinkApiKeyDailyUsage(liveSettings, keyId) }); - this.settings = nextSettings; - saveSettings(this.storagePaths, this.settings); - this.manager.setSettings(this.settings); - this.audit("INFO", "Debrid-Link-Key-Tagesnutzung zurückgesetzt", { keyId }); - return this.settings; - } - - public async openRealDebridLoginWindow(): Promise { - this.audit("INFO", "Real-Debrid Login-Fenster geöffnet"); - await this.realDebridWebFallback.openLoginWindow(); - } - - public async openAllDebridLoginWindow(): Promise { - this.audit("INFO", "AllDebrid Login-Fenster geöffnet"); - await this.allDebridWebFallback.openLoginWindow(); - } - - public async importBestDebridCookies(filePath: string): Promise { - const imported = await this.bestDebridWebFallback.importCookiesFromFile(filePath); - this.audit("INFO", "BestDebrid Cookies importiert", { - filePath, - imported - }); - return imported; - } + this.settings = nextSettings; + saveSettings(this.storagePaths, this.settings); + this.manager.setSettings(this.settings); + this.audit("INFO", "Debrid-Link-Key-Tagesnutzung zurückgesetzt", { keyId }); + return this.settings; + } + + public async openRealDebridLoginWindow(): Promise { + this.audit("INFO", "Real-Debrid Login-Fenster geöffnet"); + await this.realDebridWebFallback.openLoginWindow(); + } + + public async openAllDebridLoginWindow(): Promise { + this.audit("INFO", "AllDebrid Login-Fenster geöffnet"); + await this.allDebridWebFallback.openLoginWindow(); + } + + public async importBestDebridCookies(filePath: string): Promise { + const imported = await this.bestDebridWebFallback.importCookiesFromFile(filePath); + this.audit("INFO", "BestDebrid Cookies importiert", { + filePath, + imported + }); + return imported; + } public async getAllDebridHostInfo(host = "rapidgator"): Promise { if (this.settings.allDebridUseWebLogin) { @@ -369,38 +367,38 @@ export class AppController { return result; } - public addLinks(payload: AddLinksPayload): { addedPackages: number; addedLinks: number; invalidCount: number } { - const parsed = parseCollectorInput(payload.rawText, payload.packageName || this.settings.packageName); - if (parsed.length === 0) { - this.audit("WARN", "Links hinzufügen ohne gültigen Inhalt", { - hasPackageName: Boolean(payload.packageName) - }); - return { addedPackages: 0, addedLinks: 0, invalidCount: 1 }; - } - const result = this.manager.addPackages(parsed); - this.audit("INFO", "Links hinzugefügt", { - addedPackages: result.addedPackages, - addedLinks: result.addedLinks, - requestedPackages: parsed.length - }); - return { ...result, invalidCount: 0 }; - } + public addLinks(payload: AddLinksPayload): { addedPackages: number; addedLinks: number; invalidCount: number } { + const parsed = parseCollectorInput(payload.rawText, payload.packageName || this.settings.packageName); + if (parsed.length === 0) { + this.audit("WARN", "Links hinzufügen ohne gültigen Inhalt", { + hasPackageName: Boolean(payload.packageName) + }); + return { addedPackages: 0, addedLinks: 0, invalidCount: 1 }; + } + const result = this.manager.addPackages(parsed); + this.audit("INFO", "Links hinzugefügt", { + addedPackages: result.addedPackages, + addedLinks: result.addedLinks, + requestedPackages: parsed.length + }); + return { ...result, invalidCount: 0 }; + } public async addContainers(filePaths: string[]): Promise<{ addedPackages: number; addedLinks: number }> { const packages = await importDlcContainers(filePaths); - const merged: ParsedPackageInput[] = packages.map((pkg) => ({ - name: pkg.name, - links: pkg.links, - ...(pkg.fileNames ? { fileNames: pkg.fileNames } : {}) - })); - const result = this.manager.addPackages(merged); - this.audit("INFO", "Container importiert", { - files: filePaths.length, - addedPackages: result.addedPackages, - addedLinks: result.addedLinks - }); - return result; - } + const merged: ParsedPackageInput[] = packages.map((pkg) => ({ + name: pkg.name, + links: pkg.links, + ...(pkg.fileNames ? { fileNames: pkg.fileNames } : {}) + })); + const result = this.manager.addPackages(merged); + this.audit("INFO", "Container importiert", { + files: filePaths.length, + addedPackages: result.addedPackages, + addedLinks: result.addedLinks + }); + return result; + } public async getStartConflicts(): Promise { return this.manager.getStartConflicts(); @@ -410,163 +408,163 @@ export class AppController { return this.manager.resolveStartConflict(packageId, policy); } - public clearAll(): void { - this.audit("WARN", "Queue komplett geleert"); - this.manager.clearAll(); - } + public clearAll(): void { + this.audit("WARN", "Queue komplett geleert"); + this.manager.clearAll(); + } + + public async start(): Promise { + this.audit("INFO", "Session-Start ausgelöst"); + await this.manager.start(); + } + + public async startPackages(packageIds: string[]): Promise { + this.audit("INFO", "Paket-Start ausgelöst", { packageIds }); + await this.manager.startPackages(packageIds); + } + + public async startItems(itemIds: string[]): Promise { + this.audit("INFO", "Item-Start ausgelöst", { itemIds }); + await this.manager.startItems(itemIds); + } + + public stop(): void { + this.audit("INFO", "Session-Stopp ausgelöst"); + this.manager.stop(); + } + + public togglePause(): boolean { + const paused = this.manager.togglePause(); + this.audit("INFO", "Pause umgeschaltet", { paused }); + return paused; + } + + public retryExtraction(packageId: string): void { + this.audit("INFO", "Extraktion manuell wiederholt", { packageId }); + this.manager.retryExtraction(packageId); + } + + public extractNow(packageId: string): void { + this.audit("INFO", "Jetzt entpacken ausgelöst", { packageId }); + this.manager.extractNow(packageId); + } + + public resetPackage(packageId: string): void { + this.audit("INFO", "Paket zurückgesetzt", { packageId }); + this.manager.resetPackage(packageId); + } + + public cancelPackage(packageId: string): void { + this.audit("WARN", "Paket abgebrochen", { packageId }); + this.manager.cancelPackage(packageId); + } + + public renamePackage(packageId: string, newName: string): void { + this.audit("INFO", "Paket umbenannt", { packageId, newName }); + this.manager.renamePackage(packageId, newName); + } + + public reorderPackages(packageIds: string[]): void { + this.audit("INFO", "Paketreihenfolge geändert", { packageIds }); + this.manager.reorderPackages(packageIds); + } + + public removeItem(itemId: string): void { + this.audit("WARN", "Item entfernt", { itemId }); + this.manager.removeItem(itemId); + } - public async start(): Promise { - this.audit("INFO", "Session-Start ausgelöst"); - await this.manager.start(); - } + public togglePackage(packageId: string): void { + this.audit("INFO", "Paket aktiviert/deaktiviert", { packageId }); + this.manager.togglePackage(packageId); + } + + public exportPackageSelection(packageIds: string[]): { text: string; defaultFileName: string; packageCount: number; linkCount: number } { + const selection = buildLinkExportSelection(this.manager.getSnapshot(), packageIds, []); + this.audit("INFO", "Paket-Auswahl exportiert", { + packageCount: selection.packageCount, + linkCount: selection.linkCount, + packageIds + }); + return { + text: serializeLinkExportText(selection.packages), + defaultFileName: selection.defaultFileName, + packageCount: selection.packageCount, + linkCount: selection.linkCount + }; + } + + public exportItemSelection(itemIds: string[]): { text: string; defaultFileName: string; packageCount: number; linkCount: number } { + const selection = buildLinkExportSelection(this.manager.getSnapshot(), [], itemIds); + this.audit("INFO", "Item-Auswahl exportiert", { + packageCount: selection.packageCount, + linkCount: selection.linkCount, + itemIds + }); + return { + text: serializeLinkExportText(selection.packages), + defaultFileName: selection.defaultFileName, + packageCount: selection.packageCount, + linkCount: selection.linkCount + }; + } + + public exportQueue(): string { + return this.manager.exportQueue(); + } + + public importQueue(json: string): { addedPackages: number; addedLinks: number } { + const result = this.manager.importQueue(json); + this.audit("INFO", "Import-Datei verarbeitet", result); + return result; + } - public async startPackages(packageIds: string[]): Promise { - this.audit("INFO", "Paket-Start ausgelöst", { packageIds }); - await this.manager.startPackages(packageIds); - } + public getSessionStats(): SessionStats { + return this.manager.getSessionStats(); + } + + public resetSessionStats(): void { + this.audit("INFO", "Session-Statistik zurückgesetzt"); + this.manager.resetSessionStats(); + } + + public resetDownloadStats(): void { + this.manager.resetDownloadStats(); + this.settings = this.manager.getSettings(); + this.audit("INFO", "Download-Statistik zurückgesetzt"); + } - public async startItems(itemIds: string[]): Promise { - this.audit("INFO", "Item-Start ausgelöst", { itemIds }); - await this.manager.startItems(itemIds); - } - - public stop(): void { - this.audit("INFO", "Session-Stopp ausgelöst"); - this.manager.stop(); - } - - public togglePause(): boolean { - const paused = this.manager.togglePause(); - this.audit("INFO", "Pause umgeschaltet", { paused }); - return paused; - } - - public retryExtraction(packageId: string): void { - this.audit("INFO", "Extraktion manuell wiederholt", { packageId }); - this.manager.retryExtraction(packageId); - } - - public extractNow(packageId: string): void { - this.audit("INFO", "Jetzt entpacken ausgelöst", { packageId }); - this.manager.extractNow(packageId); - } - - public resetPackage(packageId: string): void { - this.audit("INFO", "Paket zurückgesetzt", { packageId }); - this.manager.resetPackage(packageId); - } - - public cancelPackage(packageId: string): void { - this.audit("WARN", "Paket abgebrochen", { packageId }); - this.manager.cancelPackage(packageId); - } - - public renamePackage(packageId: string, newName: string): void { - this.audit("INFO", "Paket umbenannt", { packageId, newName }); - this.manager.renamePackage(packageId, newName); - } - - public reorderPackages(packageIds: string[]): void { - this.audit("INFO", "Paketreihenfolge geändert", { packageIds }); - this.manager.reorderPackages(packageIds); - } - - public removeItem(itemId: string): void { - this.audit("WARN", "Item entfernt", { itemId }); - this.manager.removeItem(itemId); - } - - public togglePackage(packageId: string): void { - this.audit("INFO", "Paket aktiviert/deaktiviert", { packageId }); - this.manager.togglePackage(packageId); - } - - public exportPackageSelection(packageIds: string[]): { text: string; defaultFileName: string; packageCount: number; linkCount: number } { - const selection = buildLinkExportSelection(this.manager.getSnapshot(), packageIds, []); - this.audit("INFO", "Paket-Auswahl exportiert", { - packageCount: selection.packageCount, - linkCount: selection.linkCount, - packageIds - }); - return { - text: serializeLinkExportText(selection.packages), - defaultFileName: selection.defaultFileName, - packageCount: selection.packageCount, - linkCount: selection.linkCount - }; - } - - public exportItemSelection(itemIds: string[]): { text: string; defaultFileName: string; packageCount: number; linkCount: number } { - const selection = buildLinkExportSelection(this.manager.getSnapshot(), [], itemIds); - this.audit("INFO", "Item-Auswahl exportiert", { - packageCount: selection.packageCount, - linkCount: selection.linkCount, - itemIds - }); - return { - text: serializeLinkExportText(selection.packages), - defaultFileName: selection.defaultFileName, - packageCount: selection.packageCount, - linkCount: selection.linkCount - }; - } - - public exportQueue(): string { - return this.manager.exportQueue(); - } - - public importQueue(json: string): { addedPackages: number; addedLinks: number } { - const result = this.manager.importQueue(json); - this.audit("INFO", "Import-Datei verarbeitet", result); - return result; - } - - public getSessionStats(): SessionStats { - return this.manager.getSessionStats(); - } - - public resetSessionStats(): void { - this.audit("INFO", "Session-Statistik zurückgesetzt"); - this.manager.resetSessionStats(); - } - - public resetDownloadStats(): void { - this.manager.resetDownloadStats(); - this.settings = this.manager.getSettings(); - this.audit("INFO", "Download-Statistik zurückgesetzt"); - } - - public exportBackup(): Buffer { - const settings = { ...this.settings }; - const session = this.manager.getSession(); - const history = loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode); + public exportBackup(): Buffer { + const settings = { ...this.settings }; + const session = this.manager.getSession(); + const history = loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode); const payload = JSON.stringify({ version: 2, appVersion: APP_VERSION, exportedAt: new Date().toISOString(), - settings, - session, - history - }); - this.audit("INFO", "Backup exportiert", { - historyEntries: history.length, - sessionItems: Object.keys(session.items).length, - sessionPackages: Object.keys(session.packages).length - }); - return encryptBackup(payload); - } - - public exportSupportBundle(): { buffer: Buffer; defaultFileName: string } { - this.audit("INFO", "Support-Bundle exportiert"); - logTraceEvent("INFO", "support", "Support-Bundle erstellt", { - packageCount: Object.keys(this.manager.getSnapshot().session.packages).length, - itemCount: Object.keys(this.manager.getSnapshot().session.items).length - }); - return { - buffer: buildSupportBundle(this.manager, this.storagePaths.baseDir), - defaultFileName: getSupportBundleDefaultFileName() - }; - } + settings, + session, + history + }); + this.audit("INFO", "Backup exportiert", { + historyEntries: history.length, + sessionItems: Object.keys(session.items).length, + sessionPackages: Object.keys(session.packages).length + }); + return encryptBackup(payload); + } + + public exportSupportBundle(): { buffer: Buffer; defaultFileName: string } { + this.audit("INFO", "Support-Bundle exportiert"); + logTraceEvent("INFO", "support", "Support-Bundle erstellt", { + packageCount: Object.keys(this.manager.getSnapshot().session.packages).length, + itemCount: Object.keys(this.manager.getSnapshot().session.items).length + }); + return { + buffer: buildSupportBundle(this.manager, this.storagePaths.baseDir), + defaultFileName: getSupportBundleDefaultFileName() + }; + } public importBackup(data: Buffer): { restored: boolean; message: string } { let parsed: Record; @@ -588,21 +586,21 @@ export class AppController { } // Restore settings — ALL credentials are included (no more masking) - const importedSettings = parsed.settings as AppSettings; - const importedSettingsRecord = importedSettings as unknown as Record; - const currentSettingsRecord = this.settings as unknown as Record; + const importedSettings = parsed.settings as AppSettings; + const importedSettingsRecord = importedSettings as unknown as Record; + const currentSettingsRecord = this.settings as unknown as Record; // Legacy backup compatibility: if credentials were masked with ***, keep current values const SENSITIVE_KEYS: (keyof AppSettings)[] = [ "token", "megaLogin", "megaPassword", "bestToken", "allDebridToken", "ddownloadLogin", "ddownloadPassword", "oneFichierApiKey", "debridLinkApiKeys", "linkSnappyLogin", "linkSnappyPassword" ]; - for (const key of SENSITIVE_KEYS) { - const val = importedSettingsRecord[key]; - if (typeof val === "string" && val.startsWith("***")) { - importedSettingsRecord[key] = currentSettingsRecord[key]; - } - } + for (const key of SENSITIVE_KEYS) { + const val = importedSettingsRecord[key]; + if (typeof val === "string" && val.startsWith("***")) { + importedSettingsRecord[key] = currentSettingsRecord[key]; + } + } const restoredSettings = normalizeSettings(importedSettings); this.settings = restoredSettings; saveSettings(this.storagePaths, this.settings); @@ -631,94 +629,93 @@ export class AppController { } } - resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode); - - // Prevent prepareForShutdown from overwriting the restored data + resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode); + + // Prevent prepareForShutdown from overwriting the restored data this.manager.skipShutdownPersist = true; this.manager.blockAllPersistence = true; - logger.info("Backup wiederhergestellt (verschlüsseltes Format)"); - this.audit("WARN", "Backup importiert", { - historyEntries: Array.isArray(parsed.history) ? parsed.history.length : 0, - accountSummary: buildAccountSummary(this.settings) - }); - return { restored: true, message: "Backup wiederhergestellt. Bitte App neustarten." }; - } + logger.info("Backup wiederhergestellt (verschlüsseltes Format)"); + this.audit("WARN", "Backup importiert", { + historyEntries: Array.isArray(parsed.history) ? parsed.history.length : 0, + accountSummary: buildAccountSummary(this.settings) + }); + return { restored: true, message: "Backup wiederhergestellt. Bitte App neustarten." }; + } - public getSessionLogPath(): string | null { - return getSessionLogPath(); - } - - public getPackageLogPath(packageId: string): string | null { - return this.manager.getPackageLogPath(packageId) || getPackageLogPath(packageId); - } - - public getItemLogPath(itemId: string): string | null { - return this.manager.getItemLogPath(itemId) || getItemLogPath(itemId); - } - - public shutdown(): void { - if (this.runtimeStatsTimer) { - clearInterval(this.runtimeStatsTimer); - this.runtimeStatsTimer = null; - } - stopDebugServer(); + public getSessionLogPath(): string | null { + return getSessionLogPath(); + } + + public getPackageLogPath(packageId: string): string | null { + return this.manager.getPackageLogPath(packageId) || getPackageLogPath(packageId); + } + + public getItemLogPath(itemId: string): string | null { + return this.manager.getItemLogPath(itemId) || getItemLogPath(itemId); + } + + public shutdown(): void { + if (this.runtimeStatsTimer) { + clearInterval(this.runtimeStatsTimer); + this.runtimeStatsTimer = null; + } + stopDebugServer(); abortActiveUpdateDownload(); this.manager.prepareForShutdown(); this.megaWebFallback.dispose(); this.realDebridWebFallback.dispose(); - this.allDebridWebFallback.dispose(); - this.bestDebridWebFallback.dispose(); - shutdownSessionLog(); - shutdownPackageLogs(); - shutdownItemLogs(); - shutdownRenameLog(); - shutdownDailyLog(); - this.audit("INFO", "App beendet"); - shutdownTraceLog(); - shutdownAuditLog(); - if (this.settings.historyRetentionMode === "session") { - clearHistory(this.storagePaths); - } - logger.info("App beendet"); - } + this.allDebridWebFallback.dispose(); + this.bestDebridWebFallback.dispose(); + shutdownSessionLog(); + shutdownPackageLogs(); + shutdownItemLogs(); + shutdownRenameLog(); + this.audit("INFO", "App beendet"); + shutdownTraceLog(); + shutdownAuditLog(); + if (this.settings.historyRetentionMode === "session") { + clearHistory(this.storagePaths); + } + logger.info("App beendet"); + } + + public getHistory(): HistoryEntry[] { + return loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode); + } - public getHistory(): HistoryEntry[] { - return loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode); - } - - public clearHistory(): void { - this.audit("WARN", "Verlauf geleert"); - clearHistory(this.storagePaths); - } - - public setPackagePriority(packageId: string, priority: PackagePriority): void { - this.audit("INFO", "Paket-Priorität geändert", { packageId, priority }); - this.manager.setPackagePriority(packageId, priority); - } - - public skipItems(itemIds: string[]): void { - this.audit("INFO", "Items übersprungen", { itemIds }); - this.manager.skipItems(itemIds); - } - - public resetItems(itemIds: string[]): void { - this.audit("INFO", "Items zurückgesetzt", { itemIds }); - this.manager.resetItems(itemIds); - } - - public removeHistoryEntry(entryId: string): void { - this.audit("INFO", "Verlaufseintrag entfernt", { entryId }); - removeHistoryEntry(this.storagePaths, entryId); - } - - public addToHistory(entry: HistoryEntry): void { - this.audit("INFO", "Verlaufseintrag hinzugefügt", { - id: entry.id, - name: entry.name, - status: entry.status, - provider: entry.provider, - fileCount: entry.fileCount - }); - addHistoryEntry(this.storagePaths, entry); - } + public clearHistory(): void { + this.audit("WARN", "Verlauf geleert"); + clearHistory(this.storagePaths); + } + + public setPackagePriority(packageId: string, priority: PackagePriority): void { + this.audit("INFO", "Paket-Priorität geändert", { packageId, priority }); + this.manager.setPackagePriority(packageId, priority); + } + + public skipItems(itemIds: string[]): void { + this.audit("INFO", "Items übersprungen", { itemIds }); + this.manager.skipItems(itemIds); + } + + public resetItems(itemIds: string[]): void { + this.audit("INFO", "Items zurückgesetzt", { itemIds }); + this.manager.resetItems(itemIds); + } + + public removeHistoryEntry(entryId: string): void { + this.audit("INFO", "Verlaufseintrag entfernt", { entryId }); + removeHistoryEntry(this.storagePaths, entryId); + } + + public addToHistory(entry: HistoryEntry): void { + this.audit("INFO", "Verlaufseintrag hinzugefügt", { + id: entry.id, + name: entry.name, + status: entry.status, + provider: entry.provider, + fileCount: entry.fileCount + }); + addHistoryEntry(this.storagePaths, entry); + } } diff --git a/src/main/daily-log.ts b/src/main/daily-log.ts deleted file mode 100644 index 9c6594d..0000000 --- a/src/main/daily-log.ts +++ /dev/null @@ -1,235 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import { addLogListener, removeLogListener } from "./logger"; - -const DAILY_LOG_RETENTION_DAYS = 30; -const CLEANUP_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; -const FLUSH_INTERVAL_MS = 500; -const BUFFER_LIMIT_CHARS = 500_000; - -let dailyLogDir = ""; -let currentDayKey = ""; -let logListener: ((line: string) => void) | null = null; -let cleanupTimer: NodeJS.Timeout | null = null; -let lastCleanupAt = 0; - -// Async buffered writes — never blocks the event loop -let pendingLogLines: string[] = []; -let pendingLogChars = 0; -let pendingRenameLines: string[] = []; -let pendingRenameChars = 0; -let flushTimer: NodeJS.Timeout | null = null; -let flushInFlight = false; - -function getDayKey(now = new Date()): string { - const year = now.getFullYear(); - const month = String(now.getMonth() + 1).padStart(2, "0"); - const day = String(now.getDate()).padStart(2, "0"); - return `${year}-${month}-${day}`; -} - -function getMonthDir(dayKey: string): string { - return dayKey.slice(0, 7); -} - -function getDailyLogPath(dayKey: string): string { - return path.join(dailyLogDir, getMonthDir(dayKey), `${dayKey}.log`); -} - -function getDailyRenameLogPath(dayKey: string): string { - return path.join(dailyLogDir, getMonthDir(dayKey), `${dayKey}-rename.log`); -} - -function scheduleFlush(): void { - if (flushTimer || flushInFlight) return; - flushTimer = setTimeout(() => { - flushTimer = null; - void flushAsync(); - }, FLUSH_INTERVAL_MS); -} - -async function flushAsync(): Promise { - if (flushInFlight) return; - flushInFlight = true; - - try { - const dayKey = currentDayKey || getDayKey(); - - if (pendingLogLines.length > 0) { - const chunk = pendingLogLines.join(""); - pendingLogLines = []; - pendingLogChars = 0; - const filePath = getDailyLogPath(dayKey); - try { - await fs.promises.mkdir(path.dirname(filePath), { recursive: true }); - await fs.promises.appendFile(filePath, chunk, "utf8"); - } catch { /* ignore */ } - } - - if (pendingRenameLines.length > 0) { - const chunk = pendingRenameLines.join(""); - pendingRenameLines = []; - pendingRenameChars = 0; - const filePath = getDailyRenameLogPath(dayKey); - try { - await fs.promises.mkdir(path.dirname(filePath), { recursive: true }); - await fs.promises.appendFile(filePath, chunk, "utf8"); - } catch { /* ignore */ } - } - } finally { - flushInFlight = false; - if (pendingLogLines.length > 0 || pendingRenameLines.length > 0) { - scheduleFlush(); - } - } -} - -function flushSyncOnExit(): void { - const dayKey = currentDayKey || getDayKey(); - - if (pendingLogLines.length > 0) { - const chunk = pendingLogLines.join(""); - pendingLogLines = []; - pendingLogChars = 0; - try { - const filePath = getDailyLogPath(dayKey); - fs.mkdirSync(path.dirname(filePath), { recursive: true }); - fs.appendFileSync(filePath, chunk, "utf8"); - } catch { /* ignore */ } - } - - if (pendingRenameLines.length > 0) { - const chunk = pendingRenameLines.join(""); - pendingRenameLines = []; - pendingRenameChars = 0; - try { - const filePath = getDailyRenameLogPath(dayKey); - fs.mkdirSync(path.dirname(filePath), { recursive: true }); - fs.appendFileSync(filePath, chunk, "utf8"); - } catch { /* ignore */ } - } -} - -function writeToDailyLog(line: string): void { - if (!dailyLogDir) return; - - const dayKey = getDayKey(); - if (dayKey !== currentDayKey) { - // Day changed — flush previous day's buffer first - if (currentDayKey && (pendingLogLines.length > 0 || pendingRenameLines.length > 0)) { - void flushAsync(); - } - currentDayKey = dayKey; - } - - pendingLogLines.push(line); - pendingLogChars += line.length; - - // Shed oldest lines if buffer too large - while (pendingLogChars > BUFFER_LIMIT_CHARS && pendingLogLines.length > 1) { - const removed = pendingLogLines.shift(); - if (removed) pendingLogChars -= removed.length; - } - - scheduleFlush(); -} - -export function writeToDailyRenameLog(line: string): void { - if (!dailyLogDir) return; - - const dayKey = getDayKey(); - if (dayKey !== currentDayKey) { - currentDayKey = dayKey; - } - - pendingRenameLines.push(line); - pendingRenameChars += line.length; - - while (pendingRenameChars > BUFFER_LIMIT_CHARS && pendingRenameLines.length > 1) { - const removed = pendingRenameLines.shift(); - if (removed) pendingRenameChars -= removed.length; - } - - scheduleFlush(); -} - -function cleanupOldDailyLogs(): void { - if (!dailyLogDir) return; - - const now = Date.now(); - if (now - lastCleanupAt < CLEANUP_CHECK_INTERVAL_MS) return; - lastCleanupAt = now; - - const cutoffMs = now - DAILY_LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000; - - try { - const monthDirs = fs.readdirSync(dailyLogDir, { withFileTypes: true }) - .filter((e) => e.isDirectory() && /^\d{4}-\d{2}$/.test(e.name)); - - for (const monthDir of monthDirs) { - const monthPath = path.join(dailyLogDir, monthDir.name); - const files = fs.readdirSync(monthPath, { withFileTypes: true }) - .filter((e) => e.isFile() && /^\d{4}-\d{2}-\d{2}/.test(e.name)); - - for (const file of files) { - const filePath = path.join(monthPath, file.name); - try { - const stat = fs.statSync(filePath); - if (stat.mtimeMs < cutoffMs) { - fs.rmSync(filePath, { force: true }); - } - } catch { /* ignore */ } - } - - try { - const remaining = fs.readdirSync(monthPath); - if (remaining.length === 0) { - fs.rmdirSync(monthPath); - } - } catch { /* ignore */ } - } - } catch { - // ignore cleanup errors - } -} - -export function initDailyLog(baseDir: string): void { - dailyLogDir = path.join(baseDir, "daily-logs"); - - try { - fs.mkdirSync(dailyLogDir, { recursive: true }); - } catch { /* ignore */ } - - currentDayKey = getDayKey(); - - logListener = (line: string) => writeToDailyLog(line); - addLogListener(logListener); - - cleanupOldDailyLogs(); - - cleanupTimer = setInterval(cleanupOldDailyLogs, CLEANUP_CHECK_INTERVAL_MS); - if (cleanupTimer.unref) cleanupTimer.unref(); - - process.once("exit", flushSyncOnExit); -} - -export function shutdownDailyLog(): void { - if (logListener) { - removeLogListener(logListener); - logListener = null; - } - if (cleanupTimer) { - clearInterval(cleanupTimer); - cleanupTimer = null; - } - if (flushTimer) { - clearTimeout(flushTimer); - flushTimer = null; - } - flushSyncOnExit(); - currentDayKey = ""; -} - -export function getDailyLogDir(): string { - return dailyLogDir; -} diff --git a/src/main/rename-log.ts b/src/main/rename-log.ts index c03325d..72f5102 100644 --- a/src/main/rename-log.ts +++ b/src/main/rename-log.ts @@ -1,6 +1,5 @@ import fs from "node:fs"; import path from "node:path"; -import { writeToDailyRenameLog } from "./daily-log"; type RenameLogLevel = "INFO" | "WARN" | "ERROR"; @@ -94,9 +93,11 @@ export function logRenameEvent(level: RenameLogLevel, message: string, fields?: if (!fs.existsSync(renameLogPath)) { fs.writeFileSync(renameLogPath, "", "utf8"); } - const line = `${new Date().toISOString()} [${level}] ${message}${formatFields(fields)}\n`; - fs.appendFileSync(renameLogPath, line, "utf8"); - writeToDailyRenameLog(line); + fs.appendFileSync( + renameLogPath, + `${new Date().toISOString()} [${level}] ${message}${formatFields(fields)}\n`, + "utf8" + ); } catch { // ignore write errors }