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