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 ``;
+}
+
// 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;