From 29b777f12cdb6d794d15ff0f73a4ec87596e2549 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Tue, 16 Jun 2026 18:40:22 -0500 Subject: [PATCH] fremulon --- .../the_pond/assets/components/home/home.css | 10 + .../the_pond/assets/components/home/home.js | 10 +- .../the_pond/tests/test_dashboard_stats.py | 95 ++++++ starpilot/system/the_pond/the_pond.py | 17 +- starpilot/system/the_pond/utilities.py | 310 ++++++++++++++++-- 5 files changed, 405 insertions(+), 37 deletions(-) diff --git a/starpilot/system/the_pond/assets/components/home/home.css b/starpilot/system/the_pond/assets/components/home/home.css index cbd642556..d612cb76d 100644 --- a/starpilot/system/the_pond/assets/components/home/home.css +++ b/starpilot/system/the_pond/assets/components/home/home.css @@ -273,7 +273,12 @@ .dashboard-donut { --value: 0; background: conic-gradient(var(--dashboard-accent) calc(var(--value) * 1%), var(--dashboard-track) 0); + flex-basis: 116px; height: 116px; + max-height: 116px; + max-width: 116px; + min-height: 116px; + min-width: 116px; width: 116px; } @@ -471,7 +476,12 @@ .dashboard-favorite-donut { background: conic-gradient(var(--dashboard-track) 0 100%); + flex-basis: 132px; height: 132px; + max-height: 132px; + max-width: 132px; + min-height: 132px; + min-width: 132px; width: 132px; } diff --git a/starpilot/system/the_pond/assets/components/home/home.js b/starpilot/system/the_pond/assets/components/home/home.js index 46224a701..382847fb6 100644 --- a/starpilot/system/the_pond/assets/components/home/home.js +++ b/starpilot/system/the_pond/assets/components/home/home.js @@ -462,19 +462,15 @@ async function initializeHome(force = false) { } try { - const [statsResponse, unitResponse] = await Promise.all([ - withTimeout(fetch("/api/stats"), 5000, "stats request"), - withTimeout(fetch("/api/params?key=IsMetric"), 5000, "metric request"), - ]); + const statsResponse = await withTimeout(fetch("/api/stats"), 5000, "stats request"); if (!statsResponse.ok) throw new Error(`stats API error: ${statsResponse.status}`); - if (!unitResponse.ok) throw new Error(`params API error: ${unitResponse.status}`); const statsJson = await withTimeout(statsResponse.json(), 5000, "stats JSON parse"); - const isMetricText = (await withTimeout(unitResponse.text(), 5000, "metric read")).trim(); + const payloadUnit = statsJson?.dashboard?.week?.distanceUnit || statsJson?.driveStats?.all?.unit; HOME_STATE.data = statsJson; - HOME_STATE.unit = isMetricText === "1" ? "kilometers" : "miles"; + HOME_STATE.unit = payloadUnit || HOME_STATE.unit || "miles"; HOME_STATE.status = "ready"; } catch (err) { HOME_STATE.status = "error"; diff --git a/starpilot/system/the_pond/tests/test_dashboard_stats.py b/starpilot/system/the_pond/tests/test_dashboard_stats.py index ba219116f..0c6a9ee76 100644 --- a/starpilot/system/the_pond/tests/test_dashboard_stats.py +++ b/starpilot/system/the_pond/tests/test_dashboard_stats.py @@ -418,6 +418,101 @@ def test_persistent_personal_records_and_model_usage_survive_empty_routes(): assert restored_records["cleanDriveStreak"]["value"] == "3 drives" +def test_persistent_loader_accepts_decoded_param_dict(): + params = FakeParams({ + utilities.DASHBOARD_PERSISTENT_STATS_PARAM: { + "routes": { + "route-1": { + "date": "2026-06-15T08:00:00", + "distanceMeters": 1000, + "duration": 60, + "model": "Orion", + "attentionKnown": False, + }, + }, + }, + }) + + stats = utilities._load_dashboard_persistent_stats(params) + + assert stats["routes"]["route-1"]["distanceMeters"] == 1000 + assert stats["routes"]["route-1"]["attentionKnown"] is False + + +def test_lightweight_routes_surface_recent_drives_without_log_analysis(monkeypatch): + utilities._invalidate_dashboard_cache() + route_infos = [ + { + "name": "route-new", + "segments": [], + "segmentCount": 3, + "startedAt": utilities.datetime(2026, 6, 16, 9, 0, 0), + "modifiedAt": 100, + }, + { + "name": "route-old", + "segments": [], + "segmentCount": 2, + "startedAt": utilities.datetime(2026, 6, 15, 9, 0, 0), + "modifiedAt": 90, + }, + ] + params = FakeParams({ + "IsMetric": False, + "Model": "orion", + "AvailableModels": "orion", + "AvailableModelNames": "Orion", + "AvailableModelSeries": "City", + }) + monkeypatch.setattr(utilities, "_list_dashboard_routes", lambda paths: route_infos) + monkeypatch.setattr(utilities, "_start_dashboard_background_analysis", lambda *args: None) + monkeypatch.setattr(utilities, "_build_storage_summary", lambda paths: {"freeBytes": 0, "usedBytes": 0, "totalBytes": 0, "usedPercent": 0, "segmentCounts": {}}) + + dashboard = utilities.get_dashboard_stats(["/tmp/missing"], params, now=utilities.datetime(2026, 6, 16, 12, 0, 0)) + + assert [drive["name"] for drive in dashboard["recentDrives"]] == ["route-new", "route-old"] + assert dashboard["lastDrive"]["model"] == "Orion" + assert dashboard["week"]["drives"] == 2 + assert dashboard["favoriteModels"][0]["name"] == "Orion" + assert dashboard["favoriteModels"][0]["drives"] == 2 + utilities._invalidate_dashboard_cache() + + +def test_unknown_attention_rows_do_not_reset_persisted_clean_records(): + params = FakeParams() + known_drive = { + "name": "route-1", + "date": "2026-06-15T08:00:00", + "distanceMeters": 10000.0, + "duration": 3600, + "engagedSeconds": 3000.0, + "model": "Orion", + "distractedMoments": 0, + "unresponsiveMoments": 0, + "routeModifiedAt": 100, + "attentionKnown": True, + } + shell_drive = { + "name": "route-2", + "date": "2026-06-16T08:00:00", + "distanceMeters": 0.0, + "duration": 120, + "engagedSeconds": 0.0, + "model": "Orion", + "distractedMoments": 0, + "unresponsiveMoments": 0, + "routeModifiedAt": 100, + "attentionKnown": False, + } + + utilities._update_dashboard_persistent_stats(params, [known_drive], wall_now=1000) + stats = utilities._update_dashboard_persistent_stats(params, [shell_drive], wall_now=1000) + records = utilities._display_personal_records(stats, is_metric=False) + + assert records["longestUndistractedDrive"]["value"] == "1.0 hour" + assert records["cleanDriveStreak"]["value"] == "1 drive" + + def test_github_urls_accept_owner_repo_origin(): assert utilities.get_github_changelog_url("owner/repo", "main") == "https://github.com/owner/repo/commits/main/" assert utilities.get_github_changelog_url("github.com/owner/repo", "main") == "https://github.com/owner/repo/commits/main/" diff --git a/starpilot/system/the_pond/the_pond.py b/starpilot/system/the_pond/the_pond.py index 5c450af28..778ea2218 100644 --- a/starpilot/system/the_pond/the_pond.py +++ b/starpilot/system/the_pond/the_pond.py @@ -458,6 +458,11 @@ starpilot_default_params = _build_default_params() params = ParamsCompat(_params_raw) params_memory = ParamsCompat(_params_memory_raw) +STATS_RESPONSE_CACHE_SECONDS = 2.0 +_STATS_RESPONSE_CACHE = { + "updated_at": 0.0, + "payload": None, +} try: FOOTAGE_PATHS = [ @@ -5163,6 +5168,11 @@ def setup(app): @app.route("/api/stats", methods=["GET"]) def get_stats(): + cache_now = time.monotonic() + cached_payload = _STATS_RESPONSE_CACHE.get("payload") + if cached_payload is not None and cache_now - _STATS_RESPONSE_CACHE.get("updated_at", 0.0) < STATS_RESPONSE_CACHE_SECONDS: + return cached_payload + build_metadata = get_build_metadata() short_branch = build_metadata.channel @@ -5191,12 +5201,17 @@ def setup(app): except Exception: dashboard_stats = utilities.get_dashboard_stats([], params) - return { + payload = { "diskUsage": utilities.get_disk_usage(), "driveStats": utilities.get_drive_stats(), "softwareInfo": software_info, "dashboard": dashboard_stats, } + _STATS_RESPONSE_CACHE.update({ + "updated_at": cache_now, + "payload": payload, + }) + return payload @app.route("/api/plots/live", methods=["GET"]) def get_live_plots(): diff --git a/starpilot/system/the_pond/utilities.py b/starpilot/system/the_pond/utilities.py index 841c1718c..2f8e45f51 100644 --- a/starpilot/system/the_pond/utilities.py +++ b/starpilot/system/the_pond/utilities.py @@ -9,6 +9,7 @@ import secrets import shutil import subprocess import sys +import threading import time from datetime import datetime, timedelta @@ -56,15 +57,17 @@ METER_TO_MILE = 1.0 / 1609.344 METER_TO_KILOMETER = 0.001 METER_PER_SECOND_TO_MPH = CV.MS_TO_KPH * CV.KPH_TO_MPH -DASHBOARD_CACHE_TTL_SECONDS = 60.0 +DASHBOARD_CACHE_TTL_SECONDS = 5.0 DASHBOARD_ROUTE_SCAN_LIMIT = 24 -DASHBOARD_ROUTE_ANALYSIS_LIMIT = 8 -DASHBOARD_ANALYSIS_TIME_BUDGET_SECONDS = 3.0 +DASHBOARD_ROUTE_ANALYSIS_LIMIT = 0 +DASHBOARD_ANALYSIS_TIME_BUDGET_SECONDS = 0.0 +DASHBOARD_BACKGROUND_ROUTE_ANALYSIS_LIMIT = 24 DASHBOARD_RECENT_DRIVE_LIMIT = 5 -DASHBOARD_ROUTE_SEGMENT_SAMPLE_LIMIT = 6 +DASHBOARD_ROUTE_SEGMENT_SAMPLE_LIMIT = 2 DASHBOARD_PERSISTED_ROUTE_LIMIT = 5000 DASHBOARD_PERSIST_MIN_ROUTE_AGE_SECONDS = 120 DASHBOARD_PERSISTENT_STATS_PARAM = "GalaxyDashboardStats" +DASHBOARD_ANALYZER_LOG_PATH = "/tmp/galaxy_dashboard_analyzer.log" DASHBOARD_TOP_MODEL_LIMIT = 3 DASHBOARD_EVENT_DISTRACTED = "promptDriverDistracted" DASHBOARD_EVENT_UNRESPONSIVE = "driverUnresponsive" @@ -88,6 +91,8 @@ _DASHBOARD_CACHE = { "updated_at": 0.0, "value": None, } +_DASHBOARD_ANALYZER_LOCK = threading.Lock() +_DASHBOARD_ANALYZER_PROCESS = None params = Params(return_defaults=True) @@ -589,7 +594,7 @@ def get_drive_stats(): return stats -def _params_get_text(params_obj, key, default=""): +def _params_get_value(params_obj, key, default=None): if params_obj is None: return default try: @@ -602,10 +607,19 @@ def _params_get_text(params_obj, key, default=""): except Exception: return default + if value is None: + return default + return value + + +def _params_get_text(params_obj, key, default=""): + value = _params_get_value(params_obj, key, default) if value is None: return default if isinstance(value, bytes): return value.decode("utf-8", errors="replace") + if isinstance(value, (dict, list)): + return json.dumps(value, separators=(",", ":")) return str(value) @@ -742,12 +756,9 @@ def _list_dashboard_routes(footage_paths, limit=DASHBOARD_ROUTE_SCAN_LIMIT): def _dashboard_cache_key(route_infos, params_obj): param_keys = ( "IsMetric", - "AvailableModels", - "AvailableModelNames", - "AvailableModelSeries", - "CommunityFavorites", - "UserFavorites", - DASHBOARD_PERSISTENT_STATS_PARAM, + "Model", + "DrivingModel", + "DrivingModelName", ) route_sig = tuple( (route["name"], route["segmentCount"], round(route["modifiedAt"], 3)) @@ -981,6 +992,7 @@ def _analyze_route_messages(messages, route_info, model_names, is_metric, deadli "distractedMoments": distracted_moments, "unresponsiveMoments": unresponsive_moments, "routeModifiedAt": _safe_float(route_info.get("modifiedAt", 0.0), 0.0), + "attentionKnown": True, } @@ -998,7 +1010,7 @@ def _iter_route_log_messages(route_info, deadline=None): if log_path is None: continue try: - for message in LogReader(str(log_path), sort_by_time=True): + for message in LogReader(str(log_path), sort_by_time=False): if _deadline_reached(deadline): return yield message @@ -1031,6 +1043,208 @@ def _public_drive(drive, is_metric): return public +def _distance_from_meters(distance_m, is_metric): + return distance_m * (METER_TO_KILOMETER if is_metric else METER_TO_MILE) + + +def _current_model_name(params_obj, model_names): + for key in ("DrivingModelName", "DrivingModel", "Model"): + value = _params_get_text(params_obj, key, "") + model_key = canonical_model_key(value) + if model_key and model_key in model_names: + return model_names[model_key]["name"] + label = _clean_model_label(value) + if label: + return label + return "" + + +def _route_shell_drive(route_info, params_obj, model_names, is_metric): + segment_count = max(0, _safe_int(route_info.get("segmentCount", 0), 0)) + return { + "name": route_info.get("name", ""), + "date": _jsonable_time(route_info.get("startedAt")), + "distance": 0, + "distanceMeters": 0.0, + "duration": segment_count * 60, + "avgSpeed": 0, + "engagedPercent": 0, + "engagedSeconds": 0.0, + "model": _current_model_name(params_obj, model_names) or "Unknown model", + "segmentCount": segment_count, + "distractedMoments": 0, + "unresponsiveMoments": 0, + "routeModifiedAt": _safe_float(route_info.get("modifiedAt", 0.0), 0.0), + "attentionKnown": False, + } + + +def _drive_from_persistent_route(route_name, entry, is_metric): + duration = max(0, _safe_int(entry.get("duration", 0), 0)) + distance_m = max(0.0, _safe_float(entry.get("distanceMeters", 0.0), 0.0)) + engaged_seconds = max(0.0, _safe_float(entry.get("engagedSeconds", 0.0), 0.0)) + engaged_percent = round((engaged_seconds / duration) * 100) if duration > 0 else 0 + avg_speed = (distance_m / duration) * (CV.MS_TO_KPH if is_metric else METER_PER_SECOND_TO_MPH) if duration > 0 else 0.0 + return { + "name": route_name, + "date": entry.get("date", ""), + "distance": round(_distance_from_meters(distance_m, is_metric), 1), + "distanceMeters": round(distance_m, 1), + "duration": duration, + "avgSpeed": int(round(avg_speed)), + "engagedPercent": max(0, min(100, engaged_percent)), + "engagedSeconds": round(engaged_seconds, 1), + "model": _clean_model_label(entry.get("model", "")) or "Unknown model", + "segmentCount": max(0, _safe_int(entry.get("segmentCount", 0), 0)), + "distractedMoments": max(0, _safe_int(entry.get("distractedMoments", 0), 0)), + "unresponsiveMoments": max(0, _safe_int(entry.get("unresponsiveMoments", 0), 0)), + "routeModifiedAt": _safe_float(entry.get("modifiedAt", 0.0), 0.0), + "attentionKnown": bool(entry.get("attentionKnown", True)), + } + + +def _persistent_drives(stats, is_metric): + routes = stats.get("routes", {}) if isinstance(stats, dict) else {} + if not isinstance(routes, dict): + return [] + return [ + _drive_from_persistent_route(route_name, entry, is_metric) + for route_name, entry in routes.items() + if isinstance(entry, dict) + ] + + +def _merge_dashboard_drives(*drive_lists): + merged = {} + for drives in drive_lists: + for drive in drives or []: + route_name = str(drive.get("name", "")).strip() + if not route_name: + continue + existing = merged.get(route_name) + if existing is None: + merged[route_name] = dict(drive) + continue + + existing_distance = _safe_float(existing.get("distanceMeters", existing.get("distance", 0.0)), 0.0) + drive_distance = _safe_float(drive.get("distanceMeters", drive.get("distance", 0.0)), 0.0) + existing_attention = bool(existing.get("attentionKnown", False)) + drive_attention = bool(drive.get("attentionKnown", False)) + if drive_distance > existing_distance or (drive_attention and not existing_attention): + merged[route_name] = dict(drive) + + return sorted(merged.values(), key=_drive_sort_time, reverse=True) + + +def _analysis_candidates(route_infos, persistent_stats): + routes = persistent_stats.get("routes", {}) if isinstance(persistent_stats, dict) else {} + routes = routes if isinstance(routes, dict) else {} + + def needs_analysis(route_info): + route_name = route_info.get("name", "") + entry = routes.get(route_name, {}) + if not isinstance(entry, dict): + return True + if _safe_float(entry.get("modifiedAt", 0.0), 0.0) < _safe_float(route_info.get("modifiedAt", 0.0), 0.0): + return True + return not bool(entry.get("attentionKnown", True)) + + missing = [route_info for route_info in route_infos if needs_analysis(route_info)] + return missing + + +def _invalidate_dashboard_cache(): + _DASHBOARD_CACHE.update({ + "key": None, + "updated_at": 0.0, + "value": None, + }) + + +def warm_dashboard_stats(footage_paths=None): + params_obj = params + route_infos = _list_dashboard_routes(footage_paths or []) + + if not route_infos: + return + + is_metric = _params_get_bool(params_obj, "IsMetric") + model_names = _model_lookup(params_obj) + shell_drives = [ + _route_shell_drive(route_info, params_obj, model_names, is_metric) + for route_info in route_infos + ] + if shell_drives: + _update_dashboard_persistent_stats(params_obj, shell_drives, time.time()) + + persistent_stats = _load_dashboard_persistent_stats(params_obj) + candidates = _analysis_candidates(route_infos, persistent_stats)[:DASHBOARD_BACKGROUND_ROUTE_ANALYSIS_LIMIT] + for route_info in candidates: + sampled_route_info = _sample_route_info(route_info) + messages = _iter_route_log_messages(sampled_route_info) + drive = _analyze_route_messages(messages, sampled_route_info, model_names, is_metric) + _update_dashboard_persistent_stats(params_obj, [drive], time.time()) + + +def _dashboard_worker_env(repo_root): + env = os.environ.copy() + pythonpath = [ + "/usr/local/venv/lib/python3.12/site-packages", + str(repo_root / "starpilot" / "third_party"), + str(repo_root), + ] + if env.get("PYTHONPATH"): + pythonpath.append(env["PYTHONPATH"]) + env["PYTHONPATH"] = os.pathsep.join(pythonpath) + env.setdefault("OPENBLAS_NUM_THREADS", "1") + env.setdefault("OMP_NUM_THREADS", "1") + env.setdefault("MKL_NUM_THREADS", "1") + env.setdefault("NUMEXPR_NUM_THREADS", "1") + return env + + +def _start_dashboard_background_analysis(footage_paths, route_infos, persistent_stats): + global _DASHBOARD_ANALYZER_PROCESS + + if not route_infos or not _analysis_candidates(route_infos, persistent_stats): + return + + with _DASHBOARD_ANALYZER_LOCK: + if _DASHBOARD_ANALYZER_PROCESS is not None and _DASHBOARD_ANALYZER_PROCESS.poll() is None: + return + repo_root = Path(__file__).resolve().parents[3] + worker_code = ( + "import json, sys;" + "from openpilot.starpilot.system.the_pond import utilities;" + "utilities.warm_dashboard_stats(json.loads(sys.argv[1]))" + ) + command = [ + "nice", + "-n", + "19", + sys.executable or "python3", + "-c", + worker_code, + json.dumps([str(path) for path in (footage_paths or [])]), + ] + log_file = None + try: + log_file = open(DASHBOARD_ANALYZER_LOG_PATH, "ab") + _DASHBOARD_ANALYZER_PROCESS = subprocess.Popen( + command, + cwd=str(repo_root), + env=_dashboard_worker_env(repo_root), + stdout=log_file, + stderr=log_file, + start_new_session=True, + ) + except Exception: + _DASHBOARD_ANALYZER_PROCESS = None + finally: + if log_file is not None: + log_file.close() + + def _start_of_week(now): return datetime(now.year, now.month, now.day) - timedelta(days=now.weekday()) @@ -1324,13 +1538,15 @@ def _normalize_persistent_routes(raw_routes): "unresponsiveMoments": max(0, _safe_int(entry.get("unresponsiveMoments", 0), 0)), "model": _clean_model_label(entry.get("model", "")), "modelKey": canonical_model_key(entry.get("modelKey", "")), + "segmentCount": max(0, _safe_int(entry.get("segmentCount", 0), 0)), "modifiedAt": _safe_float(entry.get("modifiedAt", 0.0), 0.0), + "attentionKnown": bool(entry.get("attentionKnown", True)), } return routes def _load_dashboard_persistent_stats(params_obj): - data = _decode_json_param(_params_get_text(params_obj, DASHBOARD_PERSISTENT_STATS_PARAM, ""), {}) + data = _decode_json_param(_params_get_value(params_obj, DASHBOARD_PERSISTENT_STATS_PARAM, None), {}) if not isinstance(data, dict): data = {} data["version"] = 1 @@ -1408,10 +1624,11 @@ def _build_personal_records_raw(routes): days[day_key]["engaged"] += engaged_seconds weeks[week_key] = weeks.get(week_key, 0.0) + distance_m - if bool(entry.get("undistracted", False)) and duration > _safe_int(records["longestUndistractedDrive"].get("duration", 0), 0): + attention_known = bool(entry.get("attentionKnown", True)) + if attention_known and bool(entry.get("undistracted", False)) and duration > _safe_int(records["longestUndistractedDrive"].get("duration", 0), 0): records["longestUndistractedDrive"] = {"duration": duration, "date": date_text} - if bool(entry.get("clean", False)): + if bool(entry.get("clean", False)) and attention_known: if current_clean_streak == 0: current_clean_start = date_text current_clean_streak += 1 @@ -1421,7 +1638,7 @@ def _build_personal_records_raw(routes): "startDate": current_clean_start, "endDate": date_text, } - else: + elif attention_known: current_clean_streak = 0 current_clean_start = "" @@ -1539,19 +1756,41 @@ def _update_dashboard_persistent_stats(params_obj, drives, wall_now): model_name = _clean_model_label(drive.get("model", "")) model_key = _model_usage_key(model_name) + attention_known = bool(drive.get("attentionKnown", True)) next_entry = { "date": route_date, "distanceMeters": max(0.0, _safe_float(drive.get("distanceMeters", 0.0), 0.0)), "duration": max(0, _safe_int(drive.get("duration", 0), 0)), - "clean": _drive_is_clean(drive), - "undistracted": _drive_is_undistracted(drive), + "clean": attention_known and _drive_is_clean(drive), + "undistracted": attention_known and _drive_is_undistracted(drive), "engagedSeconds": max(0.0, _safe_float(drive.get("engagedSeconds", 0.0), 0.0)), "distractedMoments": max(0, _safe_int(drive.get("distractedMoments", 0), 0)), "unresponsiveMoments": max(0, _safe_int(drive.get("unresponsiveMoments", 0), 0)), "model": model_name, "modelKey": model_key, + "segmentCount": max(0, _safe_int(drive.get("segmentCount", 0), 0)), "modifiedAt": _safe_float(drive.get("routeModifiedAt", 0.0), 0.0), + "attentionKnown": attention_known, } + existing_entry = routes.get(route_name) + if isinstance(existing_entry, dict): + existing_distance = max(0.0, _safe_float(existing_entry.get("distanceMeters", 0.0), 0.0)) + next_distance = max(0.0, _safe_float(next_entry.get("distanceMeters", 0.0), 0.0)) + existing_current = _safe_float(existing_entry.get("modifiedAt", 0.0), 0.0) >= _safe_float(next_entry.get("modifiedAt", 0.0), 0.0) + existing_attention_known = bool(existing_entry.get("attentionKnown", True)) + if not attention_known and existing_current and existing_attention_known: + next_entry["clean"] = bool(existing_entry.get("clean", False)) + next_entry["undistracted"] = bool(existing_entry.get("undistracted", existing_entry.get("clean", False))) + next_entry["attentionKnown"] = True + if not attention_known and existing_current and existing_distance >= next_distance: + next_entry["distanceMeters"] = existing_distance + next_entry["duration"] = max(_safe_int(existing_entry.get("duration", 0), 0), next_entry["duration"]) + next_entry["engagedSeconds"] = max(0.0, _safe_float(existing_entry.get("engagedSeconds", 0.0), 0.0)) + next_entry["distractedMoments"] = max(0, _safe_int(existing_entry.get("distractedMoments", 0), 0)) + next_entry["unresponsiveMoments"] = max(0, _safe_int(existing_entry.get("unresponsiveMoments", 0), 0)) + if (not model_name or model_name == "Unknown model") and _clean_model_label(existing_entry.get("model", "")): + next_entry["model"] = _clean_model_label(existing_entry.get("model", "")) + next_entry["modelKey"] = canonical_model_key(existing_entry.get("modelKey", "")) or _model_usage_key(next_entry["model"]) if routes.get(route_name) != next_entry: routes[route_name] = next_entry changed = True @@ -1710,24 +1949,36 @@ def get_dashboard_stats(footage_paths, params_obj=None, now=None): model_names = _model_lookup(params_obj) analysis_deadline = cache_now + DASHBOARD_ANALYSIS_TIME_BUDGET_SECONDS + persistent_stats = _load_dashboard_persistent_stats(params_obj) + shell_drives = [ + _route_shell_drive(route_info, params_obj, model_names, is_metric) + for route_info in route_infos + ] + if shell_drives: + persistent_stats = _update_dashboard_persistent_stats(params_obj, shell_drives, time.time()) + analyzed_drives = [] - for route_info in route_infos[:DASHBOARD_ROUTE_ANALYSIS_LIMIT]: - if _deadline_reached(analysis_deadline): - break - sampled_route_info = _sample_route_info(route_info) - messages = _iter_route_log_messages(sampled_route_info, analysis_deadline) - analyzed_drives.append(_analyze_route_messages(messages, sampled_route_info, model_names, is_metric, analysis_deadline)) + if DASHBOARD_ROUTE_ANALYSIS_LIMIT > 0 and DASHBOARD_ANALYSIS_TIME_BUDGET_SECONDS > 0: + for route_info in _analysis_candidates(route_infos, persistent_stats)[:DASHBOARD_ROUTE_ANALYSIS_LIMIT]: + if _deadline_reached(analysis_deadline): + break + sampled_route_info = _sample_route_info(route_info) + messages = _iter_route_log_messages(sampled_route_info, analysis_deadline) + analyzed_drives.append(_analyze_route_messages(messages, sampled_route_info, model_names, is_metric, analysis_deadline)) + if analyzed_drives: + persistent_stats = _update_dashboard_persistent_stats(params_obj, analyzed_drives, time.time()) - persistent_stats = _update_dashboard_persistent_stats(params_obj, analyzed_drives, time.time()) + persisted_drives = _persistent_drives(persistent_stats, is_metric) + combined_drives = _merge_dashboard_drives(shell_drives, persisted_drives, analyzed_drives) - if not analyzed_drives: + if not combined_drives: dashboard = _dashboard_empty(is_metric, now, footage_paths, params_obj, persistent_stats) else: records = _display_personal_records(persistent_stats, is_metric) dashboard = { - "lastDrive": _public_drive(analyzed_drives[0], is_metric), - "recentDrives": [_public_drive(drive, is_metric) for drive in analyzed_drives[:DASHBOARD_RECENT_DRIVE_LIMIT]], - "week": _build_week_summary(analyzed_drives, now, is_metric), + "lastDrive": _public_drive(combined_drives[0], is_metric), + "recentDrives": [_public_drive(drive, is_metric) for drive in combined_drives[:DASHBOARD_RECENT_DRIVE_LIMIT]], + "week": _build_week_summary(combined_drives, now, is_metric), "records": records, "device": _build_device_summary(params_obj), "storage": _build_storage_summary(footage_paths), @@ -1739,6 +1990,7 @@ def get_dashboard_stats(footage_paths, params_obj=None, now=None): "updated_at": cache_now, "value": copy.deepcopy(dashboard), }) + _start_dashboard_background_analysis(footage_paths, route_infos, persistent_stats) return dashboard