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 <noreply@anthropic.com>
This commit is contained in:
2026-04-21 11:06:13 +02:00
parent 8b38d6a9c9
commit 55b9935f5b

View File

@@ -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 = '<div class="page-center"><p>No items found.</p></div>';
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 flagUrl = countryFlagUrl(country);
const flagImg = flagUrl
? `<img src="${flagUrl}" alt="" style="height:22px;width:auto;border-radius:2px;flex-shrink:0;">`
@@ -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
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'));
const colW = body.clientWidth - 40; // subtract padding
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);