# Lessons ## 2026-04-21 — DOM-Doppelrender bei Bulk-State-Changes **Symptom:** User klickt auf "Erneut versuchen" mit 500+ Jobs → App hängt sekundenlang. **Root cause:** `retrySelectedJobs()` ruft `renderQueueTable + updateQueueActionButtons + updateStatusBar` auf, `startSelectedUpload()` ruft direkt danach genau dieselben Funktionen nochmal auf. **Regel:** Wenn ein Click-Handler `await anotherHandler()` aufruft und der innere Handler seinen eigenen kompletten Render-Zyklus hat, NIEMALS noch einen davor. Einmal ist genug — der folgende innere Render sieht die frischen State-Mutationen ohnehin. **Wie anwenden:** Vor jeder `await fn()`-Folge in einem Handler prüfen: macht `fn` schon `renderQueueTable()`? Wenn ja, äußere Render-Calls löschen. ## 2026-04-21 — State-Checks MÜSSEN hinter die Semaphore-Queue **Symptom:** Pre-Job-Swap prüfte `_failedAccounts` vor `semaphore.acquire`. Bei N parallelen Workers war der Check zum Start für ALLE leer — niemand hat geswapt. Erst nachdem alle im Semaphore ordentlich gewartet hatten und einer fehlschlug, wurde _failedAccounts befüllt, aber die anderen hatten ihren Check längst hinter sich. **Regel:** State-basierte Entscheidungen (failed accounts, overrides, cached stats) gehören direkt vor die Aktion die sie betreffen — **nach** jeder async `await` die die Position in der Queue bestimmt. Nicht am Task-Start für später wichtigen State abfragen. **Wie anwenden:** Bei Queue-basierten Pipelines prüfen: "Was kann sich zwischen Task-Start und dem tatsächlichen Execute ändern?" Alles was sich ändern kann, muss direkt vor dem Execute geprüft werden, nicht davor. ## 2026-04-21 — Reaktive Config-Updates für laufende State-Maschinen **Symptom:** User fügt mid-batch einen neuen Account hinzu, aber der UploadManager merkt nicht dass die Config sich geändert hat. `account-failed` Event feuert nur einmal pro Account → keine zweite Re-Resolve-Chance. **Regel:** Wenn ein State nur bei Events neu evaluiert wird und Events "nur einmal" feuern, muss jede externe Zustandsänderung (Config-Save, User-Action) den State explizit triggern. **Wie anwenden:** Save-Handler müssen aktive State-Maschinen informieren. Lieber einen überflüssigen Re-Resolve-Call als einen verpassten. Für Upload-Manager: nach saveConfig → re-evaluate failed accounts ohne Override. ## 2026-04-21 — Error-Klassifikation: fileRejected vs accountError **Symptom:** Voller Byse-Account wurde nicht rotiert — `skip-rotation-file-rejected` geloggt für jede Datei. **Root cause:** Generisches Match auf Prefix-String (`"lehnte Datei ab"`) klassifizierte ALLE Byse-Errors als file-level, inklusive Account-voll-Meldungen. **Regel:** Hoster-Parser setzen den **spezifischen Flag** (`fileRejected` ODER `accountError`), nicht beide nie. Classifier matcht **konkrete Phrasen** (Duplicate, Not video format, …), niemals generische Wrapper-Strings die für mehrere Fehlerarten benutzt werden. **Wie anwenden:** - Bei neuen Hostern: per-status-Klassifikation bereits im Parser, nicht erst im Upload-Manager. - Classifier-Regexes auf Rejection-Kernphrasen, nicht auf UI-Prefix. - Defensive: `accountError === true` gewinnt immer gegen `fileRejected` — Account-Rotation ist weniger schlimm als endlose Fails auf einem toten Account. ## 2026-04-21 — Keine fake Build-ETAs **Symptom:** User wartet 5+ min auf Tauri-Build den ich mit "1-2min" angekündigt habe. **Regel:** Tauri-Release-Builds brauchen real 3-6 min (Rust + NSIS + MSI). Keine Zeitangabe oder ehrlich "kann 3-6min dauern" schreiben. **Wie anwenden:** Wenn User nach Status fragt: sofort `tail` des Logs + `ls` des Bundle-Ordners zitieren, nicht raten. ## 2026-05-24 — Packaged-Electron Log-Pfade: nie __dirname/.. zum Schreiben **Symptom:** doodstream-debug.log hatte auf dem Server null aktuelle Einträge; nur alte Dev-Logs. Fehler "kein Filecode" war nicht diagnostizierbar. **Root cause:** `path.join(__dirname, '..', 'x.log')` zeigt im gepackten Build in `resources/app.asar` (read-only). `fs.appendFileSync` wirft EACCES, der `try/catch` schluckt es → null Production-Logs. **Regel:** Schreibbare Pfade IMMER über `app.getPath('userData')` (lazy `require('electron')`, Fallback `__dirname/..` nur für Tests/plain-node). Gilt für jede Datei die der gepackte App schreibt. **Wie anwenden:** Bei jedem neuen Log/Cache/State-File prüfen: wohin schreibt das im NSIS-Build? Nicht ins Install-Verzeichnis, nicht in asar. ## 2026-05-24 — Hoster-Fehler: echten Status surfacen, nicht generisch schlucken **Symptom:** "upload_result Seite hat keinen filecode ()" — nichtssagend; User dachte doodstream-Format geändert. **Root cause:** XFileSharing liefert den echten Grund im `st`-Feld (Error: duplicate / file too big / …). Code ignorierte `st` komplett und warf nur den leeren Body. **Regel:** Bei Hoster-Parsefehlern immer die Server-Statusfelder (st/msg/code) + Kontext (welcher CDN-Node, war filecode da) in die Fehlermeldung packen. Format-Struktur unverändert + leerer Inhalt = Backend-Ablehnung, kein Parsing-Bug. ## 2026-05-25 — Queue leer nach Update: Auto-Dedup zu aggressiv (nicht Save/Restore) **Symptom:** Queue gestoppt, App-Update -> nach Neustart Queue leer ("Dateien hierhin ziehen"). User dachte Save/Restore kaputt. **Root cause:** Queue WIRD korrekt gespeichert (pendingQueue) + restored. ABER `_autoDeduplicateFromLog` (läuft bei init nach restore) entfernte Jobs per `fileName|hoster`-Match gegen das GESAMTE Lifetime-fileuploader.log — UNABHÄNGIG vom Status. Pending 'preview'-Jobs, deren Datei früher mal hochgeladen wurde, flogen alle raus -> komplette Queue weg. "Update-spezifisch" nur weil der Server-App nur beim Update neustartet (normaler Restart hätte dasselbe getan). **Verifiziert:** Reale electron-config.json: 4 preview-Jobs, alle 4 Keys im Log -> alte Logik entfernt 4/4. Neue Logik (nur status==='done' droppen) entfernt 0/4. **Regel:** Auto-Cleanup/Dedup darf NIE pending/actionable User-Arbeit löschen. Nur genuin abgeschlossene ('done') Jobs decluttern. Lifetime-Logs sind Historie, nicht Session-Fortschritt — nicht als "schon erledigt"-Quelle für pending Jobs missbrauchen. **Wie anwenden:** Bei jeder Filter/Remove-Logik auf User-State: nach Status gaten, nicht nur nach Identitäts-Match gegen historische Daten. ## 2026-05-28 — Doodstream "kein Filecode": Web-Scraping ist die falsche Ebene, API ist der Fix **Symptom:** Wiederkehrend "kein Filecode — Server gab leeren Link zurueck" bei großen Dateien (~1GB/7min Upload), trotz 3.3.26-3.3.29. Queue voll roter Fehler. **Root cause (recherchiert + verifiziert):** Der Web-Upload holt den Filecode aus einem XFileSharing-HTML-Formular. Bei langen Uploads kommt das Formular leer zurück, weil (a) der per-Seitenaufruf sess_id-Token über den 7min-Upload altert UND (b) der server-seitige File-Registration-Callback (cgi-bin/fs.cgi-Äquivalent) unter Last timeoutet → kein file_code gemintet. Wichtig: Das ist KEIN async-delay — die Datei taucht NICHT später in der Liste auf (die Registrierung, die sie listen würde, ist genau das was failt). File-list-Polling (wie Byse) hilft hier also kaum. **Fix:** Die offizielle doodapi.co JSON-API nutzen, wenn ein API-Key da ist — sie liefert result[0].filecode DIREKT in JSON (kein HTML-Formular) und nutzt einen persistenten api_key (kein alternder sess_id). Git-Historie: die API war der ORIGINAL-Pfad (initial commit); Web-Login kam später nur "als Alternative zum API-Key" — Key-Bevorzugung stellt also den gedachten Primärpfad wieder her, kämpft nicht gegen eine bewusste Entscheidung. **Regel:** Bei Hoster-Integrationen die offizielle API der Web-Scraping-Ebene vorziehen wo möglich. Empty-form/codeless-2xx = Hoster-Backend-Flake (hosterTransient), Account NICHT als tot markieren — auf BEIDEN Pfaden (Web + API) gleich klassifizieren. **Voraussetzung:** Engagiert nur wenn der Doodstream-Account einen gültigen API-Key hat (doodstream.com/settings). Keyless-Accounts bleiben beim Web-Pfad. ## 2026-05-28 — Doodstream empty-form: live diagnosis confirmed API path is the fix **Verifiziert mit echtem Account-Key (read-only API-Calls):** - account/info → status 200, Key gültig, Storage unlimited. Premium ABGELAUFEN (2025-10-03) — Uploads gehen TROTZDEM. - upload/server → liefert gültigen Node (cv1130ed.cloudatacdn.com) auch ohne Premium → API-Upload-Pfad nutzbar. - file/list → 90.548 Dateien; Uploads landen server-seitig INTERMITTIEREND (viele Burn-Notice-Folgen genau im "Fehler"-Zeitfenster vorhanden). Das leere Formular ist also nicht "immer kaputt", sondern manchmal — der Web-Form-Registrierungs-Callback (fs-public.intconnect.net) timeoutet sporadisch. **Konsequenz:** API-Weg (result[0].filecode inline) umgeht den failenden Callback → richtiger Fix. file/list-Recovery ist NICHT tote Last (Dateien erscheinen ja) — aber bei 90k-Accounts MUSS man sort=created&order=desc erzwingen, sonst ist die frische Datei nicht auf Seite 1. **Regel:** Bei "geht manchmal/manchmal nicht" + Hoster mit offizieller API: erst per read-only API-Call (account/info, file/list) gegen den ECHTEN Account verifizieren statt am Client weiterzuraten. Das beendet Spekulations-Schleifen. ## 2026-06-17 — Rotation-State pro-Call neu zu bauen = stiller No-Op beim Drip-Feed **Symptom:** Account-Rotation (v3.3.74) verteilte korrekt bei einem Drag-Drop-Batch (N Dateien auf einmal), aber bei Folder-Monitor / Einzeldatei-Uploads landete IMMER alles auf Account 1 — die Sekundär-Accounts bekamen nichts. **Root cause:** Der Picker wurde INNERHALB jedes `buildUploadTasks`/`buildUploadTasksFromJobs`-Calls frisch erzeugt → `rotIdx` startete jedes Mal bei 0. Der Folder-Monitor speist neue Dateien einzeln via `add-jobs-to-batch` in einen laufenden Batch (und per-Detection-autoStart feuert `start-upload` pro Datei). Jeder dieser Calls baute den Picker neu von Index 0 → `0 % len = 0` = Account 1, immer. **Verifiziert:** Unit-Tests gaben falsche Sicherheit — alle 10 nutzten EINEN Picker über N Calls (testet nur Intra-Batch). Der Bug lebte genau im Cross-Call-Verhalten, das kein Test abdeckte. **Fix:** Rotation-Cursor lebt AUSSERHALB des Pickers: modul-level `_rotationCursors` in main.js als authoritative Quelle (einmal aus config geseedet), `config.rotationCursors` als Restart-Survival-Backing (das 30-Tage-Quota überlebt App-Neustarts). Picker bekommt `{ indices }`-Seed + exponiert `indices()`/`dirty()`. Persist nur wenn `dirty()`. **Regel:** Rotations-/Round-Robin-/Cursor-State, der über mehrere Aufruf-Eintrittspunkte fair verteilen soll, MUSS die Aufrufe überdauern — und wenn das Ziel ein Zeitfenster über Sessions hinweg ist (Quota), auch Restarts. Picker/Iterator pro Call neu instanziieren ist nur korrekt, wenn jeder Call die GESAMTE zu verteilende Menge sieht. **Wie anwenden:** Bei jeder "verteile reihum"-Funktion fragen: über wie viele Eintrittspunkte/Calls läuft die zu verteilende Menge real? (Hier: drag-drop=1 Call ABER folder-monitor=1 Call pro Datei.) Tests MÜSSEN den Cross-Call/Drip-Feed-Pfad mit frisch geseedetem State nachstellen, nicht nur einen langlebigen Picker. ## 2026-06-19 — Transiente Infra-Fehler (5xx/ECONNRESET) NIE als Account-Fehler klassifizieren **Symptom:** User: "byse bissl unstabiler als sonst", Queue voll "Upload-Antwort von byse.sx war kein JSON (HTTP 502): " und "read ECONNRESET". EIN byse-Gateway-Aussetzer kaskadierte durch ALLE Failover-Accounts (Primär→Fallback #3, jeder kriegt denselben 502) und blacklistete sie für den Batch. **Root cause:** Die Upload-POST-Throw-Sites (hosters.js) warfen PLAIN Errors OHNE Klassifikations-Flag. Ein 502 matchte keinen der Classifier (_isTransientNetworkError hatte nur errno-Regexes ENOTFOUND/ECONNRESET/..., KEIN HTTP-5xx-Pattern; _shouldSkipRetryOnAccountError nur /\b(401|403|429)\b/, kein 5xx) → GENERISCH → `while(task.accountId)` → mark-failed + emit('account-failed') → Failover-Kaskade + Blacklist. (Der Server-Lookup-Pfad war schon geschützt via hosterTransient — nur der POST-Pfad nicht.) **Wichtig — Diagnose-Disziplin:** "Hat byse die API geändert?" → NEIN, per Live-Probe verifiziert (GET /upload/server liefert dokumentiertes JSON; Upload-Contract unverändert). 502/HTML + ECONNRESET = Gateway-Infra, kein Contract-Bruch. Bei "manchmal kaputt" + offizielle API: Fehler-BODY anschauen (502-HTML ≠ API-Änderung) statt raten. **Fix:** Expliziter `err.transientNetwork`-Flag am Throw-Site gesetzt wenn `statusCode >= 500` (bzw. payload.status===500); 401/403/429 bleiben PLAIN (Account-Fehler). Flag wird in _isTransientNetworkError ZUERST geprüft (vor dem `!err.message`-Guard) und macht _isFileRejectedError/_shouldSkipRetryOnAccountError autoritativ false (gegen Keyword-False-Positives im HTML-Snippet). transientNetwork (NICHT hosterTransient) = retry SAME account, dann clean fail vor der Rotation-Schleife → keine Kaskade, kein Blacklist. Server-Lookup-Pfad symmetrisch gehärtet (apiGet taggt 5xx, getUploadServer reicht den Flag auf den gewrappten Error durch). **Regel:** Hoster-Infra-Fehler (HTTP 5xx/Bad Gateway/Service Unavailable, Connection-Reset/Timeout) sind NIE Account-Faults — alle Accounts treffen dasselbe Gateway, Failover ist sinnlos, Blacklist schädlich. Fail open: retry same account, dann clean fail. Klassifikation per explizitem Flag am Throw-Site (autoritativ), Message-Regex nur als Defensive-Fallback. Throw-Site-Tagging IMMER end-to-end testen (uploadFile mit gestubbtem 5xx-Response treiben), nicht nur den Flag im Mock injizieren. **Nebenbefund:** byse-Recovery-Poll war tot — `_fetchByseFileList` baute `api.byse.sx/api/file/list` (das `/api/`-Präfix gilt NUR für doodstreams doodapi.co-Host; byse hat schon die `api.`-Subdomain). Live: `/api/file/list`→302 auf Docs, `/file/list`→200 JSON. Copy-paste vom doodstream-Pattern. Fix: Präfix raus. ## 2026-06-19 — DNS-Auflösung + Cert ≠ legitime Domain (Parking-Falle); kaputte Hoster-Daten nicht raten, sondern verwerfen **Symptom:** byse-Uploads scheiterten an `getaddrinfo ENOTFOUND s1065.filemoon` — der von byses `/upload/server` gelieferte Host hatte KEINE TLD (`sNNNN.filemoon`), unauflösbar. Intermittierend (manche Uploads klappten = vollständiger Host). **Beinahe-Fehler:** Ich wollte fast `.art` anhängen — `s1065.filemoon.art` löste auf UND hatte ein dediziertes `*.filemoon.art`-Cert. ABER: PTR der IP = `k8s-svc-lander-...parklogic.net` = DOMAIN-PARKING. `filemoon.nl` (auch auflösend) = Shared-Catch-all-Cert mit Müll-SAN (kaobei.cc/babesex.xyz). BEIDE geparkt/gesquattet, nicht byses echte Server. Uploads dahin = auf Parking-Page oder zu Fremden. **Regel:** „Löst auf + hat HTTPS-Cert" beweist NICHT, dass eine Domain der echte Service ist. Vor dem Behandeln einer Domain als legitim: **PTR/Reverse-DNS** (parklogic/lander/sedoparking = geparkt) UND **Cert-SAN** (riesige unzusammenhängende SAN-Liste = Shared-Parking-Cert) prüfen. Bei Hoster-Migrationen besetzen Squatter alte/neue Marken-TLDs. **Regel (kaputte Hoster-Daten):** Wenn ein Hoster strukturell kaputte Daten liefert (Host ohne TLD), den korrekten Wert NICHT raten/halluzinieren (würde der User-Regel „niemals halluzinieren, immer verifizieren" widersprechen). Stattdessen das offensichtlich Kaputte als ungültig VERWERFEN und die bestehende Retry-/Cache-Maschinerie nach einem gültigen Wert fragen lassen (hier: normalizeAbsoluteUrl returnt null bei `/(^|\.)filemoon$/` → getUploadServer retried + LAST_UPLOAD_SERVERS-Cache). Fail-open bleibt: alles kaputt → clean fail, kein Cascade.