Commit Graph

58 Commits

Author SHA1 Message Date
Administrator
53c3448836 feat(diag): comprehensive high-concurrency measurement build
User asked to measure everything so we can pinpoint the lag/ECONNRESET at 70 concurrent uploads. All additive instrumentation, no upload-behavior change.

main process: the eventloop-delay log line now also reports cpu=X%core (process.cpuUsage delta over wall time; >100% means multiple cores), rss, per-hoster live connection distribution (active-by-hoster), cumulative transient (ECONNRESET-class) error count, and pending count — pulled from a new UploadManager.getDiagnostics(). This shows whether the main thread is CPU-bound and which hoster is oversubscribed.

renderer: a PerformanceObserver('longtask') plus a requestAnimationFrame frame-time monitor log 'renderer-perf' every 5s while uploading — fps, jankFrames (>33ms), worstFrame, longtask count and max. This is the direct renderer ground truth (real frame rate under load), which the component-timing harness could not capture. Low fps / high jank => renderer is blocked; ~60fps while it still feels laggy => the cpu/eld numbers in the same log decide CPU-bound vs IO-bound in the main process.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 16:23:43 +02:00
Administrator
c7e884fdec perf(upload-manager): throttle progress emits on rotation/suspect paths
The rotation-retry and suspect-alternate progressCb callbacks called _emitProgress (a synchronous EventEmitter emit plus a fresh object spread) on every stream chunk — hundreds per second per job — because they lacked the 250 ms lastEmitTime gate the primary upload path already has. With many concurrent uploads in rotation or suspect mode that is real main-thread emit amplification.

Mirror the primary path's gate exactly: the activeEntry speed/bytes mutation stays ungated so the stats timer and speed monitor keep seeing fresh values; only the _emitProgress call is throttled to ~4/sec. Behavior-preserving — identical progress data, fewer emits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 03:44:37 +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
7a04060e9b feat(rotation): per-hoster toggle to disable the "Bekanntes Größen-Limit" pre-skip
The byse suspect size-memo (softened in v3.3.73 to arm only after two
confirmed suspect rejections on the same account and only for strictly
larger files) still occasionally pre-skips a primary account with
"Bekanntes Größen-Limit auf diesem Account", which the user finds annoying
when they would rather just let every file attempt the upload for real.

Add a per-hoster opt-out: a new "Größen-Limit merken" checkbox (Accounts tab,
default ON so existing behavior is preserved for everyone) that, when
unchecked, disables the memo-based pre-skip entirely for that hoster.

- config-store.js / upload-manager.js DEFAULT_SETTINGS: sizeMemoEnabled: true.
- upload-manager.js _suspectMemoBlocks(): returns false up front when the
  hoster has sizeMemoEnabled === false. That single guard covers BOTH consult
  sites — the pre-job block (skips the guaranteed-fail upload) and the
  alternate-account skip — since both route through _suspectMemoBlocks. The
  memo is still recorded; it is simply never allowed to block. This moves the
  flow toward "fail open": with the toggle off, every file always gets a real
  attempt on its account.
- The genuine suspect-reject alternates walk is gated on err.suspectReject,
  not the memo, so disabling the memo does not weaken real failover — a file
  the account actually rejects still rolls over to the next account.
- renderer/app.js: checkbox data-hs="sizeMemoEnabled", checked when
  hs.sizeMemoEnabled !== false; persisted by the existing generic checkbox
  path in saveHosterSettingsFromDom(). The running manager picks it up on the
  next batch (fresh UploadManager) and immediately on save (updateSettings).

Tests: suspect-reject-alternates gains a disabled-memo case proving the larger
third file still gets a real primary attempt (key1 tried 3x, no
suspect-memo-skip event); config-store gains a default-true + persist-false
case. Full suite 305/305. Reviewed by a 4-lens adversarial workflow
(completeness / composition / persistence-ui / conventions): 0 findings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:41:38 +02:00
Administrator
87f976faf0 fix(rotation): soften the byse suspect size-memo so one blip cannot poison a batch
The per-batch size memo previously armed on a SINGLE confirmed suspect rejection
and blocked every same-or-larger file on that account (fileSize >= memoSize).
A single spurious byse "Not video file format" at e.g. 1.3GB therefore pre-failed
the whole series of ~1.3GB files in that batch with "Bekanntes Größen-Limit auf
diesem Account", even when byse could actually take that size.

