window.statsSource.addEventListener('error', window._statsErrorHandler);
window.displaySource.addEventListener('error', window._displayErrorHandler);
+ // Reset any time the currently-active warning clears, so a future
+ // (new) occurrence shows the banner again even if this one was dismissed.
+ window._powerWarningDismissed = false;
+
+ window.dismissPowerWarningBanner = function() {
+ const banner = document.getElementById('power-warning-banner');
+ if (banner) banner.style.display = 'none';
+ window._powerWarningDismissed = true;
+ };
+
+ // Labels for whichever flags from _get_power_status() are set (pass
+ // suffix='_occurred' for the "happened earlier" variant), used to
+ // build accurate banner/tooltip text instead of hardcoding
+ // "under-voltage" for what may actually be throttling/freq-capping/
+ // thermal limiting.
+ function _activePowerConditionLabels(power, suffix) {
+ suffix = suffix || '_now';
+ const labels = [];
+ if (power['under_voltage' + suffix]) labels.push('under-voltage');
+ if (power['throttled' + suffix]) labels.push('throttling');
+ if (power['freq_capped' + suffix]) labels.push('CPU frequency capped');
+ if (power['soft_temp_limit' + suffix]) labels.push('soft thermal limit');
+ return labels;
+ }
+
+ function updatePowerStatus(power) {
+ const statEl = document.getElementById('power-stat');
+ const banner = document.getElementById('power-warning-banner');
+ const bannerText = document.getElementById('power-warning-banner-text');
+
+ if (!power) {
+ if (statEl) statEl.classList.add('hidden');
+ if (banner) {
+ banner.style.display = 'none';
+ // Let a future occurrence show the banner again rather
+ // than leaving stale text/visibility from before this
+ // (likely transient) missing-data tick.
+ window._powerWarningDismissed = false;
+ }
+ return;
+ }
+
+ const activeNow = power.under_voltage_now || power.throttled_now ||
+ power.freq_capped_now || power.soft_temp_limit_now;
+ const occurredEarlier = power.under_voltage_occurred || power.throttled_occurred ||
+ power.freq_capped_occurred || power.soft_temp_limit_occurred;
+
+ if (statEl) {
+ statEl.classList.remove('text-red-600', 'text-yellow-600');
+ if (activeNow) {
+ statEl.classList.remove('hidden');
+ statEl.classList.add('flex', 'text-red-600');
+ statEl.title = _activePowerConditionLabels(power).join('/') +
+ ' detected right now — check your power supply and cooling';
+ } else if (occurredEarlier) {
+ statEl.classList.remove('hidden');
+ statEl.classList.add('flex', 'text-yellow-600');
+ const occurredLabels = _activePowerConditionLabels(power, '_occurred');
+ statEl.title = (occurredLabels.length ? occurredLabels.join('/') : 'An issue') +
+ ' was detected earlier (currently OK)';
+ } else {
+ statEl.classList.add('hidden');
+ }
+ }
+
+ if (banner) {
+ if (activeNow) {
+ if (bannerText) {
+ const labels = _activePowerConditionLabels(power);
+ bannerText.textContent = (labels.length ? labels.join('/') : 'A power/thermal issue') +
+ ' detected right now — the display may flicker or degrade. Check your power supply and cooling.';
+ }
+ if (!window._powerWarningDismissed) {
+ banner.style.display = '';
+ }
+ } else {
+ banner.style.display = 'none';
+ // Let a future occurrence show the banner again.
+ window._powerWarningDismissed = false;
+ }
+ }
+ }
+
function updateSystemStats(data) {
// Update CPU in header
const cpuEl = document.getElementById('cpu-stat');
@@ -1467,6 +1575,9 @@
if (spans.length > 0) spans[spans.length - 1].textContent = data.cpu_temp + '°C';
}
+ // Update Power (under-voltage / throttling) status in header + banner
+ updatePowerStatus(data.power);
+
// Update Overview tab stats (if visible)
const cpuUsageEl = document.getElementById('cpu-usage');
if (cpuUsageEl && data.cpu_percent !== undefined) {
diff --git a/web_interface/templates/v3/partials/tools.html b/web_interface/templates/v3/partials/tools.html
index 3e249988..7ac8cf87 100644
--- a/web_interface/templates/v3/partials/tools.html
+++ b/web_interface/templates/v3/partials/tools.html
@@ -112,6 +112,17 @@ Maintenance
+
+
+
+ Power Supply
+ Raspberry Pi under-voltage/throttling status (via vcgencmd get_throttled). A marginal power supply is a common cause of visible flicker or dimming on LED panels.
+
+
+ Waiting for live stats…
+
+
+
@@ -316,6 +327,76 @@ Services
});
}
+ // ── power supply diagnostics panel ────────────────────────────────────────
+ // Reuses the same SSE stream (window.statsSource, set up in base.html)
+ // that already drives the header badge/banner and Overview card, instead
+ // of polling a separate endpoint.
+
+ const FLAG_LABELS = [
+ ['under_voltage_now', 'Under-voltage (right now)'],
+ ['throttled_now', 'Throttled (right now)'],
+ ['freq_capped_now', 'ARM frequency capped (right now)'],
+ ['soft_temp_limit_now', 'Soft temperature limit active (right now)'],
+ ['under_voltage_occurred', 'Under-voltage occurred (since boot)'],
+ ['throttled_occurred', 'Throttled occurred (since boot)'],
+ ['freq_capped_occurred', 'ARM frequency capped occurred (since boot)'],
+ ['soft_temp_limit_occurred', 'Soft temperature limit occurred (since boot)'],
+ ];
+
+ function renderPowerInfo(power) {
+ const panel = document.getElementById('power-info-panel');
+ if (!panel) return;
+
+ if (!power) {
+ panel.innerHTML = 'Not available on this platform (no vcgencmd found — this isn\'t a Raspberry Pi, or it\'s not on PATH).';
+ return;
+ }
+
+ const activeNow = power.under_voltage_now || power.throttled_now ||
+ power.freq_capped_now || power.soft_temp_limit_now;
+ const occurredEarlier = power.under_voltage_occurred || power.throttled_occurred ||
+ power.freq_capped_occurred || power.soft_temp_limit_occurred;
+ const summaryColor = activeNow ? 'red' : (occurredEarlier ? 'yellow' : 'green');
+ // _activePowerConditionLabels is defined in base.html's inline script;
+ // both are classic (non-module) scripts sharing the global scope.
+ const activeLabels = (typeof _activePowerConditionLabels === 'function')
+ ? _activePowerConditionLabels(power) : [];
+ const summaryText = activeNow
+ ? `Actively ${activeLabels.length ? activeLabels.join('/') : 'under-voltage/throttled'} right now`
+ : (occurredEarlier ? 'OK right now (but occurred earlier this boot)' : 'OK — no issues detected');
+
+ let html = `
+
+
+ ${escHtml(summaryText)}
+
+
+ `;
+ for (const [key, label] of FLAG_LABELS) {
+ const set = !!power[key];
+ html += `-
+
+ ${escHtml(label)}
+
`;
+ }
+ html += `
`;
+
+ if (activeNow || occurredEarlier) {
+ html += `Consider a higher-amperage 5V supply wired directly to the panel(s), or check for a loose power connector. See the README's Power Supply section for sizing guidance.
`;
+ }
+
+ panel.innerHTML = html;
+ }
+
+ if (window.statsSource) {
+ window.statsSource.addEventListener('message', function(event) {
+ try {
+ const data = JSON.parse(event.data);
+ if (data && 'power' in data) renderPowerInfo(data.power);
+ } catch (e) { /* ignore malformed frames */ }
+ });
+ }
+
// Load on first render; HTMX will have already swapped us in by this point.
loadGitInfo();
})();
Maintenance
+ +Power Supply
+Raspberry Pi under-voltage/throttling status (via vcgencmd get_throttled). A marginal power supply is a common cause of visible flicker or dimming on LED panels.
Services
}); } + // ── power supply diagnostics panel ──────────────────────────────────────── + // Reuses the same SSE stream (window.statsSource, set up in base.html) + // that already drives the header badge/banner and Overview card, instead + // of polling a separate endpoint. + + const FLAG_LABELS = [ + ['under_voltage_now', 'Under-voltage (right now)'], + ['throttled_now', 'Throttled (right now)'], + ['freq_capped_now', 'ARM frequency capped (right now)'], + ['soft_temp_limit_now', 'Soft temperature limit active (right now)'], + ['under_voltage_occurred', 'Under-voltage occurred (since boot)'], + ['throttled_occurred', 'Throttled occurred (since boot)'], + ['freq_capped_occurred', 'ARM frequency capped occurred (since boot)'], + ['soft_temp_limit_occurred', 'Soft temperature limit occurred (since boot)'], + ]; + + function renderPowerInfo(power) { + const panel = document.getElementById('power-info-panel'); + if (!panel) return; + + if (!power) { + panel.innerHTML = 'Not available on this platform (novcgencmd found — this isn\'t a Raspberry Pi, or it\'s not on PATH).';
+ return;
+ }
+
+ const activeNow = power.under_voltage_now || power.throttled_now ||
+ power.freq_capped_now || power.soft_temp_limit_now;
+ const occurredEarlier = power.under_voltage_occurred || power.throttled_occurred ||
+ power.freq_capped_occurred || power.soft_temp_limit_occurred;
+ const summaryColor = activeNow ? 'red' : (occurredEarlier ? 'yellow' : 'green');
+ // _activePowerConditionLabels is defined in base.html's inline script;
+ // both are classic (non-module) scripts sharing the global scope.
+ const activeLabels = (typeof _activePowerConditionLabels === 'function')
+ ? _activePowerConditionLabels(power) : [];
+ const summaryText = activeNow
+ ? `Actively ${activeLabels.length ? activeLabels.join('/') : 'under-voltage/throttled'} right now`
+ : (occurredEarlier ? 'OK right now (but occurred earlier this boot)' : 'OK — no issues detected');
+
+ let html = `
+ - `;
+ for (const [key, label] of FLAG_LABELS) {
+ const set = !!power[key];
+ html += `
- + + ${escHtml(label)} + `; + } + html += `
Consider a higher-amperage 5V supply wired directly to the panel(s), or check for a loose power connector. See the README's Power Supply section for sizing guidance.
`; + } + + panel.innerHTML = html; + } + + if (window.statsSource) { + window.statsSource.addEventListener('message', function(event) { + try { + const data = JSON.parse(event.data); + if (data && 'power' in data) renderPowerInfo(data.power); + } catch (e) { /* ignore malformed frames */ } + }); + } + // Load on first render; HTMX will have already swapped us in by this point. loadGitInfo(); })();