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.
This commit is contained in:
parent
9b5184f76f
commit
126b1e569a
@ -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) {
|
||||
|
||||
73
main.js
73
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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user