Allow Unpreserved Deletes Only

This commit is contained in:
dirwin31
2026-08-27 16:49:24 -07:00
parent c0b8f1cb01
commit 2d93c29890
6 changed files with 197 additions and 27 deletions
@@ -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;
@@ -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`
<footer class="dashcam-danger-zone">
<div><strong>Delete all local routes</strong><span>Preserved routes are included.</span></div>
<button class="delete-all-button" type="button" @click="${() => { state.showDeleteAllModal = true }}" disabled="${() => state.isDeletingAll || false}">${() => state.isDeletingAll ? "Deleting…" : "Delete All"}</button>
<div class="dashcam-danger-copy"><strong>Delete local routes</strong><span>Keep preserved routes, or remove everything.</span></div>
<div class="dashcam-danger-actions">
<button class="delete-all-button delete-non-preserved-button" type="button" @click="${() => { state.deleteMode = "non-preserved" }}" disabled="${() => state.isDeletingAll || state.routes.every(route => route.is_preserved) || false}">${() => state.isDeletingAll ? "Deleting…" : "Delete Non-Preserved"}</button>
<button class="delete-all-button" type="button" @click="${() => { state.deleteMode = "all" }}" disabled="${() => state.isDeletingAll || false}">${() => state.isDeletingAll ? "Deleting…" : "Delete All Including Preserved"}</button>
</div>
</footer>` : ""}
</section>
${() => 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",
}) : ""}
</div>`
}
@@ -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)
@@ -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"
+39 -5
View File
@@ -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:
+23 -5
View File
@@ -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