Skip to content

Conversation

@ChuckBuilds
Copy link
Owner

@ChuckBuilds ChuckBuilds commented Dec 28, 2025

Description

Fixes multiple critical errors in the web interface:

  1. Font catalog display error where path.startsWith fails (path is object, not string)
  2. JavaScript variable redeclaration errors when HTMX reloads content
  3. Config save API endpoints returning 500 errors due to missing CSRF tokens
  4. System action buttons not working due to CSRF protection

Changes Made

Font Display Fixes

  • Fixed updateAvailableFontsDisplay() to handle font catalog entries as objects instead of strings
  • Wrapped entire fonts script in IIFE to prevent variable redeclaration on HTMX content reload
  • Used window object to persist global variables across script reloads

Config API Error Handling

  • Updated save_main_config to use error_response() helper instead of simple jsonify()
  • Improved save_raw_main_config error handling to use error_response() consistently
  • Added proper error codes and traceback details to API responses

CSRF Protection

  • Removed CSRF protection entirely for local-only application
  • CSRF was designed for internet-facing apps; for local-only Raspberry Pi app, all endpoints were exempted anyway
  • Cleaner approach than exempting everything while pretending to have protection

Files Changed

  • web_interface/templates/v3/partials/fonts.html - Font display and script reload fixes
  • web_interface/blueprints/api_v3.py - Improved error handling
  • web_interface/app.py - Removed CSRF protection

Testing

  • ✅ Font catalog displays correctly without errors
  • ✅ Config save endpoints work without CSRF errors
  • ✅ System action buttons (restart display, restart Pi) work correctly
  • ✅ No JavaScript redeclaration errors when switching tabs

Type of Change

  • Bug fix (non-breaking change)
  • Refactoring (code cleanup)

Checklist

  • Code follows project style guidelines
  • Self-review completed
  • Comments added for complex logic
  • Error handling implemented
  • Logging added where appropriate

Summary by CodeRabbit

  • Bug Fixes

    • More consistent API error responses and status codes across endpoints.
    • Improved fonts tab state preservation and reliable reinitialization during dynamic content reloads.
  • New Features

    • Fonts UI can be reinitialized after content swaps to restore upload/preview interactions.
  • Chores

    • Simplified request/security configuration and request handling setup.

✏️ Tip: You can customize this high-level summary in your review settings.

Chuck added 7 commits December 27, 2025 18:36
- Fix font catalog display error where path.startsWith fails
  (path is object, not string)
- Update save_main_config to use error_response() helper
- Improve save_raw_main_config error handling consistency
- Add proper error codes and traceback details to API responses
- Use window object to store global font variables
- Check if script has already loaded before declaring variables
- Update both window properties and local references on assignment
- Fixes 'Identifier fontCatalog has already been declared' error
- Wrap entire script in IIFE that only runs once
- Check if script already loaded before declaring variables/functions
- Expose initializeFontsTab to window for re-initialization
- Prevents 'Identifier has already been declared' errors on HTMX reload
- Exempt save_raw_main_config, save_raw_secrets_config, and save_main_config from CSRF
- These endpoints are called via fetch from JavaScript and don't include CSRF tokens
- Fixes 500 error when saving config via raw JSON editor
- Exempt execute_system_action from CSRF
- Fixes 500 error when using system action buttons (restart display, restart Pi, etc.)
- These endpoints are called via HTMX and don't include CSRF tokens
- Add before_request handler to exempt all api_v3.* endpoints
- All API endpoints are programmatic (HTMX/fetch) and don't include CSRF tokens
- Prevents future CSRF errors on any API endpoint
- Cleaner than exempting individual endpoints
- CSRF is designed for internet-facing apps to prevent cross-site attacks
- For local-only Raspberry Pi app, threat model is different
- All endpoints were exempted anyway, so it wasn't protecting anything
- Forms use HTMX without CSRF tokens
- If exposing to internet later, can re-enable with proper token implementation
@coderabbitai
Copy link

coderabbitai bot commented Dec 28, 2025

📝 Walkthrough

Walkthrough

Removes CSRF setup from the Flask app, standardizes API v3 error handling to use a unified error_response (including ConfigError context), and refactors the fonts partial to be HTMX-idempotent by moving font state onto window and exposing initializeFontsTab().

Changes

