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:
@@ -35,6 +35,7 @@ let currentView = 'restaurants';
|
|||||||
let refreshTimer = null;
|
let refreshTimer = null;
|
||||||
let zabbixUrl = '';
|
let zabbixUrl = '';
|
||||||
let customerTagValue = 'QWE';
|
let customerTagValue = 'QWE';
|
||||||
|
let hasRendered = false;
|
||||||
const REFRESH_MS = 30_000;
|
const REFRESH_MS = 30_000;
|
||||||
|
|
||||||
/* ── Boot ────────────────────────────────────────────────────────────────── */
|
/* ── Boot ────────────────────────────────────────────────────────────────── */
|
||||||
@@ -56,6 +57,7 @@ function initControls() {
|
|||||||
document.querySelectorAll('.toggle-btn').forEach(b => b.classList.remove('active'));
|
document.querySelectorAll('.toggle-btn').forEach(b => b.classList.remove('active'));
|
||||||
btn.classList.add('active');
|
btn.classList.add('active');
|
||||||
currentView = btn.dataset.view;
|
currentView = btn.dataset.view;
|
||||||
|
hasRendered = false;
|
||||||
loadData();
|
loadData();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -77,7 +79,7 @@ function initControls() {
|
|||||||
/* ── Data loading ────────────────────────────────────────────────────────── */
|
/* ── Data loading ────────────────────────────────────────────────────────── */
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
setRefreshSpinner(true);
|
setRefreshSpinner(true);
|
||||||
showPageLoading();
|
if (!hasRendered) showPageLoading();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const endpoint = currentView === 'restaurants' ? '/api/restaurants' : '/api/devices';
|
const endpoint = currentView === 'restaurants' ? '/api/restaurants' : '/api/devices';
|
||||||
@@ -88,6 +90,7 @@ async function loadData() {
|
|||||||
renderCountryColumns(items, stats);
|
renderCountryColumns(items, stats);
|
||||||
updateSummary(items);
|
updateSummary(items);
|
||||||
setLastUpdated(new Date());
|
setLastUpdated(new Date());
|
||||||
|
hasRendered = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showPageError(e.message);
|
showPageError(e.message);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -102,7 +105,6 @@ function scheduleRefresh() {
|
|||||||
|
|
||||||
/* ── Country column layout ───────────────────────────────────────────────── */
|
/* ── Country column layout ───────────────────────────────────────────────── */
|
||||||
function renderCountryColumns(items, stats = {}) {
|
function renderCountryColumns(items, stats = {}) {
|
||||||
// Group by country
|
|
||||||
const byCountry = {};
|
const byCountry = {};
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const key = item.country || '';
|
const key = item.country || '';
|
||||||
@@ -112,20 +114,20 @@ function renderCountryColumns(items, stats = {}) {
|
|||||||
|
|
||||||
const countries = Object.keys(byCountry).sort();
|
const countries = Object.keys(byCountry).sort();
|
||||||
const wrap = document.getElementById('countries-wrap');
|
const wrap = document.getElementById('countries-wrap');
|
||||||
wrap.innerHTML = '';
|
|
||||||
|
|
||||||
if (countries.length === 0) {
|
if (countries.length === 0) {
|
||||||
wrap.innerHTML = '<div class="page-center"><p>No items found.</p></div>';
|
wrap.innerHTML = '<div class="page-center"><p>No items found.</p></div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
const refs = [];
|
||||||
|
|
||||||
countries.forEach(country => {
|
countries.forEach(country => {
|
||||||
const col = document.createElement('div');
|
const col = document.createElement('div');
|
||||||
col.className = 'country-col';
|
col.className = 'country-col';
|
||||||
|
|
||||||
const colItems = byCountry[country];
|
const colItems = byCountry[country];
|
||||||
const problems = colItems.filter(i => i.severity >= 2).length;
|
|
||||||
|
|
||||||
const flagUrl = countryFlagUrl(country);
|
const flagUrl = countryFlagUrl(country);
|
||||||
const flagImg = flagUrl
|
const flagImg = flagUrl
|
||||||
? `<img src="${flagUrl}" alt="" style="height:22px;width:auto;border-radius:2px;flex-shrink:0;">`
|
? `<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);
|
body.appendChild(grid);
|
||||||
col.appendChild(hdr);
|
col.appendChild(hdr);
|
||||||
col.appendChild(body);
|
col.appendChild(body);
|
||||||
wrap.appendChild(col);
|
frag.appendChild(col);
|
||||||
|
|
||||||
// Compute perRow after layout is in DOM
|
refs.push({ grid, body, colItems });
|
||||||
requestAnimationFrame(() => {
|
});
|
||||||
const hexW = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--hex-w'));
|
|
||||||
const hexGap = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--hex-gap'));
|
// Single atomic swap — old content replaced in one operation
|
||||||
const colW = body.clientWidth - 40; // subtract padding
|
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)));
|
const perRow = Math.max(2, Math.floor(colW / (hexW + hexGap)));
|
||||||
renderHexGrid(grid, colItems, perRow);
|
renderHexGrid(grid, colItems, perRow);
|
||||||
renderProblemList(body, colItems);
|
renderProblemList(body, colItems);
|
||||||
|
|||||||
Reference in New Issue
Block a user