Commit Graph

169 Commits

Author SHA1 Message Date
Administrator
d5bb97aefe perf(history): virtualize the History table — kill the last tab-switch layout cost (v3.3.102)
v3.3.101's gate removed the get-history parse on tab switch, but the log still
showed tab clicks at ~216ms with a ~197ms renderer long-task and NO get-history
(proc=0ms → pure browser layout, not JS). Cause: `.view{display:none}` ->
`.active{display:flex}` and the History table built up to 2000 non-virtualized
<tr>, so making the view visible laid out 2000 rows (~197ms on the RDP VM). The
queue was already virtualized; the History table was the only large non-virtual
one.

Measured the options with Playwright on the real DOM (this machine; the user's VM
is ~1.7x slower):
- current 2000 rows (auto layout): 118ms
- content-visibility + table-layout:fixed: 117ms (no help — rows still laid out)
- cap to 300 rows: 16ms, but rejected: History rows are per-file-per-hoster, so a
  single 1280-file batch is ~3840 rows and a small cap would hide a recent batch's
  links
- virtualize (40 visible of 2000): 2.3ms

Fix — virtualize renderHistoryTable, mirroring the queue's _renderVirtualRows:
- The header is always rendered; tbody#historyBody gets only the visible rows plus
  top/bottom spacer <tr> sized from VIRTUAL_ROW_HEIGHT. A rAF-coalesced scroll
  handler and a ResizeObserver on #historyContainer re-render the visible window;
  the ResizeObserver also serves as the show-trigger (a hidden 0x0 container that
  gains size on tab activation re-renders at the correct height). Sorting resets
  scrollTop and re-renders; the copy-link / sort-header click delegation is
  unchanged. _historyWorking holds the sorted working set.
- styles.css: .history-table gets table-layout:fixed plus scoped column widths so
  columns do not jump as rows scroll in and out. This is scoped to .history-table
  and does not touch the .col-* classes the queue shares.

Verified end-to-end with Playwright at 6000 rows: show cost 1.1ms (was 118ms), DOM
stays 32-42 rows, scroll maps to the right rows (top/middle/bottom), scrollbar
height exact, column widths stable, rows update on scroll. All rows remain
scrollable (no UX loss); the show cost drops ~100x.

407 tests pass; clean Electron boot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:04:10 +02:00
Administrator
bad1c665f5 perf(history): gate the unconditional History-tab reload + fix the diagnostics history regression (v3.3.101)
v3.3.100's interaction instrument pinpointed the residual UI lag exactly: every
slow click was a tab switch into the History view (button.tab / nav.tab-bar,
200-840ms), each coupled to `ipc get-history wall=150-200ms sync` +
`main-longtask blocked=242-289ms lastIpc=get-history` + a 200-248ms renderer long
task. Uploads themselves are pristine (event-loop mean 11.7ms; the only spikes line
up with the get-history tab switches).

Root cause: the History tab handler called loadHistory() UNCONDITIONALLY on every
activation (renderer/app.js) even though it tracks a `_historyDirty` flag it never
checked. Each call synchronously readFileSync + JSON.parse's the ~185MB /
30000-entry electron-history.json (no cache), ships all 30000 batches across IPC,
and the renderer flattens ~120000 row objects before .slice(-2000) for the DOM (the
DOM is already capped at 2000).

Fix (the adversary-verified safe subset):
- Gate the History-tab load: only reload when `_historyDirty || !_historyEverLoaded`.
  Added `_historyEverLoaded`, and both flags are now set inside loadHistory() after
  the fetch succeeds (so a failed load retries). Dirty-coverage is complete — every
  history append routes through batch-done -> appendHistory and upload-batch-done ->
  handleBatchDone which sets _historyDirty=true. Result: repeat History tab switches
  with no new uploads do zero IPC/parse/flatten and are instant. (The first open
  after a new batch still parses once ~450ms; removing that needs a parse-cache or
  JSONL storage, deferred.)
- Diagnostics history regression: getHistory() read loadConfig().history, but since
  the v3.3.99 history split load() returns history:[] in packaged mode, so remote
  diagnostics reported totalBatches:0 despite 30000 real batches. It now reads
  loadHistory() (injected via the collector deps), with a backward-compatible
  fallback to loadConfig().history when not provided.