Now the memo:
- arms only after the 2nd confirmed rejection on the same account (count >= 2),
  so a lone byse aussetzer no longer short-circuits anything; and
- blocks only STRICTLY LARGER files (fileSize > memoSize), so a same-size file
  always still gets one real attempt.

_suspectSizeMemo now stores { size: smallest-rejected, count } instead of a bare
number. Still per-batch (cleared at batch start, never re-primed across batches).
Updated the two memo tests to the new semantics (added a size-aware statSync mock
so a strictly-larger file can be exercised). Full suite 284 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 00:44:03 +02:00
Administrator
497317dc32 fix(rotation): suspect byse rejections now reach every account instead of dying on the first
Root cause of "4 accounts configured, rotation only ever uses 1-3": byse
returns HTTP 200 + per-file status "Not video file format" for valid MKVs
above an account's size tier (proven from the live rotation log: account
4ha0 accepted 1158 files up to 2.62 GB and deterministically rejected all
6 files >= 2.81 GB with that status, each costing a full ~20 min upload).
The parser tagged this fileRejected, which (a) skipped the 30s recovery
poll that was originally BUILT for this misleading status (guard regression
from "byse skips 30s recovery poll on explicit reject") and (b) made the
upload manager skip rotation entirely - so the remaining accounts were
never tried and the files failed forever. Account selection is
primary-first failover, so a later account only ever runs when every
earlier one is hard-failed or disabled, which matches the reported
"account 4 only gets used when I disable the others".

The fix, layered:
- parseByseResult flags "Not video file format" as err.suspectReject
  (alongside fileRejected). Genuine rejections (Duplicate, too small,
  account-level disk-full) behave exactly as before.
