diff --git a/renderer/app.js b/renderer/app.js
index f6f4c76..1e874cf 100644
--- a/renderer/app.js
+++ b/renderer/app.js
@@ -2738,6 +2738,57 @@ function getCredentialLabel(name, account) {
return 'Keine Zugangsdaten';
}
+const _STATUS_LABELS = { ok: 'Bereit', warn: 'Warnung', checking: 'Prüfe...', error: 'Fehler', unchecked: 'Nicht geprüft' };
+
+function _buildAccountCardHtml(name, account, idx) {
+ const isDisabled = account.enabled === false;
+ const st = accountStatuses[account.id] || { status: 'unchecked', message: '' };
+ const statusLabel = isDisabled ? 'Deaktiviert' : (_STATUS_LABELS[st.status] || 'Nicht geprüft');
+ const statusClass = isDisabled ? 'disabled' : st.status;
+ const credLabel = getCredentialLabel(name, account);
+ const toggleLabel = isDisabled ? 'Aktivieren' : 'Deaktivieren';
+ const priorityLabel = idx === 0 ? 'Primär' : `Fallback #${idx}`;
+
+ return `
+
+
☰
+
+
${escapeHtml(getAccountDisplayName(name, account))} ${priorityLabel}
+
${escapeHtml(credLabel)}${st.message && !isDisabled ? ` • ${escapeHtml(st.message)}` : ''}
+
+
+
+ ${statusLabel}
+
+
+ ${toggleLabel}
+ Prüfen
+ Bearbeiten
+ Löschen
+
+
`;
+}
+
+// Replace only the one card for `accountId` instead of re-rendering the whole
+// container. Runs on enable/disable, single health check, priority-badge bumps
+// after a reorder — anywhere we only change one card's state.
+function updateAccountCard(accountId) {
+ const container = document.getElementById('accountsList');
+ if (!container) return;
+ const found = findAccountById(accountId);
+ if (!found) return;
+ const card = container.querySelector(`.account-card[data-account-id="${accountId}"]`);
+ if (!card) return;
+ const accounts = config.hosters[found.name] || [];
+ const idx = accounts.findIndex(a => a.id === accountId);
+ if (idx < 0) return;
+ const tmp = document.createElement('div');
+ tmp.innerHTML = _buildAccountCardHtml(found.name, found.account, idx);
+ card.replaceWith(tmp.firstElementChild);
+}
+
+let _accountListenersBound = false;
+
function renderAccounts() {
const container = document.getElementById('accountsList');
if (!container) return;
@@ -2753,10 +2804,10 @@ function renderAccounts() {
Keine Accounts vorhanden
Klicke auf "Account hinzufügen", um einen Hoster einzurichten.
`;
+ if (!_accountListenersBound) bindAccountListeners(container);
return;
}
- // Group by hoster for drag reorder sections
const byHoster = {};
for (const { name, account } of allAccounts) {
if (!byHoster[name]) byHoster[name] = [];
@@ -2769,126 +2820,102 @@ function renderAccounts() {
if (!accounts || accounts.length === 0) continue;
html += `
${escapeHtml(getHosterLabel(name))}
`;
- accounts.forEach((account, idx) => {
- const isDisabled = account.enabled === false;
- const st = accountStatuses[account.id] || { status: 'unchecked', message: '' };
- const statusLabels = { ok: 'Bereit', warn: 'Warnung', checking: 'Prüfe...', error: 'Fehler', unchecked: 'Nicht geprüft' };
- const statusLabel = isDisabled ? 'Deaktiviert' : (statusLabels[st.status] || 'Nicht geprüft');
- const statusClass = isDisabled ? 'disabled' : st.status;
- const credLabel = getCredentialLabel(name, account);
- const toggleLabel = isDisabled ? 'Aktivieren' : 'Deaktivieren';
- const priorityLabel = idx === 0 ? 'Primär' : `Fallback #${idx}`;
-
- html += `
-
-
☰
-
-
${escapeHtml(getAccountDisplayName(name, account))} ${priorityLabel}
-
${escapeHtml(credLabel)}${st.message && !isDisabled ? ` • ${escapeHtml(st.message)}` : ''}
-
-
-
- ${statusLabel}
-
-
- ${toggleLabel}
- Prüfen
- Bearbeiten
- Löschen
-
-
`;
- });
+ accounts.forEach((account, idx) => { html += _buildAccountCardHtml(name, account, idx); });
html += '
';
}
container.innerHTML = html;
- // Wire up buttons
- container.querySelectorAll('[data-account-toggle]').forEach(btn => {
- btn.addEventListener('click', () => toggleAccount(btn.dataset.accountToggle));
- });
- container.querySelectorAll('[data-account-edit]').forEach(btn => {
- btn.addEventListener('click', () => openAccountModal(btn.dataset.accountEdit));
- });
- container.querySelectorAll('[data-account-delete]').forEach(btn => {
- btn.addEventListener('click', () => openDeleteAccountModal(btn.dataset.accountDelete));
- });
- container.querySelectorAll('[data-account-check]').forEach(btn => {
- btn.addEventListener('click', () => checkSingleAccount(btn.dataset.accountCheck));
- });
-
- // Drag-and-drop reorder within each hoster group
- setupAccountDragReorder(container);
+ if (!_accountListenersBound) bindAccountListeners(container);
}
-function setupAccountDragReorder(container) {
+// Single set of delegated listeners on the accounts container. Bound once on
+// the first render and reused for every subsequent in-place update / card
+// swap. Previously we rebound 4 × N button listeners + 5 × N drag listeners
+// per render — with 20 accounts that's 180 listener create/destroy cycles on
+// every enable/disable click.
+function bindAccountListeners(container) {
+ _accountListenersBound = true;
+ container.addEventListener('click', (e) => {
+ const btn = e.target.closest('button');
+ if (!btn) return;
+ if (btn.dataset.accountToggle) return toggleAccount(btn.dataset.accountToggle);
+ if (btn.dataset.accountEdit) return openAccountModal(btn.dataset.accountEdit);
+ if (btn.dataset.accountDelete) return openDeleteAccountModal(btn.dataset.accountDelete);
+ if (btn.dataset.accountCheck) return checkSingleAccount(btn.dataset.accountCheck);
+ });
+
let draggedCard = null;
- container.querySelectorAll('.account-card[draggable]').forEach(card => {
- card.addEventListener('dragstart', (e) => {
- draggedCard = card;
- card.classList.add('dragging');
- e.dataTransfer.effectAllowed = 'move';
- });
- card.addEventListener('dragend', () => {
- if (draggedCard) draggedCard.classList.remove('dragging');
- draggedCard = null;
- container.querySelectorAll('.account-card').forEach(c => c.classList.remove('drag-over-above', 'drag-over-below'));
- });
- card.addEventListener('dragover', (e) => {
- e.preventDefault();
- if (!draggedCard || draggedCard === card) return;
- if (draggedCard.dataset.accountHoster !== card.dataset.accountHoster) return;
- e.dataTransfer.dropEffect = 'move';
- const rect = card.getBoundingClientRect();
- const midY = rect.top + rect.height / 2;
- card.classList.toggle('drag-over-above', e.clientY < midY);
- card.classList.toggle('drag-over-below', e.clientY >= midY);
- });
- card.addEventListener('dragleave', () => {
- card.classList.remove('drag-over-above', 'drag-over-below');
- });
- card.addEventListener('drop', async (e) => {
- e.preventDefault();
- card.classList.remove('drag-over-above', 'drag-over-below');
- if (!draggedCard || draggedCard === card) return;
- const hosterName = card.dataset.accountHoster;
- if (draggedCard.dataset.accountHoster !== hosterName) return;
+ container.addEventListener('dragstart', (e) => {
+ const card = e.target.closest('.account-card[draggable]');
+ if (!card) return;
+ draggedCard = card;
+ card.classList.add('dragging');
+ if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move';
+ });
+ container.addEventListener('dragend', () => {
+ if (draggedCard) draggedCard.classList.remove('dragging');
+ draggedCard = null;
+ container.querySelectorAll('.drag-over-above, .drag-over-below').forEach(c => c.classList.remove('drag-over-above', 'drag-over-below'));
+ });
+ container.addEventListener('dragover', (e) => {
+ const card = e.target.closest('.account-card[draggable]');
+ if (!card || !draggedCard || draggedCard === card) return;
+ if (draggedCard.dataset.accountHoster !== card.dataset.accountHoster) return;
+ e.preventDefault();
+ if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
+ const rect = card.getBoundingClientRect();
+ const midY = rect.top + rect.height / 2;
+ card.classList.toggle('drag-over-above', e.clientY < midY);
+ card.classList.toggle('drag-over-below', e.clientY >= midY);
+ });
+ container.addEventListener('dragleave', (e) => {
+ const card = e.target.closest('.account-card[draggable]');
+ if (card) card.classList.remove('drag-over-above', 'drag-over-below');
+ });
+ container.addEventListener('drop', (e) => {
+ const card = e.target.closest('.account-card[draggable]');
+ if (!card || !draggedCard || draggedCard === card) return;
+ e.preventDefault();
+ card.classList.remove('drag-over-above', 'drag-over-below');
+ const hosterName = card.dataset.accountHoster;
+ if (draggedCard.dataset.accountHoster !== hosterName) return;
- const draggedId = draggedCard.dataset.accountId;
- const targetId = card.dataset.accountId;
- const accounts = config.hosters[hosterName];
- if (!Array.isArray(accounts)) return;
+ const draggedId = draggedCard.dataset.accountId;
+ const targetId = card.dataset.accountId;
+ const accounts = config.hosters[hosterName];
+ if (!Array.isArray(accounts)) return;
- const fromIdx = accounts.findIndex(a => a.id === draggedId);
- const toIdx = accounts.findIndex(a => a.id === targetId);
- if (fromIdx < 0 || toIdx < 0) return;
+ const fromIdx = accounts.findIndex(a => a.id === draggedId);
+ if (fromIdx < 0) return;
+ const [moved] = accounts.splice(fromIdx, 1);
+ const rect = card.getBoundingClientRect();
+ const insertBefore = e.clientY < rect.top + rect.height / 2;
+ const newToIdx = accounts.findIndex(a => a.id === targetId);
+ accounts.splice(insertBefore ? newToIdx : newToIdx + 1, 0, moved);
- // Move account in array
- const [moved] = accounts.splice(fromIdx, 1);
- const rect = card.getBoundingClientRect();
- const insertBefore = e.clientY < rect.top + rect.height / 2;
- const newToIdx = accounts.findIndex(a => a.id === targetId);
- accounts.splice(insertBefore ? newToIdx : newToIdx + 1, 0, moved);
+ // Move the DOM node in place — no full re-render.
+ if (insertBefore) card.before(draggedCard); else card.after(draggedCard);
- // Save and re-render
- await window.api.saveConfig({ hosters: config.hosters });
- config = await window.api.getConfig();
- renderAccounts();
- });
+ // The Primär / Fallback #N badges just changed for the whole group.
+ for (let i = 0; i < accounts.length; i++) updateAccountCard(accounts[i].id);
+
+ // Persist in the background. saveConfig is idempotent; we don't need to
+ // await here or re-fetch — our in-memory config is already the truth.
+ window.api.saveConfig({ hosters: config.hosters }).catch(() => {});
});
}
async function toggleAccount(accountId) {
const found = findAccountById(accountId);
if (!found) return;
- // Flip in place; re-render immediately so the UI responds before the
- // disk write completes. The saveConfig is fire-and-forget-ish here because
- // we already know the new state locally — a full getConfig round-trip
- // after every toggle made rapid enable/disable clicks feel laggy.
found.account.enabled = !found.account.enabled;
syncSelectedUploadHosters();
- renderAccounts();
+ // In-place: swap only the one affected card. No full re-render, no IPC
+ // refetch, no flicker. Rapid click-toggles now feel instant even with 50
+ // accounts in the list.
+ updateAccountCard(accountId);
renderHosterSummary();
- try { await window.api.saveConfig({ hosters: config.hosters }); } catch {}
+ window.api.saveConfig({ hosters: config.hosters }).catch(() => {});
}
async function checkSingleAccount(accountId) {
@@ -2897,7 +2924,7 @@ async function checkSingleAccount(accountId) {
if (!found) return;
healthCheckRunning = true;
accountStatuses[accountId] = { status: 'checking', message: '' };
- renderAccounts();
+ updateAccountCard(accountId);
try {
const result = await window.api.runHealthCheck({ hosters: [{ hoster: found.name, accountId }] });
const rows = result && Array.isArray(result.results) ? result.results : [];
@@ -2908,7 +2935,7 @@ async function checkSingleAccount(accountId) {
} finally {
healthCheckRunning = false;
}
- renderAccounts();
+ updateAccountCard(accountId);
}
function getCredsFieldsHtml(authType, account) {