Mega-Debrid: Account am Tageslimit bis Neustart parken (Streak-Heuristik) statt endlos neu testen

User-Entscheidung: ein Mega-Debrid-Account am Tageslimit soll bis zum Programm-Neustart
uebersprungen werden, nicht alle 20s/2min neu getestet.

Ground Truth (Support-Bundle gegrept): der limitierte Account liefert im Web-Pfad NIE eine
unterscheidbare Meldung — "Kein Server" = 0 Treffer, "Antwort leer" = 20.861. Tageslimit und
transienter Blip sind auf Message-Ebene nicht trennbar (generate() findet ohne processDebrid-
Code keinen Code -> return null -> "Antwort leer"). Ein Trigger auf "Kein Server" waere toter Code.

Loesung (Verhaltens-Signal statt Wortlaut):
- megaDebridEmptyResponseStreaks zaehlt aufeinanderfolgende "Antwort leer"/"Kein Server"-
  Treffer je Account; ab 3 wird der Account bis Neustart geparkt (until=MAX_SAFE_INTEGER,
  nur In-Memory -> Neustart loescht). Erfolg/anderer Fehler setzt zurueck.
- classifyAccountFailure markiert beide Signale als limitSignal (Symmetrie: ein einzelner
  evtl. transienter Treffer parkt NICHT, behaelt kurzen Cooldown).
- Skip-Branch: "uebersprungen (bis Neustart gesperrt)", traegt nicht zu earliestCooldownUntil
  bei (kein absurder Retry-Timer); Post-Loop wirft klare Endmeldung wenn alle geparkt.
- generate() surfacet "Kein Server" zusaetzlich als Page-Error (falls es doch im HTML steht).
- UI: Rotations-Verlauf zeigt "bis Neustart gesperrt".

Verifiziert: tsc 9 (Baseline), 655 Tests + 5 neue (inkl. Wiring-E2E der eine echte leere
Antwort durch unrestrictWithAccounts->classify->catch->Park treibt), Build gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe
2026-05-31 21:08:43 +02:00
co-authored by Claude Opus 4.8
parent 254fce8736
commit ffcd0817cf
6 changed files with 339 additions and 28 deletions
+7 -2
View File
@@ -1143,16 +1143,21 @@ function formatCheckedAgo(checkedAt: number): string {
return `vor ${days} Tag${days === 1 ? "" : "en"}`;
}
function rotationEventText(ev: { event: string; cooldownSec?: number; next?: string }): string {
function rotationEventText(ev: { event: string; cooldownSec?: number; next?: string; reason?: string }): string {
const untilRestart = /bis Neustart gesperrt/i.test(ev.reason || "");
switch (ev.event) {
case "OK": return "erfolgreich";
case "FAILED": {
if (untilRestart) {
const nx = ev.next && ev.next !== "ENDE" ? `${ev.next}` : "";
return `Tageslimit erreicht, bis Neustart gesperrt${nx}`;
}
const cd = ev.cooldownSec ? `, Cooldown ${ev.cooldownSec}s` : "";
const nx = ev.next && ev.next !== "ENDE" ? `${ev.next}` : "";
return `fehlgeschlagen${cd}${nx}`;
}
case "FATAL": return "abgebrochen (fataler Fehler)";
case "SKIP_COOLDOWN": return "übersprungen (Cooldown aktiv)";
case "SKIP_COOLDOWN": return untilRestart ? "übersprungen (bis Neustart gesperrt)" : "übersprungen (Cooldown aktiv)";
case "SKIP_DISABLED": return "übersprungen (deaktiviert)";
case "SKIP_DAILY_LIMIT": return "übersprungen (Tageslimit erreicht)";
case "SKIP_HOST_COOLDOWN": return "übersprungen (Host-Cooldown)";