407 tests pass (2 new diagnostics regression tests); clean Electron boot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:45:50 +02:00
Administrator
003e14dfe9 feat(diagnostics): renderer interaction timing — measure every UI switch/click, not just main-thread (v3.3.100)
Closes the last measurement gap. The main process was already fully instrumented
(every ipcMain handler timed at >=50ms, a 100ms main-thread long-task monitor with
channel attribution, config load/serialize timing). Account switches were already
covered too: switchAccount is a trivial synchronous Map-set and the rotation work
is async, so a switch cannot block the main thread, and any block that did occur
would surface in the long-task monitor.

The real gap was the renderer side: the renderer-perf line was gated on active
uploads (idle clicks were never logged) and only reported an aggregate longtask
count — no per-interaction latency and no element attribution. So a switch/sort/tab
that janked in the renderer (not main) was invisible.

renderer/app.js (additive, self-silencing, wrapped in try/catch):
- An Event Timing observer (PerformanceObserver type:'event', durationThreshold:50,
  buffered) logs `renderer-interaction <type> dur=Xms proc=Yms target=<el>` for every
  user interaction whose latency exceeds 50ms — always on, idle or under load — and
  names the element (id / first class / data-action / aria-label / title). This is
  the direct click->reaction latency the user feels.
- The longtask observer now also logs `renderer-longtask dur=Xms` immediately for any
  single renderer long task >=100ms, regardless of upload state.

405 tests pass; clean Electron boot. Every action — main or renderer, idle or under
load — now names itself in the log if it is slow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:21:41 +02:00
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
6b5349c5f7 perf(renderer): stop formatting a timestamp on every progress event
maybeAddSessionFile computed formatDateTime(new Date()) at the top, before the status==='done' guard that early-returns for every other status. formatDateTime runs two Intl locale formats (~83us measured), so it executed on every progress event — onUploadProgressBatch loops the M-item batch into handleProgress -> maybeAddSessionFile, i.e. 10xM times per second — and threw the result away for all non-done events (the vast majority while uploading).

That made each progress batch a synchronous main-thread block scaling with the active-upload count: ~2.4ms at 25 concurrent, ~5ms at 61, every 100ms, on top of render and sort — enough to blow the 16ms frame budget and stutter scrolling. It is per-event, not per-render, so it janks regardless of scroll position, matching the user's report that the lag appears above ~50 connections and when the uploading rows are scrolled out of view.

Move the formatDateTime call inside the dedup block so it runs once per genuinely-new completed upload. A faithful Blink benchmark at the user's regime (500 rows virtualized, dynamic progress sort, scrolling) shows the per-batch cost drop from 1.7/3.2/4.1ms at 25/50/61 concurrent to a flat 0ms, and frame P95 at 61 concurrent from 7.3ms to 4.2ms.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 05:05:01 +02:00
Administrator
0f9096be3c perf(renderer): keep recent-uploads panel append-only past the cap (kill ~80ms per-completion freeze)
The recent-uploads panel had a cheap append-only fast path, but it was gated on
`rows.length > _recentLastRenderedLen`. maybeAddSessionFile caps sessionFilesData
by push-then-slice (2000 -> 2001 -> sliced back to 2000), so once a session
produces more than SESSION_FILES_CAP rows the length is pinned at the cap and the
gate is false forever. Every subsequent completion then fell through to the full
`tbody.innerHTML = rows.map(...).join('')` rebuild of all ~2000 rows.

The cap is per (link x file x hoster), so with 4-5 selected hosters the 2000 cap
is hit at only ~400-500 distinct files — very reachable in a long folder-monitor
session. Profiled in Chromium (same Blink engine as Electron, table-layout:fixed):
the full 2000-row rebuild costs ~80ms and ran on EVERY completion past the cap — a
repeating ~80ms main-thread freeze. That is the "fine on a fresh start, gets laggy
after many uploads while CPU (~40%) and RAM (~6GB, stable) stay normal" symptom: a
render-thread stall, not CPU saturation or a memory leak.

