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>
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>
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>
Live diagnosis against the real account: it holds 90,548 files. The recovery
fetched only page 1 of /api/file/list without forcing order, so a just-uploaded
file could be missed if the default sort isn't newest-first. Add
sort=created&order=desc — verified to return the account's newest uploads first —
so the codeless-result recovery reliably finds the file regardless of account
size.
Diagnosis also confirmed the fix's premise: the API key is valid, /api/upload/server
returns a working node even on a lapsed-premium account, and uploads DO land
server-side (many Burn Notice episodes uploaded in the failure window are present).
So the API path returns the filecode directly and sidesteps the intermittent
web-form empty-result; entering the API key makes uploads succeed. 183/183.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Direct improvement to the login/web path (no API key needed): we were POSTing a
stale field set — sess_id + utype=reg + file — but doodstream's CURRENT upload
form dropped `utype` and added file_title / fakefilepc / submit_btn. Submitting
an incomplete/stale field set can make the CDN node accept the bytes but skip
the registration step (→ the empty result form with no fn). Now we parse the
live upload form (already fetched in _getUploadServer) and replicate ALL its
non-file fields faithfully — exactly what the browser submits — while keeping
sess_id (the fresh node token) and utype as a harmless compatibility extra.
- _parseUploadFormFields(html): pull every named input/button from the upload
form, excluding the file input (streamed separately). Adapts to whatever
fields doodstream uses now rather than hardcoding.
- upload() builds the multipart from those fields; minimal known-good fallback
if the form wasn't parsed.
- Tests: real-form extraction (incl. file-input exclusion) + no-form safety. 183/183.
Low regression risk (superset of the previously-working fields). Whether it
resolves the large-file empty-form is for the server run; the API path
(3.3.31/32) remains the reliable route when a key is available/derivable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes the gap between the unit-tested parseDoodstreamResult and the real
uploadFile orchestration. Mocks the undici transport (reassign undici.request +
refresh the hosters cache; mock.module needs an experimental flag npm test
doesn't pass) and global fetch, then drives the full doodstream API path against
the doc-verified response shapes:
- filecode returned directly in result[0].filecode → used.
- codeless 2xx → recovered by polling file/list and name-matching the title.
- codeless + file never appears → throws with err.hosterTransient=true (so the
account is not blacklisted).
Verified live this session: doodapi.co returns {"status":400,"msg":"Invalid
key"} for a bad key, so validation/list logic keys off status correctly.
Also makes the recovery poll count/delay tunable via __test.DOODSTREAM_POLL
(same 12 × 2.5 s defaults — non-behavioral) so the exhaustion test runs in ms.
Full suite 181/181.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Backstop for the API path: if the doodapi upload POST returns no filecode (the
same backend registration hiccup that empties the web form), poll
doodapi.co/api/file/list for a newly-appeared file whose normalized title
matches what we uploaded, and claim its code — instead of failing the upload.
This is the exact recovery byse already uses in this file for the identical
symptom (large MKV, server-side "OK" but empty immediate response, file shows up
in the account shortly after). Doodstream is the same XFileSharing family with
the same doodapi-style API, and it directly addresses the user's observation
that the same file often succeeds on a second run.
- _fetchDoodstreamFileList / _resolveDoodstreamUploadByName: list via
/api/file/list?key=&per_page=200, baseline-diff + exact normalized-title match
(never "take the only new one", so parallel uploads can't claim each other's
files), 12 polls × 2.5s.
- uploadFile snapshots a doodstream baseline before upload and polls after a
codeless result, before the hosterTransient throw.
Verified solo: doodapi.co is reachable and returns {"status":400,"msg":"Invalid
key"} for a bad key, so the validation/list path keys off status correctly.
178/178. The real large-file run on the server is the final confirmation.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The user uploads with username/password (login), so 3.3.30's "use API when a
key is configured" did nothing for them — and the web-form upload keeps failing
with empty forms on large files. Fix the LOGIN path itself: after logging in,
pull the account's API key out of the logged-in session and upload via the
official doodapi API (which returns result[0].filecode directly, no empty form).
The user keeps using login and configures nothing.
How the key is derived without knowing doodstream's (cookie-gated, unseen)
settings DOM: brute-force candidate extraction + API validation.
- DoodstreamUploader.deriveApiKey(): fetch the logged-in settings page
(?op=my_account / /settings), pull every plausible long token from form-field
values + element contents (ranked: tokens near an "api" mention first), and
validate each against doodapi.co/api/account/info — only the account's real
key returns status 200. A wrong guess is therefore harmless (fails validation
→ web fallback). Logs the raw settings HTML when nothing validates, so the
scrape can be refined from a real capture if doodstream's markup differs.
- upload-manager: doodstream login-path now resolves the key ONCE per batch
(cached by accountId; '' = tried-none) and routes to the API when found, else
the existing web-form upload. Keyless accounts: one extra probe-login per
batch, then unchanged.
- Tests: candidate extraction (value/textarea/api_key shapes, api-context
ranking), validate-then-pick, null→web-fallback, preset short-circuit. 178/178.
If derivation works the login path now uploads via the API. It does NOT change
doodstream's backend; the server run confirms. Falls back safely if no key.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>