Round 7 bug fixes (13 fixes)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe 2026-03-04 21:59:42 +01:00
parent fad0f1060b
commit 75775f2798
5 changed files with 38 additions and 56 deletions

View File

@ -317,6 +317,9 @@ export class AppController {
// Prevent prepareForShutdown from overwriting the restored session file // Prevent prepareForShutdown from overwriting the restored session file
// with the old in-memory session when the app quits after backup restore. // with the old in-memory session when the app quits after backup restore.
this.manager.skipShutdownPersist = true; this.manager.skipShutdownPersist = true;
// Block all persistence (including persistSoon from any IPC operations
// the user might trigger before restarting) to protect the restored backup.
this.manager.blockAllPersistence = true;
return { restored: true, message: "Backup wiederhergestellt. Bitte App neustarten." }; return { restored: true, message: "Backup wiederhergestellt. Bitte App neustarten." };
} }

View File

@ -803,6 +803,10 @@ export class DownloadManager extends EventEmitter {
public skipShutdownPersist = false; public skipShutdownPersist = false;
/** Block ALL persistence (persistSoon + shutdown). Set after importBackup to prevent
* the old in-memory session from overwriting the restored backup on disk. */
public blockAllPersistence = false;
private debridService: DebridService; private debridService: DebridService;
private invalidateMegaSessionFn?: () => void; private invalidateMegaSessionFn?: () => void;
@ -1083,7 +1087,7 @@ export class DownloadManager extends EventEmitter {
seen.add(id); seen.add(id);
return true; return true;
}); });
const remaining = this.session.packageOrder.filter((id) => !valid.includes(id)); const remaining = this.session.packageOrder.filter((id) => !seen.has(id));
this.session.packageOrder = [...valid, ...remaining]; this.session.packageOrder = [...valid, ...remaining];
this.persistSoon(); this.persistSoon();
this.emitState(true); this.emitState(true);
@ -3116,6 +3120,7 @@ export class DownloadManager extends EventEmitter {
this.session.reconnectUntil = 0; this.session.reconnectUntil = 0;
this.session.reconnectReason = ""; this.session.reconnectReason = "";
this.retryAfterByItem.clear(); this.retryAfterByItem.clear();
this.retryStateByItem.clear();
this.lastGlobalProgressBytes = this.session.totalDownloadedBytes; this.lastGlobalProgressBytes = this.session.totalDownloadedBytes;
this.lastGlobalProgressAt = nowMs(); this.lastGlobalProgressAt = nowMs();
this.speedEvents = []; this.speedEvents = [];
@ -3132,12 +3137,13 @@ export class DownloadManager extends EventEmitter {
active.abortReason = "stop"; active.abortReason = "stop";
active.abortController.abort("stop"); active.abortController.abort("stop");
} }
// Reset all non-finished items to clean "Wartet" state // Reset all non-finished items to clean "Wartet" / "Paket gestoppt" state
for (const item of Object.values(this.session.items)) { for (const item of Object.values(this.session.items)) {
if (!isFinishedStatus(item.status)) { if (!isFinishedStatus(item.status)) {
item.status = "queued"; item.status = "queued";
item.speedBps = 0; item.speedBps = 0;
item.fullStatus = "Wartet"; const pkg = this.session.packages[item.packageId];
item.fullStatus = pkg && !pkg.enabled ? "Paket gestoppt" : "Wartet";
item.updatedAt = nowMs(); item.updatedAt = nowMs();
} }
} }
@ -3229,7 +3235,7 @@ export class DownloadManager extends EventEmitter {
this.session.summaryText = ""; this.session.summaryText = "";
// Persist synchronously on shutdown to guarantee data is written before process exits // Persist synchronously on shutdown to guarantee data is written before process exits
// Skip if a backup was just imported — the restored session on disk must not be overwritten // Skip if a backup was just imported — the restored session on disk must not be overwritten
if (!this.skipShutdownPersist) { if (!this.skipShutdownPersist && !this.blockAllPersistence) {
saveSession(this.storagePaths, this.session); saveSession(this.storagePaths, this.session);
saveSettings(this.storagePaths, this.settings); saveSettings(this.storagePaths, this.settings);
} }
@ -3494,7 +3500,7 @@ export class DownloadManager extends EventEmitter {
} }
private persistSoon(): void { private persistSoon(): void {
if (this.persistTimer) { if (this.persistTimer || this.blockAllPersistence) {
return; return;
} }
@ -6343,18 +6349,9 @@ export class DownloadManager extends EventEmitter {
extractCpuPriority: this.settings.extractCpuPriority, extractCpuPriority: this.settings.extractCpuPriority,
onProgress: (progress) => { onProgress: (progress) => {
if (progress.phase === "done") { if (progress.phase === "done") {
// Mark all remaining active archives as done // Do NOT mark remaining archives as "Done" here — some may have
for (const [archName, archItems] of activeHybridArchiveMap) { // failed. The post-extraction code (result.failed check) will
const doneAt = nowMs(); // assign the correct label. Only clear the tracking maps.
const startedAt = hybridArchiveStartTimes.get(archName) || doneAt;
const doneLabel = formatExtractDone(doneAt - startedAt);
for (const entry of archItems) {
if (!isExtractedLabel(entry.fullStatus)) {
entry.fullStatus = doneLabel;
entry.updatedAt = doneAt;
}
}
}
activeHybridArchiveMap.clear(); activeHybridArchiveMap.clear();
hybridArchiveStartTimes.clear(); hybridArchiveStartTimes.clear();
return; return;
@ -6639,18 +6636,9 @@ export class DownloadManager extends EventEmitter {
extractCpuPriority: this.settings.extractCpuPriority, extractCpuPriority: this.settings.extractCpuPriority,
onProgress: (progress) => { onProgress: (progress) => {
if (progress.phase === "done") { if (progress.phase === "done") {
// Mark all remaining active archives as done // Do NOT mark remaining archives as "Done" here — some may have
for (const [archName, items] of activeArchiveItemsMap) { // failed. The post-extraction code (result.failed check) will
const doneAt = nowMs(); // assign the correct label. Only clear the tracking maps.
const startedAt = archiveStartTimes.get(archName) || doneAt;
const doneLabel = formatExtractDone(doneAt - startedAt);
for (const entry of items) {
if (!isExtractedLabel(entry.fullStatus)) {
entry.fullStatus = doneLabel;
entry.updatedAt = doneAt;
}
}
}
activeArchiveItemsMap.clear(); activeArchiveItemsMap.clear();
archiveStartTimes.clear(); archiveStartTimes.clear();
emitExtractStatus("Entpacken 100%", true); emitExtractStatus("Entpacken 100%", true);
@ -6786,9 +6774,9 @@ export class DownloadManager extends EventEmitter {
for (const entry of completedItems) { for (const entry of completedItems) {
if (/^Entpacken/i.test(entry.fullStatus || "") || /^Passwort/i.test(entry.fullStatus || "")) { if (/^Entpacken/i.test(entry.fullStatus || "") || /^Passwort/i.test(entry.fullStatus || "")) {
entry.fullStatus = "Entpacken abgebrochen (wird fortgesetzt)"; entry.fullStatus = "Entpacken abgebrochen (wird fortgesetzt)";
}
entry.updatedAt = nowMs(); entry.updatedAt = nowMs();
} }
}
pkg.status = (pkg.enabled && !this.session.paused) ? "queued" : "paused"; pkg.status = (pkg.enabled && !this.session.paused) ? "queued" : "paused";
pkg.updatedAt = nowMs(); pkg.updatedAt = nowMs();
logger.info(`Post-Processing Entpacken abgebrochen: pkg=${pkg.name}`); logger.info(`Post-Processing Entpacken abgebrochen: pkg=${pkg.name}`);
@ -6801,9 +6789,9 @@ export class DownloadManager extends EventEmitter {
for (const entry of completedItems) { for (const entry of completedItems) {
if (!isExtractedLabel(entry.fullStatus)) { if (!isExtractedLabel(entry.fullStatus)) {
entry.fullStatus = `Entpack-Fehler: ${reason}`; entry.fullStatus = `Entpack-Fehler: ${reason}`;
}
entry.updatedAt = nowMs(); entry.updatedAt = nowMs();
} }
}
pkg.status = "failed"; pkg.status = "failed";
} }
} finally { } finally {

View File

@ -2195,7 +2195,7 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
for (const nestedArchive of nestedCandidates) { for (const nestedArchive of nestedCandidates) {
if (options.signal?.aborted) throw new Error("aborted:extract"); if (options.signal?.aborted) throw new Error("aborted:extract");
const nestedName = path.basename(nestedArchive); const nestedName = path.basename(nestedArchive);
const nestedKey = archiveNameKey(nestedName); const nestedKey = archiveNameKey(`nested:${nestedName}`);
if (resumeCompleted.has(nestedKey)) { if (resumeCompleted.has(nestedKey)) {
logger.info(`Nested-Extraction übersprungen (bereits entpackt): ${nestedName}`); logger.info(`Nested-Extraction übersprungen (bereits entpackt): ${nestedName}`);
continue; continue;
@ -2258,7 +2258,7 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
} }
if (extracted > 0) { if (extracted > 0) {
const hasOutputAfter = await hasAnyEntries(options.targetDir); const hasOutputAfter = await hasAnyFilesRecursive(options.targetDir);
const hadResumeProgress = resumeCompletedAtStart > 0; const hadResumeProgress = resumeCompletedAtStart > 0;
if (!hasOutputAfter && conflictMode !== "skip" && !hadResumeProgress) { if (!hasOutputAfter && conflictMode !== "skip" && !hadResumeProgress) {
lastError = "Keine entpackten Dateien erkannt"; lastError = "Keine entpackten Dateien erkannt";

View File

@ -78,6 +78,11 @@ async function sleepWithSignal(ms: number, signal?: AbortSignal): Promise<void>
await sleep(ms); await sleep(ms);
return; return;
} }
// Check before entering the Promise constructor to avoid a race where the timer
// resolves before the aborted check runs (especially when ms=0).
if (signal.aborted) {
throw new Error("aborted");
}
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
let timer: NodeJS.Timeout | null = setTimeout(() => { let timer: NodeJS.Timeout | null = setTimeout(() => {
timer = null; timer = null;
@ -94,10 +99,6 @@ async function sleepWithSignal(ms: number, signal?: AbortSignal): Promise<void>
reject(new Error("aborted")); reject(new Error("aborted"));
}; };
if (signal.aborted) {
onAbort();
return;
}
signal.addEventListener("abort", onAbort, { once: true }); signal.addEventListener("abort", onAbort, { once: true });
}); });
} }

View File

@ -3331,24 +3331,14 @@ const PackageCard = memo(function PackageCard({ pkg, items, packageSpeed, isFirs
</span> </span>
</span> </span>
); );
case "hoster": return ( case "hoster": {
<span key={col} className="pkg-col pkg-col-hoster" title={(() => { const hosterText = [...new Set(items.map((item) => extractHoster(item.url)).filter(Boolean))].join(", ");
const hosters = [...new Set(items.map((item) => extractHoster(item.url)).filter(Boolean))]; return <span key={col} className="pkg-col pkg-col-hoster" title={hosterText}>{hosterText}</span>;
return hosters.join(", "); }
})()}>{(() => { case "account": {
const hosters = [...new Set(items.map((item) => extractHoster(item.url)).filter(Boolean))]; const accountText = [...new Set(items.map((item) => item.provider).filter(Boolean))].map((p) => providerLabels[p!] || p).join(", ");
return hosters.length > 0 ? hosters.join(", ") : ""; return <span key={col} className="pkg-col pkg-col-account" title={accountText}>{accountText}</span>;
})()}</span> }
);
case "account": return (
<span key={col} className="pkg-col pkg-col-account" title={(() => {
const providers = [...new Set(items.map((item) => item.provider).filter(Boolean))];
return providers.map((p) => providerLabels[p!] || p).join(", ");
})()}>{(() => {
const providers = [...new Set(items.map((item) => item.provider).filter(Boolean))];
return providers.length > 0 ? providers.map((p) => providerLabels[p!] || p).join(", ") : "";
})()}</span>
);
case "prio": return ( case "prio": return (
<span key={col} className={`pkg-col pkg-col-prio${pkg.priority === "high" ? " prio-high" : pkg.priority === "low" ? " prio-low" : ""}`}>{pkg.priority === "high" ? "Hoch" : pkg.priority === "low" ? "Niedrig" : ""}</span> <span key={col} className={`pkg-col pkg-col-prio${pkg.priority === "high" ? " prio-high" : pkg.priority === "low" ? " prio-low" : ""}`}>{pkg.priority === "high" ? "Hoch" : pkg.priority === "low" ? "Niedrig" : ""}</span>
); );