mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 22:35:08 +00:00
feat(web): ES-module page lifecycle and one schema field model (stage 1) (#703)
Adds a native ES-module layer to the web UI (core/boot, registry, api, facade; window.LEDMatrix as the one global), a page lifecycle that the Cache tab is converted to as the reference, text/javascript serving and revalidation for unversioned module requests, and src/plugin_system/field_model.py with a parity test against the render_field macro. Also: the cache page toggles its grey 'Not configured' style instead of only adding it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -228,6 +228,9 @@
|
||||
el.setAttribute('data-loaded', 'true');
|
||||
if (typeof htmx !== 'undefined') htmx.process(el);
|
||||
if (window.Alpine) window.Alpine.initTree(el);
|
||||
// No htmx swap events here, so start converted pages
|
||||
// (data-page roots) directly; see js/core/registry.js.
|
||||
if (window.LEDMatrix && window.LEDMatrix.pages) window.LEDMatrix.pages.refresh();
|
||||
if (id === 'plugins-content' && window.initPluginsPage) {
|
||||
if (window.pluginManager) {
|
||||
window.pluginManager.initialized = false;
|
||||
@@ -948,7 +951,15 @@
|
||||
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') }}" defer></script>
|
||||
|
||||
|
||||
<!-- ES modules: window.LEDMatrix and the page lifecycle (js/core/), and
|
||||
the converted tab pages (js/pages/), loaded on first use. Module
|
||||
scripts are deferred like the scripts above and run after them; they
|
||||
rely on none of them at load time, and a tab partial that arrived
|
||||
first is started when the registry starts. Served as-is, no bundler:
|
||||
see docs/WEB_FRONTEND_ARCHITECTURE.md. -->
|
||||
<script type="module" src="{{ url_for('static', filename='v3/js/core/boot.js') }}"></script>
|
||||
|
||||
<!-- Custom feeds table helpers live in js/widgets/custom-feeds.js (the
|
||||
deferred widget's window assignments always shadowed the inline
|
||||
copies that used to sit here, so the duplicates were removed) -->
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
{# No inline script: static/v3/js/pages/cache.js runs this page. The page
|
||||
registry (static/v3/js/core/registry.js) starts it when this root appears
|
||||
and stops it when the partial is swapped away. #}
|
||||
<div class="bg-white rounded-lg shadow p-6" data-page="cache">
|
||||
<div class="border-b border-gray-200 pb-4 mb-6">
|
||||
<h2 class="text-lg font-semibold text-gray-900">Cache Management</h2>
|
||||
<p class="mt-1 text-sm text-gray-600">View and manage cached API responses. Cache files help reduce API calls and improve performance.</p>
|
||||
@@ -11,7 +14,7 @@
|
||||
<p class="text-sm font-medium text-blue-900">Cache Directory</p>
|
||||
<p id="cache-dir" class="text-sm text-blue-700 font-mono mt-1">Loading...</p>
|
||||
</div>
|
||||
<button id="refresh-cache-btn" class="btn bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded text-sm">
|
||||
<button type="button" id="refresh-cache-btn" class="btn bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded text-sm">
|
||||
<i class="fas fa-sync-alt mr-2"></i>Refresh
|
||||
</button>
|
||||
</div>
|
||||
@@ -54,167 +57,3 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 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();
|
||||
document.getElementById('refresh-cache-btn').addEventListener('click', loadCacheFiles);
|
||||
|
||||
function loadCacheFiles() {
|
||||
const tbody = document.getElementById('cache-files-tbody');
|
||||
const emptyState = document.getElementById('cache-empty');
|
||||
const errorState = document.getElementById('cache-error');
|
||||
const errorMessage = document.getElementById('cache-error-message');
|
||||
|
||||
// Show loading state
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="5" class="px-6 py-8 text-center text-gray-500">
|
||||
<i class="fas fa-spinner fa-spin text-2xl mb-2"></i>
|
||||
<p>Loading cache files...</p>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
emptyState.classList.add('hidden');
|
||||
errorState.classList.add('hidden');
|
||||
|
||||
fetch('/api/v3/cache/list')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
displayCacheFiles(data.data);
|
||||
updateCacheInfo(data.data.cache_dir);
|
||||
} else {
|
||||
showError(data.message || 'Failed to load cache files');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showError('Error loading cache files: ' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
function displayCacheFiles(data) {
|
||||
const tbody = document.getElementById('cache-files-tbody');
|
||||
const emptyState = document.getElementById('cache-empty');
|
||||
const errorState = document.getElementById('cache-error');
|
||||
|
||||
errorState.classList.add('hidden');
|
||||
|
||||
if (!data.cache_files || data.cache_files.length === 0) {
|
||||
tbody.innerHTML = '';
|
||||
emptyState.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
emptyState.classList.add('hidden');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
data.cache_files.forEach(cacheFile => {
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'hover:bg-gray-50';
|
||||
|
||||
// Format modified time
|
||||
const modifiedDate = new Date(cacheFile.modified_datetime);
|
||||
const modifiedStr = modifiedDate.toLocaleString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
|
||||
// Age color coding
|
||||
let ageClass = 'text-gray-600';
|
||||
if (cacheFile.age_seconds < 300) { // Less than 5 minutes
|
||||
ageClass = 'text-green-600 font-medium';
|
||||
} else if (cacheFile.age_seconds < 3600) { // Less than 1 hour
|
||||
ageClass = 'text-yellow-600';
|
||||
} else { // Older than 1 hour
|
||||
ageClass = 'text-red-600';
|
||||
}
|
||||
|
||||
row.innerHTML = `
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-gray-900 font-mono">${escapeHtml(cacheFile.key)}</div>
|
||||
<div class="text-xs text-gray-500">${escapeHtml(cacheFile.filename)}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="text-sm ${ageClass}">${escapeHtml(cacheFile.age_display)}</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="text-sm text-gray-600">${escapeHtml(cacheFile.size_display)}</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="text-sm text-gray-600">${escapeHtml(modifiedStr)}</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<button onclick="deleteCacheFile(this.dataset.cacheKey)"
|
||||
data-cache-key="${escapeHtml(cacheFile.key)}"
|
||||
class="text-red-600 hover:text-red-900 px-3 py-1 rounded hover:bg-red-50 transition-colors"
|
||||
title="Delete cache file">
|
||||
<i class="fas fa-trash mr-1"></i>Delete
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function updateCacheInfo(cacheDir) {
|
||||
const cacheDirEl = document.getElementById('cache-dir');
|
||||
if (cacheDir) {
|
||||
cacheDirEl.textContent = cacheDir;
|
||||
} else {
|
||||
cacheDirEl.textContent = 'Not configured';
|
||||
cacheDirEl.classList.add('text-gray-500');
|
||||
}
|
||||
}
|
||||
|
||||
function deleteCacheFile(key) {
|
||||
if (!confirm(`Are you sure you want to delete the cache file for "${key}"?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/api/v3/cache/delete', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ key: key })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
showNotification(data.message || 'Cache file deleted successfully', 'success');
|
||||
// Reload cache files list
|
||||
loadCacheFiles();
|
||||
} else {
|
||||
showNotification(data.message || 'Failed to delete cache file', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showNotification('Error deleting cache file: ' + error.message, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const tbody = document.getElementById('cache-files-tbody');
|
||||
const errorState = document.getElementById('cache-error');
|
||||
const errorMessage = document.getElementById('cache-error-message');
|
||||
const emptyState = document.getElementById('cache-empty');
|
||||
|
||||
tbody.innerHTML = '';
|
||||
emptyState.classList.add('hidden');
|
||||
errorState.classList.remove('hidden');
|
||||
errorMessage.textContent = message;
|
||||
}
|
||||
|
||||
function escapeHtml(text) { return window.LEDEscape.html(text); }
|
||||
|
||||
window.deleteCacheFile = deleteCacheFile;
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user