mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 06:15:09 +00:00
fix(web): harden, polish and optimize the web UI per the Sept 2026 audit (#568)
* fix(web): harden, polish and optimize the web UI per the September 2026 audit Works through docs/archive/WEB_UI_AUDIT_2026-09.md (health 8/20). Implementation integrity (P0) - app.css now defines every utility class the templates and JS use, including .hidden, so the ~145 JS show/hide toggles work. Button reset, and base component rules (.btn, .form-control) wrapped in :where() so utility classes on the same element win. New static-audit test fails when a used utility class has no rule. Accessibility - Focus rings render (the old ring rule referenced undefined variables); one :focus-visible outline everywhere; skip link; labelled nav landmarks. - Shared dialog helper (js/utils/dialog.js): role/aria-modal, focus trap, Escape, focus return, applied to every modal. - Named icon-only buttons and labelled ~70 form fields. - Toasts announced once; errors persist >= 10s; one showNotification. - Captive WiFi page: live region, timeouts, dark mode, 16px inputs. Performance (Pi Zero 2 W) - SSE streams and tab timers pause when hidden or off-tab; the display stream only runs while a preview is visible. app-shell.js deferred. - Widget scripts served as one versioned bundle (/assets/widgets.js): 52 -> 21 script tags, 66 -> 35 requests on first load. - Stdlib gzip fallback when flask-compress is missing: first-load JS/CSS 1358 KB -> 291 KB on the wire. SSE untouched. Theming and responsive - File managers, form fields and Fonts upload on theme tokens; bare inputs themed in dark mode; no more white surfaces. - No horizontal overflow at 375px on any tab; 44px touch targets on coarse pointers; reduced-motion respected; header title truncates. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(web): clear Codacy findings on #568 - json-file-manager: focus-trap releases kept in a Map (no dynamic property access or delete; no value-returning forEach callback) - notification / schedule-picker: style and day-label lookups via Map - app.js: move the pending-queue assignment out of the expression - diff_viewer / error_handler: named function declarations instead of arrow consts No behavior change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test: check the OAuth widget ships in the widget bundle base.html no longer tags widget scripts one by one; they load through /assets/widgets.js. Assert the page requests the bundle and the bundle contains google-oauth.js, which is what the test was protecting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(web): address review feedback on #568 - widget bundle version fingerprints every file (name, mtime_ns, size) - gzip fallback appends Accept-Encoding to an existing Vary header - dialog helper: releasing a non-top dialog no longer moves focus out of the dialog the user is in - labels: file-upload targets its file input; fallback config fields get label for/id pairs; native color input has a fallback name - utility audit also reads class names inside bound :class expressions Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(web): give the native color-picker input an accessible name CodeRabbit flagged this on PR #568 as an outside-diff finding (never posted inline, so it was missed in the round of fixes that addressed the other 6 review comments). The <input type="color"> only carried a title attribute; screen readers don't reliably announce title, and there's no other label naming the control when showHexInput is false. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(web): clear Codacy findings in app-shell.js - drop the unused catch binding on the SSE JSON parse - move the pending-notification queue assignment out of the expression No behavior change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(web): contain plugin widgets/ dir and bound style-editor retries From CodeRabbit review on #568 (code that arrived with the main merge): - serve_plugin_widget resolves widgets/ with resolve_under before resolving the manifest script under it, so a symlinked widgets directory can't become the containment base (CWE-22). New test. - style-editor init stops polling after ~10s when the widget never registers and leaves the plain fallback fields in place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -204,7 +204,7 @@
|
||||
clearTimeout(timeout);
|
||||
console.error('Failed to load plugins:', err);
|
||||
content.removeAttribute('data-loaded');
|
||||
content.innerHTML = '<div class="bg-red-50 border border-red-200 rounded-lg p-4"><p class="text-red-800">Failed to load Plugin Manager. Please refresh the page.</p></div>';
|
||||
content.innerHTML = '<div class="bg-red-50 border border-red-200 rounded-lg p-4" role="alert"><p class="text-red-800">Couldn’t load the Plugin Manager. Check that the device is still reachable, then try again.</p><button type="button" onclick="loadPluginsDirect()" class="mt-3 px-3 py-1.5 text-sm font-medium bg-red-600 hover:bg-red-700 text-white rounded-md">Try again</button></div>';
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -229,6 +229,11 @@
|
||||
after DOM parse but before DOMContentLoaded, which is exactly what
|
||||
Alpine wants — so no deferLoadingAlpine gymnastics are needed.
|
||||
The inline rescue below only fires if the local file is missing. -->
|
||||
<!-- App shell (full app(), Alpine stores, SSE streams). Deferred so it
|
||||
doesn't block rendering, and placed before Alpine: deferred scripts run
|
||||
in document order, and its alpine:init listener must be registered
|
||||
before Alpine starts. -->
|
||||
<script defer src="{{ url_for('static', filename='v3/js/app-shell.js') }}"></script>
|
||||
<script defer src="{{ url_for('static', filename='v3/js/alpinejs.min.js') }}"></script>
|
||||
<script>
|
||||
// Rescue: if the local Alpine didn't load for any reason, pull the CDN
|
||||
@@ -238,7 +243,7 @@
|
||||
if (typeof window.Alpine === 'undefined') {
|
||||
console.warn('[Alpine] Local file failed to load, falling back to CDN');
|
||||
const s = document.createElement('script');
|
||||
s.src = 'https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js';
|
||||
s.src = 'https://unpkg.com/alpinejs@3.13.5/dist/cdn.min.js';
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
});
|
||||
@@ -291,11 +296,12 @@
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='v3/app.css') }}">
|
||||
</head>
|
||||
<body x-data="app()" class="bg-gray-50 min-h-screen">
|
||||
<a href="#tab-content" class="skip-link">Skip to main content</a>
|
||||
<!-- Header -->
|
||||
<header class="bg-white shadow-md border-b border-gray-200">
|
||||
<div class="mx-auto px-4 sm:px-6 lg:px-8 xl:px-12 2xl:px-16" style="max-width: 100%;">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center min-w-0">
|
||||
<!-- Mobile nav toggle (hamburger) — hidden at md and up -->
|
||||
<button type="button"
|
||||
class="theme-toggle-btn p-2 rounded-md mr-2 md:hidden"
|
||||
@@ -307,14 +313,13 @@
|
||||
title="Menu">
|
||||
<i class="fas fa-bars" aria-hidden="true"></i>
|
||||
</button>
|
||||
<h1 class="text-xl font-bold text-gray-900">
|
||||
<i class="fas fa-tv text-blue-600 mr-2"></i>
|
||||
LED Matrix Control
|
||||
<h1 class="text-lg sm:text-xl font-bold text-gray-900 truncate">
|
||||
<i class="fas fa-tv text-blue-600 mr-2" aria-hidden="true"></i>LED Matrix Control
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- Connection status and theme toggle -->
|
||||
<div id="header-widgets" class="flex items-center space-x-4">
|
||||
<div id="header-widgets" class="flex items-center flex-shrink-0 ml-2 space-x-4">
|
||||
<!-- Global settings search -->
|
||||
<div class="relative hidden sm:block" id="settings-search-wrap">
|
||||
<input id="settings-search"
|
||||
@@ -353,20 +358,20 @@
|
||||
|
||||
<!-- System stats (populated via SSE) -->
|
||||
<div id="system-stats" class="hidden lg:flex items-center space-x-4 text-sm text-gray-600 xl:space-x-6 2xl:space-x-8">
|
||||
<span id="cpu-stat" class="flex items-center space-x-1">
|
||||
<i class="fas fa-microchip"></i>
|
||||
<span id="cpu-stat" class="flex items-center space-x-1" title="CPU usage">
|
||||
<i class="fas fa-microchip" aria-hidden="true"></i>
|
||||
<span>--%</span>
|
||||
</span>
|
||||
<span id="memory-stat" class="flex items-center space-x-1">
|
||||
<i class="fas fa-memory"></i>
|
||||
<span id="memory-stat" class="flex items-center space-x-1" title="Memory usage">
|
||||
<i class="fas fa-memory" aria-hidden="true"></i>
|
||||
<span>--%</span>
|
||||
</span>
|
||||
<span id="temp-stat" class="flex items-center space-x-1">
|
||||
<i class="fas fa-thermometer-half"></i>
|
||||
<span id="temp-stat" class="flex items-center space-x-1" title="CPU temperature">
|
||||
<i class="fas fa-thermometer-half" aria-hidden="true"></i>
|
||||
<span>--°C</span>
|
||||
</span>
|
||||
<span id="power-stat" class="hidden items-center space-x-1" title="">
|
||||
<i class="fas fa-bolt"></i>
|
||||
<i class="fas fa-bolt" aria-hidden="true"></i>
|
||||
<span>Power</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -381,7 +386,7 @@
|
||||
<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-arrow-circle-up text-lg"></i>
|
||||
<i class="fas fa-arrow-circle-up text-lg" aria-hidden="true"></i>
|
||||
<span class="text-sm font-medium" id="update-banner-text"
|
||||
aria-live="polite" aria-atomic="true">
|
||||
A new LEDMatrix update is available
|
||||
@@ -391,12 +396,12 @@
|
||||
<button onclick="applyUpdate()" id="update-banner-btn"
|
||||
class="inline-flex items-center px-3 py-1 text-xs font-semibold rounded-md
|
||||
update-banner-action transition-colors duration-150">
|
||||
<i class="fas fa-download mr-1"></i> Update Now
|
||||
<i class="fas fa-download mr-1" aria-hidden="true"></i> Update Now
|
||||
</button>
|
||||
<button type="button" onclick="dismissUpdateBanner()"
|
||||
class="update-banner-dismiss rounded p-1 transition-colors duration-150"
|
||||
title="Dismiss" aria-label="Dismiss update">
|
||||
<i class="fas fa-times text-sm"></i>
|
||||
<i class="fas fa-times text-sm" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -412,7 +417,7 @@
|
||||
<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-rotate text-lg"></i>
|
||||
<i class="fas fa-rotate text-lg" aria-hidden="true"></i>
|
||||
<span class="text-sm font-medium" aria-live="polite"
|
||||
id="restart-pending-text">
|
||||
Configuration saved — restart the display to apply the changes
|
||||
@@ -422,12 +427,12 @@
|
||||
<button onclick="window.restartPendingNow()" id="restart-pending-btn"
|
||||
class="inline-flex items-center px-3 py-1 text-xs font-semibold rounded-md
|
||||
update-banner-action transition-colors duration-150">
|
||||
<i class="fas fa-power-off mr-1"></i> Restart Now
|
||||
<i class="fas fa-power-off mr-1" aria-hidden="true"></i> Restart Now
|
||||
</button>
|
||||
<button type="button" onclick="window.dismissRestartPending()"
|
||||
class="update-banner-dismiss rounded p-1 transition-colors duration-150"
|
||||
title="Dismiss" aria-label="Dismiss restart reminder">
|
||||
<i class="fas fa-times text-sm"></i>
|
||||
<i class="fas fa-times text-sm" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -440,7 +445,7 @@
|
||||
<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>
|
||||
<i class="fas fa-exclamation-triangle text-lg" aria-hidden="true"></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.
|
||||
@@ -449,7 +454,7 @@
|
||||
<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>
|
||||
<i class="fas fa-times text-sm" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -471,90 +476,90 @@
|
||||
<div id="drawer-widgets"></div>
|
||||
|
||||
<!-- Navigation tabs -->
|
||||
<nav class="mb-8">
|
||||
<div class="mb-8">
|
||||
<!-- First row - System tabs -->
|
||||
<div class="border-b border-gray-200 mb-4">
|
||||
<nav class="-mb-px flex flex-wrap gap-y-2 gap-x-2 lg:gap-x-3 xl:gap-x-4">
|
||||
<nav aria-label="System settings" class="-mb-px flex flex-wrap gap-y-2 gap-x-2 lg:gap-x-3 xl:gap-x-4">
|
||||
<button @click="activeTab = 'overview'"
|
||||
:class="activeTab === 'overview' ? 'nav-tab-active' : ''"
|
||||
class="nav-tab">
|
||||
<i class="fas fa-tachometer-alt"></i>Overview
|
||||
<i class="fas fa-tachometer-alt" aria-hidden="true"></i>Overview
|
||||
</button>
|
||||
<button @click="activeTab = 'general'"
|
||||
:class="activeTab === 'general' ? 'nav-tab-active' : ''"
|
||||
class="nav-tab">
|
||||
<i class="fas fa-sliders-h"></i>General
|
||||
<i class="fas fa-sliders-h" aria-hidden="true"></i>General
|
||||
</button>
|
||||
<button @click="activeTab = 'wifi'"
|
||||
:class="activeTab === 'wifi' ? 'nav-tab-active' : ''"
|
||||
class="nav-tab">
|
||||
<i class="fas fa-wifi"></i>WiFi
|
||||
<i class="fas fa-wifi" aria-hidden="true"></i>WiFi
|
||||
</button>
|
||||
<button @click="activeTab = 'schedule'"
|
||||
:class="activeTab === 'schedule' ? 'nav-tab-active' : ''"
|
||||
class="nav-tab">
|
||||
<i class="fas fa-clock"></i>Schedule
|
||||
<i class="fas fa-clock" aria-hidden="true"></i>Schedule
|
||||
</button>
|
||||
<button @click="activeTab = 'display'"
|
||||
:class="activeTab === 'display' ? 'nav-tab-active' : ''"
|
||||
class="nav-tab">
|
||||
<i class="fas fa-desktop"></i>Display
|
||||
<i class="fas fa-desktop" aria-hidden="true"></i>Display
|
||||
</button>
|
||||
<button @click="activeTab = 'durations'"
|
||||
:class="activeTab === 'durations' ? 'nav-tab-active' : ''"
|
||||
class="nav-tab">
|
||||
<i class="fas fa-rotate"></i>Rotation
|
||||
<i class="fas fa-rotate" aria-hidden="true"></i>Rotation
|
||||
</button>
|
||||
<button @click="activeTab = 'config-editor'"
|
||||
:class="activeTab === 'config-editor' ? 'nav-tab-active' : ''"
|
||||
class="nav-tab">
|
||||
<i class="fas fa-file-code"></i>Config Editor
|
||||
<i class="fas fa-file-code" aria-hidden="true"></i>Config Editor
|
||||
</button>
|
||||
<button @click="activeTab = 'backup-restore'"
|
||||
:class="activeTab === 'backup-restore' ? 'nav-tab-active' : ''"
|
||||
class="nav-tab">
|
||||
<i class="fas fa-save"></i>Backup & Restore
|
||||
<i class="fas fa-save" aria-hidden="true"></i>Backup & Restore
|
||||
</button>
|
||||
<button @click="activeTab = 'fonts'"
|
||||
:class="activeTab === 'fonts' ? 'nav-tab-active' : ''"
|
||||
class="nav-tab">
|
||||
<i class="fas fa-font"></i>Fonts
|
||||
<i class="fas fa-font" aria-hidden="true"></i>Fonts
|
||||
</button>
|
||||
<button @click="activeTab = 'logs'"
|
||||
:class="activeTab === 'logs' ? 'nav-tab-active' : ''"
|
||||
class="nav-tab">
|
||||
<i class="fas fa-file-alt"></i>Logs
|
||||
<i class="fas fa-file-alt" aria-hidden="true"></i>Logs
|
||||
</button>
|
||||
<button @click="activeTab = 'cache'"
|
||||
:class="activeTab === 'cache' ? 'nav-tab-active' : ''"
|
||||
class="nav-tab">
|
||||
<i class="fas fa-database"></i>Cache
|
||||
<i class="fas fa-database" aria-hidden="true"></i>Cache
|
||||
</button>
|
||||
<button @click="activeTab = 'operation-history'"
|
||||
:class="activeTab === 'operation-history' ? 'nav-tab-active' : ''"
|
||||
class="nav-tab">
|
||||
<i class="fas fa-history"></i>Operation History
|
||||
<i class="fas fa-history" aria-hidden="true"></i>Operation History
|
||||
</button>
|
||||
<button @click="activeTab = 'tools'"
|
||||
:class="activeTab === 'tools' ? 'nav-tab-active' : ''"
|
||||
class="nav-tab">
|
||||
<i class="fas fa-tools"></i>Tools
|
||||
<i class="fas fa-tools" aria-hidden="true"></i>Tools
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Second row - Plugin tabs (populated dynamically) -->
|
||||
<div id="plugin-tabs-row" class="border-b border-gray-200">
|
||||
<nav class="-mb-px flex flex-wrap gap-y-2 gap-x-2 lg:gap-x-3 xl:gap-x-4">
|
||||
<nav aria-label="Plugins" class="-mb-px flex flex-wrap gap-y-2 gap-x-2 lg:gap-x-3 xl:gap-x-4">
|
||||
<button @click="activeTab = 'plugins'; if (typeof htmx === 'undefined') { $nextTick(() => loadPluginsDirect()); }"
|
||||
:class="activeTab === 'plugins' ? 'nav-tab-active' : ''"
|
||||
class="nav-tab">
|
||||
<i class="fas fa-plug"></i>Plugin Manager
|
||||
<i class="fas fa-plug" aria-hidden="true"></i>Plugin Manager
|
||||
</button>
|
||||
<!-- Installed plugin tabs will be added here dynamically -->
|
||||
</nav>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div> <!-- /#site-nav -->
|
||||
|
||||
<!-- Floating mini display preview: available on every tab except
|
||||
@@ -563,16 +568,16 @@
|
||||
persists in localStorage. -->
|
||||
<div id="floating-preview" class="floating-preview" style="display:none">
|
||||
<div class="flex items-center justify-between px-2 py-1">
|
||||
<span class="text-xs text-gray-300"><i class="fas fa-tv mr-1"></i>Live Preview</span>
|
||||
<span class="text-xs text-gray-300"><i class="fas fa-tv mr-1" aria-hidden="true"></i>Live Preview</span>
|
||||
<span class="flex items-center">
|
||||
<button type="button" onclick="window.cycleFloatingPreviewSize()"
|
||||
class="text-gray-400 hover:text-white ml-2" aria-label="Cycle preview size"
|
||||
title="Cycle preview size (drag the corner to resize on desktop)">
|
||||
<i class="fas fa-up-right-and-down-left-from-center text-xs"></i>
|
||||
<i class="fas fa-up-right-and-down-left-from-center text-xs" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button" onclick="window.toggleFloatingPreview(false)"
|
||||
class="text-gray-400 hover:text-white ml-2" aria-label="Close live preview">
|
||||
<i class="fas fa-times text-xs"></i>
|
||||
<i class="fas fa-times text-xs" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
@@ -582,12 +587,12 @@
|
||||
<button id="floating-preview-toggle" type="button" onclick="window.toggleFloatingPreview(true)"
|
||||
class="floating-preview-toggle" style="display:none"
|
||||
title="Show live display preview" aria-label="Show live display preview">
|
||||
<i class="fas fa-tv"></i>
|
||||
<i class="fas fa-tv" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
|
||||
<!-- Tab content -->
|
||||
<div id="tab-content" class="space-y-6">
|
||||
<div id="tab-content" class="space-y-6" tabindex="-1">
|
||||
<!-- Overview tab -->
|
||||
<div x-show="activeTab === 'overview'" x-transition>
|
||||
<div id="overview-content" hx-get="/v3/partials/overview" hx-trigger="loadtab" hx-swap="innerHTML" hx-on::htmx:response-error="loadOverviewDirect()">
|
||||
@@ -606,7 +611,10 @@
|
||||
const overviewContent = document.getElementById('overview-content');
|
||||
if (overviewContent && !overviewContent.hasAttribute('data-loaded')) {
|
||||
fetch('/v3/partials/overview')
|
||||
.then(response => response.text())
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error(response.status + ' ' + response.statusText);
|
||||
return response.text();
|
||||
})
|
||||
.then(html => {
|
||||
overviewContent.innerHTML = html;
|
||||
overviewContent.setAttribute('data-loaded', 'true');
|
||||
@@ -620,7 +628,7 @@
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to load overview content:', err);
|
||||
overviewContent.innerHTML = '<div class="bg-red-50 border border-red-200 rounded-lg p-4"><p class="text-red-800">Failed to load overview page. Please refresh the page.</p></div>';
|
||||
overviewContent.innerHTML = '<div class="bg-red-50 border border-red-200 rounded-lg p-4" role="alert"><p class="text-red-800">Couldn’t load the Overview. Check that the device is still reachable, then try again.</p><button type="button" onclick="loadOverviewDirect()" class="mt-3 px-3 py-1.5 text-sm font-medium bg-red-600 hover:bg-red-700 text-white rounded-md">Try again</button></div>';
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -698,7 +706,10 @@
|
||||
const wifiContent = document.getElementById('wifi-content');
|
||||
if (wifiContent && !wifiContent.hasAttribute('data-loaded')) {
|
||||
fetch('/v3/partials/wifi')
|
||||
.then(response => response.text())
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error(response.status + ' ' + response.statusText);
|
||||
return response.text();
|
||||
})
|
||||
.then(html => {
|
||||
wifiContent.innerHTML = html;
|
||||
wifiContent.setAttribute('data-loaded', 'true');
|
||||
@@ -709,7 +720,7 @@
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to load WiFi content:', err);
|
||||
wifiContent.innerHTML = '<div class="bg-red-50 border border-red-200 rounded-lg p-4"><p class="text-red-800">Failed to load WiFi setup page. Please refresh the page.</p></div>';
|
||||
wifiContent.innerHTML = '<div class="bg-red-50 border border-red-200 rounded-lg p-4" role="alert"><p class="text-red-800">Couldn’t load WiFi setup. Check that the device is still reachable, then try again.</p><button type="button" onclick="loadWifiDirect()" class="mt-3 px-3 py-1.5 text-sm font-medium bg-red-600 hover:bg-red-700 text-white rounded-md">Try again</button></div>';
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -952,9 +963,6 @@
|
||||
<!-- Notifications -->
|
||||
<div id="notifications" class="fixed top-4 right-4 z-50 space-y-2"></div>
|
||||
|
||||
<!-- SSE connection for real-time updates -->
|
||||
<script src="{{ url_for('static', filename='v3/js/app-shell.js') }}"></script>
|
||||
|
||||
<!-- Custom v3 JavaScript -->
|
||||
<script src="{{ url_for('static', filename='v3/app.js') }}" defer></script>
|
||||
|
||||
@@ -964,6 +972,7 @@
|
||||
|
||||
<!-- Modular Plugin Management JavaScript -->
|
||||
<!-- Load utilities first -->
|
||||
<script src="{{ url_for('static', filename='v3/js/utils/dialog.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/utils/error_handler.js') }}" defer></script>
|
||||
|
||||
<!-- Load core API client first (used by other modules) -->
|
||||
@@ -979,43 +988,10 @@
|
||||
<!-- Load config utilities -->
|
||||
<script src="{{ url_for('static', filename='v3/js/config/diff_viewer.js') }}" defer></script>
|
||||
|
||||
<!-- Widget Registry System -->
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/registry.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/base-widget.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/notification.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/plugin-order-list.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/file-upload.js') }}?v=20260307" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/checkbox-group.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/custom-feeds.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/array-table.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/google-calendar-picker.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/google-oauth.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/day-selector.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/time-range.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/time-picker.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/file-upload-single.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/plugin-file-manager.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/schedule-picker.js') }}" defer></script>
|
||||
<!-- Basic input widgets -->
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/text-input.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/number-input.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/textarea.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/select-dropdown.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/font-selector.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/style-editor.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/toggle-switch.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/radio-group.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/date-picker.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/slider.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/color-picker.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/email-input.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/url-input.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/password-input.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/timezone-selector.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/plugin-loader.js') }}" defer></script>
|
||||
|
||||
<!-- Reusable JSON file manager widget (used by of-the-day and others via x-widget: json-file-manager) -->
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/json-file-manager.js') }}" defer></script>
|
||||
<!-- Widget Registry System: all widget scripts in one request (see
|
||||
web_interface/widget_bundle.py). The individual files stay served at
|
||||
their own URLs for plugin-loader.js and debugging. -->
|
||||
<script src="{{ widgets_bundle_url() }}" defer></script>
|
||||
|
||||
<!-- plugins_manager.js: loaded LAST on purpose — it defines the live
|
||||
window.* implementations for several plugin actions (installPlugin,
|
||||
@@ -1030,17 +1006,17 @@
|
||||
|
||||
<!-- On-Demand Modal (moved here from plugins.html so it's always available) -->
|
||||
<div id="on-demand-modal" class="fixed inset-0 modal-backdrop flex items-center justify-center z-50" style="display: none;">
|
||||
<div class="modal-content p-6 w-full max-w-md bg-white rounded-lg shadow-lg">
|
||||
<div class="modal-content p-6 w-full max-w-md bg-white rounded-lg shadow-lg" role="dialog" aria-modal="true" aria-labelledby="on-demand-modal-title">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 id="on-demand-modal-title" class="text-lg font-semibold">Run Plugin On-Demand</h3>
|
||||
<button id="close-on-demand-modal" class="text-gray-400 hover:text-gray-600">
|
||||
<i class="fas fa-times"></i>
|
||||
<h2 id="on-demand-modal-title" class="text-lg font-semibold">Run Plugin On-Demand</h2>
|
||||
<button type="button" id="close-on-demand-modal" class="text-gray-500 hover:text-gray-700 p-2 -m-2 rounded-md" aria-label="Close">
|
||||
<i class="fas fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Service Status Alert -->
|
||||
<div id="on-demand-service-warning" class="hidden mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-md">
|
||||
<div class="flex items-start">
|
||||
<i class="fas fa-exclamation-triangle text-yellow-600 mt-0.5 mr-2"></i>
|
||||
<i class="fas fa-exclamation-triangle text-yellow-600 mt-0.5 mr-2" aria-hidden="true"></i>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-yellow-800">Display service is not running</p>
|
||||
<p class="text-xs text-yellow-700 mt-1">
|
||||
@@ -1148,8 +1124,8 @@
|
||||
|
||||
window.applyUpdate = function() {
|
||||
var btn = document.getElementById('update-banner-btn');
|
||||
var originalHTML = '<i class="fas fa-download mr-1"></i> Update Now';
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i> Updating...';
|
||||
var originalHTML = '<i class="fas fa-download mr-1" aria-hidden="true"></i> Update Now';
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-1" aria-hidden="true"></i> Updating...';
|
||||
btn.disabled = true;
|
||||
fetch('/api/v3/system/action', {
|
||||
method: 'POST',
|
||||
@@ -1191,9 +1167,18 @@
|
||||
});
|
||||
};
|
||||
|
||||
// Initial check shortly after page load, then periodic
|
||||
setTimeout(checkForUpdate, 2000);
|
||||
setInterval(checkForUpdate, CHECK_INTERVAL);
|
||||
// Initial check shortly after page load, then every CHECK_INTERVAL while
|
||||
// the page is visible. A hidden tab makes no requests and catches up as
|
||||
// soon as it is shown again.
|
||||
var lastCheck = 0;
|
||||
function checkIfDue() {
|
||||
if (document.hidden || Date.now() - lastCheck < CHECK_INTERVAL) return;
|
||||
lastCheck = Date.now();
|
||||
checkForUpdate();
|
||||
}
|
||||
setTimeout(checkIfDue, 2000);
|
||||
setInterval(checkIfDue, 60 * 1000);
|
||||
document.addEventListener('visibilitychange', checkIfDue);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user