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) {
|
switchAccount(hoster, fallbackAccount) {
|
||||||
|
const prev = this._accountOverrides.get(hoster);
|
||||||
this._accountOverrides.set(hoster, fallbackAccount);
|
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) {
|
updateSettings(hosterSettings, globalSettings) {
|
||||||
@ -138,6 +148,7 @@ class UploadManager extends EventEmitter {
|
|||||||
// straight to the fallback even after the original recovered.
|
// straight to the fallback even after the original recovered.
|
||||||
this._failedAccounts.clear();
|
this._failedAccounts.clear();
|
||||||
this._accountOverrides.clear();
|
this._accountOverrides.clear();
|
||||||
|
this._rotLog('batch-start', { taskCount: tasks.length });
|
||||||
|
|
||||||
const { signal } = this.abortController;
|
const { signal } = this.abortController;
|
||||||
const batchId = `batch-${Date.now()}`;
|
const batchId = `batch-${Date.now()}`;
|
||||||
@ -262,10 +273,19 @@ class UploadManager extends EventEmitter {
|
|||||||
if (task.accountId && this._failedAccounts.has(task.hoster + ':' + task.accountId)) {
|
if (task.accountId && this._failedAccounts.has(task.hoster + ':' + task.accountId)) {
|
||||||
const override = this._accountOverrides.get(task.hoster);
|
const override = this._accountOverrides.get(task.hoster);
|
||||||
if (override && !this._failedAccounts.has(task.hoster + ':' + override.id)) {
|
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.accountId = override.id;
|
||||||
task.username = override.username;
|
task.username = override.username;
|
||||||
task.password = override.password;
|
task.password = override.password;
|
||||||
task.apiKey = override.apiKey;
|
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
|
// 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
|
// for hosters with 3+ accounts (the old code only did one level: A → B
|
||||||
// and stopped, even if C would have worked).
|
// 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)) {
|
while (task.accountId && !this._failedAccounts.has(task.hoster + ':' + task.accountId)) {
|
||||||
if (signal.aborted || this.stopAfterActive) break;
|
if (signal.aborted || this.stopAfterActive) break;
|
||||||
this._failedAccounts.set(task.hoster + ':' + task.accountId, true);
|
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 });
|
this.emit('account-failed', { hoster: task.hoster, accountId: task.accountId });
|
||||||
await this._sleep(800, signal);
|
await this._sleep(800, signal);
|
||||||
const override = this._accountOverrides.get(task.hoster);
|
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
|
// 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.accountId = override.id;
|
||||||
task.username = override.username;
|
task.username = override.username;
|
||||||
task.password = override.password;
|
task.password = override.password;
|
||||||
@ -551,6 +596,9 @@ class UploadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const error = lastError && lastError.message ? lastError.message : 'Unbekannter Fehler';
|
const error = lastError && lastError.message ? lastError.message : 'Unbekannter Fehler';
|
||||||
|
this._rotLog('final-error', {
|
||||||
|
hoster: task.hoster, fileName, lastFailedAccountId: task.accountId, error
|
||||||
|
});
|
||||||
emitFinalStatus('error', { error });
|
emitFinalStatus('error', { error });
|
||||||
recordFinalResult('error', { error });
|
recordFinalResult('error', { error });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
73
main.js
73
main.js
@ -68,6 +68,58 @@ function debugLog(msg) {
|
|||||||
} catch {}
|
} 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
|
// Catch unhandled rejections from fire-and-forget async calls
|
||||||
process.on('unhandledRejection', (reason) => {
|
process.on('unhandledRejection', (reason) => {
|
||||||
debugLog(`UNHANDLED REJECTION: ${reason && reason.stack ? reason.stack : reason}`);
|
debugLog(`UNHANDLED REJECTION: ${reason && reason.stack ? reason.stack : reason}`);
|
||||||
@ -802,6 +854,12 @@ app.on('before-quit', () => {
|
|||||||
_uploadLogBuffer.length = 0;
|
_uploadLogBuffer.length = 0;
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
try {
|
||||||
|
if (_rotLogBuffer.length) {
|
||||||
|
fs.appendFileSync(getRotLogPath(), _rotLogBuffer.join(''), 'utf-8');
|
||||||
|
_rotLogBuffer.length = 0;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- IPC Handlers ---
|
// --- IPC Handlers ---
|
||||||
@ -1019,13 +1077,24 @@ ipcMain.handle('start-upload', (_event, payload) => {
|
|||||||
const cfg = configStore.load();
|
const cfg = configStore.load();
|
||||||
const fallback = getNextFallbackAccount(cfg, hoster, accountId);
|
const fallback = getNextFallbackAccount(cfg, hoster, accountId);
|
||||||
if (fallback) {
|
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);
|
uploadManager.switchAccount(hoster, fallback);
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.webContents.send('account-switched', { hoster, fromAccountId: accountId, toAccountId: fallback.id });
|
mainWindow.webContents.send('account-switched', { hoster, fromAccountId: accountId, toAccountId: fallback.id });
|
||||||
}
|
}
|
||||||
} else {
|
} 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) => {
|
onUploadLogFallback: (callback) => {
|
||||||
ipcRenderer.on('upload-log-fallback', (_event, data) => callback(data));
|
ipcRenderer.on('upload-log-fallback', (_event, data) => callback(data));
|
||||||
},
|
},
|
||||||
|
onAccountRotationLog: (callback) => {
|
||||||
|
ipcRenderer.on('account-rotation-log', (_event, data) => callback(data));
|
||||||
|
},
|
||||||
// Remote Control
|
// Remote Control
|
||||||
remoteGetSettings: () => ipcRenderer.invoke('remote:get-settings'),
|
remoteGetSettings: () => ipcRenderer.invoke('remote:get-settings'),
|
||||||
remoteSaveSettings: (settings) => ipcRenderer.invoke('remote:save-settings', settings),
|
remoteSaveSettings: (settings) => ipcRenderer.invoke('remote:save-settings', settings),
|
||||||
|
|||||||
@ -120,6 +120,19 @@ async function init() {
|
|||||||
window.api.onUploadLogFallback((data) => {
|
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.');
|
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
|
// Folder monitor: auto-queue new files
|
||||||
window.api.onFolderMonitorNewFiles((files) => {
|
window.api.onFolderMonitorNewFiles((files) => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user