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:
@@ -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';
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user