mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-01 08:48:05 +00:00
* 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>
404 lines
21 KiB
HTML
404 lines
21 KiB
HTML
<div class="space-y-6" id="tools-root">
|
|
|
|
<!-- Git & Updates -->
|
|
<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">Git & Updates</h2>
|
|
<p class="mt-1 text-sm text-gray-600">Inspect the current git state and pull or reset to the latest remote code.</p>
|
|
</div>
|
|
|
|
<!-- Git status info -->
|
|
<div id="git-info-panel" class="mb-6 bg-gray-50 border border-gray-200 rounded-lg p-4 text-sm">
|
|
<div class="animate-pulse text-gray-400">Loading git info…</div>
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
<!-- Pull latest -->
|
|
<div class="flex items-start justify-between gap-4">
|
|
<div>
|
|
<p class="text-sm font-medium text-gray-900">Pull latest (rebase)</p>
|
|
<p class="text-xs text-gray-500 mt-0.5">Stashes any local changes, then runs <code class="bg-gray-100 px-1 rounded">git pull --rebase</code>. The stash is preserved but not re-applied.</p>
|
|
</div>
|
|
<button id="btn-git-pull" onclick="toolsAction('git_pull', 'btn-git-pull', 'result-git-pull')"
|
|
class="shrink-0 inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
|
<i class="fas fa-download mr-2"></i>Pull Latest
|
|
</button>
|
|
</div>
|
|
<div id="result-git-pull" class="hidden"></div>
|
|
|
|
<!-- Force reset -->
|
|
<div class="flex items-start justify-between gap-4 pt-4 border-t border-gray-100">
|
|
<div>
|
|
<p class="text-sm font-medium text-gray-900">Force reset to <code class="bg-gray-100 px-1 rounded">origin/main</code></p>
|
|
<p class="text-xs text-gray-500 mt-0.5">Runs <code class="bg-gray-100 px-1 rounded">git fetch origin</code> then <code class="bg-gray-100 px-1 rounded">git reset --hard origin/main</code>. Discards all local changes.</p>
|
|
</div>
|
|
<div class="shrink-0 flex flex-col items-end gap-2">
|
|
<button id="btn-force-reset-confirm" onclick="showForceResetConfirm()"
|
|
class="inline-flex items-center px-3 py-2 border border-red-300 text-sm font-medium rounded-md text-red-700 bg-white hover:bg-red-50">
|
|
<i class="fas fa-exclamation-triangle mr-2"></i>Force Reset…
|
|
</button>
|
|
<div id="force-reset-confirm-row" class="hidden flex items-center gap-2">
|
|
<span class="text-xs text-red-700 font-medium">This discards all local changes. Sure?</span>
|
|
<button onclick="toolsAction('force_git_reset', 'btn-force-reset-confirm', 'result-force-reset'); hideForceResetConfirm()"
|
|
class="inline-flex items-center px-3 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700">
|
|
Yes, reset
|
|
</button>
|
|
<button onclick="hideForceResetConfirm()"
|
|
class="inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="result-force-reset" class="hidden"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Python Dependencies -->
|
|
<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">Python Dependencies</h2>
|
|
<p class="mt-1 text-sm text-gray-600">Re-run <code class="bg-gray-100 px-1 rounded">pip install</code> to fix missing or broken packages.</p>
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
<!-- Base requirements -->
|
|
<div class="flex items-start justify-between gap-4">
|
|
<div>
|
|
<p class="text-sm font-medium text-gray-900">Reinstall base requirements</p>
|
|
<p class="text-xs text-gray-500 mt-0.5">Installs from <code class="bg-gray-100 px-1 rounded">requirements.txt</code> in the project root.</p>
|
|
</div>
|
|
<button id="btn-base-reqs" onclick="toolsAction('install_base_requirements', 'btn-base-reqs', 'result-base-reqs', true)"
|
|
class="shrink-0 inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
|
<i class="fas fa-box mr-2"></i>Reinstall Base
|
|
</button>
|
|
</div>
|
|
<div id="result-base-reqs" class="hidden"></div>
|
|
|
|
<!-- Plugin requirements -->
|
|
<div class="flex items-start justify-between gap-4 pt-4 border-t border-gray-100">
|
|
<div>
|
|
<p class="text-sm font-medium text-gray-900">Reinstall plugin requirements</p>
|
|
<p class="text-xs text-gray-500 mt-0.5">Runs <code class="bg-gray-100 px-1 rounded">pip install</code> for every installed plugin that has a <code class="bg-gray-100 px-1 rounded">requirements.txt</code>.</p>
|
|
</div>
|
|
<button id="btn-plugin-reqs" onclick="toolsAction('install_plugin_requirements', 'btn-plugin-reqs', 'result-plugin-reqs', false, true)"
|
|
class="shrink-0 inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
|
<i class="fas fa-puzzle-piece mr-2"></i>Reinstall Plugin Deps
|
|
</button>
|
|
</div>
|
|
<div id="result-plugin-reqs" class="hidden"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Maintenance -->
|
|
<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">Maintenance</h2>
|
|
<p class="mt-1 text-sm text-gray-600">Housekeeping operations that don't affect config or plugins.</p>
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
<div class="flex items-start justify-between gap-4">
|
|
<div>
|
|
<p class="text-sm font-medium text-gray-900">Clear Python cache</p>
|
|
<p class="text-xs text-gray-500 mt-0.5">Deletes all <code class="bg-gray-100 px-1 rounded">__pycache__</code> directories in the project. Useful after switching branches or debugging import issues.</p>
|
|
</div>
|
|
<button id="btn-clear-pycache" onclick="toolsAction('clear_pycache', 'btn-clear-pycache', 'result-clear-pycache')"
|
|
class="shrink-0 inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
|
<i class="fas fa-broom mr-2"></i>Clear Cache
|
|
</button>
|
|
</div>
|
|
<div id="result-clear-pycache" class="hidden"></div>
|
|
</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">
|
|
<h2 class="text-lg font-semibold text-gray-900">Services</h2>
|
|
<p class="mt-1 text-sm text-gray-600">Quick access to service restarts.</p>
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
<div class="flex items-start justify-between gap-4">
|
|
<div>
|
|
<p class="text-sm font-medium text-gray-900">Restart display service</p>
|
|
<p class="text-xs text-gray-500 mt-0.5">Restarts <code class="bg-gray-100 px-1 rounded">ledmatrix.service</code>.</p>
|
|
</div>
|
|
<button id="btn-restart-display" onclick="toolsAction('restart_display_service', 'btn-restart-display', 'result-restart-display')"
|
|
class="shrink-0 inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
|
<i class="fas fa-sync-alt mr-2"></i>Restart Display
|
|
</button>
|
|
</div>
|
|
<div id="result-restart-display" class="hidden"></div>
|
|
|
|
<div class="flex items-start justify-between gap-4 pt-4 border-t border-gray-100">
|
|
<div>
|
|
<p class="text-sm font-medium text-gray-900">Restart web interface</p>
|
|
<p class="text-xs text-gray-500 mt-0.5">Restarts <code class="bg-gray-100 px-1 rounded">ledmatrix-web.service</code>. The page will go offline briefly.</p>
|
|
</div>
|
|
<button id="btn-restart-web" onclick="toolsAction('restart_web_service', 'btn-restart-web', 'result-restart-web')"
|
|
class="shrink-0 inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
|
<i class="fas fa-globe mr-2"></i>Restart Web
|
|
</button>
|
|
</div>
|
|
<div id="result-restart-web" class="hidden"></div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
// ── helpers ──────────────────────────────────────────────────────────────
|
|
|
|
function setBusy(btnId, busy) {
|
|
const btn = document.getElementById(btnId);
|
|
if (!btn) return;
|
|
btn.disabled = busy;
|
|
btn.style.opacity = busy ? '0.6' : '';
|
|
btn.style.cursor = busy ? 'wait' : '';
|
|
const icon = btn.querySelector('i');
|
|
if (icon) {
|
|
if (busy) {
|
|
icon.dataset.origClass = icon.className;
|
|
icon.className = 'fas fa-spinner fa-spin mr-2';
|
|
} else if (icon.dataset.origClass) {
|
|
icon.className = icon.dataset.origClass;
|
|
}
|
|
}
|
|
}
|
|
|
|
function showResult(resultId, ok, message, output, pluginDetails) {
|
|
const el = document.getElementById(resultId);
|
|
if (!el) return;
|
|
el.classList.remove('hidden');
|
|
|
|
const color = ok ? 'green' : 'red';
|
|
const icon = ok ? 'fa-check-circle' : 'fa-times-circle';
|
|
|
|
let html = `
|
|
<div class="mt-3 rounded-md p-3 bg-${color}-50 border border-${color}-200">
|
|
<div class="flex items-start gap-2">
|
|
<i class="fas ${icon} text-${color}-600 mt-0.5"></i>
|
|
<span class="text-sm text-${color}-800">${escHtml(message)}</span>
|
|
</div>`;
|
|
|
|
if (output) {
|
|
html += `
|
|
<details class="mt-2">
|
|
<summary class="text-xs text-${color}-700 cursor-pointer hover:underline">Show output</summary>
|
|
<pre class="mt-2 text-xs bg-gray-900 text-gray-100 rounded p-3 overflow-x-auto whitespace-pre-wrap">${escHtml(output)}</pre>
|
|
</details>`;
|
|
}
|
|
|
|
if (pluginDetails && pluginDetails.length > 0) {
|
|
html += `<ul class="mt-3 space-y-1">`;
|
|
for (const d of pluginDetails) {
|
|
const dc = d.ok ? 'green' : 'red';
|
|
const di = d.ok ? 'fa-check' : 'fa-times';
|
|
html += `<li class="text-xs flex items-start gap-1">
|
|
<i class="fas ${di} text-${dc}-600 mt-0.5 w-3"></i>
|
|
<span class="text-gray-700">${escHtml(d.plugin)}</span>`;
|
|
if (d.output) {
|
|
html += ` <details class="inline"><summary class="cursor-pointer text-gray-400 hover:underline ml-1">output</summary>
|
|
<pre class="mt-1 text-xs bg-gray-900 text-gray-100 rounded p-2 overflow-x-auto whitespace-pre-wrap">${escHtml(d.output)}</pre></details>`;
|
|
}
|
|
html += `</li>`;
|
|
}
|
|
html += `</ul>`;
|
|
}
|
|
|
|
html += `</div>`;
|
|
el.innerHTML = html;
|
|
}
|
|
|
|
function escHtml(s) {
|
|
return String(s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
}
|
|
|
|
// ── main action dispatcher ────────────────────────────────────────────────
|
|
|
|
window.toolsAction = function(action, btnId, resultId, showOutput, showPluginDetails) {
|
|
setBusy(btnId, true);
|
|
const el = document.getElementById(resultId);
|
|
if (el) el.classList.add('hidden');
|
|
|
|
fetch('/api/v3/system/action', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({action})
|
|
})
|
|
.then(r => {
|
|
if (!r.ok) {
|
|
return r.json()
|
|
.then(d => Promise.reject(new Error(d.message || `HTTP ${r.status}`)))
|
|
.catch(() => Promise.reject(new Error(`HTTP ${r.status}`)));
|
|
}
|
|
return r.json();
|
|
})
|
|
.then(data => {
|
|
const ok = data.status === 'success';
|
|
showResult(
|
|
resultId, ok,
|
|
data.message || (ok ? 'Done' : 'Failed'),
|
|
showOutput ? (data.output || '') : '',
|
|
showPluginDetails ? (data.details || []) : null
|
|
);
|
|
})
|
|
.catch(err => {
|
|
showResult(resultId, false, 'Request failed: ' + err.message);
|
|
})
|
|
.finally(() => setBusy(btnId, false));
|
|
};
|
|
|
|
// ── force-reset confirm helpers ───────────────────────────────────────────
|
|
|
|
window.showForceResetConfirm = function() {
|
|
document.getElementById('force-reset-confirm-row').classList.remove('hidden');
|
|
document.getElementById('btn-force-reset-confirm').classList.add('hidden');
|
|
};
|
|
|
|
window.hideForceResetConfirm = function() {
|
|
document.getElementById('force-reset-confirm-row').classList.add('hidden');
|
|
document.getElementById('btn-force-reset-confirm').classList.remove('hidden');
|
|
};
|
|
|
|
// ── git info panel ────────────────────────────────────────────────────────
|
|
|
|
function loadGitInfo() {
|
|
const panel = document.getElementById('git-info-panel');
|
|
if (!panel) return;
|
|
|
|
fetch('/api/v3/system/git-info')
|
|
.then(r => {
|
|
if (!r.ok) return r.json().then(d => Promise.reject(d.message || `HTTP ${r.status}`));
|
|
return r.json();
|
|
})
|
|
.then(d => {
|
|
if (d.status === 'error') {
|
|
panel.innerHTML = `<span class="text-sm text-red-600">${escHtml(d.message || 'Git info unavailable.')}</span>`;
|
|
return;
|
|
}
|
|
|
|
|
|
const dirtyBadge = d.dirty
|
|
? '<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800">dirty</span>'
|
|
: '<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">clean</span>';
|
|
|
|
let html = `<div class="space-y-2">
|
|
<div class="flex items-center gap-2">
|
|
<i class="fas fa-code-branch text-gray-400"></i>
|
|
<span class="font-mono text-gray-800">${escHtml(d.branch || 'unknown')}</span>
|
|
${dirtyBadge}
|
|
</div>`;
|
|
|
|
if (d.dirty && d.status) {
|
|
html += `<pre class="text-xs bg-yellow-50 border border-yellow-200 rounded p-2 overflow-x-auto whitespace-pre-wrap text-yellow-900">${escHtml(d.status)}</pre>`;
|
|
}
|
|
|
|
if (d.recent_commits) {
|
|
html += `<div class="mt-2">
|
|
<p class="text-xs text-gray-500 mb-1">Recent commits</p>
|
|
<pre class="text-xs bg-gray-50 border border-gray-200 rounded p-2 overflow-x-auto whitespace-pre-wrap text-gray-700">${escHtml(d.recent_commits)}</pre>
|
|
</div>`;
|
|
}
|
|
|
|
if (d.remote_url) {
|
|
html += `<p class="text-xs text-gray-400 mt-1"><i class="fas fa-cloud mr-1"></i>${escHtml(d.remote_url)}</p>`;
|
|
}
|
|
|
|
html += `</div>`;
|
|
panel.innerHTML = html;
|
|
})
|
|
.catch(err => {
|
|
panel.innerHTML = `<span class="text-sm text-red-600">Could not load git info: ${escHtml(String(err))}</span>`;
|
|
});
|
|
}
|
|
|
|
// ── 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();
|
|
})();
|
|
</script>
|