mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-10 09:06:36 +00:00
Merge commit 'b9416ef803028ec37e0f2f756c051e52d52622d7' into claude/hdpi-scroll-performance-antialiasing-4ae609
# Conflicts: # src/vegas_mode/config.py # src/vegas_mode/coordinator.py
This commit is contained in:
@@ -168,54 +168,109 @@
|
||||
</script>
|
||||
<script src="{{ url_for('static', filename='v3/js/htmx-config.js') }}"></script>
|
||||
|
||||
<!-- Fallback for loading plugins if HTMX fails -->
|
||||
<!-- Direct-fetch fallback for tab panels. Used when HTMX never loads, and
|
||||
by the hx-on::response-error handlers on the Overview, WiFi and
|
||||
Plugin Manager panels when HTMX's own request fails. -->
|
||||
<script>
|
||||
function loadPluginsDirect() {
|
||||
const content = document.getElementById('plugins-content');
|
||||
if (content && !content.hasAttribute('data-loaded')) {
|
||||
content.setAttribute('data-loaded', 'true');
|
||||
debugLog('Loading plugins directly via fetch (HTMX fallback)...');
|
||||
(function() {
|
||||
const PANEL_LABELS = {
|
||||
'overview-content': 'the Overview',
|
||||
'wifi-content': 'WiFi setup',
|
||||
'plugins-content': 'the Plugin Manager'
|
||||
};
|
||||
|
||||
// Fetches `url` into the element `id`, runs the partial's inline
|
||||
// scripts, and lets HTMX and Alpine process the new markup. The
|
||||
// scripts run before Alpine sees the content for the same reason
|
||||
// htmx-config.js runs them on htmx:afterSwap: partials define their
|
||||
// x-data component functions in those scripts.
|
||||
window.loadPartialDirect = function(id, url) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el || !url || el.hasAttribute('data-loaded') || el.hasAttribute('data-direct-loading')) return;
|
||||
el.setAttribute('data-direct-loading', 'true');
|
||||
debugLog('Loading ' + url + ' directly via fetch (HTMX fallback)');
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 10000);
|
||||
fetch('/v3/partials/plugins', { signal: controller.signal })
|
||||
fetch(url, { signal: controller.signal })
|
||||
.then(r => {
|
||||
if (!r.ok) throw new Error(r.status + ' ' + r.statusText);
|
||||
return r.text();
|
||||
})
|
||||
.then(html => {
|
||||
clearTimeout(timeout);
|
||||
content.innerHTML = html;
|
||||
if (typeof htmx !== 'undefined') {
|
||||
htmx.process(content);
|
||||
}
|
||||
// Trigger full initialization chain
|
||||
if (window.pluginManager) {
|
||||
window.pluginManager.initialized = false;
|
||||
window.pluginManager.initializing = false;
|
||||
}
|
||||
if (window.initPluginsPage) {
|
||||
el.innerHTML = html;
|
||||
el.querySelectorAll('script').forEach(oldScript => {
|
||||
const script = document.createElement('script');
|
||||
for (const attr of oldScript.attributes) script.setAttribute(attr.name, attr.value);
|
||||
script.textContent = oldScript.textContent;
|
||||
oldScript.replaceWith(script);
|
||||
});
|
||||
el.setAttribute('data-loaded', 'true');
|
||||
if (typeof htmx !== 'undefined') htmx.process(el);
|
||||
if (window.Alpine) window.Alpine.initTree(el);
|
||||
if (id === 'plugins-content' && window.initPluginsPage) {
|
||||
if (window.pluginManager) {
|
||||
window.pluginManager.initialized = false;
|
||||
window.pluginManager.initializing = false;
|
||||
}
|
||||
window.initPluginsPage();
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to load ' + url + ':', err);
|
||||
const label = PANEL_LABELS[id] || 'this page';
|
||||
el.innerHTML = '<div class="bg-red-50 border border-red-200 rounded-lg p-4" role="alert"><p class="text-red-800">Couldn’t load ' + label + '. Check that the device is still reachable, then try again.</p><button type="button" 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>';
|
||||
el.querySelector('button').addEventListener('click', () => window.loadPartialDirect(id, url));
|
||||
})
|
||||
.finally(() => {
|
||||
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" 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>';
|
||||
el.removeAttribute('data-direct-loading');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Fallback if HTMX doesn't load within 5 seconds
|
||||
var _pluginsFallbackTimer = setTimeout(() => {
|
||||
if (typeof htmx === 'undefined') {
|
||||
console.warn('HTMX not loaded after 5 seconds, using direct fetch for plugins');
|
||||
// Load plugins tab content directly regardless of active tab,
|
||||
// so it's ready when the user navigates to it
|
||||
loadPluginsDirect();
|
||||
function activeTab() {
|
||||
const appEl = document.querySelector('[x-data="app()"]');
|
||||
try {
|
||||
return (window.Alpine && appEl) ? window.Alpine.$data(appEl).activeTab : null;
|
||||
} catch (e) {
|
||||
return null; // Alpine has not initialised the app yet
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
window.addEventListener('htmx:ready', function() { clearTimeout(_pluginsFallbackTimer); }, { once: true });
|
||||
|
||||
function loadActiveTabDirect() {
|
||||
const el = document.getElementById((activeTab() || 'overview') + '-content');
|
||||
if (el) window.loadPartialDirect(el.id, el.getAttribute('hx-get'));
|
||||
}
|
||||
|
||||
// HTMX loaded but never initialised, even from the CDN rescue.
|
||||
window.addEventListener('htmx-load-failed', function() {
|
||||
console.warn('HTMX failed to load, loading the active tab directly');
|
||||
setTimeout(loadActiveTabDirect, 2000);
|
||||
});
|
||||
|
||||
// HTMX still missing after 5 seconds: load the active tab, and the
|
||||
// Plugin Manager regardless of tab so the plugin tabs work.
|
||||
const fallbackTimer = setTimeout(function() {
|
||||
if (typeof htmx !== 'undefined') return;
|
||||
console.warn('HTMX not loaded after 5 seconds, loading tabs directly');
|
||||
window.loadPartialDirect('plugins-content', '/v3/partials/plugins');
|
||||
loadActiveTabDirect();
|
||||
}, 5000);
|
||||
|
||||
// AP mode (setting up WiFi) opens on the WiFi tab and can least
|
||||
// afford to wait, so it gets an earlier check.
|
||||
const apTimer = setTimeout(function() {
|
||||
const host = window.location.hostname;
|
||||
if (typeof htmx === 'undefined' && host.startsWith('192.168.4.')) {
|
||||
console.warn('HTMX not loaded, loading WiFi setup directly');
|
||||
window.loadPartialDirect('wifi-content', '/v3/partials/wifi');
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
window.addEventListener('htmx:ready', function() {
|
||||
clearTimeout(fallbackTimer);
|
||||
clearTimeout(apTimer);
|
||||
}, { once: true });
|
||||
})();
|
||||
</script>
|
||||
<!-- Alpine.js app function - defined early so it's available when Alpine initializes -->
|
||||
<script src="{{ url_for('static', filename='v3/js/app-early.js') }}"></script>
|
||||
@@ -246,45 +301,6 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- CodeMirror for JSON editing - lazy loaded when needed -->
|
||||
<link rel="preload" href="{{ url_for('static', filename='v3/vendor/codemirror/codemirror.min.css') }}" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
||||
<noscript><link rel="stylesheet" href="{{ url_for('static', filename='v3/vendor/codemirror/codemirror.min.css') }}"></noscript>
|
||||
<link rel="preload" href="{{ url_for('static', filename='v3/vendor/codemirror/theme/monokai.min.css') }}" as="style" onload="this.onload=null;this.rel='stylesheet'">
|
||||
<noscript><link rel="stylesheet" href="{{ url_for('static', filename='v3/vendor/codemirror/theme/monokai.min.css') }}"></noscript>
|
||||
<!-- CodeMirror scripts loaded on demand when JSON editor is opened -->
|
||||
<script>
|
||||
// Lazy load CodeMirror when needed
|
||||
window.loadCodeMirror = function() {
|
||||
if (window.CodeMirror) return Promise.resolve();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Vendored CodeMirror 5.65.2 (note: the previously listed CDN
|
||||
// mode/json/json.min.js never existed — it 404'd on every
|
||||
// editor open; JSON highlighting comes from the javascript
|
||||
// mode, so it's simply dropped here)
|
||||
const scripts = [
|
||||
'/static/v3/vendor/codemirror/codemirror.min.js',
|
||||
'/static/v3/vendor/codemirror/mode/javascript/javascript.min.js',
|
||||
'/static/v3/vendor/codemirror/addon/edit/closebrackets.min.js',
|
||||
'/static/v3/vendor/codemirror/addon/edit/matchbrackets.min.js'
|
||||
];
|
||||
|
||||
let loaded = 0;
|
||||
scripts.forEach((src, index) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = src;
|
||||
script.defer = true;
|
||||
script.onload = () => {
|
||||
loaded++;
|
||||
if (loaded === scripts.length) resolve();
|
||||
};
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Font Awesome icons (vendored 6.0.0 — css/all.min.css references
|
||||
../webfonts/ relatively, so both live under v3/vendor/fontawesome/) -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='v3/vendor/fontawesome/css/all.min.css') }}">
|
||||
@@ -548,7 +564,7 @@
|
||||
<!-- Second row - Plugin tabs (populated dynamically) -->
|
||||
<div id="plugin-tabs-row" class="border-b border-gray-200">
|
||||
<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()); }"
|
||||
<button @click="activeTab = 'plugins'; if (typeof htmx === 'undefined') { $nextTick(() => loadPartialDirect('plugins-content', '/v3/partials/plugins')); }"
|
||||
:class="activeTab === 'plugins' ? 'nav-tab-active' : ''"
|
||||
class="nav-tab">
|
||||
<i class="fas fa-plug" aria-hidden="true"></i>Plugin Manager
|
||||
@@ -592,7 +608,7 @@
|
||||
<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()">
|
||||
<div id="overview-content" hx-get="/v3/partials/overview" hx-trigger="loadtab" hx-swap="innerHTML" hx-on::response-error="loadPartialDirect(this.id, this.getAttribute('hx-get'))">
|
||||
<div class="animate-pulse">
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="h-4 bg-gray-200 rounded w-1/4 mb-4"></div>
|
||||
@@ -601,65 +617,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Fallback: Load overview content directly if HTMX fails
|
||||
function loadOverviewDirect() {
|
||||
const overviewContent = document.getElementById('overview-content');
|
||||
if (overviewContent && !overviewContent.hasAttribute('data-loaded')) {
|
||||
fetch('/v3/partials/overview')
|
||||
.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');
|
||||
if (typeof htmx !== 'undefined') {
|
||||
htmx.process(overviewContent);
|
||||
}
|
||||
// Re-initialize Alpine.js for the new content
|
||||
if (window.Alpine) {
|
||||
window.Alpine.initTree(overviewContent);
|
||||
}
|
||||
})
|
||||
.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" 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>';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for HTMX load failure
|
||||
window.addEventListener('htmx-load-failed', function() {
|
||||
console.warn('HTMX failed to load, setting up direct content loading fallbacks');
|
||||
// Try to load content directly after a delay
|
||||
setTimeout(() => {
|
||||
const appElement = document.querySelector('[x-data="app()"]');
|
||||
if (appElement && appElement.__x) {
|
||||
const activeTab = appElement.__x.$data.activeTab || 'overview';
|
||||
if (activeTab === 'overview') {
|
||||
loadOverviewDirect();
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
// Also try direct load if HTMX doesn't load within 5 seconds
|
||||
var _overviewFallbackTimer = setTimeout(() => {
|
||||
if (typeof htmx === 'undefined') {
|
||||
console.warn('HTMX not loaded after 5 seconds, using direct fetch for content');
|
||||
const appElement = document.querySelector('[x-data="app()"]');
|
||||
if (appElement && appElement.__x) {
|
||||
const activeTab = appElement.__x.$data.activeTab || 'overview';
|
||||
if (activeTab === 'overview') {
|
||||
loadOverviewDirect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
window.addEventListener('htmx:ready', function() { clearTimeout(_overviewFallbackTimer); }, { once: true });
|
||||
</script>
|
||||
|
||||
<!-- General tab -->
|
||||
<div x-show="activeTab === 'general'" x-transition>
|
||||
@@ -683,7 +640,7 @@
|
||||
hx-get="/v3/partials/wifi"
|
||||
hx-trigger="loadtab"
|
||||
hx-swap="innerHTML"
|
||||
hx-on::htmx:response-error="loadWifiDirect()">
|
||||
hx-on::response-error="loadPartialDirect(this.id, this.getAttribute('hx-get'))">
|
||||
<div class="animate-pulse">
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="h-4 bg-gray-200 rounded w-1/4 mb-4"></div>
|
||||
@@ -696,42 +653,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Fallback: Load WiFi content directly if HTMX fails
|
||||
function loadWifiDirect() {
|
||||
const wifiContent = document.getElementById('wifi-content');
|
||||
if (wifiContent && !wifiContent.hasAttribute('data-loaded')) {
|
||||
fetch('/v3/partials/wifi')
|
||||
.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');
|
||||
// Re-initialize Alpine.js for the new content
|
||||
if (window.Alpine) {
|
||||
window.Alpine.initTree(wifiContent);
|
||||
}
|
||||
})
|
||||
.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" 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>';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Also try direct load if HTMX doesn't load within 3 seconds (AP mode detection)
|
||||
setTimeout(() => {
|
||||
const isAPMode = window.location.hostname === '192.168.4.1' ||
|
||||
window.location.hostname.startsWith('192.168.4.');
|
||||
if (isAPMode && typeof htmx === 'undefined') {
|
||||
console.warn('HTMX not loaded, using direct fetch for WiFi content');
|
||||
loadWifiDirect();
|
||||
}
|
||||
}, 3000);
|
||||
</script>
|
||||
|
||||
<!-- Schedule tab -->
|
||||
<div x-show="activeTab === 'schedule'" x-transition>
|
||||
@@ -806,7 +727,7 @@
|
||||
<!-- Plugins tab -->
|
||||
<div x-show="activeTab === 'plugins'" x-transition>
|
||||
<div id="plugins-content" hx-get="/v3/partials/plugins" hx-trigger="loadtab" hx-swap="innerHTML"
|
||||
hx-on::response-error="loadPluginsDirect()">
|
||||
hx-on::response-error="loadPartialDirect(this.id, this.getAttribute('hx-get'))">
|
||||
<div class="animate-pulse">
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="h-4 bg-gray-200 rounded w-1/4 mb-4"></div>
|
||||
@@ -986,11 +907,10 @@
|
||||
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,
|
||||
uninstallPlugin, executePluginAction, ...) and intentionally wins
|
||||
over same-named definitions in js/app.js and js/app-shell.js. Do not
|
||||
remove or reorder without resolving that overlap first. -->
|
||||
<!-- plugins_manager.js: loaded last. It uses PluginAPI, ListFilter,
|
||||
LEDDialog, the widgets and app-shell.js's updatePlugin at load time
|
||||
or when a card is clicked, so it must follow all of them. No other
|
||||
script defines the same globals. -->
|
||||
<script src="{{ url_for('static', filename='v3/plugins_manager.js') }}?v=20260307" defer></script>
|
||||
|
||||
<!-- Custom feeds table helpers live in js/widgets/custom-feeds.js (the
|
||||
@@ -1147,16 +1067,12 @@
|
||||
'services to run the new code');
|
||||
}
|
||||
}
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification(data.message || 'Update complete', data.status || 'success');
|
||||
}
|
||||
showNotification(data.message || 'Update complete', data.status || 'success');
|
||||
})
|
||||
.catch(function() {
|
||||
btn.innerHTML = originalHTML;
|
||||
btn.disabled = false;
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification('Update failed — check your connection', 'error');
|
||||
}
|
||||
showNotification('Update failed — check your connection', 'error');
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -121,11 +121,7 @@
|
||||
let inspectedFile = null;
|
||||
|
||||
function notify(message, kind) {
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification(message, kind || 'info');
|
||||
} else {
|
||||
console.log('[backup]', kind || 'info', message);
|
||||
}
|
||||
showNotification(message, kind || 'info');
|
||||
}
|
||||
|
||||
function formatSize(bytes) {
|
||||
@@ -136,11 +132,7 @@
|
||||
return size.toFixed(i === 0 ? 0 : 1) + ' ' + units[i];
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value == null ? '' : value).replace(/[&<>"']/g, function (c) {
|
||||
return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c];
|
||||
});
|
||||
}
|
||||
function escapeHtml(value) { return window.LEDEscape.html(value); }
|
||||
|
||||
async function loadPreview() {
|
||||
const el = document.getElementById('export-preview');
|
||||
|
||||
@@ -55,13 +55,12 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Initialize cache management when partial loads
|
||||
// Scoped: this script runs at global scope after every HTMX swap, and the Logs
|
||||
// partial has helpers with the same names (showError, escapeHtml). Only
|
||||
// deleteCacheFile, which the row buttons call from onclick, is exported.
|
||||
(function() {
|
||||
loadCacheFiles();
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('refresh-cache-btn').addEventListener('click', loadCacheFiles);
|
||||
})();
|
||||
loadCacheFiles();
|
||||
document.getElementById('refresh-cache-btn').addEventListener('click', loadCacheFiles);
|
||||
|
||||
function loadCacheFiles() {
|
||||
const tbody = document.getElementById('cache-files-tbody');
|
||||
@@ -190,21 +189,15 @@ function deleteCacheFile(key) {
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
if (typeof showNotification !== 'undefined') {
|
||||
showNotification(data.message || 'Cache file deleted successfully', 'success');
|
||||
}
|
||||
showNotification(data.message || 'Cache file deleted successfully', 'success');
|
||||
// Reload cache files list
|
||||
loadCacheFiles();
|
||||
} else {
|
||||
if (typeof showNotification !== 'undefined') {
|
||||
showNotification(data.message || 'Failed to delete cache file', 'error');
|
||||
}
|
||||
showNotification(data.message || 'Failed to delete cache file', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (typeof showNotification !== 'undefined') {
|
||||
showNotification('Error deleting cache file: ' + error.message, 'error');
|
||||
}
|
||||
showNotification('Error deleting cache file: ' + error.message, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -220,17 +213,8 @@ function showError(message) {
|
||||
errorMessage.textContent = message;
|
||||
}
|
||||
|
||||
// Utility function to escape HTML
|
||||
// Quotes too: the result is interpolated into quoted attribute values, and the
|
||||
// textContent/innerHTML round-trip only escapes &, < and >.
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
function escapeHtml(text) { return window.LEDEscape.html(text); }
|
||||
|
||||
// Make deleteCacheFile available globally for onclick handlers
|
||||
window.deleteCacheFile = deleteCacheFile;
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
hx-ext="json-enc"
|
||||
hx-headers='{"Content-Type": "application/json"}'
|
||||
hx-swap="none"
|
||||
hx-on:htmx:after-request="showDisplaySaveResult(event.detail.xhr)"
|
||||
hx-on:htmx:after-request="showSaveResult(event.detail.xhr, 'Display settings saved', 'Display settings were not saved. Check your connection and try again.')"
|
||||
class="space-y-6"
|
||||
novalidate
|
||||
onsubmit="fixInvalidNumberInputs(this); return true;">
|
||||
@@ -863,62 +863,12 @@ document.getElementById('brightness').addEventListener('input', function() {
|
||||
document.getElementById('brightness-display').textContent = this.value;
|
||||
});
|
||||
|
||||
// Fix invalid number inputs function (if not already defined globally)
|
||||
if (typeof window.fixInvalidNumberInputs !== 'function') {
|
||||
window.fixInvalidNumberInputs = function(form) {
|
||||
if (!form) return;
|
||||
const allInputs = form.querySelectorAll('input[type="number"]');
|
||||
allInputs.forEach(input => {
|
||||
const min = parseFloat(input.getAttribute('min'));
|
||||
const max = parseFloat(input.getAttribute('max'));
|
||||
const value = parseFloat(input.value);
|
||||
|
||||
if (!isNaN(value)) {
|
||||
if (!isNaN(min) && value < min) {
|
||||
input.value = min;
|
||||
} else if (!isNaN(max) && value > max) {
|
||||
input.value = max;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Report the outcome of a display-settings save. XMLHttpRequest has no
|
||||
// `responseJSON` (that's a jQuery property) — read `responseText` and the real
|
||||
// status code, otherwise a failed save reports success.
|
||||
window.showDisplaySaveResult = function(xhr) {
|
||||
// Only 2xx counts as saved. A network failure reports status 0, which any
|
||||
// `>= 400` test would wave through as success.
|
||||
const httpSuccess = xhr.status >= 200 && xhr.status < 300;
|
||||
let message = httpSuccess
|
||||
? 'Display settings saved'
|
||||
: 'Display settings were not saved. Check your connection and try again.';
|
||||
let status = httpSuccess ? 'success' : 'error';
|
||||
try {
|
||||
const data = JSON.parse(xhr.responseText);
|
||||
if (data.message) message = data.message;
|
||||
// A body can refine a successful verdict but never overturn a failed one.
|
||||
if (httpSuccess && data.status) status = data.status;
|
||||
} catch {
|
||||
// Non-JSON body — fall back to the status-code verdict above.
|
||||
}
|
||||
showNotification(message, status);
|
||||
};
|
||||
|
||||
// Vegas Scroll Mode Settings
|
||||
(function() {
|
||||
// Escape HTML to prevent XSS
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = String(text || '');
|
||||
return div.innerHTML;
|
||||
}
|
||||
function escapeHtml(text) { return window.LEDEscape.html(text); }
|
||||
|
||||
// Escape for use in HTML attributes
|
||||
function escapeAttr(text) {
|
||||
return escapeHtml(text).replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
function escapeAttr(text) { return window.LEDEscape.attr(text); }
|
||||
|
||||
// Toggle settings visibility
|
||||
const vegasEnabledCheckbox = document.getElementById('vegas_scroll_enabled');
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
hx-ext="json-enc"
|
||||
hx-headers='{"Content-Type": "application/json"}'
|
||||
hx-swap="none"
|
||||
hx-on:htmx:after-request="showNotification(event.detail.xhr.responseJSON?.message || 'Durations saved', event.detail.xhr.responseJSON?.status || 'success')"
|
||||
hx-on:htmx:after-request="showSaveResult(event.detail.xhr, 'Durations saved', 'Durations were not saved. Check your connection and try again.')"
|
||||
class="space-y-6"
|
||||
novalidate
|
||||
onsubmit="fixInvalidNumberInputs(this); return true;">
|
||||
|
||||
@@ -164,21 +164,6 @@ function initializeFontsTab() {
|
||||
// Reset retry counter on successful init
|
||||
initRetryCount = 0;
|
||||
|
||||
// showNotification is implemented by the notification widget
|
||||
// (widgets/notification.js). If it hasn't loaded, queue the message; the
|
||||
// widget shows queued messages as soon as it registers.
|
||||
if (typeof window.showNotification !== 'function') {
|
||||
window.showNotification = function(message, type = 'info') {
|
||||
const registry = window.LEDMatrixWidgets;
|
||||
const widget = registry && typeof registry.get === 'function' ? registry.get('notification') : null;
|
||||
if (widget && typeof widget.show === 'function') {
|
||||
return widget.show(message, typeof type === 'string' ? { type: type } : (type || {}));
|
||||
}
|
||||
(window.__pendingNotifications = window.__pendingNotifications || []).push([message, type]);
|
||||
console.log(`[${String((type && type.type) || type).toUpperCase()}]`, message);
|
||||
};
|
||||
}
|
||||
|
||||
debugLog('Initializing font management...');
|
||||
initializeFontManagement();
|
||||
|
||||
@@ -349,14 +334,7 @@ async function loadFontData() {
|
||||
// Show error states
|
||||
availableContainer.innerHTML = '<div class="text-red-400">Error loading font data. Please refresh the page.</div>';
|
||||
|
||||
// Only show notification if showNotification is available
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification('Error loading font configuration', 'error');
|
||||
} else if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('Error loading font configuration', 'error');
|
||||
} else {
|
||||
console.error('Error loading font configuration:', error.message);
|
||||
}
|
||||
showNotification('Error loading font configuration', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,24 +11,7 @@
|
||||
hx-ext="json-enc"
|
||||
hx-headers='{"Content-Type": "application/json"}'
|
||||
hx-swap="none"
|
||||
hx-on:htmx:after-request="
|
||||
var xhr = event.detail.xhr;
|
||||
var isSuccess = xhr.status >= 200 && xhr.status < 300;
|
||||
var message = '';
|
||||
var status = 'success';
|
||||
try {
|
||||
var data = JSON.parse(xhr.responseText);
|
||||
message = data.message || '';
|
||||
status = data.status || status;
|
||||
} catch (e) {}
|
||||
if (isSuccess) {
|
||||
message = message || 'Settings saved';
|
||||
} else {
|
||||
message = message || 'Failed to save settings';
|
||||
status = 'error';
|
||||
}
|
||||
showNotification(message, status);
|
||||
"
|
||||
hx-on:htmx:after-request="showSaveResult(event.detail.xhr, 'Settings saved', 'Failed to save settings')"
|
||||
class="space-y-6">
|
||||
{# Marks this post as the whole form, so an unchecked box saves as
|
||||
false. Without it the save endpoint treats absent keys as unchanged
|
||||
|
||||
@@ -126,8 +126,10 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Global variables - use window properties to avoid redeclaration errors with HTMX reloads
|
||||
// Initialize only if not already defined
|
||||
// Scoped: this script runs at global scope after every HTMX swap, and the
|
||||
// Cache partial has helpers with the same names (showError, escapeHtml). State
|
||||
// that must survive a reload of this partial lives on window._logs* instead.
|
||||
(function() {
|
||||
if (typeof window._logsEventSource === 'undefined') {
|
||||
window._logsEventSource = null;
|
||||
}
|
||||
@@ -624,9 +626,7 @@ function toggleRealtime() {
|
||||
|
||||
function refreshLogs() {
|
||||
loadLogs();
|
||||
if (typeof showNotification !== 'undefined') {
|
||||
showNotification('Logs refreshed', 'success');
|
||||
}
|
||||
showNotification('Logs refreshed', 'success');
|
||||
}
|
||||
|
||||
function clearLogs() {
|
||||
@@ -637,16 +637,12 @@ function clearLogs() {
|
||||
}
|
||||
showEmptyState();
|
||||
updateLogStats();
|
||||
if (typeof showNotification !== 'undefined') {
|
||||
showNotification('Logs cleared', 'info');
|
||||
}
|
||||
showNotification('Logs cleared', 'info');
|
||||
}
|
||||
|
||||
function downloadLogs() {
|
||||
if (window._filteredLogs.length === 0) {
|
||||
if (typeof showNotification !== 'undefined') {
|
||||
showNotification('No logs to download', 'warning');
|
||||
}
|
||||
showNotification('No logs to download', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -662,9 +658,7 @@ function downloadLogs() {
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
if (typeof showNotification !== 'undefined') {
|
||||
showNotification('Logs downloaded', 'success');
|
||||
}
|
||||
showNotification('Logs downloaded', 'success');
|
||||
}
|
||||
|
||||
// Track if user is near bottom for smart auto-scroll
|
||||
@@ -776,16 +770,7 @@ function showError(message) {
|
||||
}
|
||||
}
|
||||
|
||||
// Utility function to escape HTML. Quotes too: the result is interpolated into
|
||||
// quoted attribute values (option value=, title=), and the
|
||||
// textContent/innerHTML round-trip only escapes &, < and >.
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
function escapeHtml(text) { return window.LEDEscape.html(text); }
|
||||
|
||||
function refreshCurrentPluginStatus() {
|
||||
fetch('/api/v3/display/current-status')
|
||||
@@ -920,15 +905,13 @@ function clearPluginErrors() {
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (typeof showNotification !== 'undefined') {
|
||||
showNotification(data.status === 'success' ? 'Plugin errors cleared'
|
||||
: (data.message || 'Could not clear plugin errors'),
|
||||
data.status === 'success' ? 'success' : 'error');
|
||||
}
|
||||
showNotification(data.status === 'success' ? 'Plugin errors cleared'
|
||||
: (data.message || 'Could not clear plugin errors'),
|
||||
data.status === 'success' ? 'success' : 'error');
|
||||
refreshPluginErrors();
|
||||
})
|
||||
.catch(() => {
|
||||
if (typeof showNotification !== 'undefined') showNotification('Could not clear plugin errors', 'error');
|
||||
showNotification('Could not clear plugin errors', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -946,4 +929,5 @@ window.addEventListener('beforeunload', function() {
|
||||
window._pluginErrorsPollTimer = null;
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -260,18 +260,10 @@
|
||||
return 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
function escapeHtml(text) { return window.LEDEscape.html(text); }
|
||||
|
||||
function showError(message) {
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification(message, 'error');
|
||||
} else {
|
||||
alert(message);
|
||||
}
|
||||
showNotification(message, 'error');
|
||||
}
|
||||
|
||||
// Populate plugin filter
|
||||
|
||||
@@ -216,11 +216,7 @@
|
||||
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));
|
||||
var data = window.getApp();
|
||||
if (data) {
|
||||
data.activeTab = btn.dataset.tab;
|
||||
if ('mobileNavOpen' in data) data.mobileNavOpen = false;
|
||||
@@ -443,7 +439,7 @@
|
||||
<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); }"
|
||||
hx-on:htmx:after-request="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
|
||||
@@ -452,7 +448,7 @@
|
||||
<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); }"
|
||||
hx-on:htmx:after-request="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
|
||||
@@ -462,7 +458,7 @@
|
||||
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); }"
|
||||
hx-on:htmx:after-request="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
|
||||
@@ -472,7 +468,7 @@
|
||||
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); }"
|
||||
hx-on:htmx:after-request="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
|
||||
@@ -482,7 +478,7 @@
|
||||
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); }"
|
||||
hx-on:htmx:after-request="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
|
||||
@@ -491,7 +487,7 @@
|
||||
<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); }"
|
||||
hx-on:htmx:after-request="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
|
||||
@@ -500,7 +496,7 @@
|
||||
<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); }"
|
||||
hx-on:htmx:after-request="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
|
||||
|
||||
@@ -423,11 +423,10 @@
|
||||
{% if img.get('size') %}{{ (img.get('size') / 1024)|round }} KB{% endif %}
|
||||
{% if img.get('uploaded_at') %} • {{ img.get('uploaded_at') }}{% endif %}
|
||||
</p>
|
||||
{% if has_schedule %}
|
||||
<p class="text-xs text-blue-600 mt-1">
|
||||
<i class="fas fa-clock mr-1"></i>Scheduled
|
||||
{# file-upload.js rewrites .image-schedule-summary in place when the schedule is edited #}
|
||||
<p class="text-xs text-blue-600 mt-1 image-schedule-summary">
|
||||
<i class="fas fa-clock mr-1"></i>{{ 'Scheduled' if has_schedule else 'Always shown' }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 ml-4">
|
||||
|
||||
@@ -540,18 +540,6 @@
|
||||
background-color: #e5e7eb;
|
||||
}
|
||||
|
||||
/* CodeMirror editor styles */
|
||||
.CodeMirror {
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 14px;
|
||||
height: auto;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.CodeMirror.cm-error {
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
/* Plugin config view styles */
|
||||
.plugin-config-view {
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
fieldId: 'schedule'
|
||||
});
|
||||
|
||||
console.log('[Schedule] Schedule picker widget initialized');
|
||||
debugLog('[Schedule] Schedule picker widget initialized');
|
||||
}
|
||||
|
||||
// Handle form submission response
|
||||
@@ -165,28 +165,7 @@
|
||||
const type = response.status || 'info';
|
||||
|
||||
// Use global notification function if available
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification(message, type);
|
||||
} else {
|
||||
// Fallback notification
|
||||
const colors = {
|
||||
success: 'bg-green-500',
|
||||
error: 'bg-red-500',
|
||||
warning: 'bg-yellow-500',
|
||||
info: 'bg-blue-500'
|
||||
};
|
||||
|
||||
const notification = document.createElement('div');
|
||||
notification.className = 'fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 ' + (colors[type] || colors.info) + ' text-white';
|
||||
notification.textContent = message;
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(function() {
|
||||
notification.style.transition = 'opacity 0.5s';
|
||||
notification.style.opacity = '0';
|
||||
setTimeout(function() { notification.remove(); }, 500);
|
||||
}, 3000);
|
||||
}
|
||||
window.showNotification(message, type);
|
||||
};
|
||||
|
||||
// Initialize when DOM is ready or if already loaded
|
||||
@@ -265,7 +244,7 @@
|
||||
fieldId: 'dim_schedule'
|
||||
});
|
||||
|
||||
console.log('[DimSchedule] Dim schedule picker widget initialized');
|
||||
debugLog('[DimSchedule] Dim schedule picker widget initialized');
|
||||
}
|
||||
|
||||
// Handle dim schedule form submission response
|
||||
@@ -282,28 +261,7 @@
|
||||
const type = response.status || 'info';
|
||||
|
||||
// Use global notification function if available
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification(message, type);
|
||||
} else {
|
||||
// Fallback notification
|
||||
const colors = {
|
||||
success: 'bg-green-500',
|
||||
error: 'bg-red-500',
|
||||
warning: 'bg-yellow-500',
|
||||
info: 'bg-blue-500'
|
||||
};
|
||||
|
||||
const notification = document.createElement('div');
|
||||
notification.className = 'fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 ' + (colors[type] || colors.info) + ' text-white';
|
||||
notification.textContent = message;
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(function() {
|
||||
notification.style.transition = 'opacity 0.5s';
|
||||
notification.style.opacity = '0';
|
||||
setTimeout(function() { notification.remove(); }, 500);
|
||||
}, 3000);
|
||||
}
|
||||
window.showNotification(message, type);
|
||||
};
|
||||
|
||||
// Initialize when DOM is ready
|
||||
|
||||
@@ -344,21 +344,15 @@ function forceRenderStarlarkApp(appId) {
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.status === 'success') {
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification('Rendered successfully! ' + (data.frame_count || 0) + ' frame(s)', 'success');
|
||||
} else {
|
||||
alert('Rendered successfully! ' + (data.frame_count || 0) + ' frame(s)');
|
||||
}
|
||||
showNotification('Rendered successfully! ' + (data.frame_count || 0) + ' frame(s)', 'success');
|
||||
} else {
|
||||
var msg = 'Render failed: ' + (data.message || 'Unknown error');
|
||||
if (typeof showNotification === 'function') showNotification(msg, 'error');
|
||||
else alert(msg);
|
||||
showNotification(msg, 'error');
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
var msg = 'Render failed: ' + err.message;
|
||||
if (typeof showNotification === 'function') showNotification(msg, 'error');
|
||||
else alert(msg);
|
||||
showNotification(msg, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -371,22 +365,20 @@ function toggleStarlarkApp(appId, enabled) {
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.status === 'success') {
|
||||
if (typeof loadInstalledPlugins === 'function') loadInstalledPlugins();
|
||||
else if (typeof window.loadInstalledPlugins === 'function') window.loadInstalledPlugins();
|
||||
// A failed reload has already been reported by loadInstalledPlugins.
|
||||
window.pluginManager.loadInstalledPlugins(true).catch(function() {});
|
||||
var container = document.getElementById('plugin-config-starlark:' + appId);
|
||||
if (container && window.htmx) {
|
||||
htmx.ajax('GET', '/v3/partials/plugin-config/starlark:' + encodeURIComponent(appId), {target: container, swap: 'innerHTML'});
|
||||
}
|
||||
} else {
|
||||
var msg = 'Toggle failed: ' + (data.message || 'Unknown error');
|
||||
if (typeof showNotification === 'function') showNotification(msg, 'error');
|
||||
else alert(msg);
|
||||
showNotification(msg, 'error');
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
var msg = 'Toggle failed: ' + err.message;
|
||||
if (typeof showNotification === 'function') showNotification(msg, 'error');
|
||||
else alert(msg);
|
||||
showNotification(msg, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -436,8 +428,7 @@ function saveStarlarkConfig(appId) {
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.status === 'success') {
|
||||
if (typeof showNotification === 'function') showNotification('Configuration saved!', 'success');
|
||||
else alert('Configuration saved!');
|
||||
showNotification('Configuration saved!', 'success');
|
||||
// Reload partial to reflect updated status
|
||||
var container = document.getElementById('plugin-config-starlark:' + appId);
|
||||
if (container && window.htmx) {
|
||||
@@ -445,14 +436,12 @@ function saveStarlarkConfig(appId) {
|
||||
}
|
||||
} else {
|
||||
var msg = 'Save failed: ' + (data.message || 'Unknown error');
|
||||
if (typeof showNotification === 'function') showNotification(msg, 'error');
|
||||
else alert(msg);
|
||||
showNotification(msg, 'error');
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
var msg = 'Save failed: ' + err.message;
|
||||
if (typeof showNotification === 'function') showNotification(msg, 'error');
|
||||
else alert(msg);
|
||||
showNotification(msg, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -435,9 +435,7 @@
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
return String(s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
function escHtml(s) { return window.LEDEscape.html(s); }
|
||||
|
||||
// ── main action dispatcher ────────────────────────────────────────────────
|
||||
|
||||
@@ -910,11 +908,7 @@
|
||||
};
|
||||
|
||||
// ── plugin health panel ──────────────────────────────────────────────────
|
||||
function phEscape(s) {
|
||||
return String(s).replace(/[&<>"']/g, function (c) {
|
||||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
||||
});
|
||||
}
|
||||
function phEscape(s) { return window.LEDEscape.html(s); }
|
||||
function phFmtSecs(v) {
|
||||
if (typeof v !== 'number' || !isFinite(v)) return '—';
|
||||
return v.toFixed(3) + 's';
|
||||
|
||||
Reference in New Issue
Block a user