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:
parent
796aeb520d
commit
655fb6230b
@ -56,6 +56,34 @@ class UploadManager extends EventEmitter {
|
||||
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) {
|
||||
this.hosterSettings = hosterSettings || this.hosterSettings;
|
||||
this.globalSettings = globalSettings || this.globalSettings;
|
||||
@ -469,6 +497,15 @@ class UploadManager extends EventEmitter {
|
||||
}
|
||||
|
||||
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;
|
||||
// Wait 3 seconds before retry
|
||||
await this._sleep(3000, signal);
|
||||
|
||||
39
main.js
39
main.js
@ -108,12 +108,24 @@ function rotLog(msg, ts) {
|
||||
try {
|
||||
const iso = new Date(ts || Date.now()).toISOString();
|
||||
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.
|
||||
_debugLogBuffer.push(`[${iso}] [ROT] ${msg}\n`);
|
||||
if (!_rotLogFlushTimer) {
|
||||
_rotLogFlushTimer = setTimeout(() => { _rotLogFlushTimer = null; _flushRotLog(); }, 500);
|
||||
}
|
||||
if (!_debugLogFlushTimer) {
|
||||
_debugLogFlushTimer = setTimeout(() => { _debugLogFlushTimer = null; _flushDebugLog(); }, 500);
|
||||
}
|
||||
@ -1189,6 +1201,25 @@ ipcMain.handle('finish-after-active', () => {
|
||||
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 () => {
|
||||
await configStore.clearHistory();
|
||||
return true;
|
||||
|
||||
@ -107,6 +107,7 @@ contextBridge.exposeInMainWorld('api', {
|
||||
onAccountRotationLog: (callback) => {
|
||||
ipcRenderer.on('account-rotation-log', (_event, data) => callback(data));
|
||||
},
|
||||
openLogFolder: () => ipcRenderer.invoke('open-log-folder'),
|
||||
// Remote Control
|
||||
remoteGetSettings: () => ipcRenderer.invoke('remote:get-settings'),
|
||||
remoteSaveSettings: (settings) => ipcRenderer.invoke('remote:save-settings', settings),
|
||||
|
||||
@ -2291,6 +2291,7 @@ function renderSettings() {
|
||||
<label>FileUploader Log</label>
|
||||
<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="openLogFolderBtn" title="Log-Ordner im Explorer öffnen">Öffnen</button>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<label>Neues Log pro Tag</label>
|
||||
@ -2584,6 +2585,7 @@ function renderSettings() {
|
||||
}
|
||||
|
||||
document.getElementById('chooseLogFilePathBtn')?.addEventListener('click', chooseLogFilePath);
|
||||
document.getElementById('openLogFolderBtn')?.addEventListener('click', () => window.api.openLogFolder());
|
||||
document.getElementById('manualUpdateCheckBtn')?.addEventListener('click', async (e) => {
|
||||
const btn = e.target;
|
||||
btn.disabled = true;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user