Fix: track newly-pushed rows in _recentPendingAppends (incremented in
maybeAddSessionFile, consumed every render) and gate the fast path on
`pendingAppends > 0` instead of length growth, so it survives the cap. Prepend the
new rows, then evict the same overflow count from the DOM bottom (oldest rows,
which is where the date-desc view places the front-of-array entries the cap slices
off). DOM work is O(added) again. The fast path is gated behind an explicit
`appendOnly` flag passed only by scheduleRecentRender's rAF, so selection / delete
/ clear / sort / batch-done renders stay full rebuilds and cannot wrong-evict or
double-prepend.

Verified in Blink over a simulated 5000-completion session (3/frame, far past the
cap): per-frame render 80ms -> median 7.4ms (>10x), and the DOM stays exactly equal
to the data (cap held at 2000, newest-on-top, oldest evicted, zero duplicates).
397/397 tests pass, eslint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 01:47:59 +02:00
Administrator
0c6c502aab feat(diagnostics): network bind + fail-closed IP allowlist + host-in-code (Tailscale, like rd-diagnostics-mcp)
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>
2026-06-19 19:32:29 +02:00
Administrator
d69e5c39bf feat(diagnostics): MCP gateway + harden redaction so no secret ever leaves the box
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>
2026-06-19 17:39:31 +02:00
Administrator
ab7313f32c feat(diagnostics): read-only remote diagnostics agent over the existing WS transport (app side)
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>
2026-06-19 17:24:32 +02:00
Administrator
b1ad04d9c2 fix(queue): close three more ghost / lost-work holes found by a second adversarial hunt
A 36-agent adversarial sweep over the queue-persistence context surfaced three
NEW, reachable defects (all renderer-only) that the v3.3.80-82 work missed:

#1 retrySelectedJobs (the reuploadBtn / "erneut hochladen" path) is a THIRD
   re-add path that never cleared the completed-dedup guard. _completedUploadKeys
   gains file|hoster when a job reaches done; retry of a done row reset the job to
   pending and re-added its path to selectedFiles but kept the key. On a restart
   before the re-upload re-completes, restoreQueueStateFromConfig re-seeds the key
   and buildQueuePreview then refuses to recreate the row, so the deliberate
   re-upload silently vanished (lost work). Retry now clears the exact file|hoster
   keys it re-activates (per key, NOT per path, so a sibling hosters completed
   ghost is not resurrected).

#7 The folder-monitor pre-selected-hosters branch re-adds a path and calls
   buildQueuePreview WITHOUT going through applyHosterSelection, so the v3.3.82
   key-clear never ran. With removeFromQueueOnDone ON, a re-encoded / re-dropped
   file whose row was auto-removed was suppressed forever (silent no-upload that
   even survived a restart). The branch now clears the dedup keys for the freshly
   re-added paths, matching the manual modal flow.

#4 Deleting one hosters row of a multi-hoster file was silently undone. A
   manually deleted non-completed file|hoster pair is recreated by buildQueuePreview
   the next time it runs (adding another file, or a folder-monitor drop), because
   the recreate guard only consulted _completedUploadKeys, never the per-cell
   deletion. Result: the file got uploaded to a hoster the user explicitly
   removed, burning quota and publishing an unwanted link. Introduces
   _suppressedPreviewKeys: a manual delete of a non-done job whose file stays
   selected (pinned by a sibling job) suppresses that exact file|hoster from
   re-creation; the suppression is persisted in pendingQueue.suppressedKeys and
   re-seeded on restore (mirroring completedKeys), and is cleared whenever the
   user deliberately re-adds the file (modal or folder monitor) so a real re-add
   still works.

The clear-on-re-add logic for both sets is unified into clearDedupKeysForPaths()
and reused by applyHosterSelection and the folder-monitor branch.

Held for a separate decision/round (surfaced to the user): single-instance lock,
the sync-close write-sequence guard (its ghost symptom is masked by the existing
microtask-ordered removal + restore-time ts-gate; its real residual is rotation/
history integrity), and the upload-log timezone/same-basename edges (need a log
format change). 362 tests pass, lint clean, smoke boots identically to baseline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 16:19:47 +02:00
Administrator
0a607adb29 fix(queue): stop finished uploads from re-appearing as pending ghosts across restart
The completed-upload dedup guard (_completedUploadKeys, "file|hoster") is the
single source of truth that keeps a finished job from being re-materialised as a
"Bereit" preview by buildQueuePreview(). Three independent paths leaked ghosts
back into the queue:

