diff --git a/main.js b/main.js index 943d14c..92fd80d 100644 --- a/main.js +++ b/main.js @@ -836,35 +836,52 @@ async function runHosterHealthCheck(config, requestedChecks) { checks = requestedChecks; } - const results = await Promise.all(checks.map(async ({ hoster, accountId, otp }) => { + { + const seen = new Set(); + const cleaned = []; + for (const c of checks) { + if (!c || !c.hoster) continue; + if (!c.accountId) { cleaned.push({ ...c, _invalid: true }); continue; } + const key = `${c.hoster}|${c.accountId}`; + if (seen.has(key)) continue; + seen.add(key); + cleaned.push(c); + } + checks = cleaned; + } + + const runOne = async ({ hoster, accountId, otp, _invalid }) => { + if (_invalid) { + return { hoster, accountId, status: 'error', message: 'Account-ID fehlt im Check-Payload' }; + } if (!allowed.includes(hoster)) { return { hoster, accountId, status: 'skipped', message: 'Kein Health-Check fuer diesen Hoster' }; } - - // Find specific account const accounts = config.hosters[hoster]; const hosterConfig = Array.isArray(accounts) ? accounts.find(a => a.id === accountId) : null; - try { - let result; - if (hoster === 'doodstream.com') { - result = await withTimeout(checkDoodstreamHealth(hosterConfig, otp), HEALTH_CHECK_TIMEOUT, 'Doodstream-Check'); - } else if (hoster === 'vidmoly.me') { - result = await withTimeout(checkVidmolyHealth(hosterConfig), HEALTH_CHECK_TIMEOUT, 'Vidmoly-Check'); - } else if (hoster === 'voe.sx') { - result = await withTimeout(checkVoeHealth(hosterConfig), HEALTH_CHECK_TIMEOUT, 'VOE-Check'); - } else if (hoster === 'byse.sx') { - result = await withTimeout(checkByseHealth(hosterConfig), HEALTH_CHECK_TIMEOUT, 'Byse-Check'); - } else if (hoster === 'clouddrop.cc') { - result = await withTimeout(checkClouddropHealth(hosterConfig), HEALTH_CHECK_TIMEOUT, 'Clouddrop-Check'); - } else { - return { hoster, accountId, status: 'skipped', message: 'Kein Health-Check fuer diesen Hoster' }; - } + const result = await _dispatchHealthCheck(hoster, hosterConfig, otp || ''); return { hoster, accountId, ...result }; } catch (err) { return { hoster, accountId, status: 'error', message: err && err.message ? err.message : 'Health-Check fehlgeschlagen' }; } + }; + + const groups = new Map(); + for (const c of checks) { + if (!groups.has(c.hoster)) groups.set(c.hoster, []); + groups.get(c.hoster).push(c); + } + const groupResults = await Promise.all(Array.from(groups.values()).map(async (group) => { + const out = []; + for (const c of group) { + out.push(await runOne(c)); + } + return out; })); + const indexByCheck = new Map(); + groupResults.flat().forEach((r) => { indexByCheck.set(`${r.hoster}|${r.accountId || ''}`, r); }); + const results = checks.map(c => indexByCheck.get(`${c.hoster}|${c.accountId || ''}`)); return { checkedAt: new Date().toISOString(), results }; } diff --git a/renderer/app.js b/renderer/app.js index cd32912..0b37479 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -2421,19 +2421,9 @@ function updateStatusBar() { // --- Health Check --- -function renderHealthCheckResults(results) { +function renderHealthCheckResults(_results) { const container = document.getElementById('healthCheckResults'); - if (!container) return; - if (!results || results.length === 0) { container.innerHTML = ''; return; } - - container.innerHTML = results.map(item => { - const status = item.status || 'skipped'; - return `
- ${escapeHtml(item.hoster ? getHosterLabel(item.hoster) : '')} - [${status.toUpperCase()}] - ${escapeHtml(item.message || '')} -
`; - }).join(''); + if (container) container.innerHTML = ''; } async function executeHealthCheck(hosters, _mode) { @@ -3084,6 +3074,40 @@ function updateAccountCard(accountId) { const tmp = document.createElement('div'); tmp.innerHTML = _buildAccountCardHtml(found.name, found.account, idx); card.replaceWith(tmp.firstElementChild); + _refreshHosterGroupHeader(found.name); +} + +function _refreshHosterGroupHeader(name) { + const container = document.getElementById('accountsList'); + if (!container) return; + const group = container.querySelector(`.account-hoster-group[data-hoster-group="${name}"]`); + if (!group) return; + const accounts = config.hosters[name] || []; + const summary = _summarizeHosterGroup(accounts); + let dot = 'unchecked'; + if (summary.error > 0) dot = 'error'; + else if (summary.checking > 0) dot = 'checking'; + else if (summary.ok > 0 && summary.unchecked === 0) dot = 'ok'; + const dotEl = group.querySelector('.account-hoster-group-header .account-status-dot'); + if (dotEl) dotEl.className = `account-status-dot status-${dot}`; + const countEl = group.querySelector('.account-hoster-group-count'); + if (countEl) countEl.textContent = `${summary.ok}/${summary.total}`; + group.querySelectorAll('.account-hoster-group-meta').forEach(el => el.remove()); + const header = group.querySelector('.account-hoster-group-header'); + if (header) { + if (summary.disabled) { + const meta = document.createElement('span'); + meta.className = 'account-hoster-group-meta'; + meta.textContent = `${summary.disabled} deaktiviert`; + header.appendChild(meta); + } + if (summary.error) { + const meta = document.createElement('span'); + meta.className = 'account-hoster-group-meta error'; + meta.textContent = `${summary.error} Fehler`; + header.appendChild(meta); + } + } } let _accountListenersBound = false; @@ -3117,16 +3141,65 @@ function renderAccounts() { for (const name of HOSTERS) { const accounts = byHoster[name]; if (!accounts || accounts.length === 0) continue; - html += `
-
${escapeHtml(getHosterLabel(name))}
`; - accounts.forEach((account, idx) => { html += _buildAccountCardHtml(name, account, idx); }); - html += '
'; + html += _buildAccountHosterGroupHtml(name, accounts); } container.innerHTML = html; if (!_accountListenersBound) bindAccountListeners(container); } +function _summarizeHosterGroup(accounts) { + let ok = 0, error = 0, checking = 0, unchecked = 0, disabled = 0; + for (const a of accounts) { + if (a.enabled === false) { disabled++; continue; } + const s = (accountStatuses[a.id] && accountStatuses[a.id].status) || 'unchecked'; + if (s === 'ok' || s === 'warn') ok++; + else if (s === 'error') error++; + else if (s === 'checking') checking++; + else unchecked++; + } + return { ok, error, checking, unchecked, disabled, total: accounts.length }; +} + +function _hosterGroupOpenState(name, summary) { + const prev = _hosterGroupOpenMemory.get(name); + if (prev && typeof prev === 'object') { + if (summary.error > (prev.errorsAtClose || 0)) { + _hosterGroupOpenMemory.delete(name); + return true; + } + return prev.state === 'open'; + } + return summary.error > 0 || summary.checking > 0 || summary.unchecked > 0; +} + +const _hosterGroupOpenMemory = new Map(); + +function _buildAccountHosterGroupHtml(name, accounts) { + const summary = _summarizeHosterGroup(accounts); + const isOpen = _hosterGroupOpenState(name, summary); + let dot = 'unchecked'; + if (summary.error > 0) dot = 'error'; + else if (summary.checking > 0) dot = 'checking'; + else if (summary.ok > 0 && summary.unchecked === 0) dot = 'ok'; + const countLabel = `${summary.ok}/${summary.total}`; + const arrow = isOpen ? '▼' : '▶'; + let cardsHtml = ''; + accounts.forEach((account, idx) => { cardsHtml += _buildAccountCardHtml(name, account, idx); }); + const bodyStyle = isOpen ? '' : 'style="display:none"'; + return `
+
+ ${arrow} + + + + ${summary.disabled ? `` : ''} + ${summary.error ? `` : ''} +
+
${cardsHtml}
+
`; +} + // Single set of delegated listeners on the accounts container. Bound once on // the first render and reused for every subsequent in-place update / card // swap. Previously we rebound 4 × N button listeners + 5 × N drag listeners @@ -3135,6 +3208,21 @@ function renderAccounts() { function bindAccountListeners(container) { _accountListenersBound = true; container.addEventListener('click', (e) => { + const header = e.target.closest('[data-hoster-toggle]'); + if (header && !e.target.closest('button')) { + const name = header.dataset.hosterToggle; + const group = header.closest('.account-hoster-group'); + const body = group && group.querySelector('.account-hoster-group-body'); + const arrow = header.querySelector('.panel-arrow'); + if (body) { + const willOpen = body.style.display === 'none'; + body.style.display = willOpen ? '' : 'none'; + if (arrow) arrow.innerHTML = willOpen ? '▼' : '▶'; + const summary = _summarizeHosterGroup(config.hosters[name] || []); + _hosterGroupOpenMemory.set(name, { state: willOpen ? 'open' : 'closed', errorsAtClose: summary.error }); + } + return; + } const btn = e.target.closest('button'); if (!btn) return; if (btn.dataset.accountToggle) return toggleAccount(btn.dataset.accountToggle); diff --git a/renderer/styles.css b/renderer/styles.css index af70ea5..f514590 100644 --- a/renderer/styles.css +++ b/renderer/styles.css @@ -875,17 +875,62 @@ body.col-resizing, body.col-resizing * { cursor: col-resize !important; user-sel .account-hoster-group { margin-bottom: 12px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg-card); + overflow: hidden; } +.account-hoster-group-header { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + cursor: pointer; + user-select: none; + background: var(--bg-card); + transition: background 0.1s; +} +.account-hoster-group-header:hover { background: var(--bg-card-hover); } .account-hoster-group-title { font-size: 12px; font-weight: 600; - color: var(--text-muted); + color: var(--text); text-transform: uppercase; letter-spacing: 0.5px; - margin-bottom: 6px; - padding-left: 4px; + flex: 1; +} +.account-hoster-group-count { + font-size: 12px; + color: var(--text-muted); + font-variant-numeric: tabular-nums; +} +.account-hoster-group-meta { + font-size: 11px; + color: var(--text-muted); + padding: 1px 6px; + border-radius: 4px; + background: rgba(255,255,255,0.04); +} +.account-hoster-group-meta.error { + color: var(--danger, #e57373); + background: rgba(229, 115, 115, 0.12); +} +.account-hoster-group-body { + padding: 8px; + border-top: 1px solid var(--border); } .account-hoster-group .account-card { margin-bottom: 4px; } +.account-hoster-group .account-card:last-child { margin-bottom: 0; } +.account-status-dot.status-ok { background: #4caf50; } +.account-status-dot.status-error { background: #e57373; } +.account-status-dot.status-checking { background: #f0c36c; } +.account-status-dot.status-unchecked { background: #6c757d; } +.account-hoster-group-header .account-status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + display: inline-block; +} .accounts-empty { text-align: center;