From 505fed70e3fd85c42c0d11d1f3afa1ffc45b8375 Mon Sep 17 00:00:00 2001 From: Chuck Date: Sat, 23 May 2026 16:30:24 -0400 Subject: [PATCH] Address review feedback: error leaks, ok:false, htmx:ready coverage - Backup endpoints: replace raw str(e) in user-facing responses with a generic message; full exception still logged via exc_info=True - hardware/status: change ok:null to ok:false for PermissionError and json.JSONDecodeError so the UI's hw.ok===false check triggers correctly - base.html: dispatch htmx:ready from the fallback load path so any deferred listeners fire on CDN-fallback loads too - loadTabContent: also listen for htmx-load-failed so overview/wifi/plugins fall back to direct fetch when HTMX is completely unavailable Co-Authored-By: Claude Sonnet 4.6 --- web_interface/blueprints/api_v3.py | 17 +++++++++-------- web_interface/templates/v3/base.html | 16 ++++++++++++---- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/web_interface/blueprints/api_v3.py b/web_interface/blueprints/api_v3.py index 42b5a3c9..7ad45ba8 100644 --- a/web_interface/blueprints/api_v3.py +++ b/web_interface/blueprints/api_v3.py @@ -1603,10 +1603,10 @@ def get_hardware_status(): return jsonify({"status": "success", "data": {"ok": None, "error": "Display service not yet started"}}) except PermissionError: logger.warning("Permission denied reading hardware status file; display service may be running as a different user") - return jsonify({"status": "success", "data": {"ok": None, "error": "Hardware status temporarily unavailable"}}) + return jsonify({"status": "success", "data": {"ok": False, "error": "Hardware status temporarily unavailable"}}) except json.JSONDecodeError: logger.error("Failed to parse hardware status file", exc_info=True) - return jsonify({"status": "success", "data": {"ok": None, "error": "Hardware status file corrupted"}}) + return jsonify({"status": "success", "data": {"ok": False, "error": "Hardware status file corrupted"}}) except Exception: logger.error("Unexpected error reading hardware status", exc_info=True) return jsonify({"status": "error", "message": "Unable to read hardware status"}), 500 @@ -7099,7 +7099,7 @@ def backup_preview(): return jsonify({'status': 'success', 'data': data}) except Exception as e: logger.error("backup_preview failed: %s", e, exc_info=True) - return jsonify({'status': 'error', 'message': str(e)}), 500 + return jsonify({'status': 'error', 'message': 'An internal error occurred; see logs for details'}), 500 @api_v3.route('/backup/list', methods=['GET']) @@ -7120,7 +7120,7 @@ def backup_list(): return jsonify({'status': 'success', 'data': entries}) except Exception as e: logger.error("backup_list failed: %s", e, exc_info=True) - return jsonify({'status': 'error', 'message': str(e)}), 500 + return jsonify({'status': 'error', 'message': 'An internal error occurred; see logs for details'}), 500 @api_v3.route('/backup/export', methods=['POST']) @@ -7132,7 +7132,7 @@ def backup_export(): return jsonify({'status': 'success', 'filename': zip_path.name}) except Exception as e: logger.error("backup_export failed: %s", e, exc_info=True) - return jsonify({'status': 'error', 'message': str(e)}), 500 + return jsonify({'status': 'error', 'message': 'An internal error occurred; see logs for details'}), 500 @api_v3.route('/backup/validate', methods=['POST']) @@ -7158,7 +7158,7 @@ def backup_validate(): return jsonify({'status': 'success', 'data': manifest}) except Exception as e: logger.error("backup_validate failed: %s", e, exc_info=True) - return jsonify({'status': 'error', 'message': str(e)}), 500 + return jsonify({'status': 'error', 'message': 'An internal error occurred; see logs for details'}), 500 @api_v3.route('/backup/restore', methods=['POST']) @@ -7218,7 +7218,7 @@ def backup_restore(): return jsonify({'status': 'success', 'data': data}) except Exception as e: logger.error("backup_restore failed: %s", e, exc_info=True) - return jsonify({'status': 'error', 'message': str(e)}), 500 + return jsonify({'status': 'error', 'message': 'An internal error occurred; see logs for details'}), 500 @api_v3.route('/backup/download/', methods=['GET']) @@ -7241,4 +7241,5 @@ def backup_delete(filename): path.unlink() return jsonify({'status': 'success'}) except OSError as e: - return jsonify({'status': 'error', 'message': str(e)}), 500 \ No newline at end of file + logger.error("backup_delete failed: %s", e, exc_info=True) + return jsonify({'status': 'error', 'message': 'An internal error occurred; see logs for details'}), 500 \ No newline at end of file diff --git a/web_interface/templates/v3/base.html b/web_interface/templates/v3/base.html index 2e75073d..081d0870 100644 --- a/web_interface/templates/v3/base.html +++ b/web_interface/templates/v3/base.html @@ -136,6 +136,7 @@ setTimeout(function() { if (typeof htmx !== 'undefined') { console.log('HTMX loaded from fallback'); + window.dispatchEvent(new Event('htmx:ready')); // Load extensions after core loads loadScript(sseSrc, isAPMode ? 'https://unpkg.com/htmx.org/dist/ext/sse.js' : '/static/v3/js/htmx-sse.js'); loadScript(jsonEncSrc, isAPMode ? 'https://unpkg.com/htmx.org/dist/ext/json-enc.js' : '/static/v3/js/htmx-json-enc.js'); @@ -1839,11 +1840,18 @@ htmx.trigger(contentEl, 'revealed'); } } else { - // HTMX is still loading asynchronously — retry once it signals ready + // HTMX is still loading asynchronously — retry when it signals ready, + // or fall back to direct fetch if it fails to load entirely. const self = this; - window.addEventListener('htmx:ready', function retry() { - self.loadTabContent(tab); - }, { once: true }); + function onReady() { window.removeEventListener('htmx-load-failed', onFailed); self.loadTabContent(tab); } + function onFailed() { + window.removeEventListener('htmx:ready', onReady); + if (tab === 'overview' && typeof loadOverviewDirect === 'function') loadOverviewDirect(); + else if (tab === 'wifi' && typeof loadWifiDirect === 'function') loadWifiDirect(); + else if (tab === 'plugins' && typeof loadPluginsDirect === 'function') loadPluginsDirect(); + } + window.addEventListener('htmx:ready', onReady, { once: true }); + window.addEventListener('htmx-load-failed', onFailed, { once: true }); } },