A) removeJobFromIndex() unconditionally DELETED the dedup key. The
   removeFromQueueOnDone auto-remove path (handleProgress), the batch-done sweep,
   and the terminal-job prune all call removeJobFromIndex on a *finished* job, so
   the guard handleProgress had just added was immediately wiped and
   buildQueuePreview re-created the job as a preview both mid-session and after
   restart. removeJobFromIndex now takes keepCompletedKey; the three auto-removal
   call sites pass true (keep the guard), while the manual-delete sites keep the
   old behaviour (drop the guard so the user can re-add the file).

D/E) The dedup guard lived only in memory, so every restart began with an empty
   set and buildQueuePreview rebuilt ghosts from the persisted selectedFiles.
   buildPersistedQueueState() now serialises the keys whose file is still in the
   snapshot (completedKeys) and restoreQueueStateFromConfig() re-seeds them. To
   keep deliberate re-uploads working, applyHosterSelection() clears the
   persisted key for every freshly (re-)added file: re-adding through the hoster
   modal is the explicit "upload this again" signal. Both retry paths
   (retrySelectedJobs, _retryFailedFromBuckets) mutate the existing job in place
   instead of relying on buildQueuePreview, so they are unaffected.

H) buildQueuePreview() excluded error jobs from its existingKeys set, so a
   file+hoster that already had an 'error' row got a second 'preview' row stacked
   beside it. Error jobs now count as existing.

B/C) parseUploadLogLine() took the filename from a fixed field index and trimmed
   it. A pipe inside the link shifted the field (entry lost from the log-based
   dedup) and trimming broke matching against the untrimmed OS basename used as
   the queue-job key. The filename is now the last non-empty field and is no
   longer trimmed, so log lines with pipes in the URL and leading-space filenames
   both match end-to-end.

362 tests pass, lint clean on all touched files.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 14:53:04 +02:00
Administrator
6b0c515a9b fix(queue): close two lost-work / sticky-ghost edge cases found by intensive testing
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>
2026-06-19 06:51:48 +02:00
Administrator
eeec1d150c fix(queue): completed files no longer reappear in the queue after restart
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>
2026-06-19 04:35:27 +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
3204629fef feat(rotation): opt-in per-hoster account rotation to keep all accounts active
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>
2026-06-17 03:20:50 +02:00
Administrator
1aa36cdd8b fix(ui): queue re-renders on maximize + settings sub-tabs + null-safe saveSettings
Three changes, investigated and adversarially reviewed via multi-agent workflow.

1. Queue maximize bug: the virtual-scrolled upload queue derived its visible-row
   window from #queueContainer.clientHeight but only recomputed it on 'scroll'.
   Maximizing the window enlarges the container without a scroll event, so rows
   below the old viewport stayed blank until you scrolled. Fix: a ResizeObserver
   on #queueContainer reusing the existing rAF-coalesced _onQueueScroll. Strictly
   stronger than a window 'resize' listener — it also covers the hidden->visible
   view-switch case (container 0 -> real height). No feedback loop (container box
   is flex-sized, not content-sized); early-returns for <200-row queues.

2. Einstellungen tab restructured from 4 dense stacked collapsible panels into
   horizontal sub-tabs: Allgemein / Automatik / Logs & Diagnose / Fernsteuerung /
   Backup. All sub-pages render into the DOM at once (only the active one is
   shown), so every element id stays present and saveSettings keeps working. The
   cluttered "Allgemein" block is split across Allgemein + Automatik + Logs.
   Per-hoster settings already moved to Accounts in v3.3.69.

3. saveSettings hardened to be null-safe (elTxt/elChk/elInt helpers): when a
   settings element is absent from the DOM it now keeps the current config value
   instead of collapsing to a default. Behavior is identical when elements are
   present; this is insurance against a future dropped id silently overwriting a
   real setting (e.g. webhook URL, folder-monitor hoster pre-selection).