- uploadFile runs the byse recovery poll again for suspect rejections
  (the file often registers asynchronously despite the status), but skips
  it when the caller's file probe says the upload is genuinely not a video
  (new opts.probeIsVideoLike, threaded from the upload manager's probe).
  Poll helpers now swallow an abort during their sleep and return null so
  a cancelled poll surfaces the original error instead of a bare
  AbortError.
- upload-manager: on a suspect rejection of a probe-verified video, the
  file gets ONE attempt on each remaining pool account (new accountPools
  from main.js, refreshed on save-config) - WITHOUT blacklisting the
  rejecting account, which keeps working for smaller files. A per-batch
  size memo (hoster:account -> smallest rejected size) plus a known-good
  account preference prevent re-uploading every following oversized file
  through the whole chain: the second file skips known-limited accounts
  outright and goes straight to the account that took the first one.
  Alternates that fail with a genuine account error (quota/ban/full) are
  marked failed in-batch so no later file burns a multi-GB upload on them;
  deliberately no account-failed emit there, as repointing the hoster-wide
  override would reroute normal-sized files away from a healthy primary.
- Rotation hardening from the adversarial review: the rotated-to retry
  loop now fast-breaks on file-rejected/hoster-transient errors instead of
  burning maxAttempts full uploads, and a file-class error on a rotated
  account no longer blacklists that account (it fails only the file).
  Cancelling mid-alternates or mid-rotation now records the job as
  aborted instead of error (this also stops the batch webhook from firing
  on fully user-cancelled batches). New upload-failure rot-logs are
  guarded against logging user aborts as failures; rotation retries are
  now visible in the rotation log at all (previously failures on a
  rotated-to account were never logged, which is why account 2's death
  was invisible in the support log).
- file-probe: the mpeg-ts signature required only a single leading 0x47
  byte, so GIFs and "G"-prefixed text classified as video and would have
  qualified junk for pool-wide alternate uploads; it now requires TS
  sync-byte periodicity (0x47 at offsets 0/188/376) and the probe head
  read grew from 64 to 512 bytes to make that check possible. GIF gets
  its own non-video signature.

Tests: 9 new/reworked across suspect-reject-alternates.test.js (alternate
walk, no blacklist, memo short-circuit, account-error marking, abort
labeling, probe gating) and byse-reject-recovery.test.js (suspect polls +
recovers, empty poll throws suspectReject, Duplicate still fast-fails,
probe-confirmed non-video skips the poll) plus a TS/GIF probe regression
test. Full suite 276 green.
2026-06-15 01:11:37 +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
6cd7498f70 fix(critical): safeSend infinite recursion + queueMicrotask, plus 6 audit findings 2026-06-08 22:03:19 +02:00
Administrator
0f57aef7c7 fix(stability): wrap hot timers/callbacks in try/catch, safeSend, updater waits for batch 2026-06-08 21:28:12 +02:00
Administrator
9b10a4356f feat(diagnostics): full crash instrumentation — never silently die again 2026-06-08 21:18:54 +02:00
Administrator
d9199f8aaf fix(perf): chunked startBatch + async rotLog — kill remaining 30s freeze on 5k+ jobs 2026-06-08 01:29:31 +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
d280765feb fix(perf): freeze on Start with 2000+ jobs — gate probe + rot-log behind semaphore 2026-06-07 20:40:55 +02:00
Administrator
cf35f4401d feat(ui): per-hoster success rate, session-paused badge, post-batch retry, link export formats 2026-06-07 20:32:35 +02:00
Administrator
5fb313273d feat(diagnostics): file-format probe + structured upload-start/failure rot-log 2026-06-07 18:49:54 +02:00
Administrator
84c48ad7d6 fix(doodstream): login path auto-derives the API key → uploads via reliable API
The user uploads with username/password (login), so 3.3.30's "use API when a
key is configured" did nothing for them — and the web-form upload keeps failing
with empty forms on large files. Fix the LOGIN path itself: after logging in,
pull the account's API key out of the logged-in session and upload via the
official doodapi API (which returns result[0].filecode directly, no empty form).
The user keeps using login and configures nothing.

How the key is derived without knowing doodstream's (cookie-gated, unseen)
settings DOM: brute-force candidate extraction + API validation.
- DoodstreamUploader.deriveApiKey(): fetch the logged-in settings page
  (?op=my_account / /settings), pull every plausible long token from form-field
  values + element contents (ranked: tokens near an "api" mention first), and
  validate each against doodapi.co/api/account/info — only the account's real
  key returns status 200. A wrong guess is therefore harmless (fails validation
  → web fallback). Logs the raw settings HTML when nothing validates, so the
  scrape can be refined from a real capture if doodstream's markup differs.
- upload-manager: doodstream login-path now resolves the key ONCE per batch
  (cached by accountId; '' = tried-none) and routes to the API when found, else
  the existing web-form upload. Keyless accounts: one extra probe-login per
  batch, then unchanged.
- Tests: candidate extraction (value/textarea/api_key shapes, api-context
  ranking), validate-then-pick, null→web-fallback, preset short-circuit. 178/178.

If derivation works the login path now uploads via the API. It does NOT change
doodstream's backend; the server run confirms. Falls back safely if no key.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 22:05:20 +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
10ae46c44d fix(upload): re-check cancellation after _sleep in rotation while-loop
The account-rotation while-loop entered with a signal.aborted /
stopAfterActive check (line 681) but then awaited _sleep(800) on
line 690 (waiting for main.js to resolve the next fallback) without
re-checking on the way out. If the user cancelled during that 800 ms
window the loop kept going — resolved the override, set up new
credentials, fired retrying-event, started a fresh attempt loop —
before _executeUpload's own signal handling finally noticed the
abort. Cancellation latency could therefore stretch by an extra
attempt's worth of work per still-spinning hoster.

One-line fix: add the same `if (signal.aborted || this.stopAfterActive)
break` after the await. Found by deep-audit MED-5.

126/126 tests still green; the fix is a guard on an already-tested
flow, no test infrastructure exists for cancel-during-rotation
specifically (would need fake-timer + mocked override-resolution).
2026-04-28 10:39:09 +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
b96ccf851a feat(ui): per-job log modal + account label in status
Two related visibility improvements.

1. Status cell now shows which account the job is running on:
   "Upload · Primär", "Retry 2/3 · Fallback #1: <error>", etc.
   - _emitProgress passes task.accountId in every progress event
   - renderer maps accountId → position in config.hosters[hoster] and
     renders "Primär" for index 0 and "Fallback #N" for the rest
   - Applies to uploading/getting-server/retrying (static states like
     done/error already tell their own story)

2. Right-click on a job → "Log anzeigen" opens a modal with the full
   per-job trail: every rot-log entry tagged with that job's jobId
   plus every non-uploading progress transition. Replaces the need to
   grep account-rotation.log for a single filename.
   - UploadManager: all 13 job-scoped _rotLog calls now carry jobId
   - main.js: _jobLogCollector Map<jobId, Array<entry>> with 200-entry
     ring buffer per job; cleared on each new start-upload (fresh
     batch = fresh log). addJobs mid-batch keeps history.
   - New IPC 'get-job-log' returns the array; preload.js exposes
     window.api.getJobLog(jobId)
   - renderer: modal card + context-menu item "Log anzeigen";
     entries formatted as "[HH:MM:SS.mmm] [event] k=v k=v"; copy-to-
     clipboard button
2026-04-22 18:13:53 +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
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
7ed227a76e fix(byse/rotation): surface per-file rejection, skip retries and rotation
The log revealed byse's true response shape for rejected files:
  { msg: 'OK', status: 200, files: [{ filecode: '', status: 'Not video file format' }] }

HTTP 200 + msg=OK made the old code treat it as 'success but no
file_code'. The real error ('Not video file format') was buried in
files[0].status. parseByseResult now surfaces that with a dedicated
err.fileRejected flag so the rotation layer can distinguish
file-specific vs account-specific failures.

Rotation behavior:
  - file-rejected errors: no retries, no account blacklist, no
    rotation. The same file is going to get the same verdict on
    any account, so skip straight to 'error' status and keep the
    account available for other files in the batch.
  - network errors (already handled): no account blacklist either.
  - everything else: unchanged (retry then rotate).

Also added pattern matches for common rejections (Duplicate, File
too small/large, Unsupported format, etc.) so other hosters'
per-file errors get the same treatment.
2026-04-20 16:06:09 +02:00
Administrator
0ea92ad6d0 fix(rotation): transient network errors don't blacklist the account + clearer byse 'OK' error
Two bugs visible in the user's rotation log:

  1. 'error=OK' for byse.sx — the server returned a payload with
     msg='OK' and no file_code anywhere we recognized. Our generic
     uploadFile threw the bare 'OK' as the error message, which is
     useless and misleading. Now when we see an ok-ish msg without
     the expected file_code we throw a descriptive error that
     includes the first ~400 bytes of the payload so the next time
     it happens we can see what's actually being returned (API
     changed, new field name, etc.).

  2. 'getaddrinfo ENOTFOUND s1055.filemoon' was marking accounts as
     permanently failed, blacklisting BOTH byse accounts within the
     same batch even though neither was the actual problem — filemoon
     (byse's storage backend) briefly had a DNS blip. Added
     _isTransientNetworkError() covering DNS/ECONNRESET/ETIMEDOUT/etc.
     When all retries on an account exhaust with a transient error,
     we now fail just that file and emit 'skip-rotation-transient'
     instead of adding the account to _failedAccounts. Other files
     in the same batch still get a fresh try on the same account.
2026-04-20 15:56:44 +02:00
Administrator
63f87a0310 fix(rotation): concurrent jobs now reuse the override instead of failing
When multiple jobs run in parallel on the same hoster and the primary
account starts failing, the first job marks it failed + triggers
rotation. The second job's retries then also exhaust on the same
(already-failed) primary — but the old while-condition
`!_failedAccounts.has(...)` short-circuited the whole rotation loop
for anything already marked, so the second job went straight to
final-error even though a resolved override was sitting right there.

Now the loop always checks for an available override; it only skips
the mark-failed + emit step if the account was already marked by a
concurrent job. Fixed visible symptom: first job rotates A→B, every
other job in the same batch that hit A got final-error instead of
also switching to B.

Also extended fast-fail patterns to include 429 (Too many requests),
CSRF-Token / 'Bist du eingeloggt' — both were showing up as the
primary failure mode in real uploads and were wasting 5 retries
each.
2026-04-19 23:13:25 +02:00
Administrator
655fb6230b feat(rotation): fast-fail on account-specific errors + open-log-folder button + sync rot-log flush
Three related improvements that landed together while wiring up the
rotation log infrastructure:

  - Fast-fail classifier: errors that clearly indicate the account
    itself is the problem (rate limit, quota, banned/suspended, auth
    failure, 401/403, 'Kein Upload-Server' from delivery-node etc.)
    now skip the remaining retries and go straight to rotation. No
    more waiting 5 × 3s between retries just to end up rotating
    anyway. Emits a 'fast-fail' rot-log event so the shortcut is
    visible.

  - Settings: 'Öffnen' button next to the log-file-path input reveals
    the active log file (or its directory if nothing's written yet)
    in the OS file manager, so users don't have to remember paths.

  - rotLog() writes the rotation log synchronously. Only a handful
    of events fire per batch; the 500ms flush batching was saving
    nothing and made the file look empty when users checked right
    after an event. (The main debug log still uses the batched async
    path — that one is high-volume.)
2026-04-19 23:04:20 +02:00
Administrator
126b1e569a feat(account-rotation): dedicated logging + live toast notifications
To trace whether the fallback chain actually engages during real uploads,
every rotation decision now emits a structured 'rot-log' event from the
upload-manager. main.js persists each event to a new account-rotation.log
(same directory as fileuploader.log; falls back to Desktop then userData)
and also mirrors it into the main debug log with a [ROT] prefix for
single-file grepping.

Logged events:
  - batch-start (clears _failedAccounts / _accountOverrides)
  - pre-job-swap / pre-job-swap-blocked (job picks override before first try)
  - retries-exhausted / mark-failed (enters rotation loop)
  - rotate (switched to new account, retry starting)
  - rotation-end (no override / override already failed)
  - final-error (all accounts exhausted)
  - switchAccount (main resolved the next fallback)

The renderer shows a toast on 'rotate', 'rotation-end' and 'final-error'
so fallback behavior is visible live instead of buried in logs.
2026-04-19 22:57:19 +02:00
Administrator
880537dcfb fix: multi-level account rotation + clear failed-accounts per batch + size-sort staleness
Three state bugs found during audit:

  1. _failedAccounts / _accountOverrides survived across batches. A
     rate-limited account from batch 1 stayed permanently blacklisted
     for the rest of the app session, so batch 2 skipped straight to
     the fallback even after the original recovered. Now cleared in
     startBatch so each run evaluates accounts fresh.

  2. Account rotation was one level deep. With three accounts [A,B,C]
     on the same hoster and A + B both failing, the job errored out
     — C was never tried. The fallback-retry was a single if-block.
     Replaced with a while-loop that keeps asking main for the next
     override and rotating until every account is exhausted.

  3. Queue sort cache included 'size' as a static key, but bytesTotal
     goes 0 → actual when previews resolve. A queue sorted by size
     during preview would cache the all-zeros order and never update.
     Removed size from _STATIC_SORT_KEYS — it now re-sorts per render
     like status/speed/progress.
2026-04-19 22:01:20 +02:00
Administrator
2d8b3f1bf9 perf: final sweep — hot-path allocation, cached log target, sort-header skip
Last round of targeted wins:

  - upload-manager progress callback was allocating a fresh
    { jobId, speedKbs, bytesUploaded } object on every fs stream chunk
    (hundreds of times per second per active job). Now a single entry
    is created at job start and mutated in place — zero allocations
    on the steady-state progress tick.

  - upload-manager stats timer's two separate activeJobs.values()
    scans (globalSpeedKbs + inProgressBytes) merged into one pass.

  - clouddrop-upload.js reuses a single Buffer.allocUnsafe(chunkSize)
    across all chunks, taking subarray() only for the tail chunk.
    A 1 GB upload no longer allocates 64× 16 MB = 1 GB of short-lived
    buffers — real GC relief during many-file batches.

  - _resolveUploadLogTarget is now cached; the fallback ladder runs
    once per session (or when the user changes the log path / daily-log
    date rolls), not on every 500ms flush.

  - renderRecentUploadsPanel skips updateRecentSortHeaders on the
    append-only fast path — sort state hasn't changed, headers don't
    need recomputing.
2026-04-19 14:02:34 +02:00
Administrator
1164da37ea feat: add Clouddrop.cc as upload hoster (API key auth, chunked uploads)
- New lib/clouddrop-upload.js with chunked upload support (16 MB chunks)
- Auth via Bearer token (cd_XXX format)
- Files < 16 MB: simple multipart POST /api/cloud/upload
- Files > 16 MB: chunked protocol (init → PUT chunks → complete)
- After upload: auto-creates permanent share link via /api/cloud/share-link
- Health check verifies API key by listing root files

Registered in:
- lib/config-store.js (HOSTER_NAMES, templates, DEFAULTS)
- main.js (hosterAccountHasCreds, checkClouddropHealth, runHosterHealthCheck)
- lib/upload-manager.js (_executeUpload dispatch)
- renderer/app.js (HOSTERS, HOSTER_ADD_OPTIONS, getHosterLabel)

Tests: 74/74 pass. ESLint: 0/0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 06:55:21 +02:00
Administrator
8b68a7a07e fix: prevent retry jobs from getting stuck in waiting state 2026-03-26 10:17:15 +01:00
Administrator
a5b07c0f73 🐛 fix: 'Ausgewählte starten' on queued jobs now force-adds to batch
Previously, clicking 'Ausgewählte starten' on 'Wartet' jobs during an
active upload just showed a toast. But the jobs might NOT actually be
in the batch (skipped during task building).

Now: ALL selected queued/error/aborted jobs are sent to addJobsToBatch.
The upload-manager has duplicate protection (checks jobAbortControllers)
so jobs already in the batch are skipped. Jobs NOT in the batch get
added and start uploading immediately.

Toast now shows exact counts: "X hinzugefügt, Y waren schon im Batch"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 00:14:53 +01:00
Administrator
ff6f7f8612 🐛 fix: addJobs tracks promises so batch-done waits for them
Previously addJobs() was fire-and-forget — added jobs ran as orphaned
promises. When the original batch completed, batch-done fired and
uploadManager was set to null while added jobs were still running.

Now: added job promises are tracked in _additionalPromises array.
startBatch drains this array after the original tasks complete,
ensuring batch-done only fires when ALL jobs (original + added) finish.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 18:19:59 +01:00
Administrator
e1b03605fa feat: retry/start selected jobs while upload batch is running
Previously, 'Erneut versuchen' and 'Ausgewählte starten' did nothing
when a batch was already running (uploading=true). Failed jobs were
set to 'Wartet' but never actually uploaded because they couldn't be
added to the running batch.

New: upload-manager.addJobs() allows adding tasks to a running batch.
When a batch is active and user retries/starts jobs, they're injected
into the running batch via IPC 'add-jobs-to-batch'. The upload manager
starts processing them immediately using the existing semaphores.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 18:15:31 +01:00
Administrator
d538c7da4f 🐛 fix: account fallback now works for ALL files in batch, not just first
When Account A failed, only the first file got the fallback to Account B.
All subsequent files in the same batch still tried Account A (wasting
all retries), then skipped fallback because _failedAccounts already
had the key.

Now: before the retry loop, each job checks if its account is already
known-failed and immediately switches to the fallback account, avoiding
wasted retries on a known-bad account.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 08:03:44 +01:00
Administrator
cd07f52916 🐛 fix: distinguish 'file not found' from 'file empty' error message
Previously, both missing files (fs.statSync throws) and 0-byte files
produced the same error "Datei ist leer (0 Bytes)". Now:
- Missing files: "Datei nicht gefunden"
- Empty files: "Datei ist leer (0 Bytes)"

Also adds 3 edge case tests (throttle consume(0), unlimited rate,
semaphore release-without-acquire). All 66 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 15:20:37 +01:00
Administrator
a4a2eaa736 🐛 fix: scaleParallelUploads inverted, settings lost on close, IPC leak
- scaleParallelUploads used Math.max instead of Math.min, causing MORE
  concurrent uploads instead of limiting them to the global count
- Settings debounce (350ms) was not flushed on app close — user changes
  made right before closing were lost
- onRemoteClientCount IPC listener was re-registered on every
  renderSettings() call, causing listener accumulation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 14:24:14 +01:00
Administrator
27905d66de 🐛 fix: shutdown countdown ignores mode change, timer leaks
Critical: handleShutdownAfterFinish() captured shutdown mode in a
closure at scheduling time — changing mode during countdown was ignored,
causing unexpected system shutdown/restart/sleep.

Now reads shutdownMode at execution time, clears timer when mode
changes to 'nothing', clears orphaned timers before creating new ones,
and adds error handling on exec() calls.

Also: guard stats timer against double-start in upload-manager.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 13:19:07 +01:00
Administrator
61e458b8ea 🐛 fix: skip 0-byte files, fix drag-drop highlight flicker
- Upload manager now rejects empty files (0 bytes) with clear error
  message instead of sending useless uploads to the server
- Fix drag-drop zone highlight flickering caused by dragleave firing
  on child elements (classic browser bug, fixed with enter/leave counter)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 11:46:04 +01:00
Administrator
d53eea443e feat: multi-account support with primary/fallback and separate API/login types
- Multiple accounts per hoster with drag-sortable priority (primary + fallbacks)
- Separate account types: Web Login and API selectable per hoster
- Account fallback: after all retries fail, automatically switches to next fallback account
- Fix: Byse health check returning [Fehler] OK when API responds with msg "OK"
- Fix: retry during active upload sets status to "Wartet" instead of "Bereit"
- Config migration from single-object to multi-account array format

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 05:00:33 +01:00
Administrator
6b2b2ca04c perf: major rendering optimization for large concurrent uploads
- Throttle progress events to 250ms intervals (was every byte chunk)
- Batch UI updates during uploads (render/statusbar/stats every 200ms)
- In-place row updates instead of full innerHTML table rebuild
- Single-pass queue stats computation (was 9 separate array filters)
- Remove CSS transition on progress bars (caused layout thrashing)
- Event delegation for recent files table (was per-row listener rebind)
- Increase persist debounce to 10s during uploads (was 3s)
- Remove redundant "Ziele auswählen" button (hoster selection on file add)
- Dark title bar via nativeTheme

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 00:18:43 +01:00
Administrator
399e2fbe70 feat: upload progress display, semaphore fix, context menu polish
- Status bar shows uploaded/total bytes (e.g. "16 GB / 281 GB")
  Total is sum of all queue jobs (100GB x 4 hosters = 400GB)
- Fix semaphore acquisition order: hoster-first then global prevents
  jobs waiting on a hoster slot from wasting global semaphore slots,
  significantly increasing active connection utilization
- Context menu: dynamic count on all labels, singular/plural for
  single selection, user-adjusted grouping with separators

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 20:25:13 +01:00
Administrator
25a6b77650 fix: multiple bugs found in deep code analysis
- Guard startBatch against null uploadManager in nextTick (race on fast cancel)
- Fix updateSettings not creating globalThrottle when none existed at start
- Fix updateSettings not updating globalSemaphore limit live
- Fix retry pause: 2500ms → 3000ms as intended
- Remove dead isError code in history (was always false after continue)
- Add signal.aborted check in API upload generator (hosters.js)
- Add extra signal check in throttle consume loop for faster abort
- Fix doodstream debug log path (process.cwd → __dirname)
- Fix updater fetchJson signal listener leak
- Make progress column sortable in queue table

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 04:16:50 +01:00
Administrator
1af7f8a94d fix: serialize interval waits so uploads stagger correctly
Previously all jobs read the same lastStartTime simultaneously,
causing them all to start at once. Now uses a per-hoster promise
chain to ensure each job waits its turn.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 03:56:04 +01:00
Administrator
87833b5808 feat: sortable recent files, start selected context menu, live settings
- Add sortable columns in recent files panel (date, filename, host, link)
- Add "Start selected" to right-click context menu
- Live-apply settings changes during uploads (parallel count, speed limits)
- Add fallback file_code check for upload logging
- Add warning log when upload completes without link

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 03:47:00 +01:00
Administrator
d972e0e913 fix: add 3 second pause between upload retries
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 03:39:28 +01:00
Administrator
3d858b1ffd feat: global speed limit, settings cleanup, abort reset, resize panel
- Global speed throttle (shared across all uploads)
- Settings grouped into sections (Uploads, Verhalten, Log)
- Abort all resets jobs to queued (restartable without reupload)
- fileuploader.log writes immediately per upload
- Staggered interval per hoster (not parallel sleep)
- Recent files panel resizable via drag handle
- History hides aborted entries
- Done jobs removed from queue immediately when setting active

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 03:08:56 +01:00