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>
140 lines
6.2 KiB
JavaScript
140 lines
6.2 KiB
JavaScript
const { contextBridge, ipcRenderer, webUtils } = require('electron');
|
|
|
|
contextBridge.exposeInMainWorld('api', {
|
|
// Config
|
|
getConfig: () => ipcRenderer.invoke('get-config'),
|
|
saveConfig: (config) => ipcRenderer.invoke('save-config', config),
|
|
getHistory: () => ipcRenderer.invoke('get-history'),
|
|
clearHistory: () => ipcRenderer.invoke('clear-history'),
|
|
exportHistory: (format) => ipcRenderer.invoke('export-history', format),
|
|
saveTextFile: (defaultName, content, filters) => ipcRenderer.invoke('save-text-file', defaultName, content, filters),
|
|
|
|
// Hoster settings
|
|
getHosterSettings: () => ipcRenderer.invoke('get-hoster-settings'),
|
|
saveHosterSettings: (settings) => ipcRenderer.invoke('save-hoster-settings', settings),
|
|
|
|
// Global settings
|
|
getGlobalSettings: () => ipcRenderer.invoke('get-global-settings'),
|
|
saveGlobalSettings: (settings) => ipcRenderer.invoke('save-global-settings', settings),
|
|
saveGlobalSettingsSync: (settings) => ipcRenderer.sendSync('save-global-settings-sync', settings),
|
|
|
|
// Always on top
|
|
setAlwaysOnTop: (value) => ipcRenderer.invoke('set-always-on-top', value),
|
|
getAlwaysOnTop: () => ipcRenderer.invoke('get-always-on-top'),
|
|
|
|
// Shutdown after finish
|
|
setShutdownAfterFinish: (mode) => ipcRenderer.invoke('set-shutdown-after-finish', mode),
|
|
getShutdownAfterFinish: () => ipcRenderer.invoke('get-shutdown-after-finish'),
|
|
cancelShutdown: () => ipcRenderer.invoke('cancel-shutdown'),
|
|
|
|
// File selection
|
|
selectFiles: () => ipcRenderer.invoke('select-files'),
|
|
selectFolder: () => ipcRenderer.invoke('select-folder'),
|
|
resolveFolderFiles: (folderPath) => ipcRenderer.invoke('resolve-folder-files', folderPath),
|
|
|
|
// Upload control
|
|
startUpload: (payload) => ipcRenderer.invoke('start-upload', payload),
|
|
cancelUpload: () => ipcRenderer.invoke('cancel-upload'),
|
|
cancelSelectedJobs: (jobIds) => ipcRenderer.invoke('cancel-selected-jobs', jobIds),
|
|
addJobsToBatch: (payload) => ipcRenderer.invoke('add-jobs-to-batch', payload),
|
|
finishAfterActive: () => ipcRenderer.invoke('finish-after-active'),
|
|
runHealthCheck: (payload) => ipcRenderer.invoke('run-health-check', payload),
|
|
validateCredentials: (payload) => ipcRenderer.invoke('validate-credentials', payload),
|
|
|
|
// Log import
|
|
readOwnUploadLog: () => ipcRenderer.invoke('read-own-upload-log'),
|
|
importUploadLog: () => ipcRenderer.invoke('import-upload-log'),
|
|
|
|
// Clipboard
|
|
copyToClipboard: (text) => ipcRenderer.invoke('copy-to-clipboard', text),
|
|
|
|
// Updates
|
|
checkForUpdate: () => ipcRenderer.invoke('app:check-updates'),
|
|
installUpdate: () => ipcRenderer.invoke('app:install-update'),
|
|
abortUpdate: () => ipcRenderer.invoke('app:abort-update'),
|
|
getVersion: () => ipcRenderer.invoke('app:get-version'),
|
|
onUpdateAvailable: (callback) => {
|
|
ipcRenderer.on('app:update-available', (_event, data) => callback(data));
|
|
},
|
|
onUpdateProgress: (callback) => {
|
|
ipcRenderer.on('app:update-progress', (_event, data) => callback(data));
|
|
},
|
|
|
|
// Backup
|
|
exportBackup: () => ipcRenderer.invoke('export-backup'),
|
|
importBackup: (legacyPassword) => ipcRenderer.invoke('import-backup', legacyPassword),
|
|
|
|
// Folder Monitor
|
|
folderMonitorStart: (settings) => ipcRenderer.invoke('folder-monitor:start', settings),
|
|
folderMonitorStop: () => ipcRenderer.invoke('folder-monitor:stop'),
|
|
folderMonitorStatus: () => ipcRenderer.invoke('folder-monitor:status'),
|
|
folderMonitorSelectFolder: () => ipcRenderer.invoke('folder-monitor:select-folder'),
|
|
onFolderMonitorNewFiles: (callback) => {
|
|
ipcRenderer.on('folder-monitor:new-files', (_event, data) => callback(data));
|
|
},
|
|
|
|
// Account switched event
|
|
onAccountSwitched: (callback) => {
|
|
ipcRenderer.on('account-switched', (_event, data) => callback(data));
|
|
},
|
|
|
|
// Drop Target
|
|
showDropTarget: () => ipcRenderer.invoke('show-drop-target'),
|
|
hideDropTarget: () => ipcRenderer.invoke('hide-drop-target'),
|
|
onDropTargetFiles: (callback) => {
|
|
ipcRenderer.on('drop-target:files', (_event, paths) => callback(paths));
|
|
},
|
|
|
|
// Debug
|
|
debugTestUpload: () => ipcRenderer.invoke('debug-test-upload'),
|
|
debugLog: (msg) => ipcRenderer.invoke('debug-log', msg),
|
|
|
|
// Events (main -> renderer)
|
|
onUploadProgress: (callback) => {
|
|
ipcRenderer.on('upload-progress', (_event, data) => callback(data));
|
|
},
|
|
onUploadBatchDone: (callback) => {
|
|
ipcRenderer.on('upload-batch-done', (_event, data) => callback(data));
|
|
},
|
|
onUploadStats: (callback) => {
|
|
ipcRenderer.on('upload-stats', (_event, data) => callback(data));
|
|
},
|
|
onShutdownCountdown: (callback) => {
|
|
ipcRenderer.on('shutdown-countdown', (_event, data) => callback(data));
|
|
},
|
|
onUploadLogFallback: (callback) => {
|
|
ipcRenderer.on('upload-log-fallback', (_event, data) => callback(data));
|
|
},
|
|
onAccountRotationLog: (callback) => {
|
|
ipcRenderer.on('account-rotation-log', (_event, data) => callback(data));
|
|
},
|
|
openLogFolder: () => ipcRenderer.invoke('open-log-folder'),
|
|
getJobLog: (jobId) => ipcRenderer.invoke('get-job-log', jobId),
|
|
onLogPathAutoUpdated: (callback) => {
|
|
ipcRenderer.on('log-path-auto-updated', (_event, data) => callback(data));
|
|
},
|
|
// Remote Control
|
|
remoteGetSettings: () => ipcRenderer.invoke('remote:get-settings'),
|
|
remoteSaveSettings: (settings) => ipcRenderer.invoke('remote:save-settings', settings),
|
|
remoteGenerateToken: () => ipcRenderer.invoke('remote:generate-token'),
|
|
remoteStatus: () => ipcRenderer.invoke('remote:status'),
|
|
onRemoteClientCount: (callback) => {
|
|
ipcRenderer.on('remote:client-count', (_event, count) => callback(count));
|
|
},
|
|
|
|
// File path from drag & drop (Electron 33+ compatible)
|
|
getPathForFile: (file) => webUtils.getPathForFile(file),
|
|
removeAllListeners: () => {
|
|
ipcRenderer.removeAllListeners('upload-progress');
|
|
ipcRenderer.removeAllListeners('upload-batch-done');
|
|
ipcRenderer.removeAllListeners('upload-stats');
|
|
ipcRenderer.removeAllListeners('app:update-available');
|
|
ipcRenderer.removeAllListeners('app:update-progress');
|
|
ipcRenderer.removeAllListeners('shutdown-countdown');
|
|
ipcRenderer.removeAllListeners('folder-monitor:new-files');
|
|
ipcRenderer.removeAllListeners('drop-target:files');
|
|
ipcRenderer.removeAllListeners('account-switched');
|
|
ipcRenderer.removeAllListeners('remote:client-count');
|
|
}
|
|
});
|