fix(ui): queue re-renders on maximize + settings sub-tabs + null-safe saveSettings
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) <noreply@anthropic.com>
This commit is contained in:
parent
1e3f2ca51a
commit
1aa36cdd8b
452
renderer/app.js
452
renderer/app.js
@ -2906,15 +2906,30 @@ function renderSettings() {
|
|||||||
|
|
||||||
const globalSettings = config.globalSettings || {};
|
const globalSettings = config.globalSettings || {};
|
||||||
const configuredAccounts = getAvailableHosters();
|
const configuredAccounts = getAvailableHosters();
|
||||||
const generalPanel = document.createElement('div');
|
const fm = globalSettings.folderMonitor || {};
|
||||||
generalPanel.className = 'hoster-settings-panel';
|
const remoteSettings = globalSettings.remote || {};
|
||||||
generalPanel.innerHTML = `
|
|
||||||
<div class="hoster-panel-header" data-hoster="global">
|
const subtabBar = document.createElement('div');
|
||||||
<span class="panel-arrow">▼</span>
|
subtabBar.className = 'settings-subtabs';
|
||||||
<span class="panel-title">Allgemein</span>
|
subtabBar.innerHTML = `
|
||||||
<span class="panel-status active">System</span>
|
<button class="settings-subtab active" data-subtab="allgemein">Allgemein</button>
|
||||||
</div>
|
<button class="settings-subtab" data-subtab="automatik">Automatik</button>
|
||||||
<div class="hoster-panel-body" data-panel="global" style="display:block">
|
<button class="settings-subtab" data-subtab="logs">Logs & Diagnose</button>
|
||||||
|
<button class="settings-subtab" data-subtab="remote">Fernsteuerung</button>
|
||||||
|
<button class="settings-subtab" data-subtab="backup">Backup</button>
|
||||||
|
`;
|
||||||
|
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 = `
|
||||||
<div class="settings-section-label">Uploads</div>
|
<div class="settings-section-label">Uploads</div>
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<label style="min-width:185px">Globale parallele Uploads</label>
|
<label style="min-width:185px">Globale parallele Uploads</label>
|
||||||
@ -2954,29 +2969,10 @@ function renderSettings() {
|
|||||||
<label>Manuell prüfen</label>
|
<label>Manuell prüfen</label>
|
||||||
<button class="btn btn-xs btn-secondary" id="manualUpdateCheckBtn">Nach Updates suchen</button>
|
<button class="btn btn-xs btn-secondary" id="manualUpdateCheckBtn">Nach Updates suchen</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-section-label">Log</div>
|
<div class="settings-hoster-pointer">Die <strong>Upload-Einstellungen pro Hoster</strong> (Retries, Speed, Parallel, Max-Größe, Log) sind jetzt im <strong>Accounts</strong>-Tab — direkt bei den jeweiligen Hostern unter „Upload-Einstellungen".</div>
|
||||||
<div class="settings-row">
|
`;
|
||||||
<label>FileUploader Log</label>
|
|
||||||
<input type="text" class="key-input settings-autosave" id="logFilePathInput" value="${escapeAttr(globalSettings.logFilePath || '')}" placeholder="Standardpfad verwenden">
|
pages.automatik.innerHTML = `
|
||||||
<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>Log-Datei-Modus</label>
|
|
||||||
<select class="hs-input settings-autosave" id="logModeInput">
|
|
||||||
<option value="single" ${(window.LogMode ? window.LogMode.normalizeLogMode(globalSettings) : (globalSettings.logMode || 'single')) === 'single' ? 'selected' : ''}>Eine Datei</option>
|
|
||||||
<option value="daily" ${(window.LogMode ? window.LogMode.normalizeLogMode(globalSettings) : (globalSettings.logMode || 'single')) === 'daily' ? 'selected' : ''}>Pro Tag</option>
|
|
||||||
<option value="session" ${(window.LogMode ? window.LogMode.normalizeLogMode(globalSettings) : (globalSettings.logMode || 'single')) === 'session' ? 'selected' : ''}>Pro Session</option>
|
|
||||||
</select>
|
|
||||||
<span class="hint">Pro Session = neue Datei bei jedem App-Start; nach komplettem Schließen + erneutem Öffnen beginnt eine neue Session.</span>
|
|
||||||
</div>
|
|
||||||
<div class="settings-row">
|
|
||||||
<label>Verbose Logging</label>
|
|
||||||
<label class="checkbox-row" style="margin:0">
|
|
||||||
<input type="checkbox" class="settings-autosave" id="logVerboseInput" ${globalSettings.logVerbose ? 'checked' : ''}>
|
|
||||||
<span>DEBUG-Einträge in debug.log schreiben (Performance ↓, Diagnostik ↑)</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="settings-section-label">Unbeaufsichtigter Betrieb</div>
|
<div class="settings-section-label">Unbeaufsichtigter Betrieb</div>
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<label>Auto-Retry Runden</label>
|
<label>Auto-Retry Runden</label>
|
||||||
@ -2999,97 +2995,7 @@ function renderSettings() {
|
|||||||
<input type="text" class="key-input settings-autosave" id="webhookMentionInput" value="${escapeAttr(globalSettings.webhookMention || '')}" placeholder="deine User-ID, role:ROLLEN-ID, @here oder @everyone">
|
<input type="text" class="key-input settings-autosave" id="webhookMentionInput" value="${escapeAttr(globalSettings.webhookMention || '')}" placeholder="deine User-ID, role:ROLLEN-ID, @here oder @everyone">
|
||||||
<span class="hint">Damit Discord dich wirklich benachrichtigt (Push). User-ID: in Discord Entwicklermodus an → Rechtsklick auf deinen Namen → 'User-ID kopieren'. Leer = nur posten, kein Ping.</span>
|
<span class="hint">Damit Discord dich wirklich benachrichtigt (Push). User-ID: in Discord Entwicklermodus an → Rechtsklick auf deinen Namen → 'User-ID kopieren'. Leer = nur posten, kein Ping.</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-section-label">Diagnose</div>
|
<div class="settings-section-label">Ordnerüberwachung <span class="panel-status${fm.enabled && fm.folderPath ? ' active' : ''}" id="folderMonitorStatusBadge">${fm.enabled && fm.folderPath ? 'Aktiv' : 'Inaktiv'}</span></div>
|
||||||
<div class="settings-row" id="logPathsBlock">
|
|
||||||
<label>Log-Dateien</label>
|
|
||||||
<div class="log-paths-list" id="logPathsList" style="flex:1;display:flex;flex-direction:column;gap:4px">
|
|
||||||
<span class="hint">Wird geladen…</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings-row">
|
|
||||||
<label>Support-Paket</label>
|
|
||||||
<button class="btn btn-xs btn-secondary" id="createSupportBundleBtn" title="Sammelt alle Logs + sanitierte Config (Credentials maskiert) + App-Versionen in eine einzelne .txt-Datei zum Teilen.">Diagnose-Paket exportieren</button>
|
|
||||||
<span class="hint" id="supportBundleHint">Eine .txt mit Logs + sanitierter Config; Passwörter/API-Keys werden vor dem Speichern maskiert.</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
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 = `
|
|
||||||
<div class="hoster-panel-header" data-hoster="folderMonitor">
|
|
||||||
<span class="panel-arrow">▶</span>
|
|
||||||
<span class="panel-title">Ordnerüberwachung</span>
|
|
||||||
<span class="panel-status${fm.enabled && fm.folderPath ? ' active' : ''}" id="folderMonitorStatusBadge">${fm.enabled && fm.folderPath ? 'Aktiv' : 'Inaktiv'}</span>
|
|
||||||
</div>
|
|
||||||
<div class="hoster-panel-body" data-panel="folderMonitor" style="display:none">
|
|
||||||
<div class="settings-section-label">Ordner</div>
|
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<label>Ordnerpfad</label>
|
<label>Ordnerpfad</label>
|
||||||
<input type="text" class="key-input settings-autosave" id="fmFolderPathInput" value="${escapeAttr(fm.folderPath || '')}" placeholder="Ordner wählen..." style="flex:1">
|
<input type="text" class="key-input settings-autosave" id="fmFolderPathInput" value="${escapeAttr(fm.folderPath || '')}" placeholder="Ordner wählen..." style="flex:1">
|
||||||
@ -3136,52 +3042,48 @@ function renderSettings() {
|
|||||||
</div>`).join('')}
|
</div>`).join('')}
|
||||||
</div>
|
</div>
|
||||||
${configuredAccounts.length === 0 ? '<p class="hint" style="margin:0">Erst Accounts anlegen, dann hier auswählen.</p>' : '<p class="hint" style="margin:2px 0 0">Keine Auswahl = Hoster-Modal bei jeder Datei.</p>'}
|
${configuredAccounts.length === 0 ? '<p class="hint" style="margin:0">Erst Accounts anlegen, dann hier auswählen.</p>' : '<p class="hint" style="margin:2px 0 0">Keine Auswahl = Hoster-Modal bei jeder Datei.</p>'}
|
||||||
|
`;
|
||||||
|
|
||||||
|
pages.logs.innerHTML = `
|
||||||
|
<div class="settings-section-label">Log</div>
|
||||||
|
<div class="settings-row">
|
||||||
|
<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>Log-Datei-Modus</label>
|
||||||
|
<select class="hs-input settings-autosave" id="logModeInput">
|
||||||
|
<option value="single" ${(window.LogMode ? window.LogMode.normalizeLogMode(globalSettings) : (globalSettings.logMode || 'single')) === 'single' ? 'selected' : ''}>Eine Datei</option>
|
||||||
|
<option value="daily" ${(window.LogMode ? window.LogMode.normalizeLogMode(globalSettings) : (globalSettings.logMode || 'single')) === 'daily' ? 'selected' : ''}>Pro Tag</option>
|
||||||
|
<option value="session" ${(window.LogMode ? window.LogMode.normalizeLogMode(globalSettings) : (globalSettings.logMode || 'single')) === 'session' ? 'selected' : ''}>Pro Session</option>
|
||||||
|
</select>
|
||||||
|
<span class="hint">Pro Session = neue Datei bei jedem App-Start; nach komplettem Schließen + erneutem Öffnen beginnt eine neue Session.</span>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row">
|
||||||
|
<label>Verbose Logging</label>
|
||||||
|
<label class="checkbox-row" style="margin:0">
|
||||||
|
<input type="checkbox" class="settings-autosave" id="logVerboseInput" ${globalSettings.logVerbose ? 'checked' : ''}>
|
||||||
|
<span>DEBUG-Einträge in debug.log schreiben (Performance ↓, Diagnostik ↑)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="settings-section-label">Diagnose</div>
|
||||||
|
<div class="settings-row" id="logPathsBlock">
|
||||||
|
<label>Log-Dateien</label>
|
||||||
|
<div class="log-paths-list" id="logPathsList" style="flex:1;display:flex;flex-direction:column;gap:4px">
|
||||||
|
<span class="hint">Wird geladen…</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row">
|
||||||
|
<label>Support-Paket</label>
|
||||||
|
<button class="btn btn-xs btn-secondary" id="createSupportBundleBtn" title="Sammelt alle Logs + sanitierte Config (Credentials maskiert) + App-Versionen in eine einzelne .txt-Datei zum Teilen.">Diagnose-Paket exportieren</button>
|
||||||
|
<span class="hint" id="supportBundleHint">Eine .txt mit Logs + sanitierter Config; Passwörter/API-Keys werden vor dem Speichern maskiert.</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
container.appendChild(folderMonitorPanel);
|
|
||||||
|
|
||||||
// Toggle folder monitor panel
|
pages.remote.innerHTML = `
|
||||||
folderMonitorPanel.querySelector('.hoster-panel-header').addEventListener('click', () => {
|
<div class="settings-section-label">Server <span class="panel-status${remoteSettings.enabled ? ' active' : ''}" id="remoteStatusBadge">${remoteSettings.enabled ? 'Aktiv' : 'Inaktiv'}</span></div>
|
||||||
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 ? '▶' : '▼';
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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 = `
|
|
||||||
<div class="hoster-panel-header" data-hoster="remote">
|
|
||||||
<span class="panel-arrow">▶</span>
|
|
||||||
<span class="panel-title">Fernsteuerung</span>
|
|
||||||
<span class="panel-status${remoteSettings.enabled ? ' active' : ''}" id="remoteStatusBadge">${remoteSettings.enabled ? 'Aktiv' : 'Inaktiv'}</span>
|
|
||||||
</div>
|
|
||||||
<div class="hoster-panel-body" data-panel="remote" style="display:none">
|
|
||||||
<div class="settings-section-label">Server</div>
|
|
||||||
<div class="settings-grid-mini">
|
<div class="settings-grid-mini">
|
||||||
<div class="settings-row checkbox-row">
|
<div class="settings-row checkbox-row">
|
||||||
<label>Aktiviert</label>
|
<label>Aktiviert</label>
|
||||||
@ -3206,20 +3108,103 @@ function renderSettings() {
|
|||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<span id="remoteConnectionStatus" style="color:#94a3b8">Prüfe...</span>
|
<span id="remoteConnectionStatus" style="color:#94a3b8">Prüfe...</span>
|
||||||
</div>
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
pages.backup.innerHTML = `
|
||||||
|
<p class="hint" style="margin:0 0 10px">Alle Accounts und Einstellungen exportieren oder importieren. Der Upload-Verlauf bleibt lokal und wird nicht übertragen; nach einem Import ist der Verlauf-Tab leer.</p>
|
||||||
|
<div style="display:flex;gap:8px">
|
||||||
|
<button class="btn btn-secondary" id="exportBackupBtn">Backup exportieren</button>
|
||||||
|
<button class="btn btn-secondary" id="importBackupBtn">Backup importieren</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
container.appendChild(remotePanel);
|
|
||||||
|
|
||||||
// Toggle remote panel
|
subtabBar.addEventListener('click', (e) => {
|
||||||
remotePanel.querySelector('.hoster-panel-header').addEventListener('click', () => {
|
const btn = e.target.closest('[data-subtab]');
|
||||||
const body = remotePanel.querySelector('.hoster-panel-body');
|
if (!btn) return;
|
||||||
const arrow = remotePanel.querySelector('.panel-arrow');
|
const target = btn.dataset.subtab;
|
||||||
const isOpen = body.style.display !== 'none';
|
subtabBar.querySelectorAll('.settings-subtab').forEach((b) => {
|
||||||
body.style.display = isOpen ? 'none' : 'block';
|
b.classList.toggle('active', b === btn);
|
||||||
arrow.innerHTML = isOpen ? '▶' : '▼';
|
});
|
||||||
|
Object.values(pages).forEach((p) => {
|
||||||
|
p.classList.toggle('active', p.dataset.subpage === target);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
_renderLogPathsList(document.getElementById('logPathsList'));
|
||||||
|
const testWebhookBtn = document.getElementById('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 = document.getElementById('logVerboseInput');
|
||||||
|
if (verboseInput) {
|
||||||
|
verboseInput.addEventListener('change', () => {
|
||||||
|
if (window.api && window.api.setLogVerbose) window.api.setLogVerbose(verboseInput.checked).catch(() => {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const sbBtn = document.getElementById('createSupportBundleBtn');
|
||||||
|
if (sbBtn) {
|
||||||
|
sbBtn.addEventListener('click', async () => {
|
||||||
|
const hint = document.getElementById('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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Copy token
|
|
||||||
document.getElementById('remoteCopyTokenBtn').addEventListener('click', async () => {
|
document.getElementById('remoteCopyTokenBtn').addEventListener('click', async () => {
|
||||||
const token = document.getElementById('remoteTokenInput').value;
|
const token = document.getElementById('remoteTokenInput').value;
|
||||||
if (token) {
|
if (token) {
|
||||||
@ -3229,14 +3214,12 @@ function renderSettings() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Regenerate token
|
|
||||||
document.getElementById('remoteRegenerateTokenBtn').addEventListener('click', async () => {
|
document.getElementById('remoteRegenerateTokenBtn').addEventListener('click', async () => {
|
||||||
const newToken = await window.api.remoteGenerateToken();
|
const newToken = await window.api.remoteGenerateToken();
|
||||||
document.getElementById('remoteTokenInput').value = newToken;
|
document.getElementById('remoteTokenInput').value = newToken;
|
||||||
scheduleSettingsSave();
|
scheduleSettingsSave();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update status
|
|
||||||
window.api.remoteStatus().then(status => {
|
window.api.remoteStatus().then(status => {
|
||||||
const el = document.getElementById('remoteConnectionStatus');
|
const el = document.getElementById('remoteConnectionStatus');
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
@ -3249,43 +3232,9 @@ function renderSettings() {
|
|||||||
}
|
}
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
|
|
||||||
// Live client count updates (listener registered once in init, not here)
|
|
||||||
|
|
||||||
// --- Backup Panel ---
|
|
||||||
const backupPanel = document.createElement('div');
|
|
||||||
backupPanel.className = 'hoster-settings-panel';
|
|
||||||
backupPanel.innerHTML = `
|
|
||||||
<div class="hoster-panel-header" data-hoster="backup">
|
|
||||||
<span class="panel-arrow">▶</span>
|
|
||||||
<span class="panel-title">Backup</span>
|
|
||||||
<span class="panel-status active">System</span>
|
|
||||||
</div>
|
|
||||||
<div class="hoster-panel-body" data-panel="backup" style="display:none">
|
|
||||||
<p class="hint" style="margin:0 0 10px">Alle Accounts und Einstellungen exportieren oder importieren. Der Upload-Verlauf bleibt lokal und wird nicht übertragen; nach einem Import ist der Verlauf-Tab leer.</p>
|
|
||||||
<div style="display:flex;gap:8px">
|
|
||||||
<button class="btn btn-secondary" id="exportBackupBtn">Backup exportieren</button>
|
|
||||||
<button class="btn btn-secondary" id="importBackupBtn">Backup importieren</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
container.appendChild(backupPanel);
|
|
||||||
|
|
||||||
backupPanel.querySelector('.hoster-panel-header').addEventListener('click', () => {
|
|
||||||
const body = backupPanel.querySelector('.hoster-panel-body');
|
|
||||||
const arrow = backupPanel.querySelector('.panel-arrow');
|
|
||||||
const isOpen = body.style.display !== 'none';
|
|
||||||
body.style.display = isOpen ? 'none' : 'block';
|
|
||||||
arrow.innerHTML = isOpen ? '▶' : '▼';
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('exportBackupBtn').addEventListener('click', () => doBackupExport());
|
document.getElementById('exportBackupBtn').addEventListener('click', () => doBackupExport());
|
||||||
document.getElementById('importBackupBtn').addEventListener('click', () => doBackupImport());
|
document.getElementById('importBackupBtn').addEventListener('click', () => doBackupImport());
|
||||||
|
|
||||||
const hosterPointer = document.createElement('div');
|
|
||||||
hosterPointer.className = 'settings-hoster-pointer';
|
|
||||||
hosterPointer.innerHTML = 'Die <strong>Upload-Einstellungen pro Hoster</strong> (Retries, Speed, Parallel, Max-Größe, Log) sind jetzt im <strong>Accounts</strong>-Tab — direkt bei den jeweiligen Hostern unter „Upload-Einstellungen".';
|
|
||||||
container.appendChild(hosterPointer);
|
|
||||||
|
|
||||||
document.getElementById('chooseLogFilePathBtn')?.addEventListener('click', chooseLogFilePath);
|
document.getElementById('chooseLogFilePathBtn')?.addEventListener('click', chooseLogFilePath);
|
||||||
document.getElementById('openLogFolderBtn')?.addEventListener('click', () => window.api.openLogFolder());
|
document.getElementById('openLogFolderBtn')?.addEventListener('click', () => window.api.openLogFolder());
|
||||||
document.getElementById('manualUpdateCheckBtn')?.addEventListener('click', async (e) => {
|
document.getElementById('manualUpdateCheckBtn')?.addEventListener('click', async (e) => {
|
||||||
@ -3333,42 +3282,62 @@ function scheduleSettingsSave() {
|
|||||||
async function saveSettings(options = {}) {
|
async function saveSettings(options = {}) {
|
||||||
const { feedbackText = 'Gespeichert!' } = options;
|
const { feedbackText = 'Gespeichert!' } = options;
|
||||||
const newHosterSettings = { ...(config.hosterSettings || {}) };
|
const newHosterSettings = { ...(config.hosterSettings || {}) };
|
||||||
|
const cur = config.globalSettings || {};
|
||||||
|
const curFm = cur.folderMonitor || {};
|
||||||
|
const curRemote = cur.remote || {};
|
||||||
|
const elTxt = (id, fb) => { const el = document.getElementById(id); return el ? el.value : fb; };
|
||||||
|
const elChk = (id, fb) => { const el = document.getElementById(id); return el ? !!el.checked : fb; };
|
||||||
|
const elInt = (id, curVal, dflt, lo, hi) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (!el) return curVal;
|
||||||
|
const n = parseInt(el.value || String(dflt), 10) || dflt;
|
||||||
|
return Math.max(lo, Math.min(hi, n));
|
||||||
|
};
|
||||||
|
|
||||||
const globalSettings = {
|
const globalSettings = {
|
||||||
...(config.globalSettings || {}),
|
...cur,
|
||||||
logFilePath: (document.getElementById('logFilePathInput')?.value || '').trim(),
|
logFilePath: elTxt('logFilePathInput', cur.logFilePath || '').trim(),
|
||||||
logMode: (() => {
|
logMode: (() => {
|
||||||
const v = document.getElementById('logModeInput')?.value;
|
const el = document.getElementById('logModeInput');
|
||||||
|
if (!el) return cur.logMode || 'single';
|
||||||
|
const v = el.value;
|
||||||
return (v === 'single' || v === 'daily' || v === 'session') ? v : 'single';
|
return (v === 'single' || v === 'daily' || v === 'session') ? v : 'single';
|
||||||
})(),
|
})(),
|
||||||
resumeQueueOnLaunch: !!document.getElementById('resumeQueueOnLaunchInput')?.checked,
|
resumeQueueOnLaunch: elChk('resumeQueueOnLaunchInput', cur.resumeQueueOnLaunch !== false),
|
||||||
parallelUploadCount: Math.max(0, Math.min(100, parseInt(document.getElementById('parallelUploadCountInput')?.value || '0', 10) || 0)),
|
parallelUploadCount: elInt('parallelUploadCountInput', cur.parallelUploadCount ?? 0, 0, 0, 100),
|
||||||
scaleParallelUploads: !!document.getElementById('scaleParallelUploadsInput')?.checked,
|
scaleParallelUploads: elChk('scaleParallelUploadsInput', !!cur.scaleParallelUploads),
|
||||||
removeFromQueueOnDone: !!document.getElementById('removeFromQueueOnDoneInput')?.checked,
|
removeFromQueueOnDone: elChk('removeFromQueueOnDoneInput', !!cur.removeFromQueueOnDone),
|
||||||
showDropTarget: !!document.getElementById('showDropTargetInput')?.checked,
|
showDropTarget: elChk('showDropTargetInput', !!cur.showDropTarget),
|
||||||
globalMaxSpeedKbs: Math.max(0, Math.round((parseFloat(document.getElementById('globalMaxSpeedMbsInput')?.value || '0') || 0) * 1024)),
|
globalMaxSpeedKbs: (() => {
|
||||||
logVerbose: !!document.getElementById('logVerboseInput')?.checked,
|
const el = document.getElementById('globalMaxSpeedMbsInput');
|
||||||
webhookUrl: (document.getElementById('webhookUrlInput')?.value || '').trim(),
|
if (!el) return cur.globalMaxSpeedKbs ?? 0;
|
||||||
webhookMention: (document.getElementById('webhookMentionInput')?.value || '').trim(),
|
return Math.max(0, Math.round((parseFloat(el.value || '0') || 0) * 1024));
|
||||||
autoRetryRounds: Math.max(0, Math.min(5, parseInt(document.getElementById('autoRetryRoundsInput')?.value || '0', 10) || 0)),
|
})(),
|
||||||
autoRetryDelayMin: Math.max(1, Math.min(120, parseInt(document.getElementById('autoRetryDelayMinInput')?.value || '5', 10) || 5)),
|
logVerbose: elChk('logVerboseInput', !!cur.logVerbose),
|
||||||
|
webhookUrl: elTxt('webhookUrlInput', cur.webhookUrl || '').trim(),
|
||||||
|
webhookMention: elTxt('webhookMentionInput', cur.webhookMention || '').trim(),
|
||||||
|
autoRetryRounds: elInt('autoRetryRoundsInput', cur.autoRetryRounds ?? 0, 0, 0, 5),
|
||||||
|
autoRetryDelayMin: elInt('autoRetryDelayMinInput', cur.autoRetryDelayMin ?? 5, 5, 1, 120),
|
||||||
folderMonitor: {
|
folderMonitor: {
|
||||||
...((config.globalSettings || {}).folderMonitor || {}),
|
...curFm,
|
||||||
enabled: !!document.getElementById('fmEnabledInput')?.checked,
|
enabled: elChk('fmEnabledInput', !!curFm.enabled),
|
||||||
folderPath: (document.getElementById('fmFolderPathInput')?.value || '').trim(),
|
folderPath: elTxt('fmFolderPathInput', curFm.folderPath || '').trim(),
|
||||||
recursive: !!document.getElementById('fmRecursiveInput')?.checked,
|
recursive: elChk('fmRecursiveInput', !!curFm.recursive),
|
||||||
filterMode: document.getElementById('fmFilterModeInput')?.value || 'include',
|
filterMode: (() => { const el = document.getElementById('fmFilterModeInput'); return el ? (el.value || 'include') : (curFm.filterMode || 'include'); })(),
|
||||||
extensions: (document.getElementById('fmExtensionsInput')?.value || '').trim(),
|
extensions: elTxt('fmExtensionsInput', curFm.extensions || '').trim(),
|
||||||
skipDuplicates: !!document.getElementById('fmSkipDuplicatesInput')?.checked,
|
skipDuplicates: elChk('fmSkipDuplicatesInput', curFm.skipDuplicates !== false),
|
||||||
delaySec: Math.max(1, parseInt(document.getElementById('fmDelaySecInput')?.value || '3', 10) || 3),
|
delaySec: elInt('fmDelaySecInput', curFm.delaySec ?? 3, 3, 1, 300),
|
||||||
autoStart: !!document.getElementById('fmAutoStartInput')?.checked,
|
autoStart: elChk('fmAutoStartInput', curFm.autoStart !== false),
|
||||||
hosters: Array.from(document.querySelectorAll('.fm-hoster-checkbox:checked')).map(el => el.dataset.fmHoster)
|
hosters: document.querySelector('.fm-hoster-checkbox')
|
||||||
|
? Array.from(document.querySelectorAll('.fm-hoster-checkbox:checked')).map(el => el.dataset.fmHoster)
|
||||||
|
: (curFm.hosters || [])
|
||||||
},
|
},
|
||||||
remote: {
|
remote: {
|
||||||
...((config.globalSettings || {}).remote || {}),
|
...curRemote,
|
||||||
enabled: !!document.getElementById('remoteEnabledInput')?.checked,
|
enabled: elChk('remoteEnabledInput', !!curRemote.enabled),
|
||||||
port: Math.max(1024, Math.min(65535, parseInt(document.getElementById('remotePortInput')?.value || '9100', 10) || 9100)),
|
port: elInt('remotePortInput', curRemote.port || 9100, 9100, 1024, 65535),
|
||||||
token: (document.getElementById('remoteTokenInput')?.value || '').trim(),
|
token: elTxt('remoteTokenInput', curRemote.token || '').trim(),
|
||||||
allowInput: !!document.getElementById('remoteAllowInputInput')?.checked
|
allowInput: elChk('remoteAllowInputInput', curRemote.allowInput !== false)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -4771,6 +4740,9 @@ function setupListeners() {
|
|||||||
// Virtual scroll for large queues
|
// Virtual scroll for large queues
|
||||||
const queueContainer = document.getElementById('queueContainer');
|
const queueContainer = document.getElementById('queueContainer');
|
||||||
if (queueContainer) queueContainer.addEventListener('scroll', _onQueueScroll, { passive: true });
|
if (queueContainer) queueContainer.addEventListener('scroll', _onQueueScroll, { passive: true });
|
||||||
|
if (queueContainer && typeof window.ResizeObserver !== 'undefined') {
|
||||||
|
new window.ResizeObserver(_onQueueScroll).observe(queueContainer);
|
||||||
|
}
|
||||||
|
|
||||||
// Queue table sorting
|
// Queue table sorting
|
||||||
document.querySelectorAll('#queueTable th.sortable').forEach(th => {
|
document.querySelectorAll('#queueTable th.sortable').forEach(th => {
|
||||||
|
|||||||
@ -842,6 +842,37 @@ body.col-resizing, body.col-resizing * { cursor: col-resize !important; user-sel
|
|||||||
.settings-divider { height: 1px; background: var(--border); margin: 12px 0; }
|
.settings-divider { height: 1px; background: var(--border); margin: 12px 0; }
|
||||||
.hoster-panel-body h4 { font-size: 12px; color: var(--text-muted); margin-bottom: 8px; font-weight: 500; }
|
.hoster-panel-body h4 { font-size: 12px; color: var(--text-muted); margin-bottom: 8px; font-weight: 500; }
|
||||||
|
|
||||||
|
.settings-subtabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.settings-subtab {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 8px 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
transition: color 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
.settings-subtab:hover { color: var(--text); }
|
||||||
|
.settings-subtab.active { color: var(--text); border-bottom-color: var(--accent); }
|
||||||
|
.settings-subpage {
|
||||||
|
display: none;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 4px 14px 14px;
|
||||||
|
}
|
||||||
|
.settings-subpage.active { display: block; }
|
||||||
|
|
||||||
.settings-row {
|
.settings-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user