From 2d93c29890e801029e1247c7e83af9ff157f1376 Mon Sep 17 00:00:00 2001 From: dirwin31 <83434411+dirwin31@users.noreply.github.com> Date: Thu, 27 Aug 2026 16:49:24 -0700 Subject: [PATCH] Allow Unpreserved Deletes Only --- .../components/recordings/dashcam_routes.css | 29 ++++++++++ .../components/recordings/dashcam_routes.js | 44 ++++++++------ .../the_galaxy/tests/test_dashboard_stats.py | 22 +++++++ .../the_galaxy/tests/test_dashcam_routes.py | 57 +++++++++++++++++++ starpilot/system/the_galaxy/the_galaxy.py | 44 ++++++++++++-- starpilot/system/the_galaxy/utilities.py | 28 +++++++-- 6 files changed, 197 insertions(+), 27 deletions(-) diff --git a/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.css b/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.css index 6bce9771a9..2791db18d6 100644 --- a/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.css +++ b/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.css @@ -1168,10 +1168,34 @@ .dashcam-danger-zone > div { display: flex; +} + +.dashcam-danger-copy { flex-direction: column; gap: var(--gap-xs); } +.dashcam-danger-actions { + align-items: center; + flex-wrap: wrap; + gap: var(--gap-sm); + justify-content: flex-end; +} + +.dashcam-danger-actions .delete-all-button { + margin: 0; +} + +.delete-all-button.delete-non-preserved-button { + background: transparent; + border: var(--border-width-thin) solid var(--danger-bg); + color: var(--danger-bg); +} + +.delete-all-button.delete-non-preserved-button:hover { + background: rgba(224, 85, 119, 0.15); +} + .dashcam-danger-zone span { color: var(--text-muted); font-size: var(--font-size-sm); @@ -1396,6 +1420,11 @@ flex-direction: column; } + .dashcam-danger-actions { + align-items: stretch; + flex-direction: column; + } + .dashcam-toolbar { flex-direction: column; align-items: stretch; diff --git a/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.js b/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.js index 10f532df77..8f426608c2 100644 --- a/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.js +++ b/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.js @@ -25,7 +25,7 @@ const state = reactive({ viewMode: "list", progress: 0, total: 0, - showDeleteAllModal: false, + deleteMode: null, isDeletingAll: false, }) @@ -129,7 +129,11 @@ function replaceRoute(updatedRoute) { const existing = state.routes.find(route => route.name === updatedRoute.name) if (existing) Object.assign(existing, updatedRoute) const selected = state.selectedRoute - if (selected?.name === updatedRoute.name && selected !== existing) Object.assign(selected, updatedRoute) + if (selected?.name === updatedRoute.name && selected !== existing) { + Object.assign(selected, updatedRoute) + } + // Reassigning state.routes notifies ArrowJS to re-render views that depend on the routes array + state.routes = [...state.routes] } async function deleteRoute(route) { @@ -749,16 +753,17 @@ async function togglePreserved(route, event) { } } -async function deleteAllRoutes() { - state.showDeleteAllModal = false +async function deleteAllRoutes(includePreserved) { + state.deleteMode = null state.isDeletingAll = true try { - const response = await fetch("/api/routes/delete_all", { method: "DELETE" }) - if (!response.ok) throw new Error() + const response = await fetch(`/api/routes/delete_all?include_preserved=${includePreserved}`, { method: "DELETE" }) + const payload = await response.json().catch(() => ({})) + if (!response.ok) throw new Error(payload.error || "Route deletion failed") await refresh() - showSnackbar("All routes deleted!") - } catch (_) { - showSnackbar("An error occurred while deleting all routes...", "error") + showSnackbar(payload.message || "Routes deleted!") + } catch (error) { + showSnackbar(error?.message || "An error occurred while deleting routes...", "error") } finally { state.isDeletingAll = false } @@ -942,16 +947,21 @@ export function RouteRecordings() { ${() => state.routes.length ? html` ` : ""} - ${() => state.showDeleteAllModal ? Modal({ - title: "Confirm Delete All", - message: "Are you sure you want to delete all routes? This action cannot be undone...", - onConfirm: deleteAllRoutes, - onCancel: () => { state.showDeleteAllModal = false }, - confirmText: "Delete All", + ${() => state.deleteMode ? Modal({ + title: state.deleteMode === "all" ? "Delete All Routes, Including Preserved?" : "Delete All Non-Preserved Routes?", + message: state.deleteMode === "all" + ? "This permanently deletes every local route, including preserved routes. This action cannot be undone." + : "This permanently deletes every non-preserved local route. Preserved routes will be kept.", + onConfirm: () => deleteAllRoutes(state.deleteMode === "all"), + onCancel: () => { state.deleteMode = null }, + confirmText: state.deleteMode === "all" ? "Delete Everything" : "Delete Non-Preserved", }) : ""} ` } diff --git a/starpilot/system/the_galaxy/tests/test_dashboard_stats.py b/starpilot/system/the_galaxy/tests/test_dashboard_stats.py index 2f50f7ab8b..4f5d21c3d9 100644 --- a/starpilot/system/the_galaxy/tests/test_dashboard_stats.py +++ b/starpilot/system/the_galaxy/tests/test_dashboard_stats.py @@ -1091,6 +1091,28 @@ def test_clear_dashboard_route_history_keeps_durable_records(tmp_path, monkeypat assert stats["modelUsage"]["orion"]["drives"] == 3 +def test_clear_dashboard_route_history_can_retain_preserved_routes(tmp_path, monkeypatch): + monkeypatch.setattr(utilities, "DASHBOARD_PARAMS_DIR", tmp_path) + params = FakeParams({ + utilities.DASHBOARD_PERSISTENT_STATS_PARAM: { + "routes": { + "0000006a--9f0a7bdf9c": {"date": "2026-06-15T08:00:00"}, + "0000006b--9f0a7bdf9d": {"date": "2026-06-16T08:00:00"}, + }, + "ignoredRoutes": ["0000006a--9f0a7bdf9c", "0000006b--9f0a7bdf9d"], + "personalRecords": {"cleanDriveStreak": {"drives": 4}}, + }, + }) + + removed = utilities.clear_dashboard_route_history(params, retained_route_names={"0000006a--9f0a7bdf9c"}) + + assert removed == 1 + stats = utilities._load_dashboard_persistent_stats(params) + assert list(stats["routes"]) == ["0000006a--9f0a7bdf9c"] + assert stats["ignoredRoutes"] == ["0000006a--9f0a7bdf9c"] + assert stats["personalRecords"]["cleanDriveStreak"]["drives"] == 4 + + def test_lightweight_routes_surface_recent_drives_without_log_analysis(monkeypatch): utilities._invalidate_dashboard_cache() now = utilities.datetime.now().replace(hour=12, minute=0, second=0, microsecond=0) diff --git a/starpilot/system/the_galaxy/tests/test_dashcam_routes.py b/starpilot/system/the_galaxy/tests/test_dashcam_routes.py index 3c025d381b..cb5d9e51d6 100644 --- a/starpilot/system/the_galaxy/tests/test_dashcam_routes.py +++ b/starpilot/system/the_galaxy/tests/test_dashcam_routes.py @@ -393,6 +393,63 @@ def test_preserve_limit_counts_routes_not_segments(monkeypatch, tmp_path): assert client.post("/api/routes/00000099--9f0a7bdf9c/preserve").status_code == 400 +def test_delete_all_non_preserved_keeps_entire_preserved_route_across_roots(monkeypatch, tmp_path): + standard = tmp_path / "standard" + high_resolution = tmp_path / "high_resolution" + preserved_route = ROUTE_NAME + ordinary_route = "0000006b--9f0a7bdf9d" + preserved_marker = _make_segment(standard, preserved_route, 3) + preserved_other_root = _make_segment(high_resolution, preserved_route, 4) + ordinary_standard = _make_segment(standard, ordinary_route, 0) + ordinary_other_root = _make_segment(high_resolution, ordinary_route, 1) + unrelated = high_resolution / "video_cache" + unrelated.mkdir() + + client = _make_client(monkeypatch, standard) + monkeypatch.setattr(the_galaxy, "FOOTAGE_PATHS", [str(standard), str(high_resolution)]) + monkeypatch.setattr(utilities, "has_preserve_attr", lambda path: path == str(preserved_marker)) + monkeypatch.setattr(utilities, "stop_dashboard_background_analysis", lambda: None) + monkeypatch.setattr(the_galaxy, "delete_file", lambda path: Path(path).rmdir()) + history_calls = [] + monkeypatch.setattr(utilities, "clear_dashboard_route_history", lambda params, retained_route_names=None: history_calls.append(retained_route_names) or 1) + factory_delete_calls = [] + monkeypatch.setattr(the_galaxy, "_run_factory_reset_delete", factory_delete_calls.append) + + response = client.delete("/api/routes/delete_all?include_preserved=false") + + assert response.status_code == 200 + assert response.get_json()["deletedRoutes"] == 1 + assert response.get_json()["preservedRoutes"] == 1 + assert preserved_marker.is_dir() + assert preserved_other_root.is_dir() + assert not ordinary_standard.exists() + assert not ordinary_other_root.exists() + assert unrelated.is_dir() + assert history_calls == [{preserved_route}] + assert factory_delete_calls == [] + + +def test_delete_all_including_preserved_keeps_existing_full_wipe_behavior(monkeypatch, tmp_path): + first_root = tmp_path / "standard" + second_root = tmp_path / "high_resolution" + _make_segment(first_root) + _make_segment(second_root) + client = _make_client(monkeypatch, first_root) + monkeypatch.setattr(the_galaxy, "FOOTAGE_PATHS", [str(first_root) + "/", str(second_root), str(first_root)]) + monkeypatch.setattr(utilities, "stop_dashboard_background_analysis", lambda: None) + history_calls = [] + monkeypatch.setattr(utilities, "clear_dashboard_route_history", lambda params, retained_route_names=None: history_calls.append(retained_route_names) or 2) + factory_delete_calls = [] + monkeypatch.setattr(the_galaxy, "_run_factory_reset_delete", factory_delete_calls.append) + + response = client.delete("/api/routes/delete_all?include_preserved=true") + + assert response.status_code == 200 + assert factory_delete_calls == [str(first_root), str(second_root)] + assert history_calls == [None] + assert "including preserved routes" in response.get_json()["message"] + + def test_video_cache_evicts_oldest_instead_of_wiping_everything(monkeypatch, tmp_path): """A tight disk used to delete every cached mp4, so playback re-muxed on every request.""" cache = tmp_path / "video_cache" diff --git a/starpilot/system/the_galaxy/the_galaxy.py b/starpilot/system/the_galaxy/the_galaxy.py index 92d3e73feb..6ce9b21714 100644 --- a/starpilot/system/the_galaxy/the_galaxy.py +++ b/starpilot/system/the_galaxy/the_galaxy.py @@ -6366,6 +6366,7 @@ def setup(app): try: utilities.stop_dashboard_background_analysis() + include_preserved = request.args.get("include_preserved", "true").strip().lower() not in ("0", "false", "no", "off") route_paths = [] seen_paths = set() @@ -6375,18 +6376,51 @@ def setup(app): seen_paths.add(path) route_paths.append(path) - for route_path in route_paths: - _run_factory_reset_delete(route_path) + preserved_route_names = set() + deleted_route_names = set() + if include_preserved: + for route_path in route_paths: + _run_factory_reset_delete(route_path) + else: + # The preserve xattr lives on one segment, but preservation applies to the + # whole route in every footage root. + for route_path in route_paths: + if not os.path.isdir(route_path): + continue + for segment in os.listdir(route_path): + if utilities.SEGMENT_RE.fullmatch(segment) and utilities.has_preserve_attr(os.path.join(route_path, segment)): + preserved_route_names.add(segment.rsplit("--", 1)[0]) - persisted_route_count = utilities.clear_dashboard_route_history(params) + for route_path in route_paths: + if not os.path.isdir(route_path): + continue + for segment in os.listdir(route_path): + if not utilities.SEGMENT_RE.fullmatch(segment): + continue + route_name = segment.rsplit("--", 1)[0] + if route_name in preserved_route_names: + continue + delete_file(os.path.join(route_path, segment)) + deleted_route_names.add(route_name) + + persisted_route_count = utilities.clear_dashboard_route_history( + params, + retained_route_names=preserved_route_names if not include_preserved else None, + ) _STATS_RESPONSE_CACHE.update({ "updated_at": 0.0, "payload": None, }) return jsonify({ "success": True, - "message": "All local driving routes deleted. Saved personal records were kept.", - "deletedPaths": len(route_paths), + "message": ( + "All local driving routes deleted, including preserved routes. Saved personal records were kept." + if include_preserved else + "All non-preserved local driving routes deleted. Preserved routes were kept." + ), + "deletedPaths": len(route_paths) if include_preserved else 0, + "deletedRoutes": len(deleted_route_names) if not include_preserved else None, + "preservedRoutes": len(preserved_route_names) if not include_preserved else 0, "clearedDashboardRoutes": persisted_route_count, }), 200 except Exception as exception: diff --git a/starpilot/system/the_galaxy/utilities.py b/starpilot/system/the_galaxy/utilities.py index 1de1ea21a0..7cd952daa9 100644 --- a/starpilot/system/the_galaxy/utilities.py +++ b/starpilot/system/the_galaxy/utilities.py @@ -1773,12 +1773,30 @@ def _invalidate_dashboard_cache(): }) -def clear_dashboard_route_history(params_obj): - """Remove route-backed dashboard history while keeping durable records.""" +def clear_dashboard_route_history(params_obj, retained_route_names=None): + """Remove route-backed dashboard history while keeping durable records and optional retained routes.""" stats = _load_dashboard_persistent_stats(params_obj) - route_count = len(stats.get("routes", {})) - stats["routes"] = {} - stats["ignoredRoutes"] = [] + routes = stats.get("routes", {}) + retained_routes = None if retained_route_names is None else { + str(route_name or "").strip() + for route_name in retained_route_names + if ROUTE_RE.fullmatch(str(route_name or "").strip()) + } + if retained_routes is None: + stats["routes"] = {} + stats["ignoredRoutes"] = [] + else: + stats["routes"] = { + route_name: entry + for route_name, entry in routes.items() + if route_name in retained_routes + } + stats["ignoredRoutes"] = [ + route_name + for route_name in stats.get("ignoredRoutes", []) + if route_name in retained_routes + ] + route_count = len(routes) - len(stats["routes"]) serialized = json.dumps(stats, separators=(",", ":")) persisted_to_params = False