From 126b1e569a38a87a12231e218564766a8807be76 Mon Sep 17 00:00:00 2001 From: Administrator Date: Sun, 19 Apr 2026 22:57:19 +0200 Subject: [PATCH] feat(account-rotation): dedicated logging + live toast notifications To trace whether the fallback chain actually engages during real uploads, every rotation decision now emits a structured 'rot-log' event from the upload-manager. main.js persists each event to a new account-rotation.log (same directory as fileuploader.log; falls back to Desktop then userData) and also mirrors it into the main debug log with a [ROT] prefix for single-file grepping. Logged events: - batch-start (clears _failedAccounts / _accountOverrides) - pre-job-swap / pre-job-swap-blocked (job picks override before first try) - retries-exhausted / mark-failed (enters rotation loop) - rotate (switched to new account, retry starting) - rotation-end (no override / override already failed) - final-error (all accounts exhausted) - switchAccount (main resolved the next fallback) The renderer shows a toast on 'rotate', 'rotation-end' and 'final-error' so fallback behavior is visible live instead of buried in logs. --- lib/upload-manager.js | 50 ++++++++++++++++++++++++++++- main.js | 73 +++++++++++++++++++++++++++++++++++++++++-- preload.js | 3 ++ renderer/app.js | 13 ++++++++ 4 files changed, 136 insertions(+), 3 deletions(-) diff --git a/lib/upload-manager.js b/lib/upload-manager.js index ca87c57..e85269f 100644 --- a/lib/upload-manager.js +++ b/lib/upload-manager.js @@ -43,7 +43,17 @@ class UploadManager extends EventEmitter { } switchAccount(hoster, fallbackAccount) { + const prev = this._accountOverrides.get(hoster); this._accountOverrides.set(hoster, fallbackAccount); + this._rotLog('switchAccount', { + hoster, + prevOverrideId: prev ? prev.id : null, + toAccountId: fallbackAccount ? fallbackAccount.id : null + }); + } + + _rotLog(event, data) { + this.emit('rot-log', { ts: Date.now(), event, ...data }); } updateSettings(hosterSettings, globalSettings) { @@ -138,6 +148,7 @@ class UploadManager extends EventEmitter { // straight to the fallback even after the original recovered. this._failedAccounts.clear(); this._accountOverrides.clear(); + this._rotLog('batch-start', { taskCount: tasks.length }); const { signal } = this.abortController; const batchId = `batch-${Date.now()}`; @@ -262,10 +273,19 @@ class UploadManager extends EventEmitter { if (task.accountId && this._failedAccounts.has(task.hoster + ':' + task.accountId)) { const override = this._accountOverrides.get(task.hoster); if (override && !this._failedAccounts.has(task.hoster + ':' + override.id)) { + this._rotLog('pre-job-swap', { + hoster: task.hoster, fileName, fromAccountId: task.accountId, toAccountId: override.id + }); task.accountId = override.id; task.username = override.username; task.password = override.password; task.apiKey = override.apiKey; + } else { + this._rotLog('pre-job-swap-blocked', { + hoster: task.hoster, fileName, accountId: task.accountId, + hasOverride: !!override, + overrideAlsoFailed: override ? this._failedAccounts.has(task.hoster + ':' + override.id) : false + }); } } @@ -471,14 +491,39 @@ class UploadManager extends EventEmitter { // resolve the next fallback, then retry. Loop so A → B → C → ... works // for hosters with 3+ accounts (the old code only did one level: A → B // and stopped, even if C would have worked). + this._rotLog('retries-exhausted', { + hoster: task.hoster, fileName, accountId: task.accountId, + lastError: lastError ? lastError.message : null + }); while (task.accountId && !this._failedAccounts.has(task.hoster + ':' + task.accountId)) { if (signal.aborted || this.stopAfterActive) break; this._failedAccounts.set(task.hoster + ':' + task.accountId, true); + this._rotLog('mark-failed', { + hoster: task.hoster, fileName, accountId: task.accountId, + lastError: lastError ? lastError.message : null + }); this.emit('account-failed', { hoster: task.hoster, accountId: task.accountId }); await this._sleep(800, signal); const override = this._accountOverrides.get(task.hoster); - if (!override || this._failedAccounts.has(task.hoster + ':' + override.id)) break; + if (!override) { + this._rotLog('rotation-end', { + hoster: task.hoster, fileName, reason: 'no-override-set', + lastFailedAccountId: task.accountId + }); + break; + } + if (this._failedAccounts.has(task.hoster + ':' + override.id)) { + this._rotLog('rotation-end', { + hoster: task.hoster, fileName, reason: 'override-already-failed', + overrideId: override.id, lastFailedAccountId: task.accountId + }); + break; + } // Switch to fallback account and retry this file + this._rotLog('rotate', { + hoster: task.hoster, fileName, + fromAccountId: task.accountId, toAccountId: override.id + }); task.accountId = override.id; task.username = override.username; task.password = override.password; @@ -551,6 +596,9 @@ class UploadManager extends EventEmitter { } const error = lastError && lastError.message ? lastError.message : 'Unbekannter Fehler'; + this._rotLog('final-error', { + hoster: task.hoster, fileName, lastFailedAccountId: task.accountId, error + }); emitFinalStatus('error', { error }); recordFinalResult('error', { error }); } catch (err) { diff --git a/main.js b/main.js index 814a16e..6b1f421 100644 --- a/main.js +++ b/main.js @@ -68,6 +68,58 @@ function debugLog(msg) { } catch {} } +// Dedicated account-rotation log so users can trace fallback decisions +// without wading through general debug output. Writes to account-rotation.log +// in the same directory as fileuploader.log (honors user's configured path). +function getRotLogPath() { + const base = getLogFilePath(); + const dir = path.dirname(base); + return path.join(dir, 'account-rotation.log'); +} +const _rotLogBuffer = []; +let _rotLogFlushTimer = null; +let _rotLogWriting = false; + +function _flushRotLog() { + if (_rotLogWriting || _rotLogBuffer.length === 0) return; + const chunk = _rotLogBuffer.join(''); + _rotLogBuffer.length = 0; + _rotLogWriting = true; + const tryTargets = [ + getRotLogPath(), + path.join(app.getPath('desktop') || app.getPath('userData'), 'account-rotation.log'), + path.join(app.getPath('userData'), 'account-rotation.log') + ]; + const write = (i) => { + if (i >= tryTargets.length) { _rotLogWriting = false; return; } + try { + fs.mkdirSync(path.dirname(tryTargets[i]), { recursive: true }); + } catch {} + fs.appendFile(tryTargets[i], chunk, 'utf-8', (err) => { + if (err) return write(i + 1); + _rotLogWriting = false; + if (_rotLogBuffer.length) setImmediate(_flushRotLog); + }); + }; + write(0); +} + +function rotLog(msg, ts) { + try { + const iso = new Date(ts || Date.now()).toISOString(); + const line = `[${iso}] ${msg}\n`; + _rotLogBuffer.push(line); + // Mirror into the main debug log for single-file-grep convenience. + _debugLogBuffer.push(`[${iso}] [ROT] ${msg}\n`); + if (!_rotLogFlushTimer) { + _rotLogFlushTimer = setTimeout(() => { _rotLogFlushTimer = null; _flushRotLog(); }, 500); + } + if (!_debugLogFlushTimer) { + _debugLogFlushTimer = setTimeout(() => { _debugLogFlushTimer = null; _flushDebugLog(); }, 500); + } + } catch {} +} + // Catch unhandled rejections from fire-and-forget async calls process.on('unhandledRejection', (reason) => { debugLog(`UNHANDLED REJECTION: ${reason && reason.stack ? reason.stack : reason}`); @@ -802,6 +854,12 @@ app.on('before-quit', () => { _uploadLogBuffer.length = 0; } } catch {} + try { + if (_rotLogBuffer.length) { + fs.appendFileSync(getRotLogPath(), _rotLogBuffer.join(''), 'utf-8'); + _rotLogBuffer.length = 0; + } + } catch {} }); // --- IPC Handlers --- @@ -1019,13 +1077,24 @@ ipcMain.handle('start-upload', (_event, payload) => { const cfg = configStore.load(); const fallback = getNextFallbackAccount(cfg, hoster, accountId); if (fallback) { - debugLog(`account-failed: ${hoster} ${accountId} → fallback to ${fallback.id}`); + rotLog(`main: account-failed ${hoster} ${accountId} → resolved fallback ${fallback.id}`); uploadManager.switchAccount(hoster, fallback); if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('account-switched', { hoster, fromAccountId: accountId, toAccountId: fallback.id }); } } else { - debugLog(`account-failed: ${hoster} ${accountId} → no fallback available`); + rotLog(`main: account-failed ${hoster} ${accountId} → NO fallback available (end of chain)`); + } + }); + + uploadManager.on('rot-log', (entry) => { + const { ts, event, ...rest } = entry; + const pairs = Object.entries(rest) + .map(([k, v]) => `${k}=${typeof v === 'string' ? v : JSON.stringify(v)}`) + .join(' '); + rotLog(`[${event}] ${pairs}`, ts); + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('account-rotation-log', entry); } }); diff --git a/preload.js b/preload.js index 9533637..b0d4d54 100644 --- a/preload.js +++ b/preload.js @@ -104,6 +104,9 @@ contextBridge.exposeInMainWorld('api', { onUploadLogFallback: (callback) => { ipcRenderer.on('upload-log-fallback', (_event, data) => callback(data)); }, + onAccountRotationLog: (callback) => { + ipcRenderer.on('account-rotation-log', (_event, data) => callback(data)); + }, // Remote Control remoteGetSettings: () => ipcRenderer.invoke('remote:get-settings'), remoteSaveSettings: (settings) => ipcRenderer.invoke('remote:save-settings', settings), diff --git a/renderer/app.js b/renderer/app.js index 1e874cf..a635331 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -120,6 +120,19 @@ async function init() { window.api.onUploadLogFallback((data) => { alert('Der konfigurierte Log-Pfad konnte nicht beschrieben werden.\n\nNeue Einträge werden zwischenzeitlich hier gespeichert:\n' + (data && data.fallbackPath ? data.fallbackPath : '(Fallback)') + '\n\nBitte in den Einstellungen einen gültigen Pfad setzen.'); }); + window.api.onAccountRotationLog((entry) => { + // Surface only the user-visible rotation events as toasts; full detail + // goes to account-rotation.log. Keep it quiet otherwise. + if (!entry || !entry.event) return; + const hosterLabel = entry.hoster ? getHosterLabel(entry.hoster) : ''; + if (entry.event === 'rotate') { + showCopyToast(`${hosterLabel}: Account-Wechsel → Fallback`); + } else if (entry.event === 'rotation-end') { + showCopyToast(`${hosterLabel}: Keine weiteren Fallback-Accounts verfügbar`); + } else if (entry.event === 'final-error') { + showCopyToast(`${hosterLabel}: Alle Accounts ausgeschöpft`); + } + }); // Folder monitor: auto-queue new files window.api.onFolderMonitorNewFiles((files) => {