diff --git a/frogpilot/common/frogpilot_variables.py b/frogpilot/common/frogpilot_variables.py index a09af2e12..58ae1ff9c 100644 --- a/frogpilot/common/frogpilot_variables.py +++ b/frogpilot/common/frogpilot_variables.py @@ -174,7 +174,7 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [ ("CECurves", "0", 1, "0"), ("CECurvesLead", "0", 1, "0"), ("CELead", "0", 1, "0"), - ("CEModelStopTime", str(PLANNER_TIME - 2), 2, "0"), + ("CEModelStopTime", "7", 2, "0"), ("CENavigation", "0", 2, "0"), ("CENavigationIntersections", "1", 2, "0"), ("CENavigationLead", "1", 2, "0"), diff --git a/frogpilot/system/the_pond/assets/components/router.js b/frogpilot/system/the_pond/assets/components/router.js index 21827d144..a0ff96bc6 100644 --- a/frogpilot/system/the_pond/assets/components/router.js +++ b/frogpilot/system/the_pond/assets/components/router.js @@ -18,6 +18,7 @@ import { ModelManager } from "/assets/components/tools/model_manager.js?v=202603 import { LivePlots } from "/assets/components/tools/plots.js" import { ThemeMaker } from "/assets/components/tools/theme_maker.js" import { TestingGround } from "/assets/components/tools/testing_ground.js" +import { Troubleshoot } from "/assets/components/tools/troubleshoot.js" import { TmuxLog } from "/assets/components/tools/tmux.js" import { ToggleControl } from "/assets/components/tools/toggles.js" import { UpdateManager } from "/assets/components/tools/update_manager.js" @@ -50,6 +51,7 @@ function Root() { createRoute("plots", "/plots", LivePlots), createRoute("thememaker", "/theme_maker", ThemeMaker), createRoute("testing_ground", "/testing_ground", TestingGround), + createRoute("troubleshoot", "/troubleshoot", Troubleshoot), createRoute("tmux", "/manage_tmux", TmuxLog), createRoute("toggles", "/manage_toggles", ToggleControl), createRoute("updates", "/manage_updates", UpdateManager), diff --git a/frogpilot/system/the_pond/assets/components/sidebar.js b/frogpilot/system/the_pond/assets/components/sidebar.js index f083ac548..13bf1cc70 100644 --- a/frogpilot/system/the_pond/assets/components/sidebar.js +++ b/frogpilot/system/the_pond/assets/components/sidebar.js @@ -23,6 +23,7 @@ const MenuItems = { { name: "Model Manager", link: "/manage_models", icon: "bi-cpu" }, { name: "Plots", link: "/plots", icon: "bi-graph-up-arrow" }, { name: "Testing Ground", link: "/testing_ground", icon: "bi-bezier2" }, + { name: "Troubleshoot", link: "/troubleshoot", icon: "bi-tools" }, { name: "Theme Maker", link: "/theme_maker", icon: "bi-palette-fill" }, { name: "Tmux Log", link: "/manage_tmux", icon: "bi-terminal" }, { name: "Backup and Restore", link: "/manage_toggles", icon: "bi-arrow-repeat" }, diff --git a/frogpilot/system/the_pond/assets/components/tools/troubleshoot.css b/frogpilot/system/the_pond/assets/components/tools/troubleshoot.css new file mode 100644 index 000000000..bb3a7e621 --- /dev/null +++ b/frogpilot/system/the_pond/assets/components/tools/troubleshoot.css @@ -0,0 +1,124 @@ +.troubleshootPage { + display: flex; + flex-direction: column; + gap: 16px; +} + +.troubleshootCard { + background: var(--sidebar-bg); + border: 1px solid var(--sidebar-border-color); + border-radius: var(--border-radius-lg); + padding: 16px; +} + +.troubleshootIntro { + margin: 0 0 12px; + opacity: 0.9; +} + +.troubleshootActionRow { + display: flex; + gap: 10px; + flex-wrap: wrap; + margin-bottom: 10px; +} + +.troubleshootButton { + background: linear-gradient(135deg, #7a62b8, #8b6cc5); + color: #fff; + border: none; + border-radius: var(--border-radius-md); + padding: 10px 14px; + font-size: 0.95rem; + font-weight: 700; + cursor: pointer; +} + +.troubleshootButton:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.troubleshootDanger { + background: linear-gradient(135deg, #b14a6b, #d95a7b); +} + +.troubleshootError { + color: #ff6b6b; +} + +.troubleshootStatusLine { + margin: 6px 0 0; +} + +.troubleshootSectionHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 8px; +} + +.troubleshootSectionHeader h3, +.troubleshootCard h3 { + margin: 0; +} + +.troubleshootHeaderRow, +.troubleshootItemRow, +.troubleshootSnapshotRow { + display: grid; + gap: 10px; + align-items: center; + border-top: 1px solid var(--sidebar-border-color); + padding: 8px 0; +} + +.troubleshootHeaderRow { + font-weight: 700; + opacity: 0.9; +} + +.troubleshootHeaderRow { + grid-template-columns: 2fr 1fr 1fr; +} + +.troubleshootHeaderRowSnapshot { + grid-template-columns: 2fr 2fr; +} + +.troubleshootSnapshotRow { + grid-template-columns: 2fr 2fr; +} + +.troubleshootItemRow { + grid-template-columns: 2fr 1fr 1fr; +} + +.troubleshootItemLabel { + font-weight: 600; +} + +.troubleshootItemValue, +.troubleshootItemDefault { + font-family: "Open Sans", sans-serif; + word-break: break-word; +} + +@media (max-width: 720px) { + .troubleshootHeaderRow, + .troubleshootItemRow { + grid-template-columns: 1.3fr 1fr 1fr; + font-size: 0.9rem; + } + + .troubleshootSnapshotRow { + grid-template-columns: 1fr; + gap: 4px; + } + + .troubleshootSectionHeader { + align-items: flex-start; + flex-direction: column; + } +} diff --git a/frogpilot/system/the_pond/assets/components/tools/troubleshoot.js b/frogpilot/system/the_pond/assets/components/tools/troubleshoot.js new file mode 100644 index 000000000..2bddaa543 --- /dev/null +++ b/frogpilot/system/the_pond/assets/components/tools/troubleshoot.js @@ -0,0 +1,208 @@ +import { html, reactive } from "https://esm.sh/@arrow-js/core" + +const state = reactive({ + loading: true, + error: "", + snapshot: [], + sections: [], + isOnroad: false, + refreshing: false, + busySection: "", +}) + +let initialized = false + +function formatValue(value) { + if (typeof value === "boolean") return value ? "On" : "Off" + if (typeof value === "number") { + if (Number.isInteger(value)) return String(value) + return String(Number(value.toFixed(4))) + } + if (value === null || value === undefined) return "n/a" + const text = String(value).trim() + return text || "(empty)" +} + +function buildReportText() { + const lines = [] + lines.push("StarPilot Troubleshoot Report") + lines.push(`Generated: ${new Date().toISOString()}`) + lines.push(`Onroad: ${state.isOnroad ? "Yes" : "No"}`) + lines.push("") + lines.push("Snapshot") + for (const item of state.snapshot) { + lines.push(`- ${item.label}: ${formatValue(item.value)}`) + } + + for (const section of state.sections) { + lines.push("") + lines.push(section.title) + for (const item of section.items || []) { + lines.push(`- ${item.label}: ${formatValue(item.value)} (default: ${formatValue(item.defaultValue)})`) + } + } + + return lines.join("\n") +} + +async function copyToClipboard() { + const text = buildReportText() + try { + if (navigator?.clipboard?.writeText) { + await navigator.clipboard.writeText(text) + } else { + const textArea = document.createElement("textarea") + textArea.value = text + textArea.style.position = "fixed" + textArea.style.left = "-9999px" + document.body.appendChild(textArea) + textArea.focus() + textArea.select() + document.execCommand("copy") + document.body.removeChild(textArea) + } + showSnackbar("Troubleshoot report copied to clipboard.", "", 2500, { key: "troubleshoot-copy" }) + } catch (error) { + showSnackbar(error?.message || "Failed to copy report.", "error", 3000, { key: "troubleshoot-copy" }) + } +} + +async function fetchTroubleshoot(showToast = false) { + state.refreshing = true + try { + const response = await fetch("/api/troubleshoot") + const payload = await response.json() + if (!response.ok) { + throw new Error(payload.error || response.statusText || "Failed to load troubleshoot data") + } + + state.snapshot = Array.isArray(payload.snapshot) ? payload.snapshot : [] + state.sections = Array.isArray(payload.sections) ? payload.sections : [] + state.isOnroad = !!payload.isOnroad + state.error = "" + + if (showToast) { + showSnackbar("Troubleshoot data refreshed.", "", 1800, { key: "troubleshoot-refresh" }) + } + } catch (error) { + const message = error?.message || "Failed to load troubleshoot data" + state.error = message + if (showToast) { + showSnackbar(message, "error", 3000, { key: "troubleshoot-refresh" }) + } + } finally { + state.loading = false + state.refreshing = false + } +} + +async function resetSection(section) { + const sectionId = String(section?.id || "") + if (!sectionId || state.busySection) return + + const sectionTitle = String(section?.title || "this section") + if (!confirm(`Reset ${sectionTitle} to defaults?`)) return + + state.busySection = sectionId + try { + const response = await fetch("/api/troubleshoot/reset", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ sectionId }), + }) + const payload = await response.json() + if (!response.ok) { + throw new Error(payload.error || response.statusText || "Failed to reset section") + } + + showSnackbar(payload.message || `${sectionTitle} reset.`, "", 2500, { key: "troubleshoot-reset" }) + await fetchTroubleshoot(false) + } catch (error) { + showSnackbar(error?.message || "Failed to reset section.", "error", 3000, { key: "troubleshoot-reset" }) + } finally { + state.busySection = "" + } +} + +function initialize() { + if (initialized) return + initialized = true + fetchTroubleshoot(false) +} + +function itemRows(section) { + const items = Array.isArray(section?.items) ? section.items : [] + return items.map((item) => html` +
+
${item.label}
+
${formatValue(item.value)}
+
${formatValue(item.defaultValue)}
+
+ `) +} + +export function Troubleshoot() { + initialize() + + return html` +
+

Troubleshoot

+ + ${() => state.loading ? html`
Loading troubleshoot data...
` : ""} + + ${() => !state.loading ? html` +
+

+ Quick diagnostics snapshot for weird behavior reports and copy-ready debug logs. +

+
+ + +
+ ${() => state.error ? html`

Error: ${state.error}

` : ""} +

Onroad: ${state.isOnroad ? "Yes" : "No"}

+
+ +
+

Snapshot

+
+ Field + Value +
+ ${() => state.snapshot.map((item) => html` +
+
${item.label}
+
${formatValue(item.value)}
+
+ `)} +
+ + ${() => state.sections.map((section) => html` +
+
+

${section.title}

+ ${section.resettable ? html` + + ` : ""} +
+
+ Setting + Current + Default +
+ ${itemRows(section)} +
+ `)} + ` : ""} +
+ ` +} diff --git a/frogpilot/system/the_pond/templates/index.html b/frogpilot/system/the_pond/templates/index.html index 26a15f2a1..96d5bbcc6 100644 --- a/frogpilot/system/the_pond/templates/index.html +++ b/frogpilot/system/the_pond/templates/index.html @@ -32,6 +32,7 @@ + diff --git a/frogpilot/system/the_pond/the_pond.py b/frogpilot/system/the_pond/the_pond.py index 472b18424..6a2792be0 100644 --- a/frogpilot/system/the_pond/the_pond.py +++ b/frogpilot/system/the_pond/the_pond.py @@ -213,6 +213,121 @@ _plots_state = { "lastError": "", } +_TROUBLESHOOT_PERSONALITY_KEYS = [ + "CustomPersonalities", + "TrafficPersonalityProfile", + "TrafficFollow", + "TrafficJerkAcceleration", + "TrafficJerkDeceleration", + "TrafficJerkDanger", + "TrafficJerkSpeedDecrease", + "TrafficJerkSpeed", + "AggressivePersonalityProfile", + "AggressiveFollow", + "AggressiveFollowHigh", + "AggressiveJerkAcceleration", + "AggressiveJerkDeceleration", + "AggressiveJerkDanger", + "AggressiveJerkSpeedDecrease", + "AggressiveJerkSpeed", + "StandardPersonalityProfile", + "StandardFollow", + "StandardFollowHigh", + "StandardJerkAcceleration", + "StandardJerkDeceleration", + "StandardJerkDanger", + "StandardJerkSpeedDecrease", + "StandardJerkSpeed", + "RelaxedPersonalityProfile", + "RelaxedFollow", + "RelaxedFollowHigh", + "RelaxedJerkAcceleration", + "RelaxedJerkDeceleration", + "RelaxedJerkDanger", + "RelaxedJerkSpeedDecrease", + "RelaxedJerkSpeed", +] + +_TROUBLESHOOT_CEM_KEYS = [ + "ConditionalExperimental", + "CESpeed", + "CESpeedLead", + "CECurves", + "CELead", + "CESlowerLead", + "CEStoppedLead", + "CENavigation", + "CEModelStopTime", + "CESignalSpeed", + "ShowCEMStatus", +] + +_TROUBLESHOOT_ADVANCED_LATERAL_KEYS = [ + "AdvancedLateralTune", + "SteerDelay", + "SteerFriction", + "SteerOffset", + "SteerKP", + "SteerLatAccel", + "SteerRatio", + "ForceAutoTune", + "ForceAutoTuneOff", + "ForceTorqueController", +] + +_TROUBLESHOOT_ADVANCED_LONGITUDINAL_KEYS = [ + "AdvancedLongitudinalTune", + "EVTuning", + "TruckTuning", + "LongitudinalActuatorDelay", + "StartAccel", + "VEgoStarting", + "StopAccel", + "StoppingDecelRate", + "VEgoStopping", +] + +_TROUBLESHOOT_SECTION_DEFINITIONS = [ + { + "id": "personality_settings", + "title": "Personality Profile Settings", + "keys": _TROUBLESHOOT_PERSONALITY_KEYS, + }, + { + "id": "model_stop_distance", + "title": "Model Stop Distance", + "keys": ["StopDistance"], + }, + { + "id": "cem_settings", + "title": "CEM Settings", + "keys": _TROUBLESHOOT_CEM_KEYS, + }, + { + "id": "advanced_lateral_tuning", + "title": "Advanced Lateral Tuning", + "keys": _TROUBLESHOOT_ADVANCED_LATERAL_KEYS, + }, + { + "id": "advanced_longitudinal_tuning", + "title": "Advanced Longitudinal Tuning", + "keys": _TROUBLESHOOT_ADVANCED_LONGITUDINAL_KEYS, + }, +] + +_TROUBLESHOOT_SECTION_BY_ID = { + section["id"]: section + for section in _TROUBLESHOOT_SECTION_DEFINITIONS +} + +_TROUBLESHOOT_NON_RESETTABLE_SECTION_KEYS = { + "CustomPersonalities", + "TrafficPersonalityProfile", + "AggressivePersonalityProfile", + "StandardPersonalityProfile", + "RelaxedPersonalityProfile", +} + def _normalize_fingerprint_make_key(make_value): return str(make_value or "").strip().lower() @@ -1035,26 +1150,39 @@ def write_legacy_param_file(key, value): os.replace(tmp_path, value_path) _layout_type_overrides = None +_layout_param_metadata = None -def _get_layout_type_overrides(): - global _layout_type_overrides - if _layout_type_overrides is None: +def _get_layout_param_metadata(): + global _layout_param_metadata + if _layout_param_metadata is None: try: layout_path = os.path.join(os.path.dirname(__file__), "assets", "components", "tools", "device_settings_layout.json") with open(layout_path) as f: layout_data = json.load(f) - _layout_type_overrides = { - p["key"]: p["data_type"] + _layout_param_metadata = { + p["key"]: p for section in layout_data for p in section.get("params", []) - if "key" in p and "data_type" in p + if "key" in p } except Exception: - _layout_type_overrides = {} + _layout_param_metadata = {} + return _layout_param_metadata + +def _get_layout_type_overrides(): + global _layout_type_overrides + if _layout_type_overrides is None: + layout_param_metadata = _get_layout_param_metadata() + _layout_type_overrides = { + key: param_data.get("data_type") + for key, param_data in layout_param_metadata.items() + if param_data.get("data_type") + } return _layout_type_overrides _cached_allowed_keys = None _cached_param_types = None +_cached_default_values = None def _get_param_type_info(): global _cached_allowed_keys, _cached_param_types @@ -1080,6 +1208,307 @@ def _get_param_type_info(): _cached_param_types = types return _cached_allowed_keys, _cached_param_types +def _get_default_param_values(): + global _cached_default_values + if _cached_default_values is None: + _cached_default_values = { + key: default_val + for key, default_val, _, _ in frogpilot_default_params + if key not in EXCLUDED_KEYS + } + return _cached_default_values + +def _coerce_param_value(raw_value, value_type): + safe_type = value_type or str + + if safe_type == bool: + if isinstance(raw_value, bool): + return raw_value + if isinstance(raw_value, bytes): + raw_value = raw_value.decode("utf-8", errors="replace") + return str(raw_value or "").strip() in ("1", "true", "True") + + if safe_type == float: + if raw_value in (None, "", b""): + return 0.0 + try: + if isinstance(raw_value, bytes): + raw_value = raw_value.decode("utf-8", errors="replace") + return float(str(raw_value).strip()) + except Exception: + return 0.0 + + if safe_type == int: + if raw_value in (None, "", b""): + return 0 + try: + if isinstance(raw_value, bytes): + raw_value = raw_value.decode("utf-8", errors="replace") + return int(float(str(raw_value).strip())) + except Exception: + return 0 + + if isinstance(raw_value, bytes): + return raw_value.decode("utf-8", errors="replace") + return str(raw_value or "") + +def _safe_params_get(key, encoding=None, default=None): + try: + if encoding is not None: + return params.get(key, encoding=encoding) + return params.get(key) + except Exception: + return default + +def _safe_params_get_bool(key, default=False): + try: + return params.get_bool(key) + except Exception: + return bool(default) + +def _is_blank_param_raw(raw_value): + if raw_value is None: + return True + if isinstance(raw_value, bytes): + return len(raw_value.strip()) == 0 + if isinstance(raw_value, str): + return len(raw_value.strip()) == 0 + return False + +def _get_current_param_value(key, value_type): + safe_type = value_type or str + if safe_type == bool: + return params.get_bool(key) + return _coerce_param_value(params.get(key), safe_type) + +def _serialize_param_write_value(raw_value): + if isinstance(raw_value, bool): + return "1" if raw_value else "0" + if isinstance(raw_value, bytes): + return raw_value.decode("utf-8", errors="replace") + return str(raw_value or "") + +def _format_longitudinal_personality(value): + mapping = { + "0": "Aggressive", + "1": "Standard", + "2": "Relaxed", + } + text = str(value or "").strip() + if text in mapping: + return mapping[text] + return f"Unknown ({text})" if text else "Unknown" + +def _resolve_troubleshoot_current_value(key, value_type, default_values): + safe_type = value_type or str + + if safe_type == bool: + return _safe_params_get_bool(key) + + raw_value = _safe_params_get(key) + if not _is_blank_param_raw(raw_value): + return _coerce_param_value(raw_value, safe_type) + + stock_key = f"{key}Stock" + if stock_key in default_values: + stock_raw_value = _safe_params_get(stock_key) + if not _is_blank_param_raw(stock_raw_value): + return _coerce_param_value(stock_raw_value, safe_type) + + default_raw_value = default_values.get(key) + if not _is_blank_param_raw(default_raw_value): + return _coerce_param_value(default_raw_value, safe_type) + + return _coerce_param_value(raw_value, safe_type) + +def _resolve_troubleshoot_default_value(key, value_type, default_values): + safe_type = value_type or str + default_raw_value = default_values.get(key) + if not _is_blank_param_raw(default_raw_value): + return _coerce_param_value(default_raw_value, safe_type) + + stock_key = f"{key}Stock" + if stock_key in default_values: + stock_current_raw = _safe_params_get(stock_key) + if not _is_blank_param_raw(stock_current_raw): + return _coerce_param_value(stock_current_raw, safe_type) + + stock_default_raw = default_values.get(stock_key) + if not _is_blank_param_raw(stock_default_raw): + return _coerce_param_value(stock_default_raw, safe_type) + + return _coerce_param_value(default_raw_value, safe_type) + +def _get_safety_snapshot_text(): + cp_bytes = params.get("CarParamsPersistent") + if not cp_bytes: + return "Unavailable" + + try: + with car.CarParams.from_bytes(cp_bytes) as cp: + safety_configs = list(getattr(cp, "safetyConfigs", [])) + if not safety_configs: + return "Unavailable" + + entries = [] + for config in safety_configs: + model = str(getattr(config, "safetyModel", "unknown")) + safety_param = int(getattr(config, "safetyParam", 0)) + entries.append(f"{model} ({safety_param} / 0x{safety_param:X})") + + return ", ".join(entries) if entries else "Unavailable" + except Exception: + return "Unavailable" + +def _get_fingerprint_snapshot_text(): + cp_bytes = params.get("CarParamsPersistent") + cp_fingerprint = "" + try: + if cp_bytes: + with car.CarParams.from_bytes(cp_bytes) as cp: + cp_fingerprint = str(getattr(cp, "carFingerprint", "") or "").strip() + except Exception: + cp_fingerprint = "" + + model_name = str(params.get("CarModelName", encoding="utf-8") or "").strip() + model_value = str(params.get("CarModel", encoding="utf-8") or "").strip() + + if model_name and model_value: + return f"{model_name} ({model_value})" + if model_name: + return model_name + if model_value: + return model_value + if cp_fingerprint: + return cp_fingerprint + return "Unknown" + +def _build_troubleshoot_section_payload(section_definition, value_types, default_values, layout_metadata): + section_keys = [str(key).strip() for key in section_definition.get("keys", []) if str(key).strip()] + items = [] + + for key in section_keys: + param_metadata = layout_metadata.get(key, {}) if isinstance(layout_metadata.get(key, {}), dict) else {} + value_type = value_types.get(key, str) + data_type = str(param_metadata.get("data_type") or "").strip().lower() + if data_type == "float": + value_type = float + elif data_type == "int": + value_type = int + elif data_type == "bool": + value_type = bool + + label = str(param_metadata.get("label") or key) + try: + current_value = _resolve_troubleshoot_current_value(key, value_type, default_values) + default_value = _resolve_troubleshoot_default_value(key, value_type, default_values) + except Exception: + current_value = "Unavailable" + default_value = "n/a" + + items.append({ + "key": key, + "label": label, + "value": current_value, + "defaultValue": default_value, + }) + + return { + "id": section_definition["id"], + "title": section_definition["title"], + "resettable": True, + "items": items, + } + +def _build_troubleshoot_payload(): + _, value_types = _get_param_type_info() + default_values = _get_default_param_values() + layout_metadata = _get_layout_param_metadata() + + longitudinal_personality_raw = _safe_params_get("LongitudinalPersonality", encoding="utf-8", default="") or "" + snapshot_items = [ + { + "id": "safety_param", + "label": "Safety Param", + "value": _get_safety_snapshot_text(), + "resettable": False, + }, + { + "id": "fingerprint", + "label": "Fingerprint", + "value": _get_fingerprint_snapshot_text(), + "resettable": False, + }, + { + "id": "driving_model", + "label": "Current Driving Model", + "value": str(_safe_params_get("Model", encoding="utf-8", default="") or "Unknown"), + "resettable": False, + }, + { + "id": "selected_personality_profile", + "label": "Selected Personality Profile", + "value": _format_longitudinal_personality(longitudinal_personality_raw), + "resettable": False, + }, + ] + + sections = [ + _build_troubleshoot_section_payload(section_definition, value_types, default_values, layout_metadata) + for section_definition in _TROUBLESHOOT_SECTION_DEFINITIONS + ] + + return { + "snapshot": snapshot_items, + "sections": sections, + "isOnroad": params.get_bool("IsOnroad"), + } + +def _reset_troubleshoot_section(section_id): + section_definition = _TROUBLESHOOT_SECTION_BY_ID.get(str(section_id or "").strip()) + if section_definition is None: + raise ValueError("Unknown troubleshoot section.") + + allowed_keys, _ = _get_param_type_info() + default_values = _get_default_param_values() + is_onroad = params.get_bool("IsOnroad") + blocked_onroad_keys = {"Model", "AlwaysOnLateral", "ForceTorqueController", "NNFF", "NNFFLite"} + + updated_keys = [] + skipped_keys = [] + + for key in section_definition.get("keys", []): + if key in _TROUBLESHOOT_NON_RESETTABLE_SECTION_KEYS: + skipped_keys.append({"key": key, "reason": "preserved by design"}) + continue + + if key not in allowed_keys: + skipped_keys.append({"key": key, "reason": "not editable"}) + continue + + if is_onroad and key in blocked_onroad_keys: + skipped_keys.append({"key": key, "reason": "blocked while onroad"}) + continue + + if key not in default_values: + skipped_keys.append({"key": key, "reason": "default unavailable"}) + continue + + params.put(key, _serialize_param_write_value(default_values[key])) + updated_keys.append(key) + + if updated_keys: + update_frogpilot_toggles() + + return { + "sectionId": section_definition["id"], + "sectionTitle": section_definition["title"], + "updatedKeys": updated_keys, + "skippedKeys": skipped_keys, + "updatedCount": len(updated_keys), + "skippedCount": len(skipped_keys), + } + def _extract_testing_ground_variant_labels(slot_data, include_default=True): labels = {} if not isinstance(slot_data, dict): @@ -1895,6 +2324,34 @@ def setup(app): return jsonify(result), 200 + @app.route("/api/troubleshoot", methods=["GET"]) + def get_troubleshoot_data(): + try: + return jsonify(_build_troubleshoot_payload()), 200 + except Exception as exception: + return jsonify({"error": str(exception)}), 500 + + @app.route("/api/troubleshoot/reset", methods=["POST"]) + def reset_troubleshoot_section(): + request_data = request.get_json() or {} + section_id = str(request_data.get("sectionId") or "").strip() + if not section_id: + return jsonify({"error": "Missing 'sectionId' in request body."}), 400 + + try: + result = _reset_troubleshoot_section(section_id) + message = f"{result['sectionTitle']} reset to defaults." + if result["skippedCount"] > 0: + message += f" Updated {result['updatedCount']} setting(s), skipped {result['skippedCount']}." + return jsonify({ + "message": message, + **result, + }), 200 + except ValueError as exception: + return jsonify({"error": str(exception)}), 400 + except Exception as exception: + return jsonify({"error": str(exception)}), 500 + @app.route("/api/models/installed", methods=["GET"]) def get_installed_models(): catalog = get_model_catalog()