Verified: 43 sub-tab ids unique, all 26 saveSettings ids present, lint clean,
boot clean, 284 tests green, 0 confirmed defects from a 3-lens adversarial review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 21:44:38 +02:00
Administrator
3883e6f0b4 fix(accounts): per-hoster "Upload-Einstellungen" reachable without expanding the hoster
The settings section was nested inside the collapsible cards body, so you had to
expand a hoster group first before you could open its upload settings. Moved the
section out to the group level (sibling of the cards body), so the
"Upload-Einstellungen" toggle is always visible directly under each hoster header
— collapsed or not — for every hoster. Styled as a full-width row matching the
group header. Group-cards toggle and the settings toggle are now fully independent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 01:32:17 +02:00
Administrator
b1826eae00 feat(settings): move per-hoster upload settings into the Accounts tab
Declutters the Einstellungen tab: the per-hoster panels (Retries, Max Speed,
Parallel Uploads, Restart-below, Interval, Max Size, log-to-file) are no longer
listed there. Each hoster group in the Accounts tab now has a collapsible
"Upload-Einstellungen" section with exactly those controls, so everything about
a hoster lives in one place. The Einstellungen tab shows a short pointer to where
they moved.

Per-hoster edits in Accounts save via a dedicated lightweight handler
(scheduleHosterSettingsSave → saveHosterSettings) that touches only
hosterSettings — it deliberately does NOT route through the global saveSettings,
so tweaking a hoster slider can't re-run the folder-monitor start/stop or clobber
global fields. The global saveSettings still reads .hs-input[data-hoster] wherever
they live, and global inputs (which have no data-hoster) are unaffected. The new
inputs drop the .settings-autosave class and bind via delegation on the persistent
accounts container so they survive re-renders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 01:03:37 +02:00
Administrator
e5d7917cf3 feat(ui): downloader-style top menu bar (Datei / Einstellungen / Hilfe)
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>
2026-06-15 00:45:39 +02:00
Administrator
33364168f1 feat(accounts): "Alle ausklappen" / "Alle einklappen" toggle for hoster groups
Adds a bottom-right button under the accounts list that expands every hoster
group at once (and collapses them again — the label flips based on current
state). Reuses the existing _hosterGroupOpenMemory so the per-group open/closed
state stays consistent with manual header clicks. The footer is hidden when no
accounts exist. Pure DOM toggle, no re-render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 00:28:13 +02:00
Administrator
05ad08433c feat(history): configurable retention + render cap so the Verlauf tab loads fast
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>
2026-06-15 00:26:20 +02:00
Administrator
c75cb6c079 fix(ui): files panel sort cache goes stale at the 2000-row cap — newest entry never appeared without re-sorting 2026-06-10 23:16:31 +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
e69635bcf7 fix(backup): exclude upload history from export and import — Verlauf stays local 2026-06-10 15:42:09 +02:00
Administrator
0e5eaa89e6 fix(webhook): retry+429+status handling, await before shutdown, error-path notify, abort/auto-retry suppress, Discord limits 2026-06-10 00:08:02 +02:00
Administrator
e1d04d0838 feat(webhook): optional Discord ping (user-id / role / @here / @everyone) so batch-done actually notifies 2026-06-09 23:40:38 +02:00
Administrator
1d116ac4bf fix(ui+byse): live newest-on-top in files panel; byse skips 30s recovery poll on explicit reject 2026-06-09 21:55:34 +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
de371e56a3 fix(ui): hydrate missing file sizes for queued/waiting rows + show '...' fallback 2026-06-09 06:01:42 +02:00
Administrator
82e0163d3f fix(ui): 'Ausgewählte starten' during active upload also accepts preview jobs 2026-06-09 05:02:28 +02:00
Administrator
0f72478a2e fix(ui): remove batch summary modal at end of upload 2026-06-09 04:57:54 +02:00
Administrator
d3fda31243 fix(ui): ETA includes waiting jobs — folder-added files now ship with bytesTotal 2026-06-08 23:03:29 +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
f4b5fadc5f fix(ui): first click on sort header sets default direction instead of toggling 2026-06-08 19:22:29 +02:00
Administrator
1418c2bc17 feat(backup): plain JSON export/import + clearer error when decrypt fails 2026-06-08 14:19:47 +02:00
Administrator
35341b522a fix(accounts): allow health check during active uploads + toast when already running 2026-06-08 03:04:00 +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
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
f42c55c521 feat(diagnostics): log levels, support bundle export, verbose toggle, log paths panel 2026-06-07 16:34:51 +02:00
Administrator
4f41218a92 fix(ui): log mode select no longer truncates 'Pro Session' 2026-06-07 04:49:16 +02:00
Administrator
ce0bbb8b7e fix(ui): no group auto-expand while checking; only on actual error 2026-06-07 04:40:52 +02:00
Administrator
2e8e8a3819 fix(accounts): VOE CSRF burst-throttle + collapsible per-hoster groups
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>
2026-06-07 03:27:57 +02:00
Administrator
e26b7ea8ed fix(accounts): never persist unverified creds + dedupe-proof modal + label + perf
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>
2026-06-07 03:11:13 +02:00
Administrator
b5ff9b1a0b fix(ui): queue columns fit the window on resize (fullscreen → windowed)
Saved column widths were applied as fixed pixels, so switching from fullscreen
to windowed mode left the column sum wider than the viewport and the user had to
manually drag the window wider just to see the rightmost columns.

Now: a separate _idealColumnWidths map holds the user's preferred widths
(persisted), and _applyFittedColumnWidths reshapes the displayed widths to fit
the current container width. When sum(ideals) > container.clientWidth, every
column is scaled by the same factor so the row exactly fits (and a hidden
column becomes visible again).

- Two-tier widths: ideals are only updated by an explicit drag, not by a
  resize-driven refit. So dragging while the window is narrow no longer
  permanently shrinks every other column.
- saveDraggedColumnWidth(col, w) saves a single column's new ideal.
- Window-resize listener refits with a 60ms debounce.

Lint clean, full suite 200/200.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 05:39:28 +02:00
Administrator
d720ba295a feat(log): add per-session log mode (one file per app launch)
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>
2026-05-30 14:41:06 +02:00
Administrator
af51bebaf7 fix(queue): stop auto-dedup from deleting pending jobs on restart/update
Reproduced from a real saved config: pendingQueue held 4 'preview' jobs (one
file across 4 hosters); the queue saved + restored correctly. But
_autoDeduplicateFromLog (runs at init after restore) removed jobs whose
fileName|hoster appeared ANYWHERE in the lifetime fileuploader.log, regardless
of status — so all 4 pending previews were deleted and the queue showed the
empty "Dateien hierhin ziehen" state. Looked update-specific only because the
server restarts on update; a plain restart did the same.

- New lib/queue-dedup.js (pure, dual CJS/window export like queue-prune.js):
  partitionRestoredJobsByLog drops ONLY 'done' jobs that match the log. Pending
  (preview/queued) and failed (error/aborted) jobs always survive — they're
  intentional queued work (often a deliberate re-upload of a previously
  uploaded file). Manual importUploadLog stays separate/explicit.
- renderer wires it in; index.html loads the module before app.js.
- Tests: 5 cases incl. the exact reproduced scenario (4 previews all in log ->
  0 removed). Full suite 162/162.

Verified against the user's real electron-config.json + fileuploader.log: old
logic removed 4/4 (empty queue), new logic removes 0/4 (queue preserved).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 01:08:59 +02:00
Administrator
bd42c86796 ux(log): clarify logToFile also affects restart dedup
Deep bug-hunt of the per-hoster logToFile feature found the feature
itself clean (7 data flows traced: secret-store leaves hosterSettings
alone, save round-trip preserves the key for account-less hosters,
backup import/export round-trips, updateSettings full-replaces with
default-true fallback, checkbox branch precedes numeric coercion,
boolean survives IPC→JSON→parse intact).

The one real interaction effect: _autoDeduplicateFromLog reads
fileuploader.log on startup to drop already-uploaded files from the
restored queue. With logToFile off for a hoster, its entries are
absent, so the same file could be re-uploaded after a restart. The
dedup↔log coupling predates this feature; the toggle just makes it
observable.

Make it transparent in the checkbox hint rather than silently
shipping the surprise. Full decoupling (a separate always-written
dedup index independent of the user-facing log) is a larger,
separate change with its own risk surface — deferred unless wanted.

147/147 tests still green.
2026-05-23 15:46:27 +02:00