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
+100
View File
@@ -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,