From 49655dc1549af641995a68283890d0f785a47a34 Mon Sep 17 00:00:00 2001 From: Administrator Date: Tue, 10 Mar 2026 21:19:54 +0100 Subject: [PATCH] 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 --- lib/config-store.js | 9 +++- main.js | 107 +++++++++++++++++++++++++++++++++++++++++--- preload.js | 5 +++ renderer/app.js | 30 ++++++++++--- 4 files changed, 139 insertions(+), 12 deletions(-) diff --git a/lib/config-store.js b/lib/config-store.js index 7009f2a..96ee419 100644 --- a/lib/config-store.js +++ b/lib/config-store.js @@ -25,7 +25,14 @@ const DEFAULTS = { }, globalSettings: { 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: [] }; diff --git a/main.js b/main.js index 18a9280..7cd8ff0 100644 --- a/main.js +++ b/main.js @@ -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 fs = require('fs'); const ConfigStore = require('./lib/config-store'); @@ -12,6 +12,26 @@ const configStore = new ConfigStore(app); let uploadManager = null; 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) { return new Promise((resolve, reject) => { const timer = setTimeout(() => { @@ -210,6 +230,7 @@ function createWindow() { app.whenReady().then(() => { createWindow(); + // Auto-check for updates after 3 seconds setTimeout(async () => { try { @@ -227,6 +248,12 @@ app.on('window-all-closed', () => { // --- IPC Handlers --- +// Debug log from renderer +ipcMain.handle('debug-log', (_event, msg) => { + debugLog(`[RENDERER] ${msg}`); + return true; +}); + ipcMain.handle('get-config', () => { return configStore.load(); }); @@ -257,35 +284,82 @@ ipcMain.handle('select-files', async () => { 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) => { const config = configStore.load(); const { files, hosters } = payload; + debugLog(`start-upload: files=${JSON.stringify(files)}, hosters=${JSON.stringify(hosters)}`); + // Build tasks with credentials const tasks = []; for (const file of files) { for (const hoster of hosters) { const hosterConfig = config.hosters[hoster]; - if (!hosterConfig) continue; + if (!hosterConfig) { + debugLog(` skip ${hoster}: no config`); + continue; + } if (hoster === 'vidmoly.me') { // 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 }); } else { // 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 }); + 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.' }; // Pass hoster settings to the upload manager uploadManager = new UploadManager(config.hosterSettings || {}); uploadManager.on('progress', (data) => { + debugLog(`progress: ${data.fileName} ${data.hoster} ${data.status} ${data.error || ''}`); if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('upload-progress', data); } @@ -298,6 +372,7 @@ ipcMain.handle('start-upload', (_event, payload) => { }); uploadManager.on('batch-done', (summary) => { + debugLog(`batch-done: total=${summary.total} ok=${summary.succeeded} fail=${summary.failed}`); configStore.appendHistory(summary); // Write successful uploads to fileuploader.log for (const file of summary.files || []) { @@ -319,7 +394,29 @@ ipcMain.handle('start-upload', (_event, payload) => { 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 }; }); diff --git a/preload.js b/preload.js index 824a581..d209018 100644 --- a/preload.js +++ b/preload.js @@ -26,6 +26,7 @@ contextBridge.exposeInMainWorld('api', { // File selection selectFiles: () => ipcRenderer.invoke('select-files'), + selectFolder: () => ipcRenderer.invoke('select-folder'), // Upload control startUpload: (payload) => ipcRenderer.invoke('start-upload', payload), @@ -47,6 +48,10 @@ contextBridge.exposeInMainWorld('api', { 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) onUploadProgress: (callback) => { ipcRenderer.on('upload-progress', (_event, data) => callback(data)); diff --git a/renderer/app.js b/renderer/app.js index 0567987..6d7fd41 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -40,12 +40,23 @@ async function init() { window.api.onUpdateAvailable(showUpdateBanner); window.api.onUpdateProgress(handleUpdateProgress); - // Upload event listeners - window.api.onUploadProgress(handleProgress); - window.api.onUploadBatchDone(handleBatchDone); - window.api.onUploadStats(handleStats); + // Upload event listeners — with debug logging to file + window.api.onUploadProgress((data) => { + window.api.debugLog('RX upload-progress: ' + data.status + ' ' + data.hoster + ' ' + (data.fileName || '')); + 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.debugLog('init complete, all listeners registered'); + // Restore always-on-top state try { const onTop = await window.api.getAlwaysOnTop(); @@ -453,10 +464,13 @@ async function startUpload() { document.getElementById('startUploadBtn').style.display = 'none'; document.getElementById('cancelUploadBtn').style.display = 'inline-block'; - const result = await window.api.startUpload({ + const uploadPayload = { files: selectedFiles.map(f => f.path), hosters - }); + }; + console.log('[startUpload] sending payload:', uploadPayload); + const result = await window.api.startUpload(uploadPayload); + console.log('[startUpload] response:', result); if (result && result.error) { alert(result.error); @@ -476,6 +490,7 @@ async function cancelUpload() { // --- Progress handling --- function handleProgress(data) { + console.log('[upload-progress]', data.status, data.hoster, data.fileName, data.error || ''); // Find matching job by fileName + hoster, or by uploadId let job = data.uploadId ? queueJobs.find(j => j.uploadId === data.uploadId) : null; if (!job) { @@ -517,6 +532,7 @@ function handleProgress(data) { } function handleBatchDone(summary) { + console.log('[batch-done]', summary); uploading = false; selectedFiles = []; // Clear selected files after batch document.getElementById('startUploadBtn').style.display = 'inline-block'; @@ -529,6 +545,7 @@ function handleBatchDone(summary) { } function handleStats(data) { + console.log('[upload-stats]', data.state, 'active=' + data.activeJobs); document.getElementById('sbState').textContent = data.state === 'uploading' ? 'Upload laeuft...' : 'Bereit'; document.getElementById('sbSpeed').textContent = formatSpeed(data.globalSpeedKbs || 0); document.getElementById('sbTotal').textContent = formatSize(data.totalBytes || 0); @@ -726,6 +743,7 @@ async function saveSettings() { config = await window.api.getConfig(); hosterSettings = config.hosterSettings || {}; renderHosterChips(); + renderSettings(); renderHealthCheckResults([]); const feedback = document.getElementById('saveFeedback');