perf: lazy history refresh + append-only recent panel + queue-cleanup merge

Three more targeted wins:

  - loadHistory() was called unconditionally on every handleBatchDone,
    doing an IPC roundtrip + full history-table rebuild even when the
    user is on the Upload tab and can't see it. Now it sets a dirty
    flag and the actual refresh is deferred until the user switches
    to the Verlauf tab. On a fresh tab click it always runs.

  - renderRecentUploadsPanel append-only fast path: when the sort is
    'date desc' (the default) and the dataset only grew, the panel
    inserts the new rows at the top via insertAdjacentHTML instead
    of rebuilding the 5000-row tbody from scratch. Length shrinks or
    sort-change still trigger a full rebuild.

  - handleBatchDone's removeFromQueueOnDone cleanup now does one pass
    (build keep-list + detach from index together) instead of two
    separate filter() scans over queueJobs.
This commit is contained in:
Administrator 2026-04-19 13:55:37 +02:00
parent 1bcd7a2078
commit f16dd9ffa6

View File

@ -200,13 +200,21 @@ async function init() {
} }
// --- Tab switching --- // --- Tab switching ---
let _historyDirty = false;
function _isHistoryTabActive() {
const tab = document.querySelector('.tab.active');
return !!(tab && tab.dataset.view === 'history');
}
document.querySelectorAll('.tab').forEach(tab => { document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => { tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.view').forEach(v => v.classList.remove('active')); document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
tab.classList.add('active'); tab.classList.add('active');
document.getElementById(`${tab.dataset.view}-view`).classList.add('active'); document.getElementById(`${tab.dataset.view}-view`).classList.add('active');
if (tab.dataset.view === 'history') loadHistory(); if (tab.dataset.view === 'history') {
_historyDirty = false;
loadHistory();
}
}); });
}); });
@ -1820,16 +1828,25 @@ function handleBatchDone(summary) {
updateQueueActionButtons(); updateQueueActionButtons();
renderQueueTable(); renderQueueTable();
renderRecentUploadsPanel(); renderRecentUploadsPanel();
loadHistory(); // History is only visible on the Verlauf tab. Mark it dirty and refresh when
// the user actually switches to it — skips an IPC + full table rebuild per
// batch-done when the user is watching the upload view.
_historyDirty = true;
if (_isHistoryTabActive()) loadHistory();
const removeOnDone = config.globalSettings && config.globalSettings.removeFromQueueOnDone; const removeOnDone = config.globalSettings && config.globalSettings.removeFromQueueOnDone;
if (removeOnDone) { if (removeOnDone) {
const doneJobs = queueJobs.filter(j => j.status === 'done'); // Single pass: build the keep-list and clean up the index for removed jobs.
for (const job of doneJobs) { const nextJobs = [];
removeJobFromIndex(job); for (const job of queueJobs) {
selectedJobIds.delete(job.id); if (job.status === 'done') {
removeJobFromIndex(job);
selectedJobIds.delete(job.id);
} else {
nextJobs.push(job);
}
} }
queueJobs = queueJobs.filter(j => j.status !== 'done'); queueJobs = nextJobs;
renderQueueTable(); renderQueueTable();
} }
@ -3215,24 +3232,50 @@ function updateRecentSortHeaders() {
let _recentListenersBound = false; let _recentListenersBound = false;
function _buildRecentRowHtml(row) {
const cls = `recent-file-row${row.isError ? ' error' : ''}${selectedRecentIds.has(row.order) ? ' selected' : ''}`;
return `<tr class="${cls}" data-order="${row.order}" data-link="${escapeAttr(row.link)}">`
+ `<td>${escapeHtml(row.date)}</td>`
+ `<td title="${escapeAttr(row.filename)}">${escapeHtml(row.filename)}</td>`
+ `<td>${escapeHtml(row.host)}</td>`
+ `<td title="${escapeAttr(row.link)}">${escapeHtml(row.link)}</td>`
+ `</tr>`;
}
// Tracks the last rendered dataset so we can append-only when the user is just
// accumulating new uploads (the default case: sort=date desc, rows only grow).
let _recentLastRenderedSig = '';
let _recentLastRenderedLen = 0;
function renderRecentUploadsPanel() { function renderRecentUploadsPanel() {
const tbody = document.getElementById('recentFilesBody'); const tbody = document.getElementById('recentFilesBody');
if (!tbody) return; if (!tbody) return;
if (!sessionFilesData.length) { if (!sessionFilesData.length) {
tbody.innerHTML = '<tr><td colspan="4" class="empty-state">Noch keine Uploads in dieser Session.</td></tr>'; tbody.innerHTML = '<tr><td colspan="4" class="empty-state">Noch keine Uploads in dieser Session.</td></tr>';
_recentLastRenderedSig = '';
_recentLastRenderedLen = 0;
return; return;
} }
const rows = sortRecentFiles(sessionFilesData); const rows = sortRecentFiles(sessionFilesData);
const sig = `${recentSortState.key}|${recentSortState.direction}`;
const dateDescAppendOnly = sig === 'date|desc'
&& _recentLastRenderedSig === sig
&& rows.length > _recentLastRenderedLen
&& tbody.querySelectorAll('.recent-file-row').length === _recentLastRenderedLen;
tbody.innerHTML = rows.map(row => ` if (dateDescAppendOnly) {
<tr class="recent-file-row${row.isError ? ' error' : ''}${selectedRecentIds.has(row.order) ? ' selected' : ''}" data-order="${row.order}" data-link="${escapeAttr(row.link)}"> // Fast path: only new rows (date desc puts newest on top) — insert them
<td>${escapeHtml(row.date)}</td> // at the top without rebuilding the 5000-row tbody below.
<td title="${escapeAttr(row.filename)}">${escapeHtml(row.filename)}</td> const added = rows.length - _recentLastRenderedLen;
<td>${escapeHtml(row.host)}</td> let html = '';
<td title="${escapeAttr(row.link)}">${escapeHtml(row.link)}</td> for (let i = 0; i < added; i++) html += _buildRecentRowHtml(rows[i]);
</tr> tbody.insertAdjacentHTML('afterbegin', html);
`).join(''); } else {
tbody.innerHTML = rows.map(_buildRecentRowHtml).join('');
}
_recentLastRenderedSig = sig;
_recentLastRenderedLen = rows.length;
// Event delegation bind once, not per-row // Event delegation bind once, not per-row
if (!_recentListenersBound) { if (!_recentListenersBound) {