feat: add secure batch completion reports
Generate immutable post-cleanup summaries only after queue, history, and recovery finalization. Add bilingual accessible report UI, host and cleanup metrics, report-bound JSON and sanitized error CSV exports, renderer reload recovery, duplicate-filename-safe accounting, and public-source verification coverage.
This commit is contained in:
+225
@@ -61,6 +61,7 @@ function refreshLocalizedRuntimeUi() {
|
||||
const activeRecentTab = document.querySelector('.recent-tab.active');
|
||||
const hint = document.getElementById('recentFilesHint');
|
||||
if (hint && activeRecentTab) hint.textContent = localizeUiText(activeRecentTab.dataset.panel === 'statsTab' ? 'Upload-Statistiken' : 'Zuletzt erzeugte Upload-Links');
|
||||
if (_activeBatchCompletionReport) renderBatchCompletionReport(_activeBatchCompletionReport);
|
||||
}
|
||||
|
||||
// Dropdown options for "Add Account" modal: value -> label
|
||||
@@ -511,6 +512,228 @@ const modalController = (() => {
|
||||
return { open, close, isOpen };
|
||||
})();
|
||||
|
||||
const _shownBatchCompletionReportIds = new Set();
|
||||
let _activeBatchCompletionReport = null;
|
||||
let _batchCompletionReportUiReady = false;
|
||||
|
||||
function batchReportNumber(value) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) && number > 0 ? number : 0;
|
||||
}
|
||||
|
||||
function batchReportInteger(value) {
|
||||
return Math.max(0, Math.trunc(batchReportNumber(value)));
|
||||
}
|
||||
|
||||
function setBatchCompletionValue(id, value) {
|
||||
const element = document.getElementById(id);
|
||||
if (element) element.textContent = batchReportInteger(value).toLocaleString(getUiLocale());
|
||||
}
|
||||
|
||||
function getBatchCompletionOutcome(report) {
|
||||
const files = report?.files || {};
|
||||
const jobs = report?.jobs || {};
|
||||
const cleanup = report?.cleanup || {};
|
||||
return batchReportInteger(files.partiallySucceeded) > 0
|
||||
|| batchReportInteger(files.failed) > 0
|
||||
|| batchReportInteger(jobs.failed) > 0
|
||||
|| batchReportInteger(jobs.skipped) > 0
|
||||
|| batchReportInteger(jobs.aborted) > 0
|
||||
|| batchReportInteger(cleanup.blocked) > 0
|
||||
|| batchReportInteger(cleanup.failed) > 0
|
||||
|| (Array.isArray(report?.errors) && report.errors.length > 0)
|
||||
? 'mixed'
|
||||
: 'success';
|
||||
}
|
||||
|
||||
function getBatchErrorCategoryLabel(category) {
|
||||
const labels = {
|
||||
network: 'Netzwerk',
|
||||
'hoster-transient': 'Temporärer Hosterfehler',
|
||||
'file-rejected': 'Datei abgelehnt',
|
||||
'account-error': 'Account-Fehler',
|
||||
aborted: 'Abgebrochen',
|
||||
unknown: 'Unbekannt'
|
||||
};
|
||||
return localizeUiText(labels[String(category || '')] || 'Unbekannt');
|
||||
}
|
||||
|
||||
function getBatchErrorStatusLabel(status) {
|
||||
const labels = {
|
||||
done: 'Erfolgreich',
|
||||
error: 'Fehlgeschlagen',
|
||||
skipped: 'Übersprungen',
|
||||
aborted: 'Abgebrochen'
|
||||
};
|
||||
return localizeUiText(labels[String(status || '')] || 'Fehlgeschlagen');
|
||||
}
|
||||
|
||||
function renderBatchCompletionHosters(report) {
|
||||
const body = document.getElementById('batchCompletionHostersBody');
|
||||
if (!body) return;
|
||||
const rows = Object.entries(report?.hosters && typeof report.hosters === 'object' ? report.hosters : {}).map(([hoster, values]) => {
|
||||
const row = document.createElement('tr');
|
||||
row.dataset.hoster = hoster;
|
||||
const host = document.createElement('th');
|
||||
host.scope = 'row';
|
||||
host.textContent = getHosterLabel(hoster);
|
||||
row.appendChild(host);
|
||||
[
|
||||
batchReportInteger(values?.total).toLocaleString(getUiLocale()),
|
||||
batchReportInteger(values?.succeeded).toLocaleString(getUiLocale()),
|
||||
batchReportInteger(values?.failed).toLocaleString(getUiLocale()),
|
||||
batchReportInteger(values?.skipped).toLocaleString(getUiLocale()),
|
||||
batchReportInteger(values?.aborted).toLocaleString(getUiLocale()),
|
||||
formatBytes(batchReportNumber(values?.successfulBytes))
|
||||
].forEach(value => {
|
||||
const cell = document.createElement('td');
|
||||
cell.textContent = value;
|
||||
row.appendChild(cell);
|
||||
});
|
||||
return row;
|
||||
});
|
||||
body.replaceChildren(...rows);
|
||||
}
|
||||
|
||||
function renderBatchCompletionErrors(report) {
|
||||
const section = document.getElementById('batchCompletionErrorsSection');
|
||||
const list = document.getElementById('batchCompletionErrorsList');
|
||||
const count = document.getElementById('batchCompletionErrorsCount');
|
||||
const more = document.getElementById('batchCompletionErrorsMore');
|
||||
if (!section || !list || !count || !more) return;
|
||||
const errors = Array.isArray(report?.errors) ? report.errors : [];
|
||||
section.hidden = errors.length === 0;
|
||||
count.textContent = errors.length.toLocaleString(getUiLocale());
|
||||
const items = errors.slice(0, 5).map(error => {
|
||||
const item = document.createElement('li');
|
||||
const head = document.createElement('div');
|
||||
head.className = 'batch-completion-error-head';
|
||||
const file = document.createElement('strong');
|
||||
file.className = 'batch-completion-error-file';
|
||||
file.textContent = String(error?.fileName || localizeUiText('Unbekannt'));
|
||||
const hoster = document.createElement('span');
|
||||
hoster.textContent = getHosterLabel(String(error?.hoster || ''));
|
||||
head.append(file, hoster);
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'batch-completion-error-meta';
|
||||
const status = document.createElement('span');
|
||||
status.textContent = getBatchErrorStatusLabel(error?.status);
|
||||
const category = document.createElement('span');
|
||||
category.textContent = getBatchErrorCategoryLabel(error?.category);
|
||||
meta.append(status, category);
|
||||
const attempt = batchReportInteger(error?.attempt);
|
||||
const maxAttempts = batchReportInteger(error?.maxAttempts);
|
||||
if (attempt > 0 || maxAttempts > 0) {
|
||||
const attemptLabel = document.createElement('span');
|
||||
attemptLabel.textContent = `${localizeUiText('Versuch')} ${attempt}${maxAttempts > 0 ? `/${maxAttempts}` : ''}`;
|
||||
meta.appendChild(attemptLabel);
|
||||
}
|
||||
if (error?.remoteCommitUncertain === true) {
|
||||
const uncertain = document.createElement('span');
|
||||
uncertain.className = 'batch-completion-error-uncertain';
|
||||
uncertain.textContent = localizeUiText('Remote-Abschluss unklar');
|
||||
meta.appendChild(uncertain);
|
||||
}
|
||||
const message = document.createElement('p');
|
||||
message.className = 'batch-completion-error-message';
|
||||
message.textContent = String(error?.message || localizeUiText('Unbekannter Fehler'));
|
||||
item.append(head, meta, message);
|
||||
return item;
|
||||
});
|
||||
list.replaceChildren(...items);
|
||||
const remaining = Math.max(0, errors.length - items.length);
|
||||
more.hidden = remaining === 0;
|
||||
more.textContent = remaining === 1 ? localizeUiText('1 weiterer Fehler') : localizeUiText(`${remaining} weitere Fehler`);
|
||||
}
|
||||
|
||||
function renderBatchCompletionReport(report) {
|
||||
const modal = document.getElementById('batchCompletionModal');
|
||||
if (!modal || !report) return false;
|
||||
_activeBatchCompletionReport = report;
|
||||
const outcome = getBatchCompletionOutcome(report);
|
||||
modal.dataset.reportId = String(report.reportId);
|
||||
modal.dataset.outcome = outcome;
|
||||
const outcomeLabel = document.getElementById('batchCompletionOutcome');
|
||||
if (outcomeLabel) outcomeLabel.textContent = localizeUiText(outcome === 'success' ? 'Erfolgreich' : 'Mit Problemen');
|
||||
const fileCount = batchReportInteger(report.files?.total);
|
||||
const jobCount = batchReportInteger(report.jobs?.total);
|
||||
const summary = document.getElementById('batchCompletionSummary');
|
||||
if (summary) summary.textContent = `${fileCount.toLocaleString(getUiLocale())} ${localizeUiText(fileCount === 1 ? 'Datei' : 'Dateien')} · ${jobCount.toLocaleString(getUiLocale())} ${localizeUiText(jobCount === 1 ? 'Auftrag' : 'Aufträge')} · ${formatDateTime(report.completedAt).text}`;
|
||||
setBatchCompletionValue('batchCompletionFilesTotal', report.files?.total);
|
||||
setBatchCompletionValue('batchCompletionFilesFullySucceeded', report.files?.fullySucceeded);
|
||||
setBatchCompletionValue('batchCompletionFilesPartiallySucceeded', report.files?.partiallySucceeded);
|
||||
setBatchCompletionValue('batchCompletionFilesFailed', report.files?.failed);
|
||||
setBatchCompletionValue('batchCompletionJobsTotal', report.jobs?.total);
|
||||
setBatchCompletionValue('batchCompletionJobsSucceeded', report.jobs?.succeeded);
|
||||
setBatchCompletionValue('batchCompletionJobsFailed', report.jobs?.failed);
|
||||
setBatchCompletionValue('batchCompletionJobsSkipped', report.jobs?.skipped);
|
||||
setBatchCompletionValue('batchCompletionJobsAborted', report.jobs?.aborted);
|
||||
setBatchCompletionValue('batchCompletionCleanupRequested', report.cleanup?.requested);
|
||||
setBatchCompletionValue('batchCompletionCleanupDeleted', report.cleanup?.deleted);
|
||||
setBatchCompletionValue('batchCompletionCleanupBlocked', report.cleanup?.blocked);
|
||||
setBatchCompletionValue('batchCompletionCleanupFailed', report.cleanup?.failed);
|
||||
const duration = document.getElementById('batchCompletionDuration');
|
||||
const bytes = document.getElementById('batchCompletionSuccessfulBytes');
|
||||
const speed = document.getElementById('batchCompletionAverageSpeed');
|
||||
if (duration) duration.textContent = formatDuration(Math.round(batchReportNumber(report.durationSec)));
|
||||
if (bytes) bytes.textContent = formatBytes(batchReportNumber(report.transfer?.successfulBytes));
|
||||
if (speed) speed.textContent = `${formatBytes(batchReportNumber(report.transfer?.averageBytesPerSecond))}/s`;
|
||||
renderBatchCompletionHosters(report);
|
||||
renderBatchCompletionErrors(report);
|
||||
uiLocalizer.translate(modal);
|
||||
return true;
|
||||
}
|
||||
|
||||
function closeBatchCompletionReport() {
|
||||
modalController.close('batchCompletionModal', { fallbackFocus: '#addFilesBtn' });
|
||||
}
|
||||
|
||||
function showBatchCompletionReport(report) {
|
||||
const reportId = typeof report?.reportId === 'string' ? report.reportId.trim() : '';
|
||||
if (!reportId || _shownBatchCompletionReportIds.has(reportId)) return false;
|
||||
_shownBatchCompletionReportIds.add(reportId);
|
||||
if (!renderBatchCompletionReport({ ...report, reportId })) return false;
|
||||
return modalController.open('batchCompletionModal', {
|
||||
initialFocus: '#batchCompletionHeaderCloseBtn',
|
||||
fallbackFocus: '#addFilesBtn',
|
||||
onEscape: closeBatchCompletionReport
|
||||
});
|
||||
}
|
||||
|
||||
async function exportVisibleBatchCompletionReport(format, button) {
|
||||
const reportId = _activeBatchCompletionReport?.reportId;
|
||||
if (!reportId || !window.api?.exportBatchCompletionReport) return;
|
||||
button.disabled = true;
|
||||
try {
|
||||
const result = await window.api.exportBatchCompletionReport(reportId, format);
|
||||
if (result?.ok) showCopyToast(format === 'json' ? 'JSON-Bericht exportiert' : 'Fehler-CSV exportiert');
|
||||
else if (!result?.canceled) await showAppAlert(result?.error || 'Batch-Bericht konnte nicht exportiert werden.', 'Export fehlgeschlagen');
|
||||
} catch (error) {
|
||||
await showAppAlert(getLocalizedErrorDetail(error), 'Export fehlgeschlagen');
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function setupBatchCompletionReportUi() {
|
||||
if (_batchCompletionReportUiReady) return;
|
||||
_batchCompletionReportUiReady = true;
|
||||
document.getElementById('batchCompletionHeaderCloseBtn')?.addEventListener('click', closeBatchCompletionReport);
|
||||
document.getElementById('batchCompletionCloseBtn')?.addEventListener('click', closeBatchCompletionReport);
|
||||
const jsonButton = document.getElementById('batchCompletionExportJsonBtn');
|
||||
const csvButton = document.getElementById('batchCompletionExportCsvBtn');
|
||||
jsonButton?.addEventListener('click', () => exportVisibleBatchCompletionReport('json', jsonButton));
|
||||
csvButton?.addEventListener('click', () => exportVisibleBatchCompletionReport('csv', csvButton));
|
||||
window.api?.onUploadBatchReport?.(showBatchCompletionReport);
|
||||
}
|
||||
|
||||
async function showLastBatchCompletionReport() {
|
||||
if (!window.api?.getLastBatchCompletionReport) return;
|
||||
try {
|
||||
showBatchCompletionReport(await window.api.getLastBatchCompletionReport());
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Session-specific files for the "Files" panel (resets each session)
|
||||
let sessionFilesData = [];
|
||||
let _recentSeqCounter = 0;
|
||||
@@ -566,6 +789,7 @@ async function init() {
|
||||
renderRecentUploadsPanel();
|
||||
updateUploadView();
|
||||
updateStatusBar();
|
||||
await showLastBatchCompletionReport();
|
||||
const interruptedCount = queueJobs.filter(job => job.interrupted).length;
|
||||
if (interruptedCount > 0) showCopyToast(interruptedCount === 1 ? '1 unterbrochener Upload kann fortgesetzt werden.' : `${interruptedCount} unterbrochene Uploads können fortgesetzt werden.`, 7000);
|
||||
|
||||
@@ -8584,6 +8808,7 @@ function updateStatsPanel() {
|
||||
window.api.onUpdateAvailable(showUpdateBanner);
|
||||
window.api.onUpdateProgress(handleUpdateProgress);
|
||||
window.api.onPrepareClose(prepareForWindowClose);
|
||||
setupBatchCompletionReportUi();
|
||||
setupAppAlertListeners();
|
||||
init().then(() => {
|
||||
window.api.signalCloseHandshakeReady();
|
||||
|
||||
@@ -63,6 +63,32 @@
|
||||
['Hoster-Limits automatisch hochskalieren', 'Automatically scale host limits'],
|
||||
['Hoster', 'Host'],
|
||||
['Versuch', 'Attempt'],
|
||||
['Batch abgeschlossen', 'Batch complete'],
|
||||
['Der Upload-Batch wurde abgeschlossen.', 'The upload batch has completed.'],
|
||||
['Mit Problemen', 'Completed with issues'],
|
||||
['Übertragung', 'Transfer'],
|
||||
['Dauer', 'Duration'],
|
||||
['Erfolgreich übertragen', 'Successfully transferred'],
|
||||
['Durchschnitt', 'Average'],
|
||||
['Vollständig erfolgreich', 'Fully successful'],
|
||||
['Teilweise erfolgreich', 'Partially successful'],
|
||||
['Auftrag', 'Job'],
|
||||
['Aufträge', 'Jobs'],
|
||||
['Angefordert', 'Requested'],
|
||||
['Gelöscht', 'Deleted'],
|
||||
['Blockiert', 'Blocked'],
|
||||
['Hosterübersicht', 'Host overview'],
|
||||
['Übertragen', 'Transferred'],
|
||||
['Fehlerbeispiele', 'Error examples'],
|
||||
['Fehler-CSV exportieren', 'Export error CSV'],
|
||||
['Temporärer Hosterfehler', 'Temporary host error'],
|
||||
['Datei abgelehnt', 'File rejected'],
|
||||
['Account-Fehler', 'Account error'],
|
||||
['Remote-Abschluss unklar', 'Remote completion uncertain'],
|
||||
['1 weiterer Fehler', '1 more error'],
|
||||
['JSON-Bericht exportiert', 'JSON report exported'],
|
||||
['Fehler-CSV exportiert', 'Error CSV exported'],
|
||||
['Batch-Bericht konnte nicht exportiert werden.', 'The batch report could not be exported.'],
|
||||
['Importieren', 'Import'],
|
||||
['In Zwischenablage', 'To clipboard'],
|
||||
['In diesem Lauf hochgeladen:', 'Uploaded during this run:'],
|
||||
@@ -772,6 +798,7 @@
|
||||
[/^Update-Server Antwort war kein JSON (.+)$/, 'Update server response was not JSON $1'],
|
||||
[/^(\d+) Links kopiert$/, '$1 links copied'],
|
||||
[/^(\d+) Link kopiert$/, '$1 link copied'],
|
||||
[/^(\d+) weitere Fehler$/, '$1 more errors'],
|
||||
[/^Wirklich alle (\d+) Links aus diesem Panel entfernen\?$/, 'Remove all $1 links from this panel?'],
|
||||
[/^Ein ausgewählter Eintrag wird aus diesem Panel entfernt\.$/, 'One selected entry will be removed from this panel.'],
|
||||
[/^(\d+) ausgewählte Einträge werden aus diesem Panel entfernt\.$/, '$1 selected entries will be removed from this panel.'],
|
||||
@@ -865,6 +892,7 @@
|
||||
[/^Restart in (\d+)s\.\.\.$/, 'Neustart in $1s...'],
|
||||
[/^(\d+) job reset for upload$/, '$1 Job zum erneuten Upload zurückgesetzt'],
|
||||
[/^(\d+) jobs reset for upload$/, '$1 Jobs zum erneuten Upload zurückgesetzt'],
|
||||
[/^(\d+) more errors$/, '$1 weitere Fehler'],
|
||||
[/^(\d+) history entry will be permanently removed\.$/, '$1 Verlaufseintrag wird dauerhaft entfernt.'],
|
||||
[/^(\d+) history entries will be permanently removed\.$/, '$1 Verlaufseinträge werden dauerhaft entfernt.'],
|
||||
[/^Active on port (\d+) — 1 client connected$/, 'Aktiv auf Port $1 — 1 Client verbunden'],
|
||||
|
||||
@@ -667,6 +667,95 @@
|
||||
<button class="btn btn-xs btn-secondary" id="cancelStartupResumeBtn">Abbrechen</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="batchCompletionModal" style="display:none" aria-hidden="true">
|
||||
<div class="modal-card batch-completion-card" role="dialog" aria-modal="true" aria-labelledby="batchCompletionTitle" aria-describedby="batchCompletionSummary" tabindex="-1">
|
||||
<div class="modal-header batch-completion-header">
|
||||
<div class="batch-completion-heading">
|
||||
<span class="batch-completion-outcome" id="batchCompletionOutcome">Erfolgreich</span>
|
||||
<h3 id="batchCompletionTitle">Batch abgeschlossen</h3>
|
||||
<p id="batchCompletionSummary">Der Upload-Batch wurde abgeschlossen.</p>
|
||||
</div>
|
||||
<button class="icon-btn" id="batchCompletionHeaderCloseBtn" aria-label="Schließen">×</button>
|
||||
</div>
|
||||
<div class="modal-body batch-completion-body">
|
||||
<dl class="batch-completion-transfer" aria-label="Übertragung">
|
||||
<div><dt>Dauer</dt><dd id="batchCompletionDuration">00:00:00</dd></div>
|
||||
<div><dt>Erfolgreich übertragen</dt><dd id="batchCompletionSuccessfulBytes">0 B</dd></div>
|
||||
<div><dt>Durchschnitt</dt><dd id="batchCompletionAverageSpeed">0 B/s</dd></div>
|
||||
</dl>
|
||||
|
||||
<div class="batch-completion-metric-sections">
|
||||
<section class="batch-completion-section" aria-labelledby="batchCompletionFilesTitle">
|
||||
<h4 id="batchCompletionFilesTitle">Dateien</h4>
|
||||
<dl class="batch-completion-metrics">
|
||||
<div><dt>Gesamt</dt><dd id="batchCompletionFilesTotal">0</dd></div>
|
||||
<div><dt>Vollständig erfolgreich</dt><dd id="batchCompletionFilesFullySucceeded">0</dd></div>
|
||||
<div><dt>Teilweise erfolgreich</dt><dd id="batchCompletionFilesPartiallySucceeded">0</dd></div>
|
||||
<div><dt>Fehlgeschlagen</dt><dd id="batchCompletionFilesFailed">0</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="batch-completion-section" aria-labelledby="batchCompletionJobsTitle">
|
||||
<h4 id="batchCompletionJobsTitle">Aufträge</h4>
|
||||
<dl class="batch-completion-metrics batch-completion-job-metrics">
|
||||
<div><dt>Gesamt</dt><dd id="batchCompletionJobsTotal">0</dd></div>
|
||||
<div><dt>Erfolgreich</dt><dd id="batchCompletionJobsSucceeded">0</dd></div>
|
||||
<div><dt>Fehlgeschlagen</dt><dd id="batchCompletionJobsFailed">0</dd></div>
|
||||
<div><dt>Übersprungen</dt><dd id="batchCompletionJobsSkipped">0</dd></div>
|
||||
<div><dt>Abgebrochen</dt><dd id="batchCompletionJobsAborted">0</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="batch-completion-section" aria-labelledby="batchCompletionCleanupTitle">
|
||||
<h4 id="batchCompletionCleanupTitle">Quelldateien</h4>
|
||||
<dl class="batch-completion-metrics">
|
||||
<div><dt>Angefordert</dt><dd id="batchCompletionCleanupRequested">0</dd></div>
|
||||
<div><dt>Gelöscht</dt><dd id="batchCompletionCleanupDeleted">0</dd></div>
|
||||
<div><dt>Blockiert</dt><dd id="batchCompletionCleanupBlocked">0</dd></div>
|
||||
<div><dt>Fehlgeschlagen</dt><dd id="batchCompletionCleanupFailed">0</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="batch-completion-section batch-completion-hosters" aria-labelledby="batchCompletionHostersTitle">
|
||||
<h4 id="batchCompletionHostersTitle">Hosterübersicht</h4>
|
||||
<div class="batch-completion-table-scroll" tabindex="0">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Hoster</th>
|
||||
<th scope="col">Aufträge</th>
|
||||
<th scope="col">Erfolgreich</th>
|
||||
<th scope="col">Fehlgeschlagen</th>
|
||||
<th scope="col">Übersprungen</th>
|
||||
<th scope="col">Abgebrochen</th>
|
||||
<th scope="col">Übertragen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="batchCompletionHostersBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="batch-completion-section batch-completion-errors" id="batchCompletionErrorsSection" aria-labelledby="batchCompletionErrorsTitle" hidden>
|
||||
<div class="batch-completion-section-heading">
|
||||
<h4 id="batchCompletionErrorsTitle">Fehlerbeispiele</h4>
|
||||
<span id="batchCompletionErrorsCount">0</span>
|
||||
</div>
|
||||
<ol id="batchCompletionErrorsList"></ol>
|
||||
<p id="batchCompletionErrorsMore" hidden></p>
|
||||
</section>
|
||||
</div>
|
||||
<div class="modal-footer batch-completion-footer">
|
||||
<div class="batch-completion-export-actions">
|
||||
<button class="btn btn-secondary" id="batchCompletionExportJsonBtn">JSON exportieren</button>
|
||||
<button class="btn btn-secondary" id="batchCompletionExportCsvBtn">Fehler-CSV exportieren</button>
|
||||
</div>
|
||||
<button class="btn btn-primary" id="batchCompletionCloseBtn">Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shutdown-overlay" id="shutdownOverlay" style="display:none" aria-hidden="true">
|
||||
<div class="shutdown-box" role="dialog" aria-modal="true" aria-labelledby="shutdownMessage" tabindex="-1">
|
||||
<p id="shutdownMessage">System wird heruntergefahren in <span id="shutdownSeconds">60</span>s...</p>
|
||||
|
||||
@@ -735,6 +735,239 @@ body.col-resizing, body.col-resizing * { cursor: col-resize !important; user-sel
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.batch-completion-card {
|
||||
width: min(920px, 100%);
|
||||
max-height: min(calc(100vh - 48px), 820px);
|
||||
}
|
||||
.batch-completion-header {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.batch-completion-heading {
|
||||
min-width: 0;
|
||||
}
|
||||
.batch-completion-outcome {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 22px;
|
||||
margin-bottom: 7px;
|
||||
padding: 3px 8px;
|
||||
border: 1px solid rgba(52, 211, 153, .38);
|
||||
border-radius: 999px;
|
||||
background: rgba(52, 211, 153, .12);
|
||||
color: var(--success);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: .04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
#batchCompletionModal[data-outcome="mixed"] .batch-completion-outcome {
|
||||
border-color: rgba(245, 158, 11, .42);
|
||||
background: rgba(245, 158, 11, .12);
|
||||
color: var(--warning);
|
||||
}
|
||||
.batch-completion-body {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.batch-completion-transfer,
|
||||
.batch-completion-metrics {
|
||||
margin: 0;
|
||||
}
|
||||
.batch-completion-transfer {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
.batch-completion-transfer > div,
|
||||
.batch-completion-metrics > div {
|
||||
min-width: 0;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, .025);
|
||||
}
|
||||
.batch-completion-transfer > div {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
.batch-completion-transfer dt,
|
||||
.batch-completion-metrics dt {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
.batch-completion-transfer dd,
|
||||
.batch-completion-metrics dd {
|
||||
margin: 4px 0 0;
|
||||
color: var(--text-primary);
|
||||
font-size: 16px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 700;
|
||||
}
|
||||
.batch-completion-metric-sections {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.batch-completion-section {
|
||||
min-width: 0;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, .018);
|
||||
}
|
||||
.batch-completion-section h4 {
|
||||
margin: 0 0 9px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.batch-completion-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 7px;
|
||||
}
|
||||
.batch-completion-metrics > div {
|
||||
padding: 8px 9px;
|
||||
}
|
||||
.batch-completion-job-metrics > div:first-child {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.batch-completion-table-scroll {
|
||||
max-width: 100%;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.batch-completion-table-scroll:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.batch-completion-table-scroll table {
|
||||
width: 100%;
|
||||
min-width: 650px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.batch-completion-table-scroll th,
|
||||
.batch-completion-table-scroll td {
|
||||
padding: 7px 9px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.batch-completion-table-scroll th {
|
||||
color: var(--text-muted);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.batch-completion-table-scroll th:first-child,
|
||||
.batch-completion-table-scroll td:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
.batch-completion-table-scroll tbody tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.batch-completion-section-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
.batch-completion-section-heading h4 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.batch-completion-section-heading span {
|
||||
min-width: 24px;
|
||||
padding: 2px 7px;
|
||||
border-radius: 999px;
|
||||
background: rgba(239, 68, 68, .14);
|
||||
color: var(--danger);
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.batch-completion-errors ol {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin: 10px 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.batch-completion-errors li {
|
||||
min-width: 0;
|
||||
padding: 9px 10px;
|
||||
border: 1px solid rgba(239, 68, 68, .22);
|
||||
border-radius: 8px;
|
||||
background: rgba(239, 68, 68, .055);
|
||||
}
|
||||
.batch-completion-error-head,
|
||||
.batch-completion-error-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 10px;
|
||||
}
|
||||
.batch-completion-error-file {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.batch-completion-error-meta {
|
||||
margin-top: 4px;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
.batch-completion-error-message {
|
||||
margin: 7px 0 0;
|
||||
color: var(--text-secondary);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.batch-completion-error-uncertain {
|
||||
color: var(--warning);
|
||||
font-weight: 700;
|
||||
}
|
||||
#batchCompletionErrorsMore {
|
||||
margin: 9px 0 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.batch-completion-footer,
|
||||
.batch-completion-export-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.batch-completion-footer {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.batch-completion-export-actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@media (max-width: 820px) {
|
||||
.batch-completion-card {
|
||||
max-height: calc(100vh - 24px);
|
||||
}
|
||||
.batch-completion-metric-sections {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
.batch-completion-metric-sections > :last-child {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
@media (max-width: 620px) {
|
||||
.batch-completion-transfer,
|
||||
.batch-completion-metric-sections {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.batch-completion-metric-sections > :last-child {
|
||||
grid-column: auto;
|
||||
}
|
||||
.batch-completion-footer,
|
||||
.batch-completion-export-actions {
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
.batch-completion-footer > .btn,
|
||||
.batch-completion-export-actions .btn {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
.hoster-modal-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
|
||||
Reference in New Issue
Block a user