Fix critical upload stuck-at-queued bug and settings display

Root cause: startBatch() ran synchronously inside ipcMain.handle()
callback, causing webContents.send() events to conflict with the
handle response and never reach the renderer.

Fix: defer startBatch() via process.nextTick so IPC response is
sent first, then upload events flow correctly.

Also:
- Add .catch() on startBatch to surface hidden errors
- Fix settings panel not updating after save (renderSettings)
- Add select-folder IPC handler (was in preload but missing)
- Add debug-log and debug-test-upload IPC for diagnostics
- Add upload-debug.log file for tracing upload flow
- Add unhandledRejection handler for main process
- Add scramble defaults to config-store globalSettings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Administrator 2026-03-10 21:19:54 +01:00
parent 52b2e0a1e4
commit 49655dc154
4 changed files with 139 additions and 12 deletions

View File

@ -25,7 +25,14 @@ const DEFAULTS = {
}, },
globalSettings: { globalSettings: {
alwaysOnTop: false, alwaysOnTop: false,
shutdownAfterFinish: 'nothing' // nothing | sleep | shutdown | restart shutdownAfterFinish: 'nothing', // nothing | sleep | shutdown | restart
scramble: {
active: false,
prefix: '',
suffix: '',
chars: 'both', // 'letters' | 'numbers' | 'both'
length: 0 // 0 = same as original basename length
}
}, },
history: [] history: []
}; };

107
main.js
View File

@ -1,4 +1,4 @@
const { app, BrowserWindow, ipcMain, dialog, clipboard } = require('electron'); const { app, BrowserWindow, ipcMain, dialog, clipboard, powerSaveBlocker } = require('electron');
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const ConfigStore = require('./lib/config-store'); const ConfigStore = require('./lib/config-store');
@ -12,6 +12,26 @@ const configStore = new ConfigStore(app);
let uploadManager = null; let uploadManager = null;
const HEALTH_CHECK_TIMEOUT = 25000; const HEALTH_CHECK_TIMEOUT = 25000;
// --- Debug logging (writes to upload-debug.log next to the app) ---
function getDebugLogPath() {
const baseDir = app.isPackaged
? path.dirname(process.execPath)
: __dirname;
return path.join(baseDir, 'upload-debug.log');
}
function debugLog(msg) {
try {
const ts = new Date().toISOString();
fs.appendFileSync(getDebugLogPath(), `[${ts}] ${msg}\n`, 'utf-8');
} catch {}
}
// Catch unhandled rejections from fire-and-forget async calls
process.on('unhandledRejection', (reason) => {
debugLog(`UNHANDLED REJECTION: ${reason && reason.stack ? reason.stack : reason}`);
});
function withTimeout(promise, timeoutMs, label) { function withTimeout(promise, timeoutMs, label) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
@ -210,6 +230,7 @@ function createWindow() {
app.whenReady().then(() => { app.whenReady().then(() => {
createWindow(); createWindow();
// Auto-check for updates after 3 seconds // Auto-check for updates after 3 seconds
setTimeout(async () => { setTimeout(async () => {
try { try {
@ -227,6 +248,12 @@ app.on('window-all-closed', () => {
// --- IPC Handlers --- // --- IPC Handlers ---
// Debug log from renderer
ipcMain.handle('debug-log', (_event, msg) => {
debugLog(`[RENDERER] ${msg}`);
return true;
});
ipcMain.handle('get-config', () => { ipcMain.handle('get-config', () => {
return configStore.load(); return configStore.load();
}); });
@ -257,35 +284,82 @@ ipcMain.handle('select-files', async () => {
return result.canceled ? null : result.filePaths; return result.canceled ? null : result.filePaths;
}); });
// Debug self-test: runs a minimal upload in the main process to verify events work
ipcMain.handle('debug-test-upload', async () => {
const testFile = path.join(__dirname, 'test-self-check.txt');
try {
fs.writeFileSync(testFile, 'selftest ' + Date.now(), 'utf-8');
const mgr = new UploadManager({ 'voe.sx': { retries: 0, parallelCount: 1, maxSpeedKbs: 0, restartBelowKbs: 0, timeIntervalSec: 0, maxSizeMb: 0 } });
const events = [];
return new Promise((resolve) => {
mgr.on('progress', (data) => { events.push({ s: data.status, e: data.error || null }); });
mgr.on('batch-done', (summary) => {
try { fs.unlinkSync(testFile); } catch {}
resolve({ ok: true, events, summary: { ok: summary.succeeded, fail: summary.failed } });
});
mgr.startBatch([{ file: testFile, hoster: 'voe.sx', apiKey: 'invalid-test-key' }]);
setTimeout(() => {
try { fs.unlinkSync(testFile); } catch {}
resolve({ ok: false, events, timeout: true });
}, 20000);
});
} catch (err) {
try { fs.unlinkSync(testFile); } catch {}
return { ok: false, error: err.message };
}
});
ipcMain.handle('select-folder', async () => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openDirectory']
});
return result.canceled ? null : result.filePaths;
});
ipcMain.handle('start-upload', (_event, payload) => { ipcMain.handle('start-upload', (_event, payload) => {
const config = configStore.load(); const config = configStore.load();
const { files, hosters } = payload; const { files, hosters } = payload;
debugLog(`start-upload: files=${JSON.stringify(files)}, hosters=${JSON.stringify(hosters)}`);
// Build tasks with credentials // Build tasks with credentials
const tasks = []; const tasks = [];
for (const file of files) { for (const file of files) {
for (const hoster of hosters) { for (const hoster of hosters) {
const hosterConfig = config.hosters[hoster]; const hosterConfig = config.hosters[hoster];
if (!hosterConfig) continue; if (!hosterConfig) {
debugLog(` skip ${hoster}: no config`);
continue;
}
if (hoster === 'vidmoly.me') { if (hoster === 'vidmoly.me') {
// Vidmoly uses username/password login // Vidmoly uses username/password login
if (!hosterConfig.username || !hosterConfig.password) continue; if (!hosterConfig.username || !hosterConfig.password) {
debugLog(` skip ${hoster}: missing username/password`);
continue;
}
tasks.push({ file, hoster, username: hosterConfig.username, password: hosterConfig.password }); tasks.push({ file, hoster, username: hosterConfig.username, password: hosterConfig.password });
} else { } else {
// Other hosters use API key // Other hosters use API key
if (!hosterConfig.apiKey) continue; if (!hosterConfig.apiKey) {
debugLog(` skip ${hoster}: missing apiKey`);
continue;
}
tasks.push({ file, hoster, apiKey: hosterConfig.apiKey }); tasks.push({ file, hoster, apiKey: hosterConfig.apiKey });
debugLog(` task: ${hoster} key=${hosterConfig.apiKey.slice(0, 6)}...`);
} }
} }
} }
debugLog(` tasks built: ${tasks.length}`);
if (tasks.length === 0) return { error: 'Keine gueltigen Zugangsdaten fuer die gewaehlten Hoster.' }; if (tasks.length === 0) return { error: 'Keine gueltigen Zugangsdaten fuer die gewaehlten Hoster.' };
// Pass hoster settings to the upload manager // Pass hoster settings to the upload manager
uploadManager = new UploadManager(config.hosterSettings || {}); uploadManager = new UploadManager(config.hosterSettings || {});
uploadManager.on('progress', (data) => { uploadManager.on('progress', (data) => {
debugLog(`progress: ${data.fileName} ${data.hoster} ${data.status} ${data.error || ''}`);
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('upload-progress', data); mainWindow.webContents.send('upload-progress', data);
} }
@ -298,6 +372,7 @@ ipcMain.handle('start-upload', (_event, payload) => {
}); });
uploadManager.on('batch-done', (summary) => { uploadManager.on('batch-done', (summary) => {
debugLog(`batch-done: total=${summary.total} ok=${summary.succeeded} fail=${summary.failed}`);
configStore.appendHistory(summary); configStore.appendHistory(summary);
// Write successful uploads to fileuploader.log // Write successful uploads to fileuploader.log
for (const file of summary.files || []) { for (const file of summary.files || []) {
@ -319,7 +394,29 @@ ipcMain.handle('start-upload', (_event, payload) => {
handleShutdownAfterFinish(); handleShutdownAfterFinish();
}); });
uploadManager.startBatch(tasks); // Defer startBatch to next tick so the IPC response is sent first.
// This ensures webContents.send() calls from upload events
// are not interleaved with the handle() response.
process.nextTick(() => {
debugLog('nextTick: calling startBatch now');
uploadManager.startBatch(tasks).catch((err) => {
debugLog(`startBatch REJECTED: ${err && err.stack ? err.stack : err}`);
// Forward error to renderer as batch-done with failure
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('upload-batch-done', {
id: 'error',
timestamp: new Date().toISOString(),
total: tasks.length,
succeeded: 0,
failed: tasks.length,
files: [],
error: err ? err.message : 'Unbekannter Fehler'
});
}
});
});
debugLog(`start-upload returning started=true (startBatch deferred to nextTick)`);
return { started: true, taskCount: tasks.length }; return { started: true, taskCount: tasks.length };
}); });

View File

@ -26,6 +26,7 @@ contextBridge.exposeInMainWorld('api', {
// File selection // File selection
selectFiles: () => ipcRenderer.invoke('select-files'), selectFiles: () => ipcRenderer.invoke('select-files'),
selectFolder: () => ipcRenderer.invoke('select-folder'),
// Upload control // Upload control
startUpload: (payload) => ipcRenderer.invoke('start-upload', payload), startUpload: (payload) => ipcRenderer.invoke('start-upload', payload),
@ -47,6 +48,10 @@ contextBridge.exposeInMainWorld('api', {
ipcRenderer.on('app:update-progress', (_event, data) => callback(data)); ipcRenderer.on('app:update-progress', (_event, data) => callback(data));
}, },
// Debug
debugTestUpload: () => ipcRenderer.invoke('debug-test-upload'),
debugLog: (msg) => ipcRenderer.invoke('debug-log', msg),
// Events (main -> renderer) // Events (main -> renderer)
onUploadProgress: (callback) => { onUploadProgress: (callback) => {
ipcRenderer.on('upload-progress', (_event, data) => callback(data)); ipcRenderer.on('upload-progress', (_event, data) => callback(data));

View File

@ -40,12 +40,23 @@ async function init() {
window.api.onUpdateAvailable(showUpdateBanner); window.api.onUpdateAvailable(showUpdateBanner);
window.api.onUpdateProgress(handleUpdateProgress); window.api.onUpdateProgress(handleUpdateProgress);
// Upload event listeners // Upload event listeners — with debug logging to file
window.api.onUploadProgress(handleProgress); window.api.onUploadProgress((data) => {
window.api.onUploadBatchDone(handleBatchDone); window.api.debugLog('RX upload-progress: ' + data.status + ' ' + data.hoster + ' ' + (data.fileName || ''));
window.api.onUploadStats(handleStats); handleProgress(data);
});
window.api.onUploadBatchDone((data) => {
window.api.debugLog('RX upload-batch-done');
handleBatchDone(data);
});
window.api.onUploadStats((data) => {
window.api.debugLog('RX upload-stats: state=' + data.state + ' active=' + data.activeJobs);
handleStats(data);
});
window.api.onShutdownCountdown(handleShutdownCountdown); window.api.onShutdownCountdown(handleShutdownCountdown);
window.api.debugLog('init complete, all listeners registered');
// Restore always-on-top state // Restore always-on-top state
try { try {
const onTop = await window.api.getAlwaysOnTop(); const onTop = await window.api.getAlwaysOnTop();
@ -453,10 +464,13 @@ async function startUpload() {
document.getElementById('startUploadBtn').style.display = 'none'; document.getElementById('startUploadBtn').style.display = 'none';
document.getElementById('cancelUploadBtn').style.display = 'inline-block'; document.getElementById('cancelUploadBtn').style.display = 'inline-block';
const result = await window.api.startUpload({ const uploadPayload = {
files: selectedFiles.map(f => f.path), files: selectedFiles.map(f => f.path),
hosters hosters
}); };
console.log('[startUpload] sending payload:', uploadPayload);
const result = await window.api.startUpload(uploadPayload);
console.log('[startUpload] response:', result);
if (result && result.error) { if (result && result.error) {
alert(result.error); alert(result.error);
@ -476,6 +490,7 @@ async function cancelUpload() {
// --- Progress handling --- // --- Progress handling ---
function handleProgress(data) { function handleProgress(data) {
console.log('[upload-progress]', data.status, data.hoster, data.fileName, data.error || '');
// Find matching job by fileName + hoster, or by uploadId // Find matching job by fileName + hoster, or by uploadId
let job = data.uploadId ? queueJobs.find(j => j.uploadId === data.uploadId) : null; let job = data.uploadId ? queueJobs.find(j => j.uploadId === data.uploadId) : null;
if (!job) { if (!job) {
@ -517,6 +532,7 @@ function handleProgress(data) {
} }
function handleBatchDone(summary) { function handleBatchDone(summary) {
console.log('[batch-done]', summary);
uploading = false; uploading = false;
selectedFiles = []; // Clear selected files after batch selectedFiles = []; // Clear selected files after batch
document.getElementById('startUploadBtn').style.display = 'inline-block'; document.getElementById('startUploadBtn').style.display = 'inline-block';
@ -529,6 +545,7 @@ function handleBatchDone(summary) {
} }
function handleStats(data) { function handleStats(data) {
console.log('[upload-stats]', data.state, 'active=' + data.activeJobs);
document.getElementById('sbState').textContent = data.state === 'uploading' ? 'Upload laeuft...' : 'Bereit'; document.getElementById('sbState').textContent = data.state === 'uploading' ? 'Upload laeuft...' : 'Bereit';
document.getElementById('sbSpeed').textContent = formatSpeed(data.globalSpeedKbs || 0); document.getElementById('sbSpeed').textContent = formatSpeed(data.globalSpeedKbs || 0);
document.getElementById('sbTotal').textContent = formatSize(data.totalBytes || 0); document.getElementById('sbTotal').textContent = formatSize(data.totalBytes || 0);
@ -726,6 +743,7 @@ async function saveSettings() {
config = await window.api.getConfig(); config = await window.api.getConfig();
hosterSettings = config.hosterSettings || {}; hosterSettings = config.hosterSettings || {};
renderHosterChips(); renderHosterChips();
renderSettings();
renderHealthCheckResults([]); renderHealthCheckResults([]);
const feedback = document.getElementById('saveFeedback'); const feedback = document.getElementById('saveFeedback');