feat(rotation): fast-fail on account-specific errors + open-log-folder button + sync rot-log flush

Three related improvements that landed together while wiring up the
rotation log infrastructure:

  - Fast-fail classifier: errors that clearly indicate the account
    itself is the problem (rate limit, quota, banned/suspended, auth
    failure, 401/403, 'Kein Upload-Server' from delivery-node etc.)
    now skip the remaining retries and go straight to rotation. No
    more waiting 5 × 3s between retries just to end up rotating
    anyway. Emits a 'fast-fail' rot-log event so the shortcut is
    visible.

  - Settings: 'Öffnen' button next to the log-file-path input reveals
    the active log file (or its directory if nothing's written yet)
    in the OS file manager, so users don't have to remember paths.

  - rotLog() writes the rotation log synchronously. Only a handful
    of events fire per batch; the 500ms flush batching was saving
    nothing and made the file look empty when users checked right
    after an event. (The main debug log still uses the batched async
    path — that one is high-volume.)
This commit is contained in:
Administrator 2026-04-19 23:04:20 +02:00
parent 796aeb520d
commit 655fb6230b
4 changed files with 75 additions and 4 deletions

View File

@ -56,6 +56,34 @@ class UploadManager extends EventEmitter {
this.emit('rot-log', { ts: Date.now(), event, ...data }); this.emit('rot-log', { ts: Date.now(), event, ...data });
} }
// Error classes that mean "this account is the problem, retrying on it won't
// help" — we skip the remaining retries and go straight to the fallback
// account. Keeps single runs fast when an account is rate-limited, banned,
// or out of quota. Transient network issues still go through the normal
// retry loop on the same account.
_shouldSkipRetryOnAccountError(err) {
if (!err || !err.message) return false;
const m = String(err.message);
const PATTERNS = [
/Kein Upload-Server/i,
/No upload server/i,
/kein server/i,
/quota/i,
/limit (reached|exceeded|überschritten)/i,
/rate[- ]?limit/i,
/too many requests/i,
/\b(401|403)\b/,
/Falscher (User|Username|Passwort)/i,
/Incorrect (Login|Password)/i,
/invalid (credentials|api[- ]?key|token|session)/i,
/(account|user) (banned|suspended|disabled|gesperrt)/i,
/not authorized/i,
/forbidden/i,
/session (expired|abgelaufen)/i
];
return PATTERNS.some(p => p.test(m));
}
updateSettings(hosterSettings, globalSettings) { updateSettings(hosterSettings, globalSettings) {
this.hosterSettings = hosterSettings || this.hosterSettings; this.hosterSettings = hosterSettings || this.hosterSettings;
this.globalSettings = globalSettings || this.globalSettings; this.globalSettings = globalSettings || this.globalSettings;
@ -469,6 +497,15 @@ class UploadManager extends EventEmitter {
} }
lastError = err; lastError = err;
// Account-specific errors — don't waste retries on the same account,
// jump straight to rotation.
if (this._shouldSkipRetryOnAccountError(err)) {
this._rotLog('fast-fail', {
hoster: task.hoster, fileName, accountId: task.accountId,
attempt, error: err && err.message ? err.message : String(err)
});
break;
}
if (attempt >= maxAttempts) break; if (attempt >= maxAttempts) break;
// Wait 3 seconds before retry // Wait 3 seconds before retry
await this._sleep(3000, signal); await this._sleep(3000, signal);

39
main.js
View File

@ -108,12 +108,24 @@ function rotLog(msg, ts) {
try { try {
const iso = new Date(ts || Date.now()).toISOString(); const iso = new Date(ts || Date.now()).toISOString();
const line = `[${iso}] ${msg}\n`; const line = `[${iso}] ${msg}\n`;
_rotLogBuffer.push(line); // Write synchronously. Rotation events are rare (a handful per batch) so
// the batching optimization from debugLog doesn't buy us anything, and
// syncing guarantees the user can refresh the file and see fresh entries
// without waiting on a flush timer.
const candidates = [
getRotLogPath(),
path.join(app.getPath('desktop') || app.getPath('userData'), 'account-rotation.log'),
path.join(app.getPath('userData'), 'account-rotation.log')
];
for (const target of candidates) {
try {
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.appendFileSync(target, line, 'utf-8');
break;
} catch {}
}
// Mirror into the main debug log for single-file-grep convenience. // Mirror into the main debug log for single-file-grep convenience.
_debugLogBuffer.push(`[${iso}] [ROT] ${msg}\n`); _debugLogBuffer.push(`[${iso}] [ROT] ${msg}\n`);
if (!_rotLogFlushTimer) {
_rotLogFlushTimer = setTimeout(() => { _rotLogFlushTimer = null; _flushRotLog(); }, 500);
}
if (!_debugLogFlushTimer) { if (!_debugLogFlushTimer) {
_debugLogFlushTimer = setTimeout(() => { _debugLogFlushTimer = null; _flushDebugLog(); }, 500); _debugLogFlushTimer = setTimeout(() => { _debugLogFlushTimer = null; _flushDebugLog(); }, 500);
} }
@ -1189,6 +1201,25 @@ ipcMain.handle('finish-after-active', () => {
return true; return true;
}); });
ipcMain.handle('open-log-folder', async () => {
// Reveal the active log file (or its directory) in the OS file manager.
// Prefers the configured log path, then the rotation log, then just the
// parent dir.
const { shell } = require('electron');
const primary = getLogFilePath();
if (fs.existsSync(primary)) { shell.showItemInFolder(primary); return { ok: true, path: primary }; }
const rot = getRotLogPath();
if (fs.existsSync(rot)) { shell.showItemInFolder(rot); return { ok: true, path: rot }; }
try {
const dir = path.dirname(primary);
fs.mkdirSync(dir, { recursive: true });
shell.openPath(dir);
return { ok: true, path: dir };
} catch (err) {
return { ok: false, error: err.message };
}
});
ipcMain.handle('clear-history', async () => { ipcMain.handle('clear-history', async () => {
await configStore.clearHistory(); await configStore.clearHistory();
return true; return true;

View File

@ -107,6 +107,7 @@ contextBridge.exposeInMainWorld('api', {
onAccountRotationLog: (callback) => { onAccountRotationLog: (callback) => {
ipcRenderer.on('account-rotation-log', (_event, data) => callback(data)); ipcRenderer.on('account-rotation-log', (_event, data) => callback(data));
}, },
openLogFolder: () => ipcRenderer.invoke('open-log-folder'),
// 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),

View File

@ -2291,6 +2291,7 @@ function renderSettings() {
<label>FileUploader Log</label> <label>FileUploader Log</label>
<input type="text" class="key-input settings-autosave" id="logFilePathInput" value="${escapeAttr(globalSettings.logFilePath || '')}" placeholder="Standardpfad verwenden"> <input type="text" class="key-input settings-autosave" id="logFilePathInput" value="${escapeAttr(globalSettings.logFilePath || '')}" placeholder="Standardpfad verwenden">
<button class="btn btn-xs btn-secondary" id="chooseLogFilePathBtn">Ordner wählen</button> <button class="btn btn-xs btn-secondary" id="chooseLogFilePathBtn">Ordner wählen</button>
<button class="btn btn-xs btn-secondary" id="openLogFolderBtn" title="Log-Ordner im Explorer öffnen">Öffnen</button>
</div> </div>
<div class="settings-row"> <div class="settings-row">
<label>Neues Log pro Tag</label> <label>Neues Log pro Tag</label>
@ -2584,6 +2585,7 @@ function renderSettings() {
} }
document.getElementById('chooseLogFilePathBtn')?.addEventListener('click', chooseLogFilePath); document.getElementById('chooseLogFilePathBtn')?.addEventListener('click', chooseLogFilePath);
document.getElementById('openLogFolderBtn')?.addEventListener('click', () => window.api.openLogFolder());
document.getElementById('manualUpdateCheckBtn')?.addEventListener('click', async (e) => { document.getElementById('manualUpdateCheckBtn')?.addEventListener('click', async (e) => {
const btn = e.target; const btn = e.target;
btn.disabled = true; btn.disabled = true;