fix: correct host health sampling

Keep the complete loaded history snapshot, sample only valid non-future recent batches, count seven-day failures independently, ignore disabled accounts in issue counters, and preserve loading and error states across batch completion.
This commit is contained in:
Sucukdeluxe
2026-08-16 23:03:18 +02:00
parent 0ee749617c
commit 5ace779b14
4 changed files with 272 additions and 15 deletions
+37 -8
View File
@@ -39,6 +39,23 @@
return out;
}
function mergeHosterHealthHistory(history, batch) {
if (!Array.isArray(history)) return history;
if (!batch?.id) return [...history, batch];
const merged = [];
let replaced = false;
for (const existing of history) {
if (existing?.id === batch.id) {
if (!replaced) merged.push(batch);
replaced = true;
} else {
merged.push(existing);
}
}
if (!replaced) merged.push(batch);
return merged;
}
function summarizeHosterHealth(history, options = {}) {
const out = {};
const hosters = options.hosters && typeof options.hosters === 'object' ? options.hosters : {};
@@ -81,8 +98,9 @@
const accounts = Array.isArray(accountsValue) ? accountsValue : [];
bucket.configuredAccounts = accounts.length;
for (const account of accounts) {
if (account?.enabled === false) continue;
const status = accountStatuses[account?.id]?.status || 'unchecked';
const unavailable = account?.enabled === false || !hasCredentials(account);
const unavailable = !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++;
@@ -90,11 +108,13 @@
}
}
const batches = (Array.isArray(history) ? history : [])
const validBatches = (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 };
return { batch, index, timestampMs };
})
.filter(({ timestampMs }) => Number.isFinite(timestampMs) && timestampMs <= nowMs);
const batches = [...validBatches]
.sort((a, b) => b.timestampMs - a.timestampMs || b.index - a.index)
.slice(0, 50);
@@ -109,10 +129,8 @@
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 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;
@@ -122,12 +140,22 @@
bucket.skipped++;
} else {
bucket.failed++;
if (timestampMs >= recentCutoff && timestampMs <= nowMs) bucket.failuresLast7Days++;
}
}
}
}
for (const { batch, timestampMs } of validBatches) {
if (timestampMs < recentCutoff || !Array.isArray(batch?.files)) continue;
for (const file of batch.files) {
if (!Array.isArray(file?.results)) continue;
for (const result of file.results) {
if (!result?.hoster || result.status === 'done' || result.status === 'skipped') continue;
ensure(result.hoster).failuresLast7Days++;
}
}
}
for (const bucket of Object.values(out)) {
const attempted = bucket.successful + bucket.failed;
bucket.successRate = attempted > 0 ? bucket.successful / attempted : null;
@@ -269,6 +297,7 @@
const api = {
summarizePerHoster,
mergeHosterHealthHistory,
summarizeHosterHealth,
classifyErrorCategory,
summarizeBatchErrors,