mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-10 10:13:45 +08:00
Allow Unpreserved Deletes Only
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user