mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-01 16:58:06 +00:00
feat(web-ui): detect and surface Raspberry Pi under-voltage/throttling (#383)
* feat(web-ui): detect and surface Raspberry Pi under-voltage/throttling Adds a vcgencmd get_throttled check to the system-status SSE stream and surfaces it in the web UI: - A header badge (next to CPU/Memory/Temp) that stays hidden when healthy, turns red when under-voltage/throttling is happening right now, and yellow if it happened earlier this session but has since cleared. - A dismissible top banner (same pattern as the update-available banner) that appears while under-voltage/throttling is actively occurring, with guidance to check the power supply. Re-appears on a fresh occurrence even if a previous one was dismissed. - A "Power Supply" card on the Overview tab alongside CPU/Memory/Temp/ Display Status. Motivated by a real device showing intermittent brightness flicker that turned out to be ~1 under-voltage event every 30-90s (visible live via `vcgencmd get_throttled` and dmesg's "Undervoltage detected!" messages) -- there was no way to see this from the web UI, only by SSHing in. Returns None on non-Pi platforms (no vcgencmd on PATH), matching the existing guard pattern used for the CPU temperature read. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ * feat(web-ui): add power supply diagnostics detail to Tools tab The header badge/banner/Overview card added in the previous commit only show a collapsed "is it bad right now" signal. This adds a "Power Supply" section to the Tools tab with the full 8-flag breakdown (under-voltage, throttled, freq-capped, soft-temp-limit -- each split into "right now" vs "occurred since boot") for actually troubleshooting a recurring issue, plus a pointer to the README's power supply sizing guidance when something is or was flagged. Reuses the existing stats SSE stream (window.statsSource) rather than adding a new endpoint -- the same payload already drives the header/ banner/Overview card, so this just listens for it too. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ * fix(web-ui): address review findings on power supply monitoring - Overview card left its "--" placeholder forever on non-Pi platforms since the update only handled a truthy data.power. Now explicitly renders "Not available" with a neutral icon/color for that case. - vcgencmd failures logged at debug, invisible in default remote log output. Bumped to warning to match the nearby systemctl failure logging. - The banner/badge/card and the Tools summary line only looked at under_voltage_now/throttled_now (+ occurred), silently ignoring freq_capped_now/occurred and soft_temp_limit_now/occurred from _get_power_status() -- a Pi that's actively soft-thermal-limited or frequency-capped showed a green "OK" everywhere except the detailed flag table buried in Tools. All four surfaces now fold all four "now"/ "occurred" flags into the same active/occurred state. - The banner text was hardcoded to "Under-voltage detected..." even when the actual active condition was throttling/freq-capping/thermal limiting. Added _activePowerConditionLabels() (shared, non-module global scope) to build the banner/tooltip text from whichever flags are actually set. Skipped: TTL-caching _get_power_status() to avoid "multiplying forks across browser tabs" -- that premise doesn't hold against this codebase. _StreamBroadcaster (its own docstring says as much) already runs exactly one shared generator per tick regardless of client count, identical to the uncached cpu_temp file-read two lines above it; there's nothing to multiply. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ * fix(web-ui): address second round of review findings on power monitoring - updatePowerStatus's falsy-power branch only hid power-stat, leaving power-warning-banner visible with stale text if _get_power_status() fails transiently. Now hides the banner too and resets the dismissed flag, same as the "not active" branch. - The Tools tab's status badge (and the pre-existing dirty/clean badge right next to it) build class names like bg-${color}-100/text-${color}-800 at runtime. This project hand-rolls its own Tailwind-named utility classes in app.css rather than running a real Tailwind build, and the light-mode base rules for bg-red-100/bg-yellow-100/bg-green-100/ text-red-800/text-yellow-800/text-green-800 were simply never defined -- only some had dark-mode overrides, which are no-ops without a base rule in light mode. Added the missing light-mode bases plus the two missing dark-mode overrides (bg-yellow-100/bg-green-100), fixing both badges. - The "occurred earlier" tooltip was a hardcoded string regardless of which flag(s) actually fired. Generalized _activePowerConditionLabels() to take a suffix ('_now' or '_occurred') and reused it for both tooltips. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ * refactor(web-ui): move Power Supply status off the Overview tab The Overview tab's "Power Supply" stat card duplicated what the Tools tab's diagnostics section already shows (summary badge + full flag breakdown), so drop the card and its now-dead JS rather than keep two copies in sync. The header badge and warning banner (visible on every page) are unaffected -- only the Overview-tab card is removed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -913,6 +913,10 @@
|
||||
<i class="fas fa-thermometer-half"></i>
|
||||
<span>--°C</span>
|
||||
</span>
|
||||
<span id="power-stat" class="hidden items-center space-x-1" title="">
|
||||
<i class="fas fa-bolt"></i>
|
||||
<span>Power</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -947,6 +951,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Under-voltage / throttling warning banner -->
|
||||
<div id="power-warning-banner" style="display:none"
|
||||
class="power-warning-banner border-b transition-all duration-300 ease-in-out">
|
||||
<div class="mx-auto px-4 sm:px-6 lg:px-8 xl:px-12 2xl:px-16 py-2" style="max-width:100%">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-3">
|
||||
<i class="fas fa-exclamation-triangle text-lg"></i>
|
||||
<span class="text-sm font-medium" id="power-warning-banner-text"
|
||||
aria-live="polite" aria-atomic="true">
|
||||
A power/thermal issue detected right now — the display may flicker or degrade. Check your power supply and cooling.
|
||||
</span>
|
||||
</div>
|
||||
<button type="button" onclick="dismissPowerWarningBanner()"
|
||||
class="power-warning-banner-dismiss rounded p-1 transition-colors duration-150"
|
||||
title="Dismiss" aria-label="Dismiss power warning">
|
||||
<i class="fas fa-times text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content -->
|
||||
<main class="mx-auto px-4 sm:px-6 lg:px-8 xl:px-12 2xl:px-16 py-8" style="max-width: 100%;">
|
||||
<!-- Navigation tabs -->
|
||||
@@ -1445,6 +1470,89 @@
|
||||
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) {
|
||||
|
||||
@@ -112,6 +112,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Power Supply Diagnostics -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="border-b border-gray-200 pb-4 mb-6">
|
||||
<h2 class="text-lg font-semibold text-gray-900">Power Supply</h2>
|
||||
<p class="mt-1 text-sm text-gray-600">Raspberry Pi under-voltage/throttling status (via <code class="bg-gray-100 px-1 rounded">vcgencmd get_throttled</code>). A marginal power supply is a common cause of visible flicker or dimming on LED panels.</p>
|
||||
</div>
|
||||
<div id="power-info-panel" class="text-sm">
|
||||
<div class="animate-pulse text-gray-400">Waiting for live stats…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Services -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="border-b border-gray-200 pb-4 mb-6">
|
||||
@@ -316,6 +327,76 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ── 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 = '<span class="text-gray-500">Not available on this platform (no <code class="bg-gray-100 px-1 rounded">vcgencmd</code> found — this isn\'t a Raspberry Pi, or it\'s not on PATH).</span>';
|
||||
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 = `
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-${summaryColor}-100 text-${summaryColor}-800">
|
||||
<i class="fas fa-bolt mr-1"></i>${escHtml(summaryText)}
|
||||
</span>
|
||||
</div>
|
||||
<ul class="space-y-1">`;
|
||||
for (const [key, label] of FLAG_LABELS) {
|
||||
const set = !!power[key];
|
||||
html += `<li class="flex items-center gap-2">
|
||||
<i class="fas ${set ? 'fa-exclamation-circle text-red-500' : 'fa-check text-green-500'} w-4"></i>
|
||||
<span class="text-gray-700">${escHtml(label)}</span>
|
||||
</li>`;
|
||||
}
|
||||
html += `</ul>`;
|
||||
|
||||
if (activeNow || occurredEarlier) {
|
||||
html += `<p class="mt-3 text-xs text-gray-500">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.</p>`;
|
||||
}
|
||||
|
||||
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();
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user