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 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-05-23 16:30:24 -04:00
parent c8d2eaeb85
commit 505fed70e3
2 changed files with 21 additions and 12 deletions

View File

@@ -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/<path:filename>', 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
logger.error("backup_delete failed: %s", e, exc_info=True)
return jsonify({'status': 'error', 'message': 'An internal error occurred; see logs for details'}), 500

View File

@@ -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 });
}
},