From cd6efec324e071f0cc07b7c40da7c4b15fbf4165 Mon Sep 17 00:00:00 2001 From: "Valentin V. Bartenev" Date: Sat, 9 May 2026 17:48:06 +0300 Subject: [PATCH] Web: add auto-refresh toggle to stats page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a split-button next to the Refresh button on the stats page. The right half (↻) toggles a 60-second auto-refresh countdown. While active, the button shows the remaining seconds and fires loadStatsPage() each time the counter reaches zero, then resets. A manual Refresh while the timer is running resets the countdown display to keep it in sync. The syncNavButton helper is simplified: visibility is now toggled on the whole split-group container instead of the individual button, and the manual gridTemplateColumns override is removed because the flex split-group handles its own layout. CSS: introduce .btn-split, .btn-split button:first-child, and .btn-split button:last-child rules for the joined-button appearance. --- src/helpers/web/WebPanelServer.cpp | 45 ++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/src/helpers/web/WebPanelServer.cpp b/src/helpers/web/WebPanelServer.cpp index 2621438d..37587d7f 100644 --- a/src/helpers/web/WebPanelServer.cpp +++ b/src/helpers/web/WebPanelServer.cpp @@ -423,6 +423,9 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML( button.action-caution:hover { background:linear-gradient(135deg,#c75656,#e57b70); } button.action-dreamy { background:linear-gradient(135deg,#4e7ac7,#8b7cf6); color:#f7f7ff; border:none; } button.action-dreamy:hover { background:linear-gradient(135deg,#5c89d4,#9a8cff); } + .btn-split { display:flex; } + .btn-split button:first-child { border-radius:10px 0 0 10px; } + .btn-split button:last-child { border-radius:0 10px 10px 0; border-left:1px solid rgba(255,255,255,.25); width:52px; text-align:center; padding-left:0; padding-right:0; } .stack { display:grid; gap:12px; } .field-card { display:grid; gap:10px; } .section-group { background:var(--surface2); border:1px solid var(--border); border-radius:12px; padding:14px; display:grid; gap:12px; } @@ -586,7 +589,10 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
- +
+ + +
@@ -1056,7 +1062,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML( const centerGroup = document.querySelector("#actionsPanel .actions-group.center"); const advertBtn = document.getElementById("advertBtn"); const otaBtn = document.getElementById("otaBtn"); - const refreshBtn = document.getElementById("refreshPageBtn"); + const refreshSplitGroup = document.getElementById("refreshSplitGroup"); if (appBtn) { appBtn.classList.toggle("active", !isStatsPage); appBtn.title = "Open app page"; @@ -1069,12 +1075,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML( } if (advertBtn) advertBtn.style.display = isStatsPage ? "none" : ""; if (otaBtn) otaBtn.style.display = isStatsPage ? "none" : ""; - if (refreshBtn) refreshBtn.style.display = isStatsPage ? "" : "none"; - if (centerGroup) { - centerGroup.style.gridTemplateColumns = isStatsPage - ? "1fr" - : "repeat(2,minmax(0,1fr))"; - } + if (refreshSplitGroup) refreshSplitGroup.style.display = isStatsPage ? "" : "none"; } function toggleTheme() { const next = rootEl.dataset.theme === "dark" ? "light" : "dark"; @@ -2513,13 +2514,41 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML( await runCommand("start ota"); } }; + const AUTO_REFRESH_INTERVAL = 60; + let autoRefreshCountdown = 0; + let autoRefreshTimer = null; document.getElementById("refreshPageBtn").onclick = async () => { if (isStatsPage) { + if (autoRefreshTimer) { + autoRefreshCountdown = AUTO_REFRESH_INTERVAL; + document.getElementById("autoRefreshBtn").textContent = `${autoRefreshCountdown}s`; + } await loadStatsPage(); } else { await initApp(); } }; + document.getElementById("autoRefreshBtn").onclick = () => { + const btn = document.getElementById("autoRefreshBtn"); + if (autoRefreshTimer) { + btn.textContent = "↻"; + clearInterval(autoRefreshTimer); + autoRefreshTimer = null; + } else { + autoRefreshCountdown = AUTO_REFRESH_INTERVAL; + btn.textContent = `${autoRefreshCountdown}s` + autoRefreshTimer = setInterval(async () => { + autoRefreshCountdown--; + if (autoRefreshCountdown <= 0) { + autoRefreshCountdown = AUTO_REFRESH_INTERVAL; + btn.textContent = `${autoRefreshCountdown}s`; + await loadStatsPage(); + } else { + btn.textContent = `${autoRefreshCountdown}s`; + } + }, 1000); + } + }; document.getElementById("logoutBtn").onclick = () => { redirectToLogin(); };