From a46a908c7931bff93511e0a8fa2637e38fa88189 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:25:24 -0400 Subject: [PATCH] refactor(web): remove the two Fonts-tab panels that reported invented data "Element Font Overrides" let a user configure an override, showed a success toast, and changed nothing. All three endpoints behind it were stubs -- GET returned a hardcoded {}, POST and DELETE returned success without calling anything -- each marked "This would integrate with the actual font system". Wiring them to FontManager would not have fixed it. The machinery there is real (_load_overrides/_save_overrides persist config/font_overrides.json, resolve_font applies them, and the countdown plugin genuinely consumes it), but the panel's element dropdown offered eleven invented keys -- nfl.live.score, clock.time, weather.current -- that no plugin has ever read. An override saved against one of those would have persisted correctly and still done nothing. "Detected Manager Fonts" goes for the same reason. It claimed to show "fonts currently in use by managers (auto-detected)"; its own comment said "we'll simulate this", and it listed every font in the catalog with a hardcoded usage_count of 1 -- the panel beside it, with fabricated numbers attached. Per-element font choice now lives in each plugin's own config editor, against the elements that plugin actually has, and covers size, colour, offsets, visibility, alignment and scale rather than family and size. Kept: the font library (upload, preview, delete), which works, and /fonts/tokens, which is a stub but genuinely feeds the preview's size dropdown. FontManager's override methods are untouched -- countdown uses them. Verified in a browser with the tab's JS running: no console errors, 35 fonts listed, upload and preview intact. Removing the panel meant unwiring it from populateFontSelects too, which would otherwise have bailed out early on the missing select and left the preview dropdown empty. Co-Authored-By: Claude Opus 5 --- test/test_web_api.py | 37 +- web_interface/blueprints/api_v3.py | 36 -- .../templates/v3/partials/fonts.html | 362 +----------------- 3 files changed, 28 insertions(+), 407 deletions(-) diff --git a/test/test_web_api.py b/test/test_web_api.py index 8cadf922..340b4b16 100644 --- a/test/test_web_api.py +++ b/test/test_web_api.py @@ -789,28 +789,21 @@ class TestFontsAPI: data = json.loads(response.data) assert 'tokens' in data.get('data', {}) or 'data' in data - def test_get_fonts_overrides(self, client): - """Test getting font overrides.""" - response = client.get('/api/v3/fonts/overrides') - - assert response.status_code == 200 - data = json.loads(response.data) - assert 'overrides' in data.get('data', {}) or 'data' in data - - def test_save_fonts_overrides(self, client): - """Test saving font overrides.""" - request_data = { - 'weather': 'small', - 'clock': 'regular' - } - - response = client.post( - '/api/v3/fonts/overrides', - data=json.dumps(request_data), - content_type='application/json' - ) - - assert response.status_code == 200 + def test_the_font_override_endpoints_are_gone(self, client): + """They reported success without doing anything: GET returned a + hardcoded {}, POST and DELETE saved and deleted nothing. The panel + they served offered eleven element keys -- nfl.live.score, + clock.time -- that no plugin has ever read, so wiring them to the + real FontManager methods would still have changed nothing on the + panel. Per-element font choice lives in each plugin's own config + editor now, against the elements that plugin actually has.""" + # 405, not 404: the path still matches DELETE /fonts/, + # which now reads "overrides" as a font name. Nothing is routed to a + # handler for GET or POST, which is what matters here. + assert client.get('/api/v3/fonts/overrides').status_code != 200 + assert client.post('/api/v3/fonts/overrides', + data=json.dumps({}), + content_type='application/json').status_code != 200 class TestAPIErrorHandling: diff --git a/web_interface/blueprints/api_v3.py b/web_interface/blueprints/api_v3.py index 054feb7f..92da3c66 100644 --- a/web_interface/blueprints/api_v3.py +++ b/web_interface/blueprints/api_v3.py @@ -6809,42 +6809,6 @@ def get_font_tokens(): logger.error('Unhandled exception', exc_info=True) return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details', 'details': describe_exception(e)}), 500 -@api_v3.route('/fonts/overrides', methods=['GET']) -def get_fonts_overrides(): - """Get font overrides""" - try: - # This would integrate with the actual font system - # For now, return empty overrides - overrides = {} - return jsonify({'status': 'success', 'data': {'overrides': overrides}}) - except Exception as e: - logger.error('Unhandled exception', exc_info=True) - return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details', 'details': describe_exception(e)}), 500 - -@api_v3.route('/fonts/overrides', methods=['POST']) -def save_fonts_overrides(): - """Save font overrides""" - try: - data = request.get_json(silent=True) - if not data: - return jsonify({'status': 'error', 'message': 'No data provided'}), 400 - - # This would integrate with the actual font system - return jsonify({'status': 'success', 'message': 'Font overrides saved'}) - except Exception as e: - logger.error('Unhandled exception', exc_info=True) - return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details', 'details': describe_exception(e)}), 500 - -@api_v3.route('/fonts/overrides/', methods=['DELETE']) -def delete_font_override(element_key): - """Delete font override""" - try: - # This would integrate with the actual font system - return jsonify({'status': 'success', 'message': f'Font override for {element_key} deleted'}) - except Exception as e: - logger.error('Unhandled exception', exc_info=True) - return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details', 'details': describe_exception(e)}), 500 - @api_v3.route('/fonts/upload', methods=['POST']) def upload_font(): """Upload font file""" diff --git a/web_interface/templates/v3/partials/fonts.html b/web_interface/templates/v3/partials/fonts.html index fec1d16a..29038eba 100644 --- a/web_interface/templates/v3/partials/fonts.html +++ b/web_interface/templates/v3/partials/fonts.html @@ -1,20 +1,11 @@

Font Management

-

Manage custom fonts, overrides, and system font configuration for your LED matrix display.

+

Upload, preview and manage the fonts available to every plugin.

-
- -
-

Detected Manager Fonts

-
-
Loading...
-
-

Fonts currently in use by managers (auto-detected)

-
- +

Available Font Families

@@ -78,73 +69,6 @@
- -
-

Element Font Overrides

-

Override fonts for specific display elements. Changes take effect immediately.

- - -
-
- - -
- -
- - -
- -
- - -
- -
- -
-
- - -
-

Current Overrides

-
- -
No font overrides configured
-
-
-
-

Font Preview

@@ -206,13 +130,11 @@ // Initialize global variables on window object window.fontCatalog = window.fontCatalog || {}; window.fontTokens = window.fontTokens || {}; - window.fontOverrides = window.fontOverrides || {}; window.selectedFontFiles = window.selectedFontFiles || []; // Create references that can be reassigned var fontCatalog = window.fontCatalog; var fontTokens = window.fontTokens; - var fontOverrides = window.fontOverrides; var selectedFontFiles = window.selectedFontFiles; // Retry counter for initialization @@ -222,17 +144,15 @@ function initializeFontsTab() { // Allow re-initialization on each HTMX content swap // The window._fontsScriptLoaded guard prevents function redeclaration - const detectedEl = document.getElementById('detected-fonts'); const availableEl = document.getElementById('available-fonts'); - if (!detectedEl || !availableEl) { + if (!availableEl) { initRetryCount++; if (initRetryCount >= MAX_INIT_RETRIES) { console.error('Fonts tab elements not found after max retries, giving up'); return; } console.log('Fonts tab elements not found, retrying...', { - detectedFonts: !!detectedEl, availableFonts: !!availableEl, attempt: initRetryCount }); @@ -259,7 +179,6 @@ function initializeFontsTab() { const fontFileInput = document.getElementById('font-file-input'); const uploadFontsBtn = document.getElementById('upload-fonts-btn'); const cancelUploadBtn = document.getElementById('cancel-upload-btn'); - const addOverrideBtn = document.getElementById('add-override-btn'); const updatePreviewBtn = document.getElementById('update-preview-btn'); if (uploadDropzone && fontFileInput) { @@ -280,9 +199,6 @@ function initializeFontsTab() { cancelUploadBtn.addEventListener('click', cancelFontUpload); } - if (addOverrideBtn) { - addOverrideBtn.addEventListener('click', addFontOverride); - } if (updatePreviewBtn) { updatePreviewBtn.addEventListener('click', updateFontPreview); @@ -319,9 +235,8 @@ window.initializeFontsTab = initializeFontsTab; // Function to initialize when fonts content is loaded function tryInitializeFontsTab() { const fontsContent = document.getElementById('fonts-content'); - const detectedFonts = document.getElementById('detected-fonts'); - if (fontsContent && detectedFonts) { + if (fontsContent) { console.log('Fonts content detected, initializing...'); setTimeout(() => { initializeFontsTab(); @@ -355,7 +270,6 @@ async function initializeFontManagement() { try { await loadFontData(); populateFontSelects(); - displayCurrentOverrides(); updateFontPreview(); initializeFontUpload(); } catch (error) { @@ -365,44 +279,39 @@ async function initializeFontManagement() { } async function loadFontData() { - const detectedContainer = document.getElementById('detected-fonts'); const availableContainer = document.getElementById('available-fonts'); // Ensure containers exist before proceeding - if (!detectedContainer || !availableContainer) { + if (!availableContainer) { console.error('Font containers not found, cannot load font data'); return; } // Show loading states - detectedContainer.innerHTML = '
Loading font data...
'; availableContainer.innerHTML = '
Loading font data...
'; try { // Use absolute URLs to ensure they work when loaded via HTMX - const [catalogRes, tokensRes, overridesRes] = await Promise.all([ + const [catalogRes, tokensRes] = await Promise.all([ fetch(`/api/v3/fonts/catalog`), - fetch(`/api/v3/fonts/tokens`), - fetch(`/api/v3/fonts/overrides`) + fetch(`/api/v3/fonts/tokens`) ]); // Check if all responses are successful - if (!catalogRes.ok || !tokensRes.ok || !overridesRes.ok) { - const statusText = `HTTP ${catalogRes.status}/${tokensRes.status}/${overridesRes.status}`; + if (!catalogRes.ok || !tokensRes.ok) { + const statusText = `HTTP ${catalogRes.status}/${tokensRes.status}`; console.error('Font API error:', statusText); throw new Error(`Failed to load font data: ${statusText}`); } const catalogData = await catalogRes.json(); const tokensData = await tokensRes.json(); - const overridesData = await overridesRes.json(); // Validate response structure - if (!catalogData || !catalogData.data || !tokensData || !tokensData.data || !overridesData || !overridesData.data) { + if (!catalogData || !catalogData.data || !tokensData || !tokensData.data) { console.error('Invalid font API response structure:', { catalog: !!catalogData?.data, - tokens: !!tokensData?.data, - overrides: !!overridesData?.data + tokens: !!tokensData?.data }); throw new Error('Invalid response format from font API'); } @@ -410,27 +319,22 @@ async function loadFontData() { // Update both window properties and local references window.fontCatalog = catalogData.data.catalog || {}; window.fontTokens = tokensData.data.tokens || {}; - window.fontOverrides = overridesData.data.overrides || {}; // Update local variable references fontCatalog = window.fontCatalog; fontTokens = window.fontTokens; - fontOverrides = window.fontOverrides; // Update displays - updateDetectedFontsDisplay(); updateAvailableFontsDisplay(); console.log('Font data loaded successfully', { catalogSize: Object.keys(fontCatalog).length, - tokensSize: Object.keys(fontTokens).length, - overridesSize: Object.keys(fontOverrides).length + tokensSize: Object.keys(fontTokens).length }); } catch (error) { console.error('Error loading font data:', error); // Show error states - detectedContainer.innerHTML = '
Error loading font data. Please refresh the page.
'; availableContainer.innerHTML = '
Error loading font data. Please refresh the page.
'; // Only show notification if showNotification is available @@ -444,52 +348,6 @@ async function loadFontData() { } } -function updateDetectedFontsDisplay() { - const container = document.getElementById('detected-fonts'); - if (!container) return; - - // In a real implementation, this would collect font usage from all active managers - // For now, we'll simulate this by analyzing the font overrides and catalog - const detectedFonts = {}; - - // Check font overrides for active elements - for (const [elementKey, override] of Object.entries(fontOverrides)) { - if (override.family) { - detectedFonts[elementKey] = { - family: override.family, - size_px: override.size_px || 8, - usage_count: 1, // Would be actual usage count in real implementation - source: 'override' - }; - } - } - - // Check font catalog for commonly used fonts - for (const [fontKey, fontPath] of Object.entries(fontCatalog)) { - // Add some commonly used system fonts if not already in overrides - if (!detectedFonts[fontKey]) { - detectedFonts[fontKey] = { - family: fontKey, - size_px: 8, - usage_count: 1, - source: 'system' - }; - } - } - - if (Object.keys(detectedFonts).length === 0) { - container.innerHTML = '
No fonts detected yet (managers will register fonts when they render)
'; - return; - } - - const lines = []; - for (const [elementKey, fontInfo] of Object.entries(detectedFonts)) { - const sourceStr = fontInfo.source === 'override' ? ' [OVERRIDE]' : ' [SYSTEM]'; - lines.push(`${elementKey}: ${fontInfo.family}@${fontInfo.size_px}px (used ${fontInfo.usage_count}x)${sourceStr}`); - } - container.textContent = lines.join('\n'); -} - function updateAvailableFontsDisplay() { const container = document.getElementById('available-fonts'); if (!container) return; @@ -594,10 +452,9 @@ async function deleteFont(fontFamily) { function populateFontSelects() { // Populate font family dropdowns from catalog - const overrideSelect = document.getElementById('override-family'); const previewSelect = document.getElementById('preview-family'); - if (!overrideSelect || !previewSelect) return; + if (!previewSelect) return; // Get font entries sorted by display name const fontEntries = Object.entries(fontCatalog).map(([key, info]) => { @@ -608,13 +465,6 @@ function populateFontSelects() { }).sort((a, b) => a.displayName.localeCompare(b.displayName)); // Build options using DOM APIs to prevent XSS - // Clear and add default option for override select - overrideSelect.innerHTML = ''; - const defaultOption = document.createElement('option'); - defaultOption.value = ''; - defaultOption.textContent = 'Use default'; - overrideSelect.appendChild(defaultOption); - // Clear preview select previewSelect.innerHTML = ''; @@ -622,11 +472,6 @@ function populateFontSelects() { fontEntries.forEach(font => { const typeLabel = font.fontType ? ` (${font.fontType})` : ''; - const overrideOpt = document.createElement('option'); - overrideOpt.value = font.filename; - overrideOpt.textContent = font.displayName + typeLabel; - overrideSelect.appendChild(overrideOpt); - const previewOpt = document.createElement('option'); previewOpt.value = font.filename; previewOpt.textContent = font.displayName + typeLabel; @@ -641,187 +486,6 @@ function populateFontSelects() { console.log(`Populated font selects with ${fontEntries.length} fonts`); } -async function addFontOverride() { - const element = document.getElementById('override-element').value; - const family = document.getElementById('override-family').value; - const sizeToken = document.getElementById('override-size').value; - - if (!element) { - showNotification('Please select an element', 'warning'); - return; - } - - if (!family && !sizeToken) { - showNotification('Please specify at least a font family or size', 'warning'); - return; - } - - try { - const overrideData = {}; - if (family) overrideData.family = family; - if (sizeToken) { - const sizePx = fontTokens[sizeToken]; - if (sizePx) overrideData.size_px = sizePx; - } - - const response = await fetch(`/api/v3/fonts/overrides`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - [element]: overrideData - }) - }); - - if (!response.ok) { - const text = await response.text(); - let message; - try { - const errorData = JSON.parse(text); - message = errorData.message || `Server error: ${response.status}`; - } catch { - message = `Server error: ${response.status}`; - } - showNotification('Error adding font override: ' + message, 'error'); - return; - } - - const data = await response.json(); - if (data.status === 'success') { - showNotification('Font override added successfully', 'success'); - await loadFontData(); - displayCurrentOverrides(); - // Clear form - document.getElementById('override-element').value = ''; - document.getElementById('override-family').value = ''; - document.getElementById('override-size').value = ''; - } else { - showNotification('Error adding font override: ' + data.message, 'error'); - } - } catch (error) { - console.error('Error adding font override:', error); - showNotification('Error adding font override: ' + error, 'error'); - } -} - -async function deleteFontOverride(elementKey) { - if (!confirm(`Are you sure you want to remove the font override for "${elementKey}"?`)) { - return; - } - - try { - const response = await fetch(`/api/v3/fonts/overrides/${elementKey}`, { - method: 'DELETE' - }); - - if (!response.ok) { - const text = await response.text(); - let message; - try { - const errorData = JSON.parse(text); - message = errorData.message || `Server error: ${response.status}`; - } catch { - message = `Server error: ${response.status}`; - } - showNotification('Error removing font override: ' + message, 'error'); - return; - } - - const data = await response.json(); - if (data.status === 'success') { - showNotification('Font override removed successfully', 'success'); - await loadFontData(); - displayCurrentOverrides(); - } else { - showNotification('Error removing font override: ' + data.message, 'error'); - } - } catch (error) { - console.error('Error deleting font override:', error); - showNotification('Error removing font override: ' + error, 'error'); - } -} - -function displayCurrentOverrides() { - const container = document.getElementById('overrides-list'); - if (!container) return; - - if (Object.keys(fontOverrides).length === 0) { - container.innerHTML = '
No font overrides configured
'; - return; - } - - // Build list using DOM APIs to prevent XSS - container.innerHTML = ''; - Object.entries(fontOverrides).forEach(([elementKey, override]) => { - const elementName = getElementDisplayName(elementKey); - const settings = []; - - if (override.family) { - const familyName = getFontDisplayName(override.family); - settings.push(`Family: ${familyName}`); - } - - if (override.size_px) { - settings.push(`Size: ${override.size_px}px`); - } - - const row = document.createElement('div'); - row.className = 'flex items-center justify-between p-3 bg-white rounded border'; - - const infoDiv = document.createElement('div'); - - const nameDiv = document.createElement('div'); - nameDiv.className = 'font-medium text-gray-900'; - nameDiv.textContent = elementName; - - const settingsDiv = document.createElement('div'); - settingsDiv.className = 'text-sm text-gray-600'; - settingsDiv.textContent = settings.join(', '); - - infoDiv.appendChild(nameDiv); - infoDiv.appendChild(settingsDiv); - - const deleteBtn = document.createElement('button'); - deleteBtn.className = 'btn bg-red-600 hover:bg-red-700 text-white px-3 py-1 text-sm'; - const trashIcon = document.createElement('i'); - trashIcon.className = 'fas fa-trash mr-1'; - deleteBtn.appendChild(trashIcon); - deleteBtn.appendChild(document.createTextNode('Remove')); - deleteBtn.dataset.elementKey = elementKey; - deleteBtn.addEventListener('click', function() { - deleteFontOverride(this.dataset.elementKey); - }); - - row.appendChild(infoDiv); - row.appendChild(deleteBtn); - container.appendChild(row); - }); -} - -function getElementDisplayName(elementKey) { - const names = { - 'nfl.live.score': 'NFL Live Score', - 'nfl.live.time': 'NFL Live Time', - 'nfl.live.team': 'NFL Live Team', - 'mlb.live.score': 'MLB Live Score', - 'nhl.live.score': 'NHL Live Score', - 'nba.live.score': 'NBA Live Score', - 'clock.time': 'Clock Time', - 'clock.date': 'Clock Date', - 'weather.current': 'Weather Current', - 'weather.forecast': 'Weather Forecast' - }; - return names[elementKey] || elementKey; -} - -function getFontDisplayName(fontKey) { - const names = { - 'press_start': 'Press Start 2P', - 'four_by_six': '4x6 Font', - 'matrix_light_6': 'Matrix Light 6' - }; - return names[fontKey] || fontKey; -} - async function updateFontPreview() { const previewImage = document.getElementById('font-preview-image'); const loadingText = document.getElementById('font-preview-loading');