Fix: Paket-Endstatus + Webhook gingen verloren, wenn das letzte Item fehlschlug

Audit-Befunde N1/DEDUP-2 (HIGH), N2 (HIGH), N4 (MEDIUM):

N2: Drei terminale Fehler-Pfade (HTTP-416 erschoepft, permanent toter Link,
Debrid-Link-Terminalfehler) returnten VOR dem gemeinsamen Schwanz mit
refreshPackageStatus. Schlug das LETZTE offene Item eines Pakets ueber einen
dieser Pfade fehl, blieb das Paket bis zum Neustart auf "downloading"/"queued"
haengen und die Fehler-Benachrichtigung kam nie — bei Paketen voller toter
Links (haeufigster Fehlerfall) deterministisch. Jetzt rufen alle drei Pfade
refreshPackageStatus auf (defensiv eingefuegt statt fall-through, damit kein
nachfolgender Retry-Branch ein endgueltig gefailtes Item wieder einreiht).

N1: refreshPackageStatus benachrichtigte beim Failed-Uebergang nur bei
success===0. Gemischte Pakete (teils OK, teils Fehler), deren letztes Item
FEHLSCHLAEGT, erreichen das Post-Processing aber nie (Trigger haengt nur an
Completion-Pfaden) — die Fehler-Benachrichtigung war weg, und zwar genau in
der haeufigen Reihenfolge (Fehl-Items brennen ihre Retries nach den
Geschwistern ab). Gate entfernt; das Dedup-Set verhindert Doppel-Sends, falls
Post-Processing doch laeuft. Zusaetzlich wird fuer den gemischten Fall jetzt
der History-Eintrag geschrieben (fehlte komplett; dedup-sicher via
historyRecordedPackages).

N4: skipItems triggert Post-Processing jetzt fuer JEDES durch den Skip
terminal gewordene Paket (vorher nur autoExtract&&!hasFailed&&unextracted) —
sonst fehlten Completed-/Failed-Benachrichtigung, History-Eintrag und
package_done-Bereinigung, z.B. mit autoExtract aus oder wenn hybrid schon
alles entpackt hatte. Label-Reset bleibt auf den Entpack-Fall begrenzt.
This commit is contained in:
Sucukdeluxe 2026-06-10 00:22:49 +02:00
parent f060d0238e
commit 99a24592c5

View File

@ -5249,24 +5249,26 @@ export class DownloadManager extends EventEmitter {
const pkg = this.session.packages[pkgId];
if (pkg) this.refreshPackageStatus(pkg);
}
if (this.settings.autoExtract) {
for (const pkgId of affectedPackageIds) {
const pkg = this.session.packages[pkgId];
if (!pkg || pkg.cancelled || this.packagePostProcessTasks.has(pkgId)) continue;
const pkgItems = pkg.itemIds.map((id) => this.session.items[id]).filter(Boolean) as DownloadItem[];
const hasPending = pkgItems.some((i) => i.status !== "completed" && i.status !== "failed" && i.status !== "cancelled");
const hasFailed = pkgItems.some((i) => i.status === "failed");
const hasUnextracted = pkgItems.some((i) => i.status === "completed" && shouldAutoRetryExtraction(i.fullStatus || ""));
if (!hasPending && !hasFailed && hasUnextracted) {
for (const it of pkgItems) {
if (it.status === "completed" && shouldAutoRetryExtraction(it.fullStatus || "")) {
it.fullStatus = "Entpacken - Ausstehend";
it.updatedAt = nowMs();
}
// A skip can be the package's LAST terminal event; without this trigger the
// post-processing terminal block (notify, history, cleanup policy) never runs
// for it — earlier rounds returned while the skipped item was still pending.
for (const pkgId of affectedPackageIds) {
const pkg = this.session.packages[pkgId];
if (!pkg || pkg.cancelled || this.packagePostProcessTasks.has(pkgId)) continue;
const pkgItems = pkg.itemIds.map((id) => this.session.items[id]).filter(Boolean) as DownloadItem[];
const hasPending = pkgItems.some((i) => i.status !== "completed" && i.status !== "failed" && i.status !== "cancelled");
if (hasPending) continue;
const hasFailed = pkgItems.some((i) => i.status === "failed");
const hasUnextracted = pkgItems.some((i) => i.status === "completed" && shouldAutoRetryExtraction(i.fullStatus || ""));
if (this.settings.autoExtract && !hasFailed && hasUnextracted) {
for (const it of pkgItems) {
if (it.status === "completed" && shouldAutoRetryExtraction(it.fullStatus || "")) {
it.fullStatus = "Entpacken - Ausstehend";
it.updatedAt = nowMs();
}
void this.runPackagePostProcessing(pkgId).catch((err) => logger.warn(`Post-processing nach Skip: ${compactErrorText(err)}`));
}
}
void this.runPackagePostProcessing(pkgId).catch((err) => logger.warn(`Post-processing nach Skip: ${compactErrorText(err)}`));
}
this.persistSoon();
this.emitState();
@ -9125,6 +9127,8 @@ export class DownloadManager extends EventEmitter {
item.fullStatus = `Fehler: ${item.lastError}`;
item.speedBps = 0;
item.updatedAt = nowMs();
const failPkg416 = this.session.packages[item.packageId];
if (failPkg416) this.refreshPackageStatus(failPkg416);
this.persistSoon();
this.emitState();
this.retryStateByItem.delete(item.id);
@ -9161,6 +9165,8 @@ export class DownloadManager extends EventEmitter {
item.speedBps = 0;
item.updatedAt = nowMs();
this.retryStateByItem.delete(item.id);
const failPkgDead = this.session.packages[item.packageId];
if (failPkgDead) this.refreshPackageStatus(failPkgDead);
this.persistSoon();
this.emitState();
return;
@ -9219,6 +9225,8 @@ export class DownloadManager extends EventEmitter {
item.speedBps = 0;
item.updatedAt = nowMs();
this.retryStateByItem.delete(item.id);
const failPkgDl = this.session.packages[item.packageId];
if (failPkgDl) this.refreshPackageStatus(failPkgDl);
this.persistSoon();
this.emitState();
return;
@ -10544,10 +10552,17 @@ export class DownloadManager extends EventEmitter {
pkg.status = "completed";
}
pkg.updatedAt = nowMs();
// A package whose items ALL failed never enters post-processing, so the
// post-process notify hook can't fire for it — cover that case here.
if (pkg.status === "failed" && prevStatus !== "failed" && success === 0) {
// A package whose LAST terminal event is a failure never (re-)enters
// post-processing (its trigger sits only on completion paths), so the
// post-process notify hook can't fire — cover every failed-transition here.
// That includes mixed packages (success > 0): the dedup set prevents a
// double-fire if post-processing does run later.
if (pkg.status === "failed" && prevStatus !== "failed") {
this.notifyPackageOutcome(pkg, "failed", `${failed} von ${total} Datei(en) fehlgeschlagen`);
if (success > 0) {
const items = pkg.itemIds.map((id) => this.session.items[id]).filter(Boolean) as DownloadItem[];
this.recordPackageHistory(pkg.id, pkg, items);
}
}
}