release: v2.1.16

Add queue search and filters, safe upload diagnostics, session report exports, account check visibility, and explicit interrupted-upload recovery.\n\nSanitize diagnostic response snippets in persisted results and rotation logs, protect CSV exports against formula injection, and add regression coverage for diagnostics and reports.
This commit is contained in:
Sucukdeluxe
2026-08-12 12:04:47 +02:00
parent c843b4e623
commit a299fc204b
16 changed files with 361 additions and 17 deletions
+73
View File
@@ -0,0 +1,73 @@
function normalNumber(value) {
const number = Number(value);
return Number.isFinite(number) && number > 0 ? number : 0;
}
const { normalizeFailureDetails } = require('./upload-diagnostics');
function safeError(value) {
const text = normalizeFailureDetails({ payloadSnippet: value })?.responseSnippet || '';
return text || 'Unbekannter Fehler';
}
function createHosterSummary() {
return { total: 0, succeeded: 0, failed: 0, skipped: 0, aborted: 0, bytes: 0, durationSec: 0, attempts: 0, errors: {} };
}
function buildSessionReport(summary) {
const hosters = {};
const totals = createHosterSummary();
for (const file of Array.isArray(summary && summary.files) ? summary.files : []) {
const size = normalNumber(file && file.size);
for (const result of Array.isArray(file && file.results) ? file.results : []) {
const hoster = String(result && result.hoster || 'Unbekannt');
const bucket = hosters[hoster] || (hosters[hoster] = createHosterSummary());
const status = String(result && result.status || 'error');
for (const target of [bucket, totals]) {
target.total++;
target.bytes += size;
target.durationSec += normalNumber(result && result.durationSec);
target.attempts += normalNumber(result && result.attempt);
if (status === 'done') target.succeeded++;
else if (status === 'skipped') target.skipped++;
else if (status === 'aborted') target.aborted++;
else target.failed++;
if (status !== 'done' && result && result.error) {
const error = safeError(result.error);
target.errors[error] = (target.errors[error] || 0) + 1;
}
}
}
}
for (const target of [totals, ...Object.values(hosters)]) {
target.successRate = target.total ? target.succeeded / target.total : 0;
}
return {
generatedAt: new Date().toISOString(),
batchId: summary && summary.id ? String(summary.id) : '',
batchTimestamp: summary && summary.timestamp ? String(summary.timestamp) : '',
totals,
hosters
};
}
function csvCell(value) {
let text = value === null || value === undefined ? '' : String(value);
if (/^[=+\-@]/.test(text)) text = `'${text}`;
return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
}
function buildSessionReportCsv(report) {
const header = ['Hoster', 'Gesamt', 'Erfolgreich', 'Fehler', 'Übersprungen', 'Abgebrochen', 'Erfolgsrate', 'Bytes', 'Dauer Sekunden', 'Versuche', 'Fehlerdetails'];
const lines = [header.join(',')];
for (const [hoster, row] of Object.entries(report && report.hosters || {})) {
lines.push([
hoster, row.total, row.succeeded, row.failed, row.skipped, row.aborted,
`${Math.round((row.successRate || 0) * 100)}%`, row.bytes, row.durationSec, row.attempts,
Object.entries(row.errors || {}).map(([error, count]) => `${count}× ${error}`).join(' | ')
].map(csvCell).join(','));
}
return `${lines.join('\n')}\n`;
}
module.exports = { buildSessionReport, buildSessionReportCsv };