Clicking 'install update' while many uploads were running left the update stuck at 'Update wird heruntergeladen...' forever, requiring a manual restart. Two causes, both fixed:
1) The 95 MB installer download competed with the active uploads for the (saturated) main process, CPU, network and connection pool, so it barely progressed. The install handler now calls uploadManager.cancel() first — the app quits and relaunches for the update anyway, and the queue is already persisted (non-terminal jobs restore as 'Bereit'), so freeing the resources is correct and makes the download fast.
2) The download read loop had no timeout, so a stalled stream hung indefinitely with no error. Added a 45 s stall-timeout: if no data arrives, the download aborts with a clear message ('Download haengt — seit 45 s keine Daten ... bitte erneut versuchen') instead of freezing, so the retry button works.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Make the single high-concurrency measurement decisive. The ELD line now appends process.getActiveResourcesInfo() as a compact type-histogram, so one run splits all three readings in a single line: high mean/p99 means the main thread is CPU-blocked (workers/cap justified); low delay with many TCPSocketWrap/FSReqCallback/GetAddrInfoReqWrap resources means IO-bound queueing (threadpool/socket tuning, not workers); low delay with few resources means it is not saturated at all. Pure metrics — no credential-redaction path touched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The user runs 50+ concurrent uploads (parallel counts raised deliberately). Every async uploader feeds undici from fs.createReadStream and resolves DNS via getaddrinfo — both go through the libuv threadpool, whose default size is 4. At 50 concurrent uploads, file reads and lookups serialize 4-at-a-time: a hard cliff at a small connection count that matches the 'lags from X connections onward' symptom. Raise the cap to 64 as the first statement (before require('electron'), so libuv reads it when it lazily inits the pool; an explicit env override still wins). Threads are created on demand, so a higher max costs nothing when unused — reversible, zero upload-core change.
Also enable perf_hooks.monitorEventLoopDelay and log mean/p99/max/stddev every ~5s while uploading. This is the ground-truth instrument that splits the two competing explanations for the lag: a high event-loop delay means the main thread is CPU-blocked (TLS/crypto) and only workers or a concurrency cap will help; a low delay while uploads stall means the work is IO-bound and the threadpool/socket config is the lever, not workers. The numbers are pure metrics — they never touch the credential-redaction path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
doodstream-upload.js's _debugLog ran with NO verbose gate: every call did a
synchronous statSync (via maybeRotateLogFile) + appendFileSync directly on the
main-process event loop, ~8-15x per upload (server probe, response, redirect,
result page, filecode parse, hidden fields, submit/follow, plus retry branches).
DoodstreamUploader runs in the main process, so each pair of sync fs syscalls
blocked the event loop while uploading — delaying IPC, upload-progress-batch
forwarding, tray-tooltip and webhook handling for every other concurrent upload.
A constant per-upload main-thread tax (not history-scaling), surfaced by the
session-wide lag audit and confirmed as the one finding that bites in the real
upload scenario.
Fix: gate _debugLog behind the existing globalSettings.logVerbose setting
(default false), exactly mirroring main.js logDebug/_logVerbose. A module-level
_debugVerbose flag + setDebugVerbose() setter, an early-return at the top of
_debugLog, and one wire at main.js's single setLogVerbose chokepoint (covers
boot + save-config + the verbose toggle). When verbose is off the doodstream
trace simply isn't written — same contract as the main debug.log — and the
per-upload sync fs disappears. When a doodstream issue needs tracing, enabling
verbose restores the full trace.
The config-store audit findings (load() history-clone cost, per-write history
serialize) are real but scale only with history size — tens of microseconds at
this user's 8-batch config, and the safe fix is risky persistence surgery on
credential-bearing code for a latent micro-cost; deferred and documented in
tasks/todo.md rather than shipped.
397/397 tests pass, eslint clean, gate wiring verified (shared module instance,
default-off, toggles).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A 13-agent hunt pinned the "wird mit der Zeit laggy" symptom (CPU/RAM normal, UI
sluggish after many uploads) to the main process re-doing config I/O that scales
with the ever-growing history, stalling the synchronous main event loop so the
renderer's IPC round-trips feel laggy. The renderer render path was already
optimized (virtualized queue, capped panels) — confirmed clean.
This commit lands the two contained fixes (T1 + T3); the queue-persist rewrite (T2)
follows separately.
config-store (T1):
- load() now has an in-memory cache keyed on the file mtime+size. The processed
config (merged + credential-decrypted) is re-read/re-parsed/re-DPAPI-decrypted
ONLY when the file actually changes; our writes invalidate it, external edits
change mtime/size so the cache misses. Eliminates a full disk read + JSON.parse of
the whole growing history + per-credential decrypt on the vast majority of the ~38
load() call sites. load() always returns a structuredClone so callers can mutate
freely without corrupting the cache.
- _serializeForDisk clones ONLY the hosters subtree (the only thing encryptCredentials
touches) instead of JSON.parse(JSON.stringify(whole config)) — no more deep-cloning
an 8 MB history on every write.
- _atomicWrite refreshes the .bak with a raw fs.copyFileSync instead of
read + JSON.parse + write (it was re-parsing the full config a 2nd time per write).
main.js (T3):
- The log-flush paths resolved the log file via configStore.load() ~8x/second during
uploads (re-reading + cloning the whole config just to read logMode/logFilePath).
Cache those two strings in module scope, invalidate on the settings-save handlers.
Verified: 26 config-store tests (incl. new cache-correctness: independent clones,
external-change invalidation, save invalidation) + full 394-test suite green, lint 0
errors. Benchmark (8000-batch / 4.6 MB history): log flush no longer calls load() at
all; the remaining per-write history serialize is what T2 removes from the hot path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Matches the Real-Debrid-Downloader's rd-diagnostics-mcp model so the read-only
diagnostics agent is reachable over Tailscale (or any private tunnel) the same way
the downloader is, instead of requiring an SSH local-forward.
- lib/ip-allowlist.js (NEW): fail-closed IP allowlist — normalizeIp strips
::ffff:, loopback is always allowed, an empty allowlist accepts loopback ONLY
(fail-closed), exact IP + CIDR (incl. the Tailscale CGNAT range 100.64.0.0/10) +
wildcard rules. The real socket peer IP is the authority (never a forwarded header).
- remote-server.js: rejects non-allowlisted peers at connection (close 4005). Opt-in
via config.allowlist (the existing remote-control server, which passes none, is
unaffected). Loopback always passes, so local + SSH-forward use keeps working.
- Two bind modes (config diagnostics.bindMode): "local" -> 127.0.0.1 (default),
"network" -> 0.0.0.0 but ONLY when a non-empty allowlist is set (else it stays
loopback, fail-closed). The allowlist + token gate access; the tunnel
(Tailscale/WireGuard) is the confidentiality layer (transport is still plaintext ws://).
- The connection code now carries the host: mhu1_<base64url{v,h,p,t,n,fp?,s?}>. The
gateway decode is tolerant of the legacy {port,token,label} keys; connect_server
takes the host from the code (host arg is an optional override). Proven end-to-end:
the integration harness now connects with NO host arg and resolves it from the code.
- Renderer: Sichtbarkeit selector (local/network), public-host input with
suggested-host chips (os.networkInterfaces — the Tailscale IP shows up there),
allowlist textarea (network mode), and network-requires-allowlist validation.
- main.js: bindMode->host, getSuggestedRemoteHosts, host-in-code, allowlist plumbed
into startDiagnosticAgent + the diagnostics IPC (get/save/status).
- docs: rewritten for Tailscale (set the allowlist to your tailnet, put the Tailscale
IP/MagicDNS in the code address — no SSH forward needed).
This supersedes the v3.3.85 hard loopback-lock with the downloader's allowlist model.
Tests: lib/ip-allowlist (8) + remote-server allowlist wiring/loopback (2) + gateway
decode (host short-key + legacy tolerance). 393 app tests + 9 gateway tests + e2e +
host-in-code integration + adversarial all green; lint 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the connect-by-code side of remote diagnostics and closes two real
secret-leak vectors that an end-to-end gateway<->agent test surfaced.
Gateway (gateway/, local stdio MCP, Claude connects once):
- 14 read-only tools (server_health hub, read_log, list_logs, list_errors,
get_queue_state, get_history, get_config_redacted, get_system_info,
get_rotation_state, get_app_events + connect/disconnect/list/current).
- The HOST is always supplied by the operator, never taken from the code.
- TLS fingerprint pinning is enforced in the socket 'open' handler BEFORE the
token is sent (wss opt-in); plain ws is loopback-only.
- registry.json (holds bearer tokens) is gitignored; only an empty example ships.
Security hardening (gates every off-box payload):
- redactLogText now scrubs opaque bearer/token-family secrets that are NOT
stored config credentials (e.g. a session token a hoster returns inside an
error string): bare token/auth_token/refresh_token/session_token + standalone
"Bearer <opaque>". Benign "token bucket" prose is left intact.
- get_config_redacted deep-redacts every string leaf (JSON-safe, per-leaf, so
the cookie/sess line patterns can't gobble across a compact-JSON field) and
drops the history subtree (served by get_history with its own per-error
redaction). This plugs leaks via globalSettings.pendingQueue[].error etc.
Bind-address safety:
- _safeDiagBindAddress() forces the diagnostic agent to 127.0.0.1/::1; the
0.0.0.0 UI option is removed. Direct LAN/Internet bind stays disabled until
encrypted transport (wss) exists — remote access goes through an SSH/VPN
tunnel to loopback. (Never plaintext ws:// on all interfaces.)
Tests: end-to-end gateway<->agent gate (connect -> server_health/read_log/
get_config_redacted, asserts zero secret leakage, rejects doodstream log, path
traversal and write ops); + redaction regression tests in the main suite.
385 app tests + 9 gateway tests pass; lint 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the server/app half of a remote-diagnostics system so Claude (via a local
MCP gateway, added separately) can read a server's full state to diagnose
problems: logs, errors, queue/app state, redacted config, system health. All
read-only; security review folded in as hard requirements.
Architecture (design-verified):
- A SECOND, independent RemoteServer instance (diagnosticMode) on its own port
(default 9110, bind 127.0.0.1) with NO capture/input callbacks, so screen
capture and sendInputEvent are structurally unreachable from the diagnostic
path. The existing screen-share server (port 9100) is byte-for-byte unchanged.
- lib/remote-server.js: a `host` bind option, a post-auth `diag-request` branch
that delegates to onDiagnosticRequest and replies with a reqId-correlated
`diag-response`, a `diagnosticMode` guard (capture never spawns), a
timing-safe token compare (length-guarded), and getLastAccess().
- lib/diagnostics-agent.js: handle(op,args) enforces a HARDCODED read-only op
whitelist as the sole authority — no write/exec op, no run_health_check.
- lib/diagnostics-collectors.js: pure, dependency-injected collectors
(server_health one-shot hub, read_log, list_errors, get_queue_state,
get_history, get_config_redacted, get_system_info, list_logs, get_app_events,
get_rotation_state, get_health) reusing support-bundle + stats.
Security (all mandatory, implemented):
- Redaction gates every off-box payload. support-bundle now adds webhookUrl +
diagToken to CRED_KEYS, and exports redactLogText (value-scrub of live secret
strings + pattern-scrub of Discord webhooks / Bearer / api_key= / cookies /
sess ids) + valueScrub + collectSecretValues. read_log takes a logical NAME
(no path traversal); doodstream-debug.log is excluded from the readable set
(it logs live api-key-bearing HTML). grep is length-capped (ReDoS guard).
- diagnostics config subtree (enabled/port/token/label/codeIssuedAt/bindAddress)
defaults OFF, bind 127.0.0.1; deep-merge makes it migration-free. The
server-owned token is preserved against renderer clobber in both
save-global-settings handlers; the diagnostics:* IPC is the only mutator.
- app.requestSingleInstanceLock() (also the approved fix#6) so a relaunch can't
EADDRINUSE-kill the agent and two instances can't clobber the config.
main.js: buildDiagnosticCode (mhu1_ base64url, no host embedded), start/stop +
auto-start-on-launch + stop-on-quit, the four diagnostics:* IPC handlers.
renderer: a "Diagnose-Zugriff" settings subtab (toggle, port, bind-address,
copyable connection code + regenerate, status, read-only security note).
382 tests pass (incl. 11 new: collectors+redaction, agent whitelist, and a live
RemoteServer protocol test proving auth->diag-response correlation, that a
diagnostic client never triggers the capture window, and brute-force lockout).
Lint clean (0 errors). Smoke boots identically to baseline. The standalone MCP
gateway package + operator setup docs land in a follow-up commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up hardening on the v3.3.80 queue-persistence fix after a deeper adversarial
sweep + reviewer pass over the WHOLE subsystem (not just the diff). Two real defects:
1. Basename-collision lost work (regression introduced by v3.3.80 FIX A).
restoreQueueStateFromConfig collapses jobs on the FULL path while the ts-gate keys
on basename|hoster. Two genuinely different files with the same basename queued to
the same hoster from different folders therefore share a gate key: if one was logged
after savedAt, the ts-rule dropped BOTH — silently losing the still-pending one.
Pre-FIX-A only 'done' jobs were dropped, so a pending file was never at risk.
Fix: an ambiguity guard in partitionRestoredJobsByLog — the ts-rule is suppressed
when a basename|hoster key maps to more than one distinct file path (the log records
only basenames, so it can't say which physical file completed). The done-in-log rule
is unchanged. Fails safe: worst case a visible ghost survives, never silent data loss.
2. selectedFiles re-materialization with removeFromQueueOnDone=ON (second mechanism,
independent of the stale snapshot). When that setting is on, a completed job is
stripped from queueJobs but its path stays in selectedFiles (syncSelectedFilesFromQueue
only runs at batch-done, never on a mid-upload close). On restart the ts-gate operates
on queueJobs and never sees it, then the startup updateUploadView -> buildQueuePreview
re-creates it as a preview ghost AFTER the gate ran, and it re-persists with a fresh
savedAt — sticky. Fix: completedSelectionKeys() seeds _completedUploadKeys (the set
buildQueuePreview already consults) from the log at startup, keyed on full path, with
the same ambiguity guard. Log-based so it survives a hard kill, consistent with FIX A.
Also extracts the orphan-tmp sweep decision into lib/orphan-tmp.js (was untested inline
code in main.js; behavior-preserving) and adds executable coverage for the paths that
were previously only argued from logic: orphan-tmp sweep, config-store pendingQueue+savedAt
round-trip, an end-to-end scenario in the exact user-reported shape (300 queued / ~200
finished mid-session), and a 3000+500-iteration property fuzz of the gate invariant
including the lost-work guarantee. 359/359 green, ESLint clean, smoke-boot unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closing the app (especially during an active upload or on a hard kill) and
reopening sometimes left already-uploaded files sitting in the queue as if
still pending. Root cause is three layers stacked:
1. Persist-starvation: persistQueueStateSoon() reset a 10s debounce on every
progress event, so during an upload the on-disk queue snapshot was never
rewritten and stayed frozen at the pre-upload state (all jobs "preview").
2. The beforeunload sync flush only covers a clean close; a hard kill / crash
leaves that stale snapshot on disk.
3. Startup auto-dedup only dropped jobs with status "done". The completed
files were stored as "preview" in the stale snapshot, so they survived and
reappeared.
Fix (mechanism-independent — holds whether the stale snapshot came from
starvation, a mid-upload close race, or a hard kill):
FIX A (core, durable): the upload log is the source of truth. Each persisted
snapshot is now stamped with savedAt; on restart any restored job whose newest
matching log entry is timestamped at/after floor(savedAt) is dropped regardless
of status — it provably completed after the snapshot, so a "preview" row for it
is a ghost. lib/queue-dedup.js gains an additive 3rd savedAt param; without
savedAt or without log timestamps it behaves exactly as before (the 5 canary
tests stay green, so intentional re-uploads of older files still survive).
FIX B: new lib/throttle-timer.js with a max-wait. During uploads the snapshot
is now written at most ~20s into a continuous progress burst instead of never;
idle stays a pure debounce. The fallback shim honors max-wait too, so a missing
library can never silently reintroduce the starvation.
FIX C: the synchronous close-write retries renameSync on EBUSY/EPERM/EACCES and
uses a pid-unique tmp (de-conflicts it from config-store._atomicWrite's fixed
.tmp). A startup sweep reclaims orphaned <config>.<pid>.tmp files left by a hard
kill between write and rename.
lib/upload-log.js extracts formatUploadLogLine + parseUploadLogLine from main.js
so the real writer -> reader -> gate seam is unit-tested (a future log-format or
epoch-basis change can no longer pass green while breaking the fix).
Verified: 334/334 tests green (incl. throttle fake-clock starvation/maxWait,
ts-gate multi-hoster partial-completion, and the real-format seam tests), ESLint
clean, smoke-boot identical to baseline, and an adversarial multi-agent review
(15 findings, 14 refuted, 1 low — the tmp orphan, now swept) on the diff.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v3.3.74 created a fresh account picker inside every buildUploadTasks /
buildUploadTasksFromJobs call, so its in-memory rotation index reset to 0
on each call. That is correct for a one-shot batch (drag-drop N files at
once distributes fine), but it silently no-ops in exactly the pattern the
byse 80-uploads/30-days quota cares about: the folder monitor feeds new
files into a *running* batch one detection at a time via add-jobs-to-batch
(renderer -> add-jobs-to-batch IPC), and per-detection autoStart likewise
fires start-upload per file. Each of those calls rebuilt the picker from
index 0, so account 1 won every single time and the secondary accounts
never received anything.
The rotation cursor now lives outside the picker and survives across calls
and across app restarts (the quota spans a rolling 30-day window, so a
session-only counter would still starve secondaries for users who restart
between small uploads):
- account-rotation.js: createAccountPicker() accepts a seed { indices } map,
resumes the per-hoster cursor from it, and exposes indices() (the advanced
cursors) + dirty() (whether any rotation actually happened this call). The
cursor is a monotonic counter taken mod the current enabled-account count,
so it keeps wrapping correctly even if an account is later enabled/disabled.
- config-store.js: new top-level rotationCursors map (added to DEFAULTS, read
back in load() which otherwise reconstructs the result and would drop
unknown keys) plus a saveRotationCursors() method. The existing
read-modify-write save() preserves it across unrelated settings/credential
saves; secret-store never touches it.
- main.js: a module-level _rotationCursors is the authoritative source of
truth (seeded once from disk on first use, updated synchronously per batch),
with config as restart-survival backing. makeAccountPicker() seeds the
picker from it; persistRotation() folds the advance back and flushes to
config only when dirty. Authoritative in-memory state also closes the
disk-read race two rapid batches would otherwise hit. Both task builders
now receive the picker instead of constructing their own.
Composition with failover is unchanged and verified: the picker only chooses
each file's *initial* account and is never called on the failover path. The
pre-job-swap reroutes a task only when that task's own account is in
_failedAccounts, so a dead account's jobs reroute while healthy accounts keep
their rotation share. No double-advance of the cursor.
Tests: account-rotation gains seeded-resume, drip-feed-across-pickers (the
regression), dirty()-semantics, carry-forward, and count-shrink-wrap cases;
config-store gains rotationCursors default, save round-trip, no-clobber, and
credentials-undisturbed cases. Full suite 303/303.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
byse.sx requires each account to upload >=80 videos in the last 30 days
(measured across a rolling 3-month window) or it goes dormant. With a
single primary account doing all the work, the other configured accounts
decay and eventually get suspended. This adds an opt-in "Accounts rotieren"
toggle per hoster that round-robins files across every enabled account that
has credentials: file 1 -> account 1, file 2 -> account 2, file 3 ->
account 3, then wraps. Off by default, so existing single-account behavior
is unchanged.
Mechanics:
- New lib/account-rotation.js: createAccountPicker() returns a stateful
pick(hoster) closure holding a per-hoster round-robin index. It only
rotates when rotateAccounts === true AND more than one usable account
exists; otherwise it returns the first enabled account (the old primary).
enabledAccountsFor() filters disabled + credential-less accounts while
preserving configured order, so a disabled account is simply skipped in
the cycle rather than leaving a gap.
- main.js: both buildUploadTasks() and buildUploadTasksFromJobs() now build
one picker per call and use pick(hoster) instead of getPrimaryAccount(),
which is now removed (dead code). A fresh picker per batch means each
upload session starts the cycle at account 1, matching "die erste Datei
auf Account eins".
- config-store.js: rotateAccounts: false added to HOSTER_SETTINGS_DEFAULTS.
- renderer/app.js: "Accounts rotieren" checkbox in the per-hoster upload
settings (Accounts tab). Persisted by the existing generic checkbox path
in saveHosterSettingsFromDom().
Composes with failover: rotation only chooses the *initial* account per
file. On a hard account failure the existing failover (_failedAccounts +
pre-job account swap) reroutes just that account's jobs; the healthy
accounts keep their rotation share.
Single enabled account (or rotation off) = no behavior change.
Tests: tests/account-rotation.test.js (10 cases) covers off=primary,
on=round-robin+wrap, single-account no-op, skip disabled, skip no-creds,
null when none usable, per-hoster index independence, byse-only rotation,
100-file 50/50 split, and the enabledAccountsFor filter. Full suite 294/294.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Adds a custom HTML menu bar above the tab bar, replicating the Real-Debrid
Downloader's look: click-to-open triggers, hover-switch between open menus,
click-outside / Escape to close, right-flyout submenus.
- Datei: Dateien hinzufügen, Ordner hinzufügen, Sicherung submenu
(Export/Import backup), Neustart, Beenden.
- Einstellungen: "Einstellungen öffnen" (jumps to the settings tab) plus an
inline live grid with the max-parallel-uploads spinner and a speed-limit
checkbox + MB/s spinner, both two-way-synced with the Settings tab fields
via saveGlobalSettings (parallelUploadCount / globalMaxSpeedKbs).
- Hilfe: Log-Ordner öffnen, Diagnose-Paket exportieren, Suche Aktualisierungen.
Wiring reuses existing handlers (addFiles/addFolder buttons, doBackupExport/
Import, openLogFolder, createSupportBundle, checkForUpdate) and two new tiny
IPC handlers app:restart (app.relaunch+quit) / app:quit. initMenuBar() is
wrapped in try/catch in setupListeners so a menu fault can never break core
app init. CSS maps the downloader's menu styles onto this app's theme vars.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Upload-Verlauf tab took 30s+ to open on large, never-cleared histories.
Measured on the live config: 18.9 MB config / 41 batches / 53,403 rendered rows.
Two independent problems, fixed together:
1. Render side (the 30s killer): renderHistoryTable built ALL rows into one
innerHTML. Now the build is capped to the newest 2,000 rows (HISTORY_RENDER_CAP)
after slicing the chronological tail, then sorted for display. A notice shows
"Zeige neueste 2000 von N" so nothing looks deleted; full history stays on disk
and "Verlauf exportieren" still emits everything (export reads loadHistory()).
2. Storage side: history grew unbounded because appendHistory only ever pushed.
New globalSettings.historyRetention ('all' default, non-destructive) with
policies: 7d / 30d / 90d (time-based) and 1000 / 100 (newest-uploads-based).
appendHistory now prunes after each batch; a new prune-history IPC re-applies
the policy immediately when the user changes it, gated behind a confirm() that
shows the exact removal count (dry-run first).
Prune model keeps whole batches (atomic): count policies accumulate rendered rows
from the newest batch backward and always keep >= the newest batch even if it
alone exceeds the target; time policies keep batches whose timestamp is missing
or unparseable (old entries have none) so ambiguous data is never silently dropped.
New retention dropdown lives in the Verlauf header. 9 unit tests cover the prune
edge cases incl. a realistic 41-batch fixture. Full suite: 272 pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
User reported two coupled issues in the accounts panel:
- "VOE Upload: CSRF-Token nicht gefunden. Bist du eingeloggt?" fires
intermittently across multiple VOE accounts when "Accounts prüfen" runs.
Each retry "fixes" one and breaks another — classic anti-bot burst response.
- The flat badge strip becomes unreadable with many accounts; user wants
collapsible per-hoster groups with "N/M" headers and green/red indicators,
click to expand to per-account detail.
DISCRIMINATOR CHECK (cheap before serializing): grep'd lib/voe-upload.js for
module-level state — none. Each new VoeUploader() carries its own cookie Map.
Burst-throttle on VOE's side is the only plausible root cause.
CONCURRENCY FIX in main.js runHosterHealthCheck:
- Group checks by hoster, run each hoster's group SEQUENTIALLY, groups in
parallel (Promise.all of sequential runners). Cross-hoster parallelism
preserved; intra-hoster bursts eliminated.
- Result array preserves input order via a result-index map.
- Hardening per review: dedup duplicate {hoster, accountId} entries before
grouping (no wasted API calls if a caller ever sends duplicates), and entries
missing accountId now return a clean "Account-ID fehlt" error instead of
silently calling per-hoster checker with null config.
- Validate-credentials and checkSingleAccount paths unchanged (single-check
payloads run the same way regardless).
- Latency trade-off acknowledged: 5 VOE accounts ~5x faster path → up to 25s
for that hoster's column. That's the cost for reliability; the user's
alternative was 0/5 working on burst-failed runs.
UI FIX in renderer:
- New _buildAccountHosterGroupHtml emits a collapsible per-hoster group
reusing the existing .hoster-panel-header / .panel-arrow CSS pattern.
- Header shows "VOE 4/5" (ok-count / total-accounts), a green/red/amber/gray
status dot, plus pills for "N deaktiviert" and "N Fehler".
- Default: auto-expand any hoster with errors, checking, or unchecked
accounts; collapse all-green.
- Open-state memory tracks user clicks. Per review: also tracks errorsAtClose
snapshot so a NEW failure since the user's close forces re-expand once.
Prevents the "I closed it once and now silent failures hide forever" risk.
- Single-card updates also refresh the parent group's header counter via
_refreshHosterGroupHeader.
- Flat badge strip in renderHealthCheckResults is now a no-op stub — the
per-hoster headers carry the same info, less duplication.
Three-lens review (workflow wch4p9ee9): concurrency PASS_WITH_NOTES, ui-state
PASS_WITH_NOTES, comment-policy PASS (zero new // or /* */ comments).
Latent concerns from review applied as hardenings.
210/210 tests green, lint clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User reported three coupled bugs in account add/edit:
(1) Invalid logins still create the account
(2) Doodstream gets created multiple times when "Prüfen & Anlegen" is
double-clicked or repeatedly OTP-retried
(3) Add/Delete in the accounts panel feel laggy
Plus a UX/feature request: account label + two-step "Prüfen → Anlegen" flow.
Map (workflow wf44zpud4, 3 parallel subagents + adversarial verify) confirmed:
- saveAccount() persisted to disk BEFORE the health check (lines 3407-3409)
- saveBtn.disabled was set AFTER two awaited IPC roundtrips → 5-100ms race window
- OTP-retry path generated a new accountId on every click (editingAccountId
stayed null in ADD mode) → DETERMINISTIC duplication on every OTP attempt
- runHealthCheck IPC required the account to be already persisted → that's
why the old code wrote-first-check-second
Fix architecture (advisor: Option A — make the invariant real, not cleanup-based):
- main.js + preload.js: NEW `validate-credentials` IPC. Accepts ephemeral
{hoster, authType, username, password, apiKey, otp} payload, builds an
ephemeral hosterConfig, runs the same per-hoster checker via a shared
_dispatchHealthCheck helper. Nothing touches config.hosters.
- renderer: two-step modal state machine.
- "Prüfen" click → validateCredentials (ephemeral) → green flips button to
"Anlegen"/"Speichern" AND caches a snapshot of the validated creds.
- "Anlegen"/"Speichern" click → only fires if cached snapshot matches the
currently-typed credential-identity (username+password or apiKey;
label and OTP are not part of the snapshot key).
- Input listeners on the identity fields drop the snapshot the moment any
cred is edited post-green → user can't sneak unverified creds through.
- _accountModalBusy is set SYNCHRONOUSLY at the top of the click handler,
before any await, so a double-click is a no-op.
- _accountModalSession token bumps on every modal reset → a stale late
response from a closed-and-reopened modal can't stomp the new session's
busy flag or UI (lens-2 review fix).
- Edit mode flows through the same path → bad edits never reach disk
before being validated (fixes the silent good-creds clobber).
- closeAccountModal cancels the auto-close timer + clears modal state so
a stale 600 ms timer can't close a freshly-reopened modal.
- Label field (new): persisted on the account, shown in the card subtitle as
"Label: XYZ • API: ABC… — API Key gültig" so identical-looking API accounts
are disambiguable. Excluded from snapshot key on purpose — label is metadata.
- Perf: drop the redundant `await getConfig()` round-trip in commit+delete
(in-memory state was already the source of truth and the old reload was the
main lag source). deleteAccount fires-and-forgets the saveConfig and closes
the modal synchronously. Commit path uses updateAccountCard for the
single-card edit case instead of a 4-panel cascade.
Multi-lens review (workflow wyoc3iq4k, 3 reviewers): OTP-correctness SHIP,
race-guard SHIP-WITH-FIXES (session-id token + busy-inside-try applied),
edit-mode+label SHIP. No blockers.
Tests: 6 new regression tests (tests/validate-credentials.test.js) covering
the three reported bugs as executable spec:
(a) failed validation persists nothing to config.hosters
(b) second click with guard set persists exactly one entry
(c) OTP-required persists nothing; OTP retry re-validates ephemerally
plus snapshot-key identity, post-validation edit invalidation, and the
ephemeral hosterConfig shape contract. 210/210 green, lint clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User report: in session mode the upload-log lines split across two files — the
first few before the auto-persist fallback fired, the rest after into a path
with the session-stamp DOUBLED.
Root cause in main.js _persistFallbackLogPath:
1. The strip was gated on the legacy `sessionLog` boolean, which 3.3.35 retired
in favour of `logMode`. So in session/daily mode the gate was false and the
resolved path got persisted with its stamp intact.
2. Even when the gate triggered, its regex matched only the daily YYYY-MM-DD
suffix, not the session "session-YYYY-MM-DD_HH-MM-SS-pid" suffix.
The next getLogFilePath() call read that saved path as the "base", treated the
already-stamped filename as the base name, and re-applied another stamp on top.
First flush hit the original session file; everything after hit a doubly-
stamped one — exactly the symptom (top file: 2 lines, bottom file: the rest).
- lib/log-mode.js: new pure stripModeStampFromFileName helper that removes both
the daily and the session suffix patterns. Anchored to $, no nested
quantifiers (linear).
- main.js: gate on logMode (not sessionLog) and call the helper for daily AND
session, so logFilePath always persists as a bare base.
- Tests: 4 new — strip behaviour + an idempotence regression that locks in
"resolve → strip → resolve = same path" so this can't silently come back. 204/200.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a third choice next to the existing single-file and per-day modes: a new
log file is created at every app start (process boot) and used until the app is
closed. A close → reopen of the app starts a new session, hence a new file.
File pattern: fileuploader-session-YYYY-MM-DD_HH-MM-SS-<pid>.log.
The boolean sessionLog field — misnamed: it actually toggled daily mode — is
replaced by a logMode enum: "single" | "daily" | "session". The misnomer made
the migration the trap to watch: existing users with sessionLog:true must land
on "daily", NOT "session". normalizeLogMode handles this and is unit-tested.
- lib/log-mode.js (new, pure, dual CJS/window export): normalizeLogMode +
resolveLogFileName + format helpers. No fs, no Date.now() at call time.
- config-store.js: normalize at the single load() boundary so downstream
readers consume logMode only. logMode is deliberately NOT seeded in DEFAULTS
(would beat the legacy migration after merge).
- main.js: stamp SESSION_ID once at process start (with pid hedge against
same-second restart collisions); getLogFilePath and buildFallbackLogName
switch on mode via the lib. _resolveUploadLogTarget cache key is now just
the primary path, which already encodes mode/date/session — self-invalidates.
- renderer: <select> with three German labels replaces the old checkbox;
saveSettings writes logMode; index.html loads the lib so window.LogMode is
available in renderSettings.
- Tests: 14 log-mode tests (incl. legacy-migration regression), 3 config-store
tests (defaults, legacy migration, round-trip all three values). 200/200.
End-to-end simulated locally: two launches → two distinct session files; PID
hedge produces distinct names even within the same second.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Root cause of the recurring "kein Filecode — Server gab leeren Link zurueck":
the web-session upload flow gets the filecode back inside an XFileSharing HTML
form, and on long/large uploads that form comes back empty (no fn). Verified
research: doodstream's server-side file-registration callback times out under
large-file load, so the upload "succeeds" (bytes sent, HTTP 200) but no filecode
is minted — and because registration failed, the file is NOT in the file list
either, so polling can't recover it. The web path also rides a per-page-load
sess_id token that ages over the multi-minute upload.
The official doodapi.co JSON API has no such failure mode for result retrieval:
the upload response returns result[0].filecode directly, and it authenticates
with a persistent api_key (no aging sess_id). Git history confirms the API was
doodstream's ORIGINAL upload path (initial commit); web login was added later
only "as an alternative to API key" — so preferring the key restores the
intended primary path rather than fighting a deliberate choice.
- lib/account-auth.js (new, pure, unit-tested): selectUploadAuth() prefers the
doodstream API key over username/password; all other hosters unchanged.
- main.js buildTaskFromAccount delegates to it → a doodstream account with an
apiKey now routes through hosters.uploadFile (doodapi API) instead of the web
uploader; keyless accounts keep using web login.
- hosters.js: drop the stale hardcoded fallback node from the doodstream API
config (same dead tr1128ve host removed from the web path) so a failed server
lookup throws cleanly instead of uploading into a dead end.
- Tests: 8 routing cases (doodstream key-preference, keyless fallback, voe
unaffected, authType=api, null-safety). Full suite 173/173.
This eliminates the empty-form failure mode for result retrieval when a key is
configured. It does NOT change doodstream's backend — whether the large-file
timeout recurs (now as a structured JSON error, not a silent empty form) is for
the server run to confirm. Requires a doodstream API key on the account.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New per-hoster setting "Links in Log schreiben" (logToFile, default
on). When unchecked for a hoster, that hoster's successful upload
links are no longer written to fileuploader.log — other hosters keep
logging independently.
- lib/config-store.js: logToFile: true added to HOSTER_SETTINGS_DEFAULTS;
merge-on-load gives every hoster the key (old configs included).
- renderer/app.js: checkbox per hoster panel + collection loop now
handles type=checkbox (boolean) alongside the numeric fields. The
autosave bind already special-cased checkboxes (change event).
- lib/log-policy.js (new): hosterLogToFileEnabled() — pure, opt-out
semantics. Only an explicit logToFile===false disables; missing/
malformed/non-true values all default ON so links are never
silently dropped.
- main.js: shouldLogHosterToFile() reads the LIVE uploadManager
.hosterSettings (so a mid-batch toggle takes effect at once), falls
back to persisted config, then to enabled. Guards appendUploadLog
in the done handler; skipped writes get a debugLog line.
Tests: 8 log-policy (defaults, opt-out, per-hoster independence,
malformed input) + 2 config-store (default true, persisted false
survives reload). 147/147 green, eslint clean.
In packaged builds path.dirname(process.execPath) resolves to
%LOCALAPPDATA%\Programs\Multi-Hoster-Upload — a hidden install
directory the user never visits and that NSIS may prune on
uninstall. Existing files written there were effectively invisible.
Change the unconfigured-default to app.getPath('desktop') instead.
If Desktop isn't available (rare), fall back to userData (Roaming),
and finally to the exe dir as a last resort. Dev mode (isPackaged
false) is unchanged — keeps the project dir for inspection.
Custom log paths set via the Settings UI override this and continue
to work as before. Existing users with old logs in the install dir
will just see a new fileuploader.log on the Desktop going forward;
the old file stays where it is (not auto-migrated).
137/137 tests still green.
3.3.2 fixed fileuploader.log unbounded growth, but three siblings kept
growing without limit:
- upload-debug.log (verbose, every IPC + progress event log line)
- account-rotation.log (every rotation decision)
- doodstream-debug.log (per-hoster trace from lib/doodstream-upload.js)
A multi-month dev install or a heavy production user could fill the
log dir with multi-GB files and slow every appendFile.
Wire all three through the same lib/log-rotation.js helper:
- upload-debug.log → 25 MB cap, 2 numbered backups (~75 MB worst)
- account-rotation.log → 10 MB cap, 2 numbered backups (~30 MB worst)
- doodstream-debug.log → 10 MB cap, 1 numbered backup (~20 MB worst)
The rotation check runs once per flush call (each is debounced or
already a once-per-event path), so the statSync overhead is
microscopic. _flushDebugLog passes a noop logger to avoid recursing
into itself; _flushRotLog and _debugLog (doodstream) use the normal
debugLog so any rotation surprises end up in upload-debug.log.
126/126 tests still green.
Two related fixes from the deep-audit pass:
HIGH-2: save-global-settings-sync used `try {} catch {}` and always
returned `event.returnValue = true`, so a disk-full / AV-lock /
permissions failure looked like success to the renderer's beforeunload
chain. The user closes the app, comes back, settings are gone —
without any indication. Now the catch sets returnValue=false and
debugLogs the error message, and the bak refresh is in its own
nested try so a transient lock there doesn't fail the whole save.
MED-4: lib/config-store.js _atomicWrite had the same TOCTOU on the
.bak refresh — fs.existsSync(...) then fs.readFileSync(...) without
guarding the read. Wrapped the read+write of the backup in its own
try/catch: a stale .bak is preferable to dropping the new write
entirely just because Windows Defender briefly locked the file mid-
operation. The rename of tmp → live still throws on real failure,
which is what the outer reject is for.
119/119 tests still green; both fixes are defensive guards on
already-tested write paths.
The batch-done event handler awaits configStore.appendHistory(summary)
before nulling the global uploadManager reference. If the renderer
fires start-upload while that await is pending, the start-upload IPC
creates a fresh UploadManager and assigns it to the same global. The
old handler resumes, sets uploadManager = null, and orphans the new
manager: cancel-upload, add-jobs-to-batch, save-config re-resolve etc.
all see null and become no-ops, while the new batch keeps running
invisibly in the background.
Capture the manager identity at listener registration time and only
null the global if it still points at THIS manager. If a newer one
replaced it mid-await, leave it alone and log the near-miss for
diagnostics.
Found by deep-audit subagent. Tests still 119/119 (no test for this
because it needs a coordinated IPC + async-mock harness; the fix is
small and the diagnostic log will catch regressions).
The fileuploader.log rotation introduced in 3.3.2 lived inline in
main.js — fine for the runtime path, but it required electron's `app`
to even reach the function under test. Pull the rotation logic into
lib/log-rotation.js (pure fs/path, no electron deps) and cover it
properly:
- ENOENT (file missing) → no-op
- Below cap → no-op
- Over cap → live → .1, returns true
- Existing backups shift up: .1 → .2, .2 → .3
- At maxBackups limit → oldest dropped, others shift, live becomes .1
- Idempotent: rotating twice keeps the chain consistent
- maxBackups=1: never grows past .1
- Invalid maxBytes (0/negative/NaN) → safe no-op
- Provided debug callback receives a "rotated" message
- File without extension still rotates correctly
main.js now imports `maybeRotateLogFile` and calls it directly. 97/97
tests pass.
The per-job log collector was only cleared at start-upload — across a
long session with many add-jobs-to-running-batch interactions (no new
start-upload), the Map grew unbounded. At ~5000 tracked jobs that's
1 MB × 5 = 5 MB+ of stale history hanging around in the main process,
bigger as ring buffers fill.
Add a cap: when a new jobId would push size past 1000, evict the
oldest entry (Map iteration order is insertion order per spec). 1000
× 200 entries/job × ~100 B/entry ≈ 20 MB worst case, properly bounded
no matter how long the session runs. Per-job ring buffer (200 entries)
unchanged; only the count of tracked jobs is now capped.
The "Log anzeigen" modal still works for any job in the most-recent
1000 — older jobs return an empty array, which the renderer already
displays as "Keine Log-Einträge".
A long-running install can otherwise grow the upload log into the
gigabyte range, eating disk and slowing every appendFile. Add a
size-checked rotation right before each flush:
- statSync the resolved log target (cheap, ENOENT skips silently).
- If size exceeds 50 MB, drop the oldest backup (.3), shift .2→.3
and .1→.2, then rename the live file to .1 and let appendFile
create a fresh primary on the next call.
- Max 3 backups (~200 MB worst case, bounded). debugLog records
each rotation for diagnostics.
- Pure additive: skips when file is small or doesn't exist; no
effect on the daily-log mode (already date-rotated).
Bundles four findings from a stability audit plus the missing-log bug
the user reported.
1. main.js _flushUploadLog: ENOENT after the log file's directory got
deleted mid-session was swallowed; the buffer was cleared before
appendFile so entries were silently lost and the cached target kept
pointing at the dead path. Now: mkdirSync(recursive) before every
flush idempotently recreates a missing dir; on any append error we
invalidate the cache, prepend the chunk back to the buffer and
schedule a retry. Survives "user dragged the log folder into the
trash and didn't notice".
2. renderer/app.js queueJobs auto-prune: with the default
removeFromQueueOnDone=false the queue grew forever. Past ~5000
entries every render became O(N) on a perpetually-growing N and
the user saw progressive scroll/tab lag. Cap the in-queue
terminal jobs (done/skipped/error/aborted) at 500 most-recent on
each batch-done; oldest get pruned with their index entries.
3. sortQueueJobs dynamic-key throttle: status/speed/progress/size
sorts ran a full O(N log N) sort on every progress tick. Added a
200ms-window cache for the dynamic-key path so the sort is reused
within the same UI_UPDATE_INTERVAL — invisibly small reorder lag,
massive cost savings at 5000+ jobs.
4. renderHistoryTable delegated listeners: every Verlauf-tab switch
was binding one click listener per row (5000 listeners on a
long-history user) and rebuilding the entire <tbody> innerHTML.
Single delegated tbody listener covers both row-click (copy link)
and th-click (sort), bound once per container via dataset flag.
5. sessionFilesData (recent-files panel) cap at 2000 entries with
matching _sessionFileKeys cleanup using the existing separator.
Stops the lower-panel innerHTML write from inflating to multiple
MB on long sessions.
87/87 tests still green.
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.
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
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.
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.
At 500+ queued jobs these lines bloated upload-debug.log with
megabyte-sized entries per batch-start and added visible latency to
the IPC handler. Log sizes only.