feat: add host health overview

Summarize up to fifty recent batches and current account state into a local host health table with success rates, effective historical throughput, recent failures, last success, and bilingual accessible empty states.
This commit is contained in:
Sucukdeluxe
2026-08-16 22:46:15 +02:00
parent 287363aee7
commit 0ee749617c
7 changed files with 537 additions and 3 deletions
+68
View File
@@ -3612,9 +3612,21 @@ function _handleProgressImpl(data) {
persistQueueStateSoon();
}
function recordHosterHealthBatch(summary) {
if (!summary || typeof summary !== 'object' || !Array.isArray(summary.files)) return;
const current = Array.isArray(window._historyForStats) ? window._historyForStats : [];
const next = summary.id
? current.filter(batch => batch?.id !== summary.id)
: current.filter(batch => batch !== summary);
window._historyForStats = [summary, ...next].slice(0, 50);
_invalidateHosterLifetimeCache();
renderHosterHealthOverview();
}
function handleBatchDone(summary, options = {}) {
uploading = false;
applySummaryResults(summary, options.historyPersisted !== false);
if (options.historyPersisted !== false) recordHosterHealthBatch(summary);
_deletedJobIds.clear(); // Free memory — stale IDs no longer needed after batch completes
// Prune session-stats sets to current queue contents. Without this, IDs
// of jobs that were removed from queueJobs (via removeFromQueueOnDone
@@ -5851,6 +5863,7 @@ function updateAccountCard(accountId) {
_refreshHosterGroupHeader(found.name);
updateAccountSidebarSummary();
_applyAccountSidebarFilter();
renderHosterHealthOverview();
}
function _refreshHosterGroupHeader(name) {
@@ -5885,6 +5898,56 @@ function _refreshHosterGroupHeader(name) {
let _accountListenersBound = false;
function renderHosterHealthOverview() {
const body = document.getElementById('hosterHealthBody');
if (!body || !window.Stats?.summarizeHosterHealth) return;
if (window._historyForStats === undefined) {
body.innerHTML = `<tr><td colspan="8" data-hoster-health-empty>${escapeHtml(localizeUiText('Wird geladen…'))}</td></tr>`;
return;
}
if (window._historyForStats === null) {
body.innerHTML = `<tr><td colspan="8" data-hoster-health-empty>${escapeHtml(localizeUiText('Verlauf nicht verfügbar.'))}</td></tr>`;
return;
}
const summary = window.Stats.summarizeHosterHealth(window._historyForStats, {
now: new Date(),
hosters: config.hosters,
accountStatuses,
sessionFailedKeys: _sessionFailedKeys
});
const names = [...HOSTERS, ...Object.keys(summary).filter(name => !HOSTERS.includes(name))]
.filter(name => summary[name] && (summary[name].sampleSize > 0 || summary[name].configuredAccounts > 0));
if (names.length === 0) {
body.innerHTML = `<tr><td colspan="8" data-hoster-health-empty>${escapeHtml(localizeUiText('Noch keine Hoster-Daten.'))}</td></tr>`;
return;
}
body.innerHTML = names.map(name => {
const row = summary[name];
const rate = row.successRate === null ? localizeUiText('Nicht geprüft') : `${Math.round(row.successRate * 100)} %`;
const throughput = row.effectiveBytesPerSecond === null
? localizeUiText('Nicht geprüft')
: formatSpeed(row.effectiveBytesPerSecond / 1024);
const lastSuccess = row.lastSuccessAt ? formatDateTime(row.lastSuccessAt).text : localizeUiText('Nie');
let accounts = '—';
if (row.configuredAccounts > 0) {
if (row.accountProblems > 0) accounts = String(row.accountProblems);
else if (row.checkingAccounts > 0) accounts = localizeUiText('Prüfung läuft');
else if (row.uncheckedAccounts > 0) accounts = localizeUiText('Nicht geprüft');
else accounts = '0';
}
return `<tr data-hoster-health-row="${escapeAttr(name)}">
<th scope="row">${escapeHtml(getHosterLabel(name))}</th>
<td data-health="sample">${row.sampleSize}</td>
<td data-health="outcomes">${row.successful} / ${row.failed} / ${row.skipped}</td>
<td data-health="rate">${escapeHtml(rate)}</td>
<td data-health="throughput">${escapeHtml(throughput)}</td>
<td data-health="last-success">${escapeHtml(lastSuccess)}</td>
<td data-health="recent-failures">${row.failuresLast7Days}</td>
<td data-health="accounts">${escapeHtml(accounts)}</td>
</tr>`;
}).join('');
}
function renderAccounts() {
const container = document.getElementById('accountsList');
if (!container) return;
@@ -5892,6 +5955,7 @@ function renderAccounts() {
const allAccounts = getAllAccountsFlat();
updateAccountSidebarSummary(allAccounts);
renderHosterHealthOverview();
const runCheckBtn = document.getElementById('accountsRunHealthCheckBtn');
if (runCheckBtn) runCheckBtn.disabled = healthCheckRunning;
@@ -6951,6 +7015,9 @@ async function loadHistory() {
history = await window.api.getHistory();
} catch (error) {
if (generation !== _historyLoadGeneration) return;
window._historyForStats = null;
_invalidateHosterLifetimeCache();
renderHosterHealthOverview();
historyRowsData = [];
historySidebarCounts = { total: 0, success: 0, error: 0, skipped: 0 };
updateHistorySidebarSummary();
@@ -6965,6 +7032,7 @@ async function loadHistory() {
_historyDirty = false;
_invalidateHosterLifetimeCache();
_refreshAccountHosterLifetimeStats();
renderHosterHealthOverview();
const retSel = document.getElementById('historyRetentionSelect');
if (retSel) {
retSel.value = (config.globalSettings && config.globalSettings.historyRetention) || 'all';
+14
View File
@@ -176,6 +176,20 @@
['Hoster-Zugangsdaten verwalten und prüfen', 'Manage and verify host account credentials'],
['Accounts prüfen', 'Check accounts'],
['Account hinzufügen', 'Add account'],
['Hoster-Gesundheit', 'Host health'],
['Hoster-Gesundheit aus lokalem Verlauf und aktuellem Accountstatus', 'Host health from local history and current account status'],
['Lokale Werte aus höchstens 50 Batches. Der effektive historische Durchsatz kann Wartezeiten und Wiederholungen enthalten.', 'Local values from up to 50 batches. Effective historical throughput can include waits and retries.'],
['Stichprobe', 'Sample'],
['Erfolg / Fehler / Übersprungen', 'Success / Failed / Skipped'],
['Erfolgsrate', 'Success rate'],
['Effektiver historischer Durchsatz', 'Effective historical throughput'],
['Letzter Erfolg', 'Last success'],
['Fehler (7 Tage)', 'Failed (7 days)'],
['Account-Probleme', 'Account issues'],
['Noch keine Hoster-Daten.', 'No host data yet.'],
['Verlauf nicht verfügbar.', 'History unavailable.'],
['Prüfung läuft', 'Check in progress'],
['Nie', 'Never'],
['Noch keine Hoster', 'No hosts yet'],
['Füge deinen ersten Hoster-Account hinzu. Die Zugangsdaten werden vor dem Speichern geprüft.', 'Add your first host account. Credentials are verified before saving.'],
['Alle ausklappen', 'Expand all'],
+26
View File
@@ -423,6 +423,32 @@
</div>
</div>
<div class="health-check-results account-health-results" id="healthCheckResults"></div>
<section class="hoster-health-overview" id="hosterHealthOverview" role="region" aria-labelledby="hosterHealthTitle" aria-describedby="hosterHealthHint">
<div class="hoster-health-heading">
<h3 id="hosterHealthTitle">Hoster-Gesundheit</h3>
<p id="hosterHealthHint">Lokale Werte aus höchstens 50 Batches. Der effektive historische Durchsatz kann Wartezeiten und Wiederholungen enthalten.</p>
</div>
<div class="hoster-health-scroll" tabindex="0">
<table>
<caption class="hoster-health-caption">Hoster-Gesundheit aus lokalem Verlauf und aktuellem Accountstatus</caption>
<thead>
<tr>
<th scope="col">Hoster</th>
<th scope="col">Stichprobe</th>
<th scope="col">Erfolg / Fehler / Übersprungen</th>
<th scope="col">Erfolgsrate</th>
<th scope="col" data-health-column="throughput">Effektiver historischer Durchsatz</th>
<th scope="col">Letzter Erfolg</th>
<th scope="col">Fehler (7 Tage)</th>
<th scope="col">Account-Probleme</th>
</tr>
</thead>
<tbody id="hosterHealthBody">
<tr><td colspan="8" data-hoster-health-empty>Wird geladen…</td></tr>
</tbody>
</table>
</div>
</section>
<div class="accounts-list" id="accountsList"></div>
<div class="accounts-list-footer" id="accountsListFooter" style="display:none">
<button class="btn btn-secondary" id="toggleAllAccountsBtn">Alle ausklappen</button>
+109
View File
@@ -3681,6 +3681,115 @@ input[type="checkbox"] {
padding: 10px 16px 0;
}
.hoster-health-overview {
flex: 0 0 auto;
min-width: 0;
max-width: 100%;
padding: 10px 16px 0;
overflow: hidden;
}
.hoster-health-heading {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
margin-bottom: 7px;
}
.hoster-health-heading h3 {
flex: 0 0 auto;
margin: 0;
color: var(--text);
font-size: 13px;
line-height: 1.3;
}
.hoster-health-heading p {
min-width: 0;
margin: 0;
color: var(--text-dim);
font-size: 10px;
line-height: 1.35;
text-align: right;
}
.hoster-health-scroll {
max-width: 100%;
max-height: 190px;
overflow: auto;
overscroll-behavior: contain;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg-secondary);
}
.hoster-health-scroll:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.hoster-health-scroll table {
width: 100%;
min-width: 880px;
border-collapse: collapse;
table-layout: fixed;
font-size: 10px;
font-variant-numeric: tabular-nums;
}
.hoster-health-caption {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.hoster-health-scroll th,
.hoster-health-scroll td {
padding: 6px 8px;
border-bottom: 1px solid var(--border);
color: var(--text-muted);
text-align: left;
vertical-align: middle;
}
.hoster-health-scroll thead th {
position: sticky;
top: 0;
z-index: 1;
background: var(--bg-raised);
color: var(--text-dim);
font-size: 9px;
font-weight: 600;
line-height: 1.25;
text-transform: uppercase;
}
.hoster-health-scroll tbody th {
color: var(--text);
font-weight: 600;
}
.hoster-health-scroll tbody tr:last-child > * {
border-bottom: 0;
}
.hoster-health-scroll tbody tr:hover > * {
background: var(--bg-raised);
}
.hoster-health-scroll [data-hoster-health-empty] {
height: 38px;
color: var(--text-dim);
text-align: center;
}
.accounts-list {
flex: 1 1 auto;
min-height: 0;