Cohort / File(s) Summary
App security/config
web_interface/app.py
Removed CSRFProtect initialization, exemption decorator, and request checks; set module-level csrf = None. Retains flask-limiter rate limiting; added note about api_v3 blueprint exemption.
API error handling
web_interface/blueprints/api_v3.py
Replaced ad-hoc JSON error responses with error_response usage. ConfigError now includes config_path when present and returns ErrorCode.CONFIG_SAVE_FAILED (500) with structured error details/traceback; other errors map to consistent error codes.
Frontend HTMX state & initialization
web_interface/templates/v3/partials/fonts.html
Added IIFE load-guard (window._fontsScriptLoaded) to avoid redeclaration on HTMX reloads. Moved font state to window (fontCatalog, fontTokens, fontOverrides, selectedFontFiles), exposes window.initializeFontsTab(), adds htmx:afterSettle and DOMContentLoaded hooks, showNotification fallback, and supports string/object font path entries.

Sequence Diagram(s)

(silently skipped)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Fix/fonts loading error #148 — Overlapping edits to web_interface/templates/v3/partials/fonts.html introducing idempotent initialization and HTMX-resilient hooks.

Poem

🐰 I hopped through lines and cleared the maze,

CSRF footprints gone in quieter ways,
Errors sorted in tidy rows,
Fonts reawaken when HTMX flows,
A rabbit hums and softly nibbles praise.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main changes: fixing font display errors and resolving CSRF issues in the config API, which are the primary objectives across the three modified files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/font-display-and-config-api-errors

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web_interface/templates/v3/partials/fonts.html (1)

410-415: baseUrl is undefined in multiple fetch calls (runtime ReferenceError)

baseUrl is declared as a local const inside loadFontData, but it’s used in other functions (addFontOverride, deleteFontOverride, uploadSelectedFonts) that don’t see that binding. Calling any of these will throw ReferenceError: baseUrl is not defined, breaking overrides and uploads.

Define baseUrl once at the top of the IIFE and reuse it everywhere, removing the inner const:

