Commit Graph

18 Commits

Author SHA1 Message Date
Administrator
7f636258d4 perf(uploads): make the batch-start file-stat non-blocking — kill the 336ms main stall (v3.3.103)
The v3.3.102 log (real 224-job batch) confirmed History virtualization works and
steady-state uploads are pristine (fps=32, event-loop mean 11.8ms). The remaining
residual was a ~6s spin-up burst at batch start: the main event loop blocked for
336ms with cpu=0%core (i.e. blocked on I/O, not computing) and the renderer janked
196-391ms, then everything settled clean.

A multi-agent investigation plus an adversarial review corrected the obvious-looking
hypothesis. The renderer's uncapped progress-batch drain is NOT the cause:
handleProgress only mutates plain JS state and schedules already-coalesced renders
(one per frame), and main coalesces progress to ~50 latest-per-job entries per
100ms. Chunking that drain would fix nothing — and the reviewer showed it would
REGRESS correctness: requestAnimationFrame throttles to ~0 when the window is
minimized (the common state for a background uploader), so a rAF-chunked drain would
grow an unbounded backlog and defer persistQueueStateSoon for every buffered item,
losing terminal 'done' events on close (the queue-persistence ghost-fix class). So
that path is deliberately not taken.

The real cause (cpu=0%core = blocked on I/O) is a synchronous fs.statSync storm in
UploadManager.startBatch: the dedup loop ran up to DEDUP_CHUNK=200 synchronous
fs.statSync calls in a single tick before yielding (200 x ~1.68ms on the user's VM
= the exact 336ms), on a disk already saturated by the 1MB read-ahead.

Fix — make the batch-start stats non-blocking:
- The dedup loop now dedupes synchronously (cheap Map work) and then stats the
  unique files in parallel via await Promise.all(fs.promises.stat ...) per chunk, so
  the stat I/O runs on the libuv threadpool and the main thread never blocks. The
  results-Map shape ({name,size,results:[]}) and dedup semantics (size 0 on failure)
  are unchanged.
- The per-job statSync fallback is converted to await fs.promises.stat for
  consistency (it sits in an async function before the first real await; the cached
  size from dedup already lets nearly every job skip it).

Tests: the upload-manager mocks override fs.statSync; they now also override
fs.promises.stat with the same fake sizes (upload-manager.test.js x2,
suspect-reject-alternates.test.js). 407 tests pass; clean Electron boot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:53:48 +02:00
Administrator
8b2a1d7c1f fix(byse): treat 5xx/gateway upload failures as transient infra, not account faults
A single byse.sx gateway hiccup (HTTP 502 "<!doctype html>..." or a mid-upload
ECONNRESET) was cascading through the whole failover chain and blacklisting every
account for the batch. Root cause: the upload-POST throw sites threw PLAIN errors
with no classification, so a 502 was treated as a GENERIC error -> mark-failed ->
emit('account-failed') -> failover to the next account (which hits the same
gateway) -> repeat until the chain is exhausted, then poison sibling files in the
batch via the blacklist. The screenshots showed exactly this: "Primär ... Fallback
#3" all failing with the same 502. byse did NOT change their API (the upload
contract still matches their docs and was live-probed: GET /upload/server -> POST
{key}+file -> files[0].filecode); these are transient infrastructure failures.

A 5xx gateway error is not an account fault: every account hits the same gateway,
so failing over is pointless and blacklisting is harmful. Fail open instead — retry
the SAME account and, if byse stays down, fail the file cleanly without touching the
account or the rotation cursor.

- upload-manager: _isTransientNetworkError() now honors an explicit err.transientNetwork
  flag (checked before the empty-message guard) and, as a defensive fallback, matches
  /HTTP 5\d\d/, Bad Gateway, Service Unavailable, Gateway Time-out. The flag is made
  authoritative in _isFileRejectedError and _shouldSkipRetryOnAccountError (both return
  early when it is set) so a 5xx whose HTML body happens to contain a rejection/auth
  keyword can never be mis-binned as file/account. The post-rotation retry loop also
  breaks on a transient error (parity with the primary loop) to avoid burning the
  retry budget re-uploading on a fallback.
- hosters: the upload POST throw sites tag err.transientNetwork when statusCode >= 500
  (non-JSON body and non-2xx-with-JSON) and when the 2xx status-envelope carries
  status:500; 401/403/429 stay PLAIN so they remain account errors. The server-lookup
  path (apiGet/getUploadServer) is hardened symmetrically: a 5xx there tags
  transientNetwork and getUploadServer preserves the flag onto its wrapped error, so
  the heuristic shouldRetryServerLookup() can no longer be defeated by a 5xx body that
  contains an auth keyword.
- hosters: fixed a separate real bug surfaced during investigation — _fetchByseFileList
  built the wrong URL https://api.byse.sx/api/file/list (the /api/ prefix is correct
  only for doodstream's doodapi.co host; byse already carries the api. subdomain).
  Live-probed: /api/file/list 302-redirects to the docs page, /file/list returns 200
  JSON. The wrong path made the byse async-recovery poll silently dead (always []),
  removing the safety net that reclaims a large file that registered despite a 502.
  Now https://api.byse.sx/file/list.

ECONNRESET was already transient and is unchanged. doodstream's 2xx empty-form stays
hosterTransient (one attempt, no re-upload). vidmoly/clouddrop use their own uploaders
and are unaffected; doodstream/voe apiKey uploads share uploadFile and correctly
benefit from the same 5xx-is-infra logic.

Deferred follow-up: poll-first-on-5xx dedup (route a 5xx through the now-working
recovery poll before retrying, to reclaim a registered-but-502 file instead of
re-uploading). It does not add new dupe risk vs the prior cascade and a pure 502
means the backend was never reached, so retry-same-account is dupe-safe for it.

Investigated and reviewed by two multi-agent workflows (4-lens investigation with a
synthesized fix spec; 4-lens adversarial review with per-finding verification): no
API drift, 0 confirmed defects. Tests: classifier units (flag-above-message-guard,
5xx-transient, 4xx-stay-account), an end-to-end 502/503/500-envelope tag through
uploadFile, and a 502-retries-same-account-no-cascade integration case. Suite 311/311.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 01:56:11 +02:00
Administrator
6df00a0948 revert(network): remove network-outage auto-pause entirely — false positives froze batches 2026-06-10 16:52:37 +02:00
Administrator
34aaa36571 feat(unattended): network outage auto-pause/resume, post-batch auto-retry rounds, webhook notifications 2026-06-09 20:39:59 +02:00
Administrator
d59c5c1df8 perf: per-batch baseline cache, async folder walk, history-table fast path, progress IPC batching 2026-06-07 21:11:04 +02:00
Administrator
125e5f55ea fix(perf): kill per-progress renderer-to-main IPC + drop redundant queued emit + cache fileSize 2026-06-07 20:59:07 +02:00
Administrator
166b04c526 fix(upload): classify doodstream empty-form as hoster-transient (don't kill account)
The "kein Filecode — Server gab leeren Link zurueck" error was treated as a
generic upload failure → after retries exhausted, the manager called mark-failed
and added the account to _failedAccounts → next batch re-primed with
primedFailed=1 → pre-job-swap-blocked because no fallback override exists for a
single-account hoster. One server-side flake permanently poisoned the session.

It's not an account problem — same account + same file works on a later try.
This is a doodstream-backend processing flake (empty CDN form, no fn / no st),
the same class as a transient network error: don't blacklist, just fail this
file cleanly.

- doodstream-upload.js: tag the empty-form throw with err.hosterTransient=true
  (explicit flag, primary signal — matches the err.accountError / err.fileRejected
  pattern already used elsewhere).
- upload-manager.js: new _isHosterTransientError classifier (flag first, message
  regex as defensive fallback). In the retry loop: break on first hit (server
  flake won't clear in 3 s, re-uploading the file 4× is pure bandwidth waste).
  Post-loop: dedicated branch that emits the final error WITHOUT blacklisting
  the account — same shape as the existing transient-network branch.
- Tests: classifier unit tests (flag path, regex path, negatives) + regression
  test that proves the account is NOT added to _failedAccounts and mark-failed
  does NOT fire. Drops the hoster-transient test from ~19 s to ~1.5 ms,
  confirming the in-loop fast-break works.

We now fail fast on this error class instead of retrying — the next-batch
manual retry is the recovery path, and the account stays usable for it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 20:34:56 +02:00
Administrator
3553666d9d perf(rotation): rotate after 1 fail on generic errors, not after 5
Before: a non-transient / non-file-rejected / non-account-specific
error (e.g. "VOE Upload: <any generic message>") would burn the full
retries-per-account budget on the primary before the rotation logic
even kicked in. On retries=5 that's "Retry 2/5 Primär", "Retry 3/5
Primär", … all on the same broken account before the fallback gets
a shot.

Now:
- main.js pre-resolves the next fallback for every hoster at batch-
  start (stored in _accountOverrides via the existing session cache +
  primeOverrides path). Pre-job-swap still ignores it until the
  primary is actually marked failed, so jobs still begin on primary.
- upload-manager.js: in the retry loop's generic error branch,
  _hasPendingOverride() checks whether a usable fallback is ready.
  If yes and the error is NOT transient (transient = network glitch
  = retry same acc), break out to rotation. Marks primary failed,
  rotates to acc2, retries there.
- Result: for a 2-account hoster, worst case is 1 attempt on primary
  + retries-per-account on fallback, instead of N × 2. Transient
  network errors (ENOTFOUND / ECONNRESET / socket hang up) keep the
  old "retry same account" semantics because the network is the
  issue, not the account.
- Single-account hosters: unchanged. No pending override = classic
  retry-on-same-account until exhausted.

3 new tests pin: generic + override → rotate on attempt 1; transient
+ override → stay on same acc; no override → classic retry. 87/87
green.
2026-04-22 18:23:30 +02:00
Administrator
05e6d654c4 fix(rotation): retry after batch-done reuses learned fallback state
Previously: clicking "Erneut versuchen" after a batch had already
finished spawned a fresh UploadManager with empty _failedAccounts and
_accountOverrides. The first retry then burned the full retry budget
on the account we already knew was dead (e.g. disk-space-full byse
account) before rotation kicked in again — same problem we fixed for
within-batch flow but for across-batch flow.

- main.js: two module-level maps (_sessionFailedAccounts,
  _sessionAccountOverrides) cache rotation state across batches in the
  same app session. Populated on account-failed and on both
  switchAccount paths (event-driven + save-config re-resolve).

- lib/upload-manager.js: startBatch(tasks, opts) accepts
  primeFailedAccounts + primeOverrides. State is still cleared first
  (legacy behaviour for callers without opts), then re-primed from the
  passed session state. batch-start rot-log entry reports how many
  entries were primed for diagnostics.

- Tests: prime priority is honored (pre-job-swap fires on first
  attempt, no fast-fail, no upload to acc1); back-compat for callers
  that don't pass opts.

App restart remains the reset signal — matches the "neuer Tag, acc1
hat vielleicht wieder Platz" expectation.
2026-04-22 17:56:59 +02:00
Administrator
d49fe136f2 fix+obs: byse poller race-condition + transient-net tests + memory logging
Three small, unrelated reliability improvements bundled:

1. lib/hosters.js (_resolveByseUploadByName): drop the "only one new
   file → claim it" fallback. Under parallel byse uploads, job A's
   poller could claim job B's newly-uploaded file and return the wrong
   URL. Now requires exact normalized name match. Trade-off: a few
   false negatives if byse rewrites the filename beyond our
   normalizer, but parallel correctness wins.

2. tests/upload-manager.test.js: pin the transient-network classifier
   behaviour with 2 new tests covering common transient strings
   (ENOTFOUND, ECONNRESET, socket hang up, fetch failed, EAI_AGAIN…)
   and verifying real account-level / file-rejected errors are NOT
   misclassified as transient. Baseline stays clean: 82/82 green.

3. main.js: log process.memoryUsage() snapshot at batch-start and
   batch-done. One line each — harmless in the happy path, gives us
   the data points needed to spot long-session RSS/heap growth across
   batches without DevTools instrumentation.
2026-04-21 19:42:54 +02:00
Administrator
bf806cb069 fix(rotation): session-learning for account failures is now complete
Three related gaps closed so one full byse account stops wasting
attempts on every subsequent job and later-added accounts get picked
up without an app restart.

1. Pre-job-swap moved BEHIND the semaphore acquire. At scale (500 jobs
   / 1 slot) every worker was checking _failedAccounts at spawn time
   before the first upload had even tried — so none of them saw the
   failed state. Now each worker re-checks right before its first
   upload attempt.

2. save-config IPC handler re-resolves fallbacks for any account that
   is already in _failedAccounts but has no override set. Previously
   account-failed only fired once per account, so a config change
   after the first mark-failed was silently ignored and the batch
   stayed stuck on the dead account until the app restarted.

3. UploadManager exposes getFailedAccountKeys() and getOverride(hoster)
   so main.js can drive the late re-resolve without poking private
   fields.

4 new tests: pre-job-swap after semaphore, getters contract, fresh
manager resets learned state, late-added fallback is honored by
subsequent jobs. 80/80 green.
2026-04-21 17:03:59 +02:00
Administrator
17e9a419b2 fix(rotation): treat byse "disk space" as account-level, not file-rejected
Byse rejects uploads with status like "not enough disk space on your
account" when the account's storage is exhausted. The parser was
flagging every non-OK status as err.fileRejected=true, and the upload-
manager classifier additionally matched the generic "lehnte Datei ab"
prefix as file-rejected. Result: rotation was skipped on a full account
and every subsequent file failed on the same dead account.

- hosters.js: byse parser now distinguishes account-level phrases
  (disk space / storage / quota / insufficient / account full) and sets
  err.accountError=true for those. File-specific failures (Duplicate,
  wrong format, size) keep err.fileRejected=true.
- upload-manager.js: _isFileRejectedError no longer matches the generic
  "lehnte Datei ab" prefix and short-circuits when err.accountError is
  true. _shouldSkipRetryOnAccountError honors the flag and has added
  regex patterns as a safety net.
- Tests: 5 new unit tests covering disk-space/account-level/duplicate
  and the accountError-wins-over-fileRejected precedence.
2026-04-21 16:42:56 +02:00
Administrator
31d157b695 test: 3 new tests for addJobs (74/74 pass)
- addJobs injects new tasks into running batch (verified concurrent execution)
- addJobs rejects duplicate jobIds already in batch
- addJobs returns added=0 when not running

These tests verify the fix in v2.6.3 (files added during upload now
get injected into the running batch via addJobsToBatch).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 22:56:26 +02:00
Administrator
8b68a7a07e fix: prevent retry jobs from getting stuck in waiting state 2026-03-26 10:17:15 +01:00
Administrator
55d6892963 test: 4 stress tests verify critical fixes (70/70 pass)
- file-not-found produces 'nicht gefunden' (not '0 Bytes')
- zero-byte file produces '0 Bytes' error
- empty batch completes immediately with zero counts
- scaleParallelUploads correctly caps per-hoster concurrency

All 70 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 11:11:13 +01:00
Administrator
f59539e85b feat: improve account-driven uploads 2026-03-11 02:41:32 +01:00
Administrator
9c56fabce1 fix: critical updater and retry bugs, cleanup listener leaks
- updater: replace undici.request() with fetch() (fixes maxRedirections
  error that blocked auto-update from v1.0.0 to v1.1.0)
- upload-manager: move signalCleanup declaration outside try block
  (was causing ReferenceError in catch, silently breaking ALL retries)
- upload-manager: _combineSignals now returns cleanup fn to prevent
  abort listener accumulation over batch lifetime
- upload-manager: _sleep removes abort listener on normal timer fire
- hosters: apiGet removes abort listener in finally block

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 12:30:06 +01:00
Administrator
61681de9a3 test: add unit tests (41) and UI smoke tests (21), fix semaphore listener leak
- 12 Semaphore tests: FIFO ordering, abort support, limit updates, listener cleanup
- 8 Throttle tests: rate limiting, abort signal, concurrent consume, updateRate
- 9 ConfigStore tests: defaults, merge, round-trip, corruption fallback, history cap
- 12 UploadManager tests: progress events, retry, cancel, size filter, concurrency
- 21 UI smoke tests: tab navigation, settings panels, statusbar, context menu
- Fix: Semaphore.release() and updateLimit() now properly remove abort listeners

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 10:55:50 +01:00