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>
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>
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>
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>
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>
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.
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.
Two issues:
1. Verlauf-Export CSV put the opaque file_code in the Link column when
the upload had no real URL, so the column looked like just a bunch
of IDs. Now only real http(s) URLs land in that column.
2. Hoster passwords and API keys were stored as plaintext in
electron-config.json. Now wrapped with Electron's safeStorage (DPAPI
on Windows, Keychain on macOS, libsecret on Linux) and stored as
'enc:v1:<base64>'.
Credentials are decrypted on load so in-memory flows stay unchanged,
and backups still export plaintext inside the existing .mhu envelope
so they remain portable between machines/users. Legacy plaintext
configs auto-migrate on next write.
- Config write serialization via _writeQueue prevents concurrent
read-modify-write races between settings/queue/history saves
- Cancel active uploads on app quit (prevents zombie processes)
- Persist queue before update install (prevents queue loss)
- Sync IPC save in beforeunload (guarantees save before close)
- Fix double configStore.load() call
- Guard against status regression in handleProgress (done→uploading)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Multiple accounts per hoster with drag-sortable priority (primary + fallbacks)
- Separate account types: Web Login and API selectable per hoster
- Account fallback: after all retries fail, automatically switches to next fallback account
- Fix: Byse health check returning [Fehler] OK when API responds with msg "OK"
- Fix: retry during active upload sets status to "Wartet" instead of "Bereit"
- Config migration from single-object to multi-account array format
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Small always-on-top drop target window (toggle in Settings > Allgemein)
- Files dropped on it get added to the queue with hoster modal
- Auto-shows on app start if previously enabled
- Column headers now in English (Filename, Uploaded/Size, Progress)
- Statusbar labels in English (Connections, Total)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Hoster pre-selection in Ordnerüberwachung settings (only configured accounts shown)
- With preset hosters: files go directly to queue without modal
- Without preset: hoster modal opens as before
- Fix: Aktiv badge now green on initial render
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New FolderMonitor class with chokidar for watching folders
- Settings UI panel with all options (extensions filter, recursive, auto-start, skip duplicates)
- Auto-queue and auto-upload when files appear in monitored folder
- Fix statusbar to show uploaded/remaining instead of cumulative session bytes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New "Neues Log pro Session" checkbox in settings. When enabled,
each app session creates a separate log file with timestamp
(e.g. fileuploader-2026-03-11_20-30-15.log). File is only created
when an upload actually completes. When disabled, behaves as before
(single appending log file).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- All config writes now go through _atomicWrite() (write to .tmp, backup
to .bak, rename .tmp to main config)
- load() falls back to .bak if main config is empty or corrupt
- Prevents 0KB config files caused by process termination during write
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Checks alternate AppData folder names and portable exe directory
to find existing config when current path has no config file.
Prevents losing accounts, settings, and queue after updates.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Global speed throttle (shared across all uploads)
- Settings grouped into sections (Uploads, Verhalten, Log)
- Abort all resets jobs to queued (restartable without reupload)
- fileuploader.log writes immediately per upload
- Staggered interval per hoster (not parallel sleep)
- Recent files panel resizable via drag handle
- History hides aborted entries
- Done jobs removed from queue immediately when setting active
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add doodstream.com web login (email+password) as alternative to API key
- Fix doodstream login: use X-Requested-With header for JSON response
- Add "Aus der Queue entfernen bei Abschluss" setting
- Fix byse.sx download URLs to use /d/ prefix
- Make config writes async to prevent race conditions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add byse.sx health check via API upload/server endpoint
- Virtual scrolling for queue table (>200 rows renders only visible rows)
- O(1) job lookups via index Maps instead of O(n) array.find()
- Event delegation on queue tbody instead of per-row listeners
- Async config writes to avoid blocking main process
- Increase persist debounce to 3s during uploads (was 250ms)
- Reduce debug logging to state changes only
- Move save button to bottom-right in settings
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root cause: startBatch() ran synchronously inside ipcMain.handle()
callback, causing webContents.send() events to conflict with the
handle response and never reach the renderer.
Fix: defer startBatch() via process.nextTick so IPC response is
sent first, then upload events flow correctly.
Also:
- Add .catch() on startBatch to surface hidden errors
- Fix settings panel not updating after save (renderSettings)
- Add select-folder IPC handler (was in preload but missing)
- Add debug-log and debug-test-upload IPC for diagnostics
- Add upload-debug.log file for tracing upload flow
- Add unhandledRejection handler for main process
- Add scramble defaults to config-store globalSettings
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add FIFO semaphore for per-hoster concurrency control
- Add token-bucket speed limiter with abort signal support
- Rewrite upload-manager with retry loop, speed monitoring, and rich progress events
- Add per-hoster settings: retries, max speed, parallel count, restart below speed, time interval, max size
- Add context menu with shutdown-after-finish (sleep/shutdown/restart), always-on-top
- Add z-o-o-m-style queue table with 8 columns, status-colored rows, progress bars
- Add debounced queue rendering with scroll position preservation
- Add statusbar with global speed, total bytes, elapsed time
- Fix speedMonitor interval leak on error and scoping bug
- Fix throttle not respecting abort signal during cancellation
- Fix combined signal listener cleanup
- Bump version to 1.1.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>