Files
LEDMatrix/web_interface/templates/v3/partials/overview.html
T
ChuckBuildsandClaude Opus 5 2f64dbc48c fix(web): verify the onboarding timezone step, don't compare it to the default
The Getting Started card's timezone step ticked when the saved timezone
differed from the value config.template.json ships (America/New_York),
OR-ed with the saved city differing from Tampa. Both halves were wrong.

"Differs from the default" answers "did somebody edit this?", but what the
checklist needs to know is whether the value is right. A user genuinely in
America/New_York could never satisfy it, so the card nagged forever with
four of five steps done -- the case that prompted this, on a panel whose
timezone was correct all along.

The city half was worse than useless: the saved city says nothing about
whether the timezone is set, and because the two were OR-ed, saving a city
ticked the step off with the timezone still wrong. That is the direction
that actually breaks displays, since event times then render in the wrong
zone.

The browser already knows its own zone, so compare against that. No new
persisted state, no network, and it catches the reverse case the old test
got backwards: a panel still set to the old zone after a move now stays
unticked, where before it ticked the moment the value stopped being the
default. Zones are compared by the wall-clock time they produce for one
instant rather than by identifier, so aliases (Asia/Calcutta vs
Asia/Kolkata, Europe/Kiev vs Europe/Kyiv) don't read as a mismatch. When
they genuinely differ the step names the browser's zone, so an unticked box
says why. Configs with no timezone, an unparseable zone, or a browser
without Intl leave the step open for the existing manual tick.

The step still deep-links to the General tab, and the location value stays
visible in its label -- it just no longer votes on whether the timezone is
configured.

Tests render the partial across configured zones and both cities: the step
never pre-ticks server-side, carries the configured zone for the client to
check, is unmoved by the city, and the panel-size step still resolves
server-side. Reverting the template fails 9 of the 11.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STMbQE4YctTacQXfbYqKuW
2026-08-17 11:36:08 -04:00

586 lines
30 KiB
HTML

<!-- Reconciliation warning banner: shown when startup reconciliation found stale plugin config entries -->
<div id="reconciliation-banner" class="bg-yellow-50 border border-yellow-300 rounded-lg p-4 mb-4 flex items-start" style="display:none !important" role="alert">
<div class="flex-shrink-0 mr-3 mt-0.5">
<i class="fas fa-exclamation-triangle text-yellow-500"></i>
</div>
<div class="flex-1">
<p class="text-sm font-medium text-yellow-800">Plugin Config Warning</p>
<p class="text-sm text-yellow-700 mt-1" id="reconciliation-banner-text"></p>
</div>
<button type="button" onclick="window.dismissReconciliationBanner()" class="ml-4 flex-shrink-0 text-yellow-500 hover:text-yellow-700" aria-label="Dismiss">
<i class="fas fa-times"></i>
</button>
</div>
<script>
(function () {
var DISMISS_KEY = 'ledmatrix-recon-dismissed';
var _recon_timer = null;
function checkReconciliation() {
fetch('/api/v3/plugins/reconciliation-status')
.then(function (r) { return r.json(); })
.then(function (resp) {
var d = resp.data || {};
if (!d.done) {
// Reconciliation still running — poll again shortly
_recon_timer = setTimeout(checkReconciliation, 2000);
return;
}
_recon_timer = null;
if (!d.unresolved || d.unresolved.length === 0) return;
var key = d.unresolved.map(function (i) { return i.plugin_id; }).sort().join(',');
if (sessionStorage.getItem(DISMISS_KEY) === key) return;
var ids = d.unresolved.map(function (i) { return i.plugin_id; }).join(', ');
document.getElementById('reconciliation-banner-text').textContent =
'Stale plugin config entries found: ' + ids +
'. Remove them from config.json or reinstall via the Plugin Store.';
var banner = document.getElementById('reconciliation-banner');
banner.dataset.dismissKey = key;
banner.style.setProperty('display', 'flex', 'important');
})
.catch(function () {});
}
checkReconciliation();
window.dismissReconciliationBanner = function () {
var banner = document.getElementById('reconciliation-banner');
banner.style.setProperty('display', 'none', 'important');
if (_recon_timer !== null) {
clearTimeout(_recon_timer);
_recon_timer = null;
}
// Persist dismissal immediately so the banner won't reappear on reload
// even if the background sync fetch below fails.
var key = banner.dataset.dismissKey;
if (key) {
try { sessionStorage.setItem(DISMISS_KEY, key); } catch (e) {}
}
// Background sync only — do not rely on this for DISMISS_KEY or hiding.
fetch('/api/v3/plugins/reconciliation-status').catch(function () {});
};
}());
</script>
<!-- Getting Started checklist: non-gating, dismissible (localStorage), items
auto-check from existing config/endpoints — no new persisted state.
The timezone step is verified against the browser's own zone rather than
compared to the shipped default; see the data-check="timezone" block below
for why. -->
{% set _hw = main_config.display.hardware if main_config and main_config.display else {} %}
{% set _hw_done = (_hw.rows or 0) > 0 and (_hw.cols or 0) > 0 and (_hw.chain_length or 0) > 0 %}
{% set _loc = main_config.location if main_config and main_config.location else {} %}
{% set _tz = (main_config.timezone if main_config else '') or '' %}
<div id="getting-started-card" class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4" style="display:none" role="region" aria-label="Getting started checklist">
<div class="flex items-start justify-between">
<div class="flex-1">
<p class="text-sm font-semibold text-blue-900"><i class="fas fa-rocket mr-1"></i>Getting Started</p>
<p class="text-xs text-blue-700 mt-0.5 mb-2">A few steps to get your display up and running. Click a step to jump there, or click its checkbox to mark it done yourself. The card hides once everything is checked.</p>
<ul class="space-y-1 text-sm" id="getting-started-items">
<li><button type="button" class="gs-item text-left w-full" data-done="{{ '1' if _hw_done else '0' }}" data-tab="display">
<i class="far fa-square mr-2"></i>Set your panel size (Display tab)</button></li>
<li><button type="button" class="gs-item text-left w-full" data-done="0" data-check="timezone" data-tz="{{ _tz }}" data-tab="general">
<i class="far fa-square mr-2"></i>Set your timezone{% if _tz %} — currently {{ _tz }}{% if _loc.city %}, {{ _loc.city }}{% endif %}{% endif %} (General tab)<span data-gs-tz-note class="text-xs"></span></button></li>
<li><button type="button" class="gs-item text-left w-full" data-done="0" data-check="installed" data-tab="plugins">
<i class="far fa-square mr-2"></i>Install a plugin from the Plugin Store</button></li>
<li><button type="button" class="gs-item text-left w-full" data-done="0" data-check="enabled" data-tab="plugins">
<i class="far fa-square mr-2"></i>Enable a plugin</button></li>
<li><button type="button" class="gs-item text-left w-full" data-done="0" data-check="configured" data-tab="plugins">
<i class="far fa-square mr-2"></i>Configure it (each plugin gets its own tab)</button></li>
</ul>
</div>
<button type="button" onclick="window.dismissGettingStarted()" class="ml-4 flex-shrink-0 text-blue-400 hover:text-blue-600" aria-label="Dismiss getting started checklist">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<script>
(function () {
var KEY = 'ledmatrix-getting-started-dismissed';
var MANUAL_KEY = 'ledmatrix-getting-started-manual';
var card = document.getElementById('getting-started-card');
if (!card) return;
try { if (localStorage.getItem(KEY) === '1') return; } catch (e) {}
card.style.display = 'block';
// Manual per-item overrides: the auto-detection is heuristic (a value
// saved AT its default — e.g. a user genuinely in the default timezone —
// reads as "not done"), so clicking an item's checkbox marks it done by
// hand, persisted per browser.
var manual = {};
try { manual = JSON.parse(localStorage.getItem(MANUAL_KEY) || '{}') || {}; } catch (e) {}
function saveManual() {
try { localStorage.setItem(MANUAL_KEY, JSON.stringify(manual)); } catch (e) {}
}
function setDone(btn, done) {
btn.dataset.done = done ? '1' : '0';
var icon = btn.querySelector('i');
if (icon) { icon.className = done ? 'fas fa-check-square mr-2 text-green-600' : 'far fa-square mr-2'; }
btn.classList.toggle('text-gray-500', done);
btn.classList.toggle('line-through', done);
}
// Once every step is done (auto-detected or manually checked), the card
// has served its purpose — hide it without requiring an explicit dismiss.
function maybeAutoHide() {
var items = card.querySelectorAll('.gs-item');
for (var i = 0; i < items.length; i++) {
if (items[i].dataset.done !== '1') return;
}
card.style.display = 'none';
}
function markDone(btn) {
if (!btn) return;
setDone(btn, true);
maybeAutoHide();
}
// Apply server-derived + manual states, wire deep links (same app-data
// access pattern as settings-search.js). Clicking the checkbox icon
// toggles manual done; clicking the text deep-links to the tab.
Array.prototype.forEach.call(card.querySelectorAll('.gs-item'), function (btn, idx) {
if (manual[idx] === 1) btn.dataset.done = '1';
if (btn.dataset.done === '1') setDone(btn, true);
btn.addEventListener('click', function (ev) {
var icon = btn.querySelector('i');
if (icon && ev.target === icon) {
var nowDone = btn.dataset.done !== '1';
setDone(btn, nowDone);
manual[idx] = nowDone ? 1 : 0;
saveManual();
if (nowDone) maybeAutoHide();
return;
}
var appEl = document.querySelector('[x-data="app()"]') || document.querySelector('[x-data]');
// Same two-tier resolution as settings-search.js's getAppData():
// _x_dataStack on current Alpine, __x.$data as an older-API fallback.
var data = appEl && ((appEl._x_dataStack && appEl._x_dataStack[0]) ||
(appEl.__x && appEl.__x.$data));
if (data) {
data.activeTab = btn.dataset.tab;
if ('mobileNavOpen' in data) data.mobileNavOpen = false;
}
});
});
maybeAutoHide();
// Timezone: verified against the browser's own zone.
//
// This step used to tick when the saved timezone differed from the value
// config.template.json ships (America/New_York), with the saved city
// OR-ed in. Two things were wrong with that. "Differs from the default"
// answers "did somebody edit this?", but what the checklist needs to know
// is whether the value is RIGHT — so anyone who genuinely lives in the
// default zone could never satisfy it and the card nagged forever. And
// the city has no bearing on whether the timezone is set: because the two
// were OR-ed, saving a city ticked the step off with the timezone still
// wrong, which is the direction that actually breaks displays (event
// times render in the wrong zone).
//
// The browser already knows its zone, so compare against that: no new
// persisted state, no network, and it catches the reverse case too — a
// panel still set to the old zone after a move now stays unticked, where
// the old test ticked it the moment the value stopped being the default.
function sameZone(a, b) {
if (a === b) return true;
// Compare the wall-clock time each zone yields for one instant, not
// the identifiers: aliases (Asia/Calcutta vs Asia/Kolkata,
// Europe/Kiev vs Europe/Kyiv) name one zone and must not read as a
// mismatch.
try {
var now = new Date();
var stamp = function (tz) {
return new Intl.DateTimeFormat('en-US', {
timeZone: tz, dateStyle: 'short', timeStyle: 'short'
}).format(now);
};
return stamp(a) === stamp(b);
} catch (e) {
// An unparseable zone in the config is worth surfacing, not hiding.
return false;
}
}
(function () {
var tzBtn = card.querySelector('[data-check="timezone"]');
if (!tzBtn) return;
var configured = tzBtn.dataset.tz || '';
if (!configured) return; // nothing saved yet: leave it open
var local = '';
try {
local = (Intl.DateTimeFormat().resolvedOptions().timeZone) || '';
} catch (e) {
return; // no Intl: leave it to the manual tick
}
if (!local) return;
if (sameZone(configured, local)) {
markDone(tzBtn);
return;
}
// Unticked on its own says "wrong" without saying why; name the zone
// the browser is in so the step is actionable.
var note = tzBtn.querySelector('[data-gs-tz-note]');
if (note) note.textContent = ' — this browser is in ' + local;
}());
// Plugin-derived states from the existing installed-plugins endpoint.
fetch('/api/v3/plugins/installed')
.then(function (r) { return r.json(); })
.then(function (resp) {
var plugins = (resp.data && resp.data.plugins) || [];
if (plugins.length > 0) markDone(card.querySelector('[data-check="installed"]'));
var enabled = plugins.filter(function (p) { return p.enabled; });
if (enabled.length > 0) {
markDone(card.querySelector('[data-check="enabled"]'));
// "Configured" heuristic: the first enabled plugin has at least
// one saved value that differs from its schema defaults.
var pid = enabled[0].id;
Promise.all([
fetch('/api/v3/plugins/config?plugin_id=' + encodeURIComponent(pid)).then(function (r) { return r.json(); }),
fetch('/api/v3/plugins/schema?plugin_id=' + encodeURIComponent(pid)).then(function (r) { return r.json(); })
]).then(function (res) {
// GET /plugins/config returns the config dict directly in .data
var cfg = res[0].data || {};
var props = (res[1].data && res[1].data.schema && res[1].data.schema.properties) || {};
for (var k in cfg) {
if (k === 'enabled' || !(k in props)) continue;
if (props[k] && 'default' in props[k] &&
JSON.stringify(cfg[k]) !== JSON.stringify(props[k]['default'])) {
markDone(card.querySelector('[data-check="configured"]'));
return;
}
}
}).catch(function () {});
}
})
.catch(function () {});
window.dismissGettingStarted = function () {
card.style.display = 'none';
try { localStorage.setItem(KEY, '1'); } catch (e) {}
};
}());
</script>
<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">System Overview</h2>
<p class="mt-1 text-sm text-gray-600">Monitor system status and manage your LED matrix display.</p>
</div>
<!-- System Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-4 2xl:grid-cols-4 gap-6 mb-8">
<div class="bg-gray-50 rounded-lg p-4">
<div class="flex items-center">
<div class="flex-shrink-0">
<i class="fas fa-microchip text-blue-600 text-xl"></i>
</div>
<div class="ml-3 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">CPU Usage</dt>
<dd class="text-lg font-medium text-gray-900" id="cpu-usage">--%</dd>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 rounded-lg p-4">
<div class="flex items-center">
<div class="flex-shrink-0">
<i class="fas fa-memory text-green-600 text-xl"></i>
</div>
<div class="ml-3 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Memory Usage</dt>
<dd class="text-lg font-medium text-gray-900" id="memory-usage">--%</dd>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 rounded-lg p-4">
<div class="flex items-center">
<div class="flex-shrink-0">
<i class="fas fa-thermometer-half text-red-600 text-xl"></i>
</div>
<div class="ml-3 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">CPU Temperature</dt>
<dd class="text-lg font-medium text-gray-900" id="cpu-temp">--°C</dd>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 rounded-lg p-4">
<div class="flex items-center">
<div class="flex-shrink-0">
<i class="fas fa-desktop text-purple-600 text-xl"></i>
</div>
<div class="ml-3 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Display Status</dt>
<dd class="text-lg font-medium text-gray-900" id="display-status">Unknown</dd>
</dl>
</div>
</div>
</div>
</div>
<!-- Version Info -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<div class="flex items-center justify-between">
<div class="flex items-center">
<i class="fas fa-code-branch text-blue-600 text-xl mr-3"></i>
<div>
<dt class="text-sm font-medium text-gray-600">LEDMatrix Version</dt>
<dd class="text-lg font-semibold text-gray-900" id="ledmatrix-version">Loading...</dd>
</div>
</div>
<button onclick="checkForUpdates()"
class="inline-flex items-center px-3 py-2 border border-blue-300 text-sm font-medium rounded-md text-blue-700 bg-white hover:bg-blue-50">
<i class="fas fa-sync-alt mr-2"></i>
Check Updates
</button>
</div>
</div>
<!-- Quick Actions -->
<div class="border-b border-gray-200 pb-4 mb-6">
<h3 class="text-md font-medium text-gray-900 mb-4">Quick Actions</h3>
<div class="flex flex-wrap gap-3" hx-ext="json-enc">
<button hx-post="/api/v3/system/action"
hx-vals='{"action": "start_display"}'
hx-swap="none"
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined') { var m='Display started',s='success'; try { var d=JSON.parse(event.detail.xhr.responseText); m=d.message||m; s=d.status||s; } catch(e) { s=(event.detail.xhr&&event.detail.xhr.status>=400?'error':s); } showNotification(m,s); }"
class="inline-flex items-center px-4 py-2 border border-transparent text-base font-semibold rounded-md text-white bg-green-600 hover:bg-green-700">
<i class="fas fa-play mr-2"></i>
Start Display
</button>
<button hx-post="/api/v3/system/action"
hx-vals='{"action": "stop_display"}'
hx-swap="none"
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined') { var m='Display stopped',s='success'; try { var d=JSON.parse(event.detail.xhr.responseText); m=d.message||m; s=d.status||s; } catch(e) { s=(event.detail.xhr&&event.detail.xhr.status>=400?'error':s); } showNotification(m,s); }"
class="inline-flex items-center px-4 py-2 border border-transparent text-base font-semibold rounded-md text-white bg-red-600 hover:bg-red-700">
<i class="fas fa-stop mr-2"></i>
Stop Display
</button>
<button hx-post="/api/v3/system/action"
hx-vals='{"action": "git_pull"}'
hx-confirm="This will stash any local changes and update the code. Continue?"
hx-swap="none"
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined') { var m='Code update completed',s='info'; try { var d=JSON.parse(event.detail.xhr.responseText); m=d.message||m; s=d.status||s; } catch(e) { s=(event.detail.xhr&&event.detail.xhr.status>=400?'error':s); } showNotification(m,s); }"
class="inline-flex items-center px-4 py-2 border border-gray-300 text-base font-semibold rounded-md text-gray-900 bg-white hover:bg-gray-50">
<i class="fas fa-download mr-2"></i>
Update Code
</button>
<button hx-post="/api/v3/system/action"
hx-vals='{"action": "reboot_system"}'
hx-confirm="Are you sure you want to reboot the system?"
hx-swap="none"
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined') { var m='System rebooting...',s='info'; try { var d=JSON.parse(event.detail.xhr.responseText); m=d.message||m; s=d.status||s; } catch(e) { s=(event.detail.xhr&&event.detail.xhr.status>=400?'error':s); } showNotification(m,s); }"
class="inline-flex items-center px-4 py-2 border border-transparent text-base font-semibold rounded-md text-white bg-yellow-600 hover:bg-yellow-700">
<i class="fas fa-power-off mr-2"></i>
Reboot System
</button>
<button hx-post="/api/v3/system/action"
hx-vals='{"action": "shutdown_system"}'
hx-confirm="Are you sure you want to shut down the system? This will power off the Raspberry Pi."
hx-swap="none"
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined') { var m='System shutting down...',s='info'; try { var d=JSON.parse(event.detail.xhr.responseText); m=d.message||m; s=d.status||s; } catch(e) { s=(event.detail.xhr&&event.detail.xhr.status>=400?'error':s); } showNotification(m,s); }"
class="inline-flex items-center px-4 py-2 border border-transparent text-base font-semibold rounded-md text-white bg-red-800 hover:bg-red-900">
<i class="fas fa-power-off mr-2"></i>
Shutdown System
</button>
<button hx-post="/api/v3/system/action"
hx-vals='{"action": "restart_display_service"}'
hx-swap="none"
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined') { var m='Display service restarted',s='success'; try { var d=JSON.parse(event.detail.xhr.responseText); m=d.message||m; s=d.status||s; } catch(e) { s=(event.detail.xhr&&event.detail.xhr.status>=400?'error':s); } showNotification(m,s); }"
class="inline-flex items-center px-4 py-2 border border-gray-300 text-base font-semibold rounded-md text-gray-900 bg-white hover:bg-gray-50">
<i class="fas fa-redo mr-2"></i>
Restart Display Service
</button>
<button hx-post="/api/v3/system/action"
hx-vals='{"action": "restart_web_service"}'
hx-swap="none"
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined') { var m='Web service restarted',s='success'; try { var d=JSON.parse(event.detail.xhr.responseText); m=d.message||m; s=d.status||s; } catch(e) { s=(event.detail.xhr&&event.detail.xhr.status>=400?'error':s); } showNotification(m,s); }"
class="inline-flex items-center px-4 py-2 border border-gray-300 text-base font-semibold rounded-md text-gray-900 bg-white hover:bg-gray-50">
<i class="fas fa-redo mr-2"></i>
Restart Web Service
</button>
</div>
</div>
<!-- Display Preview -->
<div>
<h3 class="text-md font-medium text-gray-900 mb-4">
<i class="fas fa-desktop"></i> Live Display Preview
</h3>
<!-- overflow-x-auto: on narrow screens a wide preview scrolls at its
true pixel-perfect size instead of being squeezed (fractional
downscaling of pixel art reads as blur) -->
<div class="bg-gray-900 rounded-lg p-6 border border-gray-700 overflow-x-auto" style="position: relative;">
<div id="previewStage" class="preview-stage" style="display:none; position:relative; display:inline-block;">
<div id="previewMeta" style="position:absolute; top:-28px; left:0; color:#ddd; font-size:12px; opacity:0.85;"></div>
<img id="displayImage" style="image-rendering: pixelated; display: block;" alt="LED Matrix Display">
<canvas id="ledCanvas" style="position:absolute; top:0; left:0; pointer-events:none; display:none; z-index: 10;"></canvas>
<canvas id="gridOverlay" style="position:absolute; top:0; left:0; pointer-events:none; z-index: 20;"></canvas>
</div>
<div id="displayPlaceholder" class="text-center text-gray-400 py-8">
<i class="fas fa-spinner fa-spin text-4xl mb-3"></i>
<p>Connecting to display...</p>
</div>
</div>
<!-- Display Controls -->
<div class="mt-4 flex flex-wrap items-center gap-3">
<button onclick="takeScreenshot()" class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-900 bg-white hover:bg-gray-50">
<i class="fas fa-camera mr-2"></i> Screenshot
</button>
<label class="inline-flex items-center gap-2 bg-gray-100 px-3 py-2 rounded-md text-sm">
<span>Scale:</span>
<input type="range" id="scaleRange" min="2" max="16" value="8" class="w-20">
<span id="scaleValue" class="font-medium">8x</span>
</label>
<label class="inline-flex items-center gap-2 bg-gray-100 px-3 py-2 rounded-md text-sm cursor-pointer">
<input type="checkbox" id="toggleGrid" class="rounded">
<span>Show pixel grid</span>
</label>
<label class="inline-flex items-center gap-2 bg-gray-100 px-3 py-2 rounded-md text-sm cursor-pointer">
<input type="checkbox" id="toggleLedDots" checked class="rounded">
<span>LED dot mode</span>
</label>
<label class="inline-flex items-center gap-2 bg-gray-100 px-3 py-2 rounded-md text-sm">
<span>Dot fill:</span>
<input type="range" id="dotFillRange" min="40" max="95" value="75" class="w-16">
<span id="dotFillValue" class="font-medium">75%</span>
</label>
</div>
</div>
</div>
<!-- Stats are updated via the global updateSystemStats function in base.html -->
<script>
// Load LEDMatrix version
(function() {
fetch('/api/v3/system/version')
.then(response => response.json())
.then(data => {
const versionEl = document.getElementById('ledmatrix-version');
if (versionEl && data.status === 'success') {
versionEl.textContent = data.data.version;
}
})
.catch(error => {
const versionEl = document.getElementById('ledmatrix-version');
if (versionEl) versionEl.textContent = 'Unknown';
});
})();
// Check for updates function
window.checkForUpdates = function() {
const btn = event.target.closest('button');
if (btn) {
const originalContent = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Checking...';
fetch('/api/v3/system/action', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'git_pull' })
})
.then(response => response.json())
.then(data => {
btn.innerHTML = originalContent;
btn.disabled = false;
if (data.status === 'success') {
showNotification('Update successful: ' + (data.stdout || 'Code updated'), 'success');
// Reload version after a short delay
setTimeout(() => {
fetch('/api/v3/system/version')
.then(response => response.json())
.then(data => {
const versionEl = document.getElementById('ledmatrix-version');
if (versionEl && data.status === 'success') {
versionEl.textContent = data.data.version;
}
});
}, 1000);
} else {
showNotification('Update failed: ' + (data.stderr || data.message || 'Unknown error'), 'error');
}
})
.catch(error => {
btn.innerHTML = originalContent;
btn.disabled = false;
showNotification('Error checking for updates: ' + error.message, 'error');
});
}
};
// Setup display preview controls (runs when this partial loads)
(function() {
const scaleRange = document.getElementById('scaleRange');
const scaleValue = document.getElementById('scaleValue');
const dotFillRange = document.getElementById('dotFillRange');
const dotFillValue = document.getElementById('dotFillValue');
const toggleGrid = document.getElementById('toggleGrid');
const toggleLedDots = document.getElementById('toggleLedDots');
if (scaleRange && scaleValue) {
scaleRange.addEventListener('input', function() {
scaleValue.textContent = this.value + 'x';
// Re-render the preview with new scale
const img = document.getElementById('displayImage');
if (img && img.src) {
const data = {
image: img.src.replace('data:image/png;base64,', ''),
width: img.naturalWidth,
height: img.naturalHeight
};
updateDisplayPreview(data);
}
});
}
if (dotFillRange && dotFillValue) {
dotFillRange.addEventListener('input', function() {
dotFillValue.textContent = this.value + '%';
renderLedDots();
});
}
if (toggleGrid) {
toggleGrid.addEventListener('change', function() {
const canvas = document.getElementById('gridOverlay');
const img = document.getElementById('displayImage');
if (canvas && img && img.src) {
const scale = parseInt(scaleRange?.value || '8');
drawGrid(canvas, img.naturalWidth, img.naturalHeight, scale);
}
});
}
if (toggleLedDots) {
toggleLedDots.addEventListener('change', function() {
renderLedDots();
});
}
})();
</script>