From 55b9935f5b2b520068a2cededddc20311a3bc45c Mon Sep 17 00:00:00 2001 From: Christoph Gasser Date: Tue, 21 Apr 2026 11:06:13 +0200 Subject: [PATCH] Fix 30s auto-refresh flicker in frontend Skip the full-page loading state on background refreshes (only show it on first load or view switch). Replace per-column requestAnimationFrame appends with a single DocumentFragment swap so the browser never paints a blank container between clear and insert. Co-Authored-By: Claude Sonnet 4.6 --- public/app.js | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/public/app.js b/public/app.js index dc491af..dc189de 100644 --- a/public/app.js +++ b/public/app.js @@ -35,6 +35,7 @@ let currentView = 'restaurants'; let refreshTimer = null; let zabbixUrl = ''; let customerTagValue = 'QWE'; +let hasRendered = false; const REFRESH_MS = 30_000; /* ── Boot ────────────────────────────────────────────────────────────────── */ @@ -56,6 +57,7 @@ function initControls() { document.querySelectorAll('.toggle-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); currentView = btn.dataset.view; + hasRendered = false; loadData(); }); }); @@ -77,7 +79,7 @@ function initControls() { /* ── Data loading ────────────────────────────────────────────────────────── */ async function loadData() { setRefreshSpinner(true); - showPageLoading(); + if (!hasRendered) showPageLoading(); try { const endpoint = currentView === 'restaurants' ? '/api/restaurants' : '/api/devices'; @@ -88,6 +90,7 @@ async function loadData() { renderCountryColumns(items, stats); updateSummary(items); setLastUpdated(new Date()); + hasRendered = true; } catch (e) { showPageError(e.message); } finally { @@ -102,7 +105,6 @@ function scheduleRefresh() { /* ── Country column layout ───────────────────────────────────────────────── */ function renderCountryColumns(items, stats = {}) { - // Group by country const byCountry = {}; for (const item of items) { const key = item.country || ''; @@ -112,20 +114,20 @@ function renderCountryColumns(items, stats = {}) { const countries = Object.keys(byCountry).sort(); const wrap = document.getElementById('countries-wrap'); - wrap.innerHTML = ''; if (countries.length === 0) { wrap.innerHTML = '

No items found.

'; return; } + const frag = document.createDocumentFragment(); + const refs = []; + countries.forEach(country => { const col = document.createElement('div'); col.className = 'country-col'; - const colItems = byCountry[country]; - const problems = colItems.filter(i => i.severity >= 2).length; - + const colItems = byCountry[country]; const flagUrl = countryFlagUrl(country); const flagImg = flagUrl ? `` @@ -151,13 +153,20 @@ function renderCountryColumns(items, stats = {}) { body.appendChild(grid); col.appendChild(hdr); col.appendChild(body); - wrap.appendChild(col); + frag.appendChild(col); - // Compute perRow after layout is in DOM - requestAnimationFrame(() => { - const hexW = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--hex-w')); - const hexGap = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--hex-gap')); - const colW = body.clientWidth - 40; // subtract padding + refs.push({ grid, body, colItems }); + }); + + // Single atomic swap — old content replaced in one operation + wrap.replaceChildren(frag); + + // Measure layout and render hex items after browser has laid out the new columns + requestAnimationFrame(() => { + const hexW = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--hex-w')); + const hexGap = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--hex-gap')); + refs.forEach(({ grid, body, colItems }) => { + const colW = body.clientWidth - 40; const perRow = Math.max(2, Math.floor(colW / (hexW + hexGap))); renderHexGrid(grid, colItems, perRow); renderProblemList(body, colItems);