From 1aa36cdd8b540dff9a601e05c8da914d031278db Mon Sep 17 00:00:00 2001 From: Administrator Date: Mon, 15 Jun 2026 21:44:38 +0200 Subject: [PATCH] fix(ui): queue re-renders on maximize + settings sub-tabs + null-safe saveSettings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three changes, investigated and adversarially reviewed via multi-agent workflow. 1. Queue maximize bug: the virtual-scrolled upload queue derived its visible-row window from #queueContainer.clientHeight but only recomputed it on 'scroll'. Maximizing the window enlarges the container without a scroll event, so rows below the old viewport stayed blank until you scrolled. Fix: a ResizeObserver on #queueContainer reusing the existing rAF-coalesced _onQueueScroll. Strictly stronger than a window 'resize' listener — it also covers the hidden->visible view-switch case (container 0 -> real height). No feedback loop (container box is flex-sized, not content-sized); early-returns for <200-row queues. 2. Einstellungen tab restructured from 4 dense stacked collapsible panels into horizontal sub-tabs: Allgemein / Automatik / Logs & Diagnose / Fernsteuerung / Backup. All sub-pages render into the DOM at once (only the active one is shown), so every element id stays present and saveSettings keeps working. The cluttered "Allgemein" block is split across Allgemein + Automatik + Logs. Per-hoster settings already moved to Accounts in v3.3.69. 3. saveSettings hardened to be null-safe (elTxt/elChk/elInt helpers): when a settings element is absent from the DOM it now keeps the current config value instead of collapsing to a default. Behavior is identical when elements are present; this is insurance against a future dropped id silently overwriting a real setting (e.g. webhook URL, folder-monitor hoster pre-selection). Verified: 43 sub-tab ids unique, all 26 saveSettings ids present, lint clean, boot clean, 284 tests green, 0 confirmed defects from a 3-lens adversarial review. Co-Authored-By: Claude Opus 4.8 (1M context) --- renderer/app.js | 454 +++++++++++++++++++++----------------------- renderer/styles.css | 31 +++ 2 files changed, 244 insertions(+), 241 deletions(-) diff --git a/renderer/app.js b/renderer/app.js index 2792a6d..92c7b65 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -2906,15 +2906,30 @@ function renderSettings() { const globalSettings = config.globalSettings || {}; const configuredAccounts = getAvailableHosters(); - const generalPanel = document.createElement('div'); - generalPanel.className = 'hoster-settings-panel'; - generalPanel.innerHTML = ` -
- - Allgemein - System -
-
+ const fm = globalSettings.folderMonitor || {}; + const remoteSettings = globalSettings.remote || {}; + + const subtabBar = document.createElement('div'); + subtabBar.className = 'settings-subtabs'; + subtabBar.innerHTML = ` + + + + + + `; + container.appendChild(subtabBar); + + const pages = {}; + ['allgemein', 'automatik', 'logs', 'remote', 'backup'].forEach((id) => { + const page = document.createElement('div'); + page.className = id === 'allgemein' ? 'settings-subpage active' : 'settings-subpage'; + page.dataset.subpage = id; + pages[id] = page; + container.appendChild(page); + }); + + pages.allgemein.innerHTML = `
@@ -2954,29 +2969,10 @@ function renderSettings() {
- -
- - - - -
-
- - - Pro Session = neue Datei bei jedem App-Start; nach komplettem Schließen + erneutem Öffnen beginnt eine neue Session. -
-
- - -
+
Die Upload-Einstellungen pro Hoster (Retries, Speed, Parallel, Max-Größe, Log) sind jetzt im Accounts-Tab — direkt bei den jeweiligen Hostern unter „Upload-Einstellungen".
+ `; + + pages.automatik.innerHTML = `
@@ -2999,97 +2995,7 @@ function renderSettings() { Damit Discord dich wirklich benachrichtigt (Push). User-ID: in Discord Entwicklermodus an → Rechtsklick auf deinen Namen → 'User-ID kopieren'. Leer = nur posten, kein Ping.
- -
- -
- Wird geladen… -
-
-
- - - Eine .txt mit Logs + sanitierter Config; Passwörter/API-Keys werden vor dem Speichern maskiert. -
-
- `; - container.appendChild(generalPanel); - _renderLogPathsList(generalPanel.querySelector('#logPathsList')); - const testWebhookBtn = generalPanel.querySelector('#testWebhookBtn'); - if (testWebhookBtn) { - testWebhookBtn.addEventListener('click', async () => { - const url = (document.getElementById('webhookUrlInput')?.value || '').trim(); - const mention = (document.getElementById('webhookMentionInput')?.value || '').trim(); - const hint = document.getElementById('webhookHint'); - if (!url) { if (hint) hint.textContent = 'Keine URL eingetragen.'; return; } - testWebhookBtn.disabled = true; - const prev = testWebhookBtn.textContent; - testWebhookBtn.textContent = 'Sende…'; - try { - const res = await window.api.testWebhook({ url, mention }); - if (hint) hint.textContent = res && res.ok - ? `Test erfolgreich gesendet (HTTP ${res.status}).` - : `Test fehlgeschlagen: ${(res && (res.error || 'HTTP ' + res.status)) || 'unbekannt'}`; - } catch (err) { - if (hint) hint.textContent = `Test fehlgeschlagen: ${err.message || err}`; - } finally { - testWebhookBtn.disabled = false; - testWebhookBtn.textContent = prev; - } - }); - } - const verboseInput = generalPanel.querySelector('#logVerboseInput'); - if (verboseInput) { - verboseInput.addEventListener('change', () => { - if (window.api && window.api.setLogVerbose) window.api.setLogVerbose(verboseInput.checked).catch(() => {}); - }); - } - const sbBtn = generalPanel.querySelector('#createSupportBundleBtn'); - if (sbBtn) { - sbBtn.addEventListener('click', async () => { - const hint = generalPanel.querySelector('#supportBundleHint'); - sbBtn.disabled = true; - const prevText = sbBtn.textContent; - sbBtn.textContent = 'Exportiere…'; - try { - const res = await window.api.createSupportBundle(); - if (res && res.ok) { - if (hint) hint.textContent = `Gespeichert: ${res.path} (${(res.bytes/1024).toFixed(1)} KB)`; - } else if (res && res.canceled) { - if (hint) hint.textContent = 'Abgebrochen.'; - } else { - if (hint) hint.textContent = `Fehler: ${(res && res.error) || 'unbekannt'}`; - } - } catch (err) { - if (hint) hint.textContent = `Fehler: ${err.message || err}`; - } finally { - sbBtn.disabled = false; - sbBtn.textContent = prevText; - } - }); - } - - // Toggle general panel - generalPanel.querySelector('.hoster-panel-header').addEventListener('click', () => { - const body = generalPanel.querySelector('.hoster-panel-body'); - const arrow = generalPanel.querySelector('.panel-arrow'); - const isOpen = body.style.display !== 'none'; - body.style.display = isOpen ? 'none' : 'block'; - arrow.innerHTML = isOpen ? '▶' : '▼'; - }); - - // --- Folder Monitor Panel --- - const fm = globalSettings.folderMonitor || {}; - const folderMonitorPanel = document.createElement('div'); - folderMonitorPanel.className = 'hoster-settings-panel'; - folderMonitorPanel.innerHTML = ` -
- - Ordnerüberwachung - ${fm.enabled && fm.folderPath ? 'Aktiv' : 'Inaktiv'} -
- ${configuredAccounts.length === 0 ? '

Erst Accounts anlegen, dann hier auswählen.

' : '

Keine Auswahl = Hoster-Modal bei jeder Datei.

'} - `; - container.appendChild(folderMonitorPanel); - // Toggle folder monitor panel - folderMonitorPanel.querySelector('.hoster-panel-header').addEventListener('click', () => { - const body = folderMonitorPanel.querySelector('.hoster-panel-body'); - const arrow = folderMonitorPanel.querySelector('.panel-arrow'); - const isOpen = body.style.display !== 'none'; - body.style.display = isOpen ? 'none' : 'block'; - arrow.innerHTML = isOpen ? '▶' : '▼'; - }); + pages.logs.innerHTML = ` + +
+ + + + +
+
+ + + Pro Session = neue Datei bei jedem App-Start; nach komplettem Schließen + erneutem Öffnen beginnt eine neue Session. +
+
+ + +
+ +
+ +
+ Wird geladen… +
+
+
+ + + Eine .txt mit Logs + sanitierter Config; Passwörter/API-Keys werden vor dem Speichern maskiert. +
+ `; - // Update badge immediately on checkbox/path change - const updateFmBadge = () => { - const b = document.getElementById('folderMonitorStatusBadge'); - if (!b) return; - const enabled = document.getElementById('fmEnabledInput')?.checked; - const hasPath = (document.getElementById('fmFolderPathInput')?.value || '').trim(); - if (enabled && hasPath) { b.textContent = 'Aktiv'; b.className = 'panel-status active'; } - else { b.textContent = 'Inaktiv'; b.className = 'panel-status'; } - }; - document.getElementById('fmEnabledInput')?.addEventListener('change', updateFmBadge); - document.getElementById('fmFolderPathInput')?.addEventListener('input', updateFmBadge); - - document.getElementById('fmChooseFolderBtn')?.addEventListener('click', async () => { - const folder = await window.api.folderMonitorSelectFolder(); - if (folder) { - document.getElementById('fmFolderPathInput').value = folder; - updateFmBadge(); - scheduleSettingsSave(); - } - }); - - // --- Remote Control Panel --- - const remoteSettings = globalSettings.remote || {}; - const remotePanel = document.createElement('div'); - remotePanel.className = 'hoster-settings-panel'; - remotePanel.innerHTML = ` -
- - Fernsteuerung - ${remoteSettings.enabled ? 'Aktiv' : 'Inaktiv'} -
-