Proposed fix
 (function() {
+    // Base URL for API calls (works under HTMX reloads)
+    const baseUrl = window.location.origin;
@@
-        // Use absolute URLs to ensure they work when loaded via HTMX
-        const baseUrl = window.location.origin;
         const [catalogRes, tokensRes, overridesRes] = await Promise.all([
             fetch(`${baseUrl}/api/v3/fonts/catalog`),
             fetch(`${baseUrl}/api/v3/fonts/tokens`),
             fetch(`${baseUrl}/api/v3/fonts/overrides`)
         ]);

No other changes are needed to the fetch(${baseUrl}/...` calls below.

Also applies to: 566-572, 598-600, 789-792

🧹 Nitpick comments (3)
web_interface/blueprints/api_v3.py (1)

630-640: Standardized error response looks good; tweak f-string per Ruff

The switch to error_response with ErrorCode.CONFIG_SAVE_FAILED and traceback details is consistent and solid. The only nit is f"Error saving configuration: {str(e)}" inside the f-string, which triggers Ruff RUF010; you can drop the str() and rely on implicit conversion (or use !s):

-        error_msg = f"Error saving config: {str(e)}\n{traceback.format_exc()}"
+        error_msg = f"Error saving config: {e}\n{traceback.format_exc()}"
@@
-        return error_response(
-            ErrorCode.CONFIG_SAVE_FAILED,
-            f"Error saving configuration: {str(e)}",
-            details=traceback.format_exc(),
-            status_code=500
-        )
+        return error_response(
+            ErrorCode.CONFIG_SAVE_FAILED,
+            f"Error saving configuration: {e}",
+            details=traceback.format_exc(),
+            status_code=500
+        )
web_interface/app.py (2)

29-36: CSRF disabled: validate threat model and deployment assumptions

Explicitly disabling CSRF (csrf = None) for a “local-only” app is acceptable only if the UI truly never leaves a trusted network (no port-forwarding, reverse proxy, or public exposure). If there’s any chance this gets exposed beyond a private LAN, you’ll want a follow-up issue to reintroduce CSRF and wire tokens into HTMX forms as your comment suggests.


525-531: Remove dead CSRF exemption block for clarity

With csrf = None, the if csrf: block exempting SSE streams is now dead code, and the inline note about the api_v3 blueprint being exempted is stale. Consider deleting this block and moving the comment near the CSRF section to avoid future confusion.

Suggested minimal cleanup
-# Exempt SSE streams from CSRF and add rate limiting
-if csrf:
-    csrf.exempt(stream_stats)
-    csrf.exempt(stream_display)
-    csrf.exempt(stream_logs)
-    # Note: api_v3 blueprint is exempted above after registration
-
-if limiter:
+if limiter:
     limiter.limit("20 per minute")(stream_stats)
     limiter.limit("20 per minute")(stream_display)
     limiter.limit("20 per minute")(stream_logs)
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 33e4f36 and b508548.

📒 Files selected for processing (3)
  • web_interface/app.py
  • web_interface/blueprints/api_v3.py
  • web_interface/templates/v3/partials/fonts.html
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (.cursor/rules/coding-standards.mdc)

**/*.py: Prefer clear, readable code over clever optimizations (Simplicity First principle)
Make intentions clear through naming and structure (Explicit over Implicit principle)
Validate inputs and handle errors early (Fail Fast principle)
Use docstrings for classes and complex functions
Use PascalCase for class names (e.g., NHLRecentManager)
Use snake_case for function and variable names (e.g., fetch_game_data)
Use UPPER_SNAKE_CASE for constants (e.g., ESPN_NHL_SCOREBOARD_URL)
Use leading underscore for private methods (e.g., _fetch_data)
Use structured logging with context (e.g., [NHL Recent]) for logging messages
Catch specific exceptions, not bare except: statements
Provide user-friendly error messages that explain what went wrong and potential solutions
Implement graceful degradation to continue operation when non-critical features fail
Use type hints for function parameters and return values
Validate required configuration fields on initialization
Provide sensible default values in code rather than in configuration files
Handle different deployment contexts with environment awareness in code

**/*.py: Code must run on Raspberry Pi, not Windows development machine
Optimize code for Raspberry Pi's limited RAM and CPU capabilities
Clean up resources regularly to manage memory effectively
Implement comprehensive logging for remote debugging on Raspberry Pi
Provide clear error messages for troubleshooting

Files:

  • web_interface/blueprints/api_v3.py
  • web_interface/app.py
🪛 Ruff (0.14.10)
web_interface/blueprints/api_v3.py

637-637: Use explicit conversion flag

Replace with conversion flag

(RUF010)

🔇 Additional comments (1)
web_interface/blueprints/api_v3.py (1)

675-701: ConfigError vs UNKNOWN_ERROR flow is correct and consistent

The new error_response branches for ConfigError (including config_path in both message and context) vs generic exceptions with ErrorCode.UNKNOWN_ERROR are well-structured and align with the helper’s usage elsewhere. No functional issues from this change.

- Only prefix with 'assets/fonts/' if path is a bare filename
- If path starts with '/' (absolute) or 'assets/' (already prefixed), use as-is
- Fixes double-prefixing when get_fonts_catalog returns relative paths like 'assets/fonts/press_start.ttf'
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
web_interface/templates/v3/partials/fonts.html (2)

570-570: baseUrl is undefined, causing ReferenceError.

baseUrl is defined locally in loadFontData() (line 410) but is referenced here in addFontOverride(). This will throw a ReferenceError at runtime.

Proposed fix

Define baseUrl at the top of the IIFE so all functions can access it:

     // Create references that can be reassigned
     var fontCatalog = window.fontCatalog;
     var fontTokens = window.fontTokens;
     var fontOverrides = window.fontOverrides;
     var selectedFontFiles = window.selectedFontFiles;
     var fontsTabInitialized = window.fontsTabInitialized;
+    var baseUrl = window.location.origin;

Then remove the local declaration in loadFontData() at line 410:

     try {
         // Use absolute URLs to ensure they work when loaded via HTMX
-        const baseUrl = window.location.origin;
         const [catalogRes, tokensRes, overridesRes] = await Promise.all([

Also applies to: 602-602, 793-793


728-728: Local reassignment doesn't update window.selectedFontFiles.

Line 220 creates a local variable pointing to window.selectedFontFiles, but reassigning selectedFontFiles here only updates the local reference, leaving window.selectedFontFiles pointing to the old array. This creates state inconsistency.

Proposed fix

Update both the window property and the local reference:

-    selectedFontFiles = validFiles;
+    window.selectedFontFiles = validFiles;
+    selectedFontFiles = window.selectedFontFiles;
     showUploadForm();

Apply the same fix at line 758:

-    selectedFontFiles = [];
+    window.selectedFontFiles = [];
+    selectedFontFiles = window.selectedFontFiles;
     document.getElementById('upload-form').style.display = 'none';

Also applies to: 758-758

♻️ Duplicate comments (1)
web_interface/templates/v3/partials/fonts.html (1)

223-228: The fontsTabInitialized guard still prevents re-initialization on HTMX reloads.

This issue was previously flagged: when HTMX swaps in new DOM, the script sees _fontsScriptLoaded is true (line 196) and calls the existing window.initializeFontsTab(). However, fontsTabInitialized remains true from the previous initialization, so the function returns early at line 226. The new DOM elements (#upload-dropzone, buttons, etc.) never receive event listeners, breaking the fonts UI after reload.

Remove the fontsTabInitialized check entirely—the _fontsScriptLoaded guard already prevents redeclaration of functions.

Proposed fix
 function initializeFontsTab() {
-    // Check if already initialized
-    if (fontsTabInitialized) {
-        console.log('Fonts tab already initialized, skipping...');
-        return;
-    }
-    
     const detectedEl = document.getElementById('detected-fonts');
     const availableEl = document.getElementById('available-fonts');

Also remove the assignments at lines 243-244:

-    // Mark as initialized to prevent duplicate initialization
-    window.fontsTabInitialized = true;
-    fontsTabInitialized = true;
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b508548 and 2ccf219.

📒 Files selected for processing (1)
  • web_interface/templates/v3/partials/fonts.html
🔇 Additional comments (5)
web_interface/templates/v3/partials/fonts.html (5)

193-207: LGTM: IIFE prevents redeclaration on HTMX reloads.

The script load guard using window._fontsScriptLoaded correctly prevents redeclaration of functions and variables when HTMX swaps content. The 50ms delay before calling initializeFontsTab() allows the new DOM to settle.


209-221: LGTM: Window-scoped state enables persistence across HTMX reloads.

Moving font state to window properties ensures data persists when HTMX replaces content, while local references maintain code readability.


438-446: LGTM: Synchronizing window properties and local references.

The pattern of updating window.fontCatalog, window.fontTokens, and window.fontOverrides first, then syncing local references, ensures font data is accessible both globally (for re-initialization) and locally (for current function scope).


530-536: LGTM: Font path logic correctly avoids double-prefixing.

The updated logic properly handles all cases:

  • Absolute paths (starting with /) are used as-is
  • Paths already prefixed with assets/ are used as-is
  • Bare filenames are prefixed with assets/fonts/

This resolves the previous issue where paths like assets/fonts/press_start.ttf from get_fonts_catalog were being prefixed again.


837-837: LGTM: IIFE properly closed.

The IIFE is correctly closed, encapsulating all font management logic to prevent global scope pollution and redeclaration issues on HTMX reloads.

… on HTMX reload

- Remove fontsTabInitialized check that prevented re-initialization on HTMX content swap
- The window._fontsScriptLoaded guard is sufficient to prevent function redeclaration
- Allow initializeFontsTab() to run on each HTMX swap to attach listeners to new DOM elements
- Fixes fonts UI breaking after HTMX reload (buttons, upload dropzone, etc. not working)
@ChuckBuilds ChuckBuilds merged commit d14b5ff into main Dec 28, 2025
1 check was pending
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
web_interface/templates/v3/partials/fonts.html (4)

282-324: Event listeners accumulate on each HTMX reload, causing duplicate handler execution.

Every time initializeFontsTab() runs (including after HTMX swaps), new event listeners are attached to the same DOM elements without removing old ones. This causes each user action to trigger multiple handlers—2× after the first reload, 3× after the second, and so on.

Either:

  • Remove old listeners before adding new ones using AbortController or cloned elements
  • Check if listeners are already attached before adding
  • Use event delegation on a stable parent container
🔎 Proposed fix using AbortController
+// Store abort controller on window to cancel previous listeners
+if (window._fontsAbortController) {
+    window._fontsAbortController.abort();
+}
+window._fontsAbortController = new AbortController();
+const signal = window._fontsAbortController.signal;
+
 if (uploadDropzone && fontFileInput) {
-    uploadDropzone.addEventListener('click', () => {
+    uploadDropzone.addEventListener('click', () => {
         fontFileInput.click();
-    });
+    }, { signal });
 }
 
 if (fontFileInput) {
-    fontFileInput.addEventListener('change', handleFileSelection);
+    fontFileInput.addEventListener('change', handleFileSelection, { signal });
 }
 
 if (uploadFontsBtn) {
-    uploadFontsBtn.addEventListener('click', uploadSelectedFonts);
+    uploadFontsBtn.addEventListener('click', uploadSelectedFonts, { signal });
 }
 
 if (cancelUploadBtn) {
-    cancelUploadBtn.addEventListener('click', cancelFontUpload);
+    cancelUploadBtn.addEventListener('click', cancelFontUpload, { signal });
 }
 
 if (addOverrideBtn) {
-    addOverrideBtn.addEventListener('click', addFontOverride);
+    addOverrideBtn.addEventListener('click', addFontOverride, { signal });
 }
 
 if (updatePreviewBtn) {
-    updatePreviewBtn.addEventListener('click', updateFontPreview);
+    updatePreviewBtn.addEventListener('click', updateFontPreview, { signal });
 }
 
 // Drag and drop for upload area
 if (uploadDropzone) {
     uploadDropzone.addEventListener('dragover', (e) => {
         e.preventDefault();
         uploadDropzone.classList.add('drag-over');
-    });
+    }, { signal });

     uploadDropzone.addEventListener('dragleave', () => {
         uploadDropzone.classList.remove('drag-over');
-    });
+    }, { signal });

     uploadDropzone.addEventListener('drop', (e) => {
         e.preventDefault();
         uploadDropzone.classList.remove('drag-over');
         handleFileSelection({ target: { files: e.dataTransfer.files } });
-    });
+    }, { signal });
 }

560-560: baseUrl is undefined—breaks font override and upload functionality.

baseUrl is defined inside loadFontData() (line 400) but referenced in:

  • Line 560 (addFontOverride)
  • Line 592 (deleteFontOverride)
  • Line 783 (uploadSelectedFonts)

These calls will fail with ReferenceError: baseUrl is not defined.

🔎 Proposed fix: define baseUrl in outer scope
 })(); // End of script load guard - prevents redeclaration on HTMX reload
+
+// Define baseUrl at module scope for all functions to use
+const baseUrl = window.location.origin;

 async function initializeFontManagement() {
     try {
@@ -397,7 +400,6 @@

     try {
         // Use absolute URLs to ensure they work when loaded via HTMX
-        const baseUrl = window.location.origin;
         const [catalogRes, tokensRes, overridesRes] = await Promise.all([
             fetch(`${baseUrl}/api/v3/fonts/catalog`),
             fetch(`${baseUrl}/api/v3/fonts/tokens`),

Also applies to: 592-592, 783-783


638-638: deleteFontOverride not exposed to global scope—onclick handler will fail.

Line 638 uses onclick="deleteFontOverride('${elementKey}')" but the function (defined at line 586) is not exposed on the window object. Inline onclick handlers require global scope access.

🔎 Proposed fix: expose function to window

Add after the function definition around line 608:

     }
 }
+
+// Expose to global scope for onclick handler
+window.deleteFontOverride = deleteFontOverride;

 function displayCurrentOverrides() {

718-718: selectedFontFiles local reassignment doesn't sync to window.selectedFontFiles.

The dual-state pattern establishes both window.selectedFontFiles (line 213) and a local reference (line 219). However, lines 718 and 748 reassign only the local variable:

selectedFontFiles = validFiles;  // line 718
selectedFontFiles = [];          // line 748

This leaves window.selectedFontFiles stale. On the next HTMX reload, line 219 pulls the old value, causing inconsistent state.

🔎 Proposed fix: sync both window and local references
-    selectedFontFiles = validFiles;
+    window.selectedFontFiles = validFiles;
+    selectedFontFiles = window.selectedFontFiles;
     showUploadForm();

Similarly for line 748:

 function cancelFontUpload() {
-    selectedFontFiles = [];
+    window.selectedFontFiles = [];
+    selectedFontFiles = window.selectedFontFiles;
     document.getElementById('upload-form').style.display = 'none';
     document.getElementById('font-file-input').value = '';
 }

Also applies to: 748-748

🧹 Nitpick comments (1)
web_interface/templates/v3/partials/fonts.html (1)

543-543: Inconsistent showNotification usage—sometimes direct, sometimes via window.

The function is defined as window.showNotification (line 238) and calls like line 543 rely on automatic global scope lookup. However, checks like line 455 use typeof showNotification === 'function' which tests local scope first. For clarity and consistency:

Either consistently use window.showNotification throughout, or create a local reference at the top of initializeFontsTab():

const showNotification = window.showNotification;

Also applies to: 455-460, 570-570, 578-578, 582-582, 598-598, 602-602, 606-606, 710-710, 715-715, 774-774, 791-791, 793-793, 809-809

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2ccf219 and 4475d2b.

📒 Files selected for processing (1)
  • web_interface/templates/v3/partials/fonts.html
🔇 Additional comments (2)
web_interface/templates/v3/partials/fonts.html (2)

193-204: LGTM: IIFE guard correctly prevents redeclaration on HTMX reload.

The pattern of checking window._fontsScriptLoaded and calling window.initializeFontsTab() after a brief delay addresses the HTMX content swap scenario effectively. This resolves the previous concern about the fontsTabInitialized guard blocking reinitialization.


520-530: Path prefix logic correctly prevents double-prefixing.

The backend get_fonts_catalog returns objects with a path property containing the full relative path (e.g., 'assets/fonts/press_start.ttf'). The frontend correctly detects this already-prefixed format with fontPath.startsWith('assets/') and uses the path as-is, eliminating the double-prefix issue from the prior review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants