diff --git a/lib/stats.js b/lib/stats.js index fade21b..1b531aa 100644 --- a/lib/stats.js +++ b/lib/stats.js @@ -39,6 +39,105 @@ return out; } + function summarizeHosterHealth(history, options = {}) { + const out = {}; + const hosters = options.hosters && typeof options.hosters === 'object' ? options.hosters : {}; + const accountStatuses = options.accountStatuses && typeof options.accountStatuses === 'object' ? options.accountStatuses : {}; + const failedKeys = new Set(options.sessionFailedKeys instanceof Set + ? options.sessionFailedKeys + : (Array.isArray(options.sessionFailedKeys) ? options.sessionFailedKeys : [])); + const nowCandidate = options.now instanceof Date + ? options.now.getTime() + : (Number.isFinite(options.now) ? Number(options.now) : Date.parse(options.now)); + const nowMs = Number.isFinite(nowCandidate) ? nowCandidate : Date.now(); + const recentCutoff = nowMs - 7 * 24 * 60 * 60 * 1000; + + const ensure = (name) => out[name] || (out[name] = { + sampleSize: 0, + successful: 0, + failed: 0, + skipped: 0, + successRate: null, + effectiveBytes: 0, + effectiveDurationSec: 0, + effectiveBytesPerSecond: null, + lastSuccessAt: null, + failuresLast7Days: 0, + configuredAccounts: 0, + accountProblems: 0, + uncheckedAccounts: 0, + checkingAccounts: 0 + }); + + const hasCredentials = (account) => { + if (!account || typeof account !== 'object' || !account.id) return false; + if (account.authType === 'api') return Boolean(String(account.apiKey || '').trim()); + if (account.authType === 'login') return Boolean(String(account.username || '').trim() && String(account.password || '').trim()); + return Boolean(String(account.apiKey || '').trim() || (String(account.username || '').trim() && String(account.password || '').trim())); + }; + + for (const [name, accountsValue] of Object.entries(hosters)) { + const bucket = ensure(name); + const accounts = Array.isArray(accountsValue) ? accountsValue : []; + bucket.configuredAccounts = accounts.length; + for (const account of accounts) { + const status = accountStatuses[account?.id]?.status || 'unchecked'; + const unavailable = account?.enabled === false || !hasCredentials(account); + const problem = unavailable || failedKeys.has(`${name}:${account?.id || ''}`) || ['error', 'warn', 'otp_required'].includes(status); + if (problem) bucket.accountProblems++; + if (!unavailable && status === 'unchecked') bucket.uncheckedAccounts++; + if (!unavailable && status === 'checking') bucket.checkingAccounts++; + } + } + + const batches = (Array.isArray(history) ? history : []) + .map((batch, index) => { + const timestampMs = batch?.timestamp ? Date.parse(batch.timestamp) : NaN; + return { batch, index, timestampMs: Number.isFinite(timestampMs) ? timestampMs : -Infinity }; + }) + .sort((a, b) => b.timestampMs - a.timestampMs || b.index - a.index) + .slice(0, 50); + + for (const { batch, timestampMs } of batches) { + if (!batch || !Array.isArray(batch.files)) continue; + for (const file of batch.files) { + if (!file || !Array.isArray(file.results)) continue; + const fileSize = Number(file.size); + for (const result of file.results) { + if (!result?.hoster) continue; + const bucket = ensure(result.hoster); + bucket.sampleSize++; + if (result.status === 'done') { + bucket.successful++; + if (Number.isFinite(timestampMs) && timestampMs !== -Infinity) { + const previous = bucket.lastSuccessAt ? Date.parse(bucket.lastSuccessAt) : -Infinity; + if (timestampMs > previous) bucket.lastSuccessAt = new Date(timestampMs).toISOString(); + } + const durationSec = Number(result.durationSec); + if (Number.isFinite(fileSize) && fileSize > 0 && Number.isFinite(durationSec) && durationSec > 0) { + bucket.effectiveBytes += fileSize; + bucket.effectiveDurationSec += durationSec; + } + } else if (result.status === 'skipped') { + bucket.skipped++; + } else { + bucket.failed++; + if (timestampMs >= recentCutoff && timestampMs <= nowMs) bucket.failuresLast7Days++; + } + } + } + } + + for (const bucket of Object.values(out)) { + const attempted = bucket.successful + bucket.failed; + bucket.successRate = attempted > 0 ? bucket.successful / attempted : null; + bucket.effectiveBytesPerSecond = bucket.effectiveDurationSec > 0 + ? bucket.effectiveBytes / bucket.effectiveDurationSec + : null; + } + return out; + } + function classifyErrorCategory(err) { if (!err || typeof err !== 'string') return 'unknown'; const s = err.toLowerCase(); @@ -170,6 +269,7 @@ const api = { summarizePerHoster, + summarizeHosterHealth, classifyErrorCategory, summarizeBatchErrors, mergeSkippedIntoSummary, diff --git a/renderer/app.js b/renderer/app.js index 0cf8b36..6cbde53 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -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 = `
Lokale Werte aus höchstens 50 Batches. Der effektive historische Durchsatz kann Wartezeiten und Wiederholungen enthalten.
+| Hoster | +Stichprobe | +Erfolg / Fehler / Übersprungen | +Erfolgsrate | +Effektiver historischer Durchsatz | +Letzter Erfolg | +Fehler (7 Tage) | +Account-Probleme | +
|---|---|---|---|---|---|---|---|
| Wird geladen… | |||||||