home cleanup

This commit is contained in:
firestar5683
2026-07-20 23:53:20 -05:00
parent 104ae74901
commit cc40a28562
4 changed files with 264 additions and 42 deletions
@@ -113,6 +113,60 @@ def test_drive_stats_falls_back_to_local_totals_and_computed_records():
assert data.records[5].value == "2 drives"
def test_drive_stats_repairs_touched_filesystem_routes_without_moving_them_to_today():
params = FakeParams({
"ApiCache_DriveStats": {
"week": {"distance": 302.9, "routes": 31, "minutes": 467},
},
"GalaxyDashboardStats": {
"routes": {
"000011e4--2ed59ee965": {
"date": "2026-07-18T08:59:31",
"distanceMeters": 1609.344,
"duration": 600,
"timeSource": "log",
},
"000011e5--92dc4759b2": {
"date": "2026-07-20T11:33:49",
"distanceMeters": 16093.44,
"duration": 1800,
"timeSource": "filesystem",
},
"000011e6--8b54c54356": {
"date": "2026-07-19T07:03:36",
"distanceMeters": 3218.688,
"duration": 900,
"timeSource": "log",
},
"000011e8--09c2203d2e": {
"date": "2026-07-19T18:17:44",
"distanceMeters": 1609.344,
"duration": 600,
"timeSource": "log",
},
"000011e9--a3a4dcd6ef": {
"date": "2026-07-20T11:38:53",
"distanceMeters": 32186.88,
"duration": 1800,
"timeSource": "filesystem",
},
"000011ea--00b28940d5": {
"date": "2026-07-20T02:18:17",
"distanceMeters": 8046.72,
"duration": 900,
"timeSource": "log",
},
},
},
})
data = load_drive_stats_data(params, now=datetime(2026, 7, 20, 12, 0, 0))
assert data.past_week.distance == 302.9
assert round(data.this_week.distance, 1) == 25.0
assert round(data.daily_distance[0].distance, 1) == 25.0
def test_drive_stats_uses_clean_empty_record_labels():
data = load_drive_stats_data(FakeParams({}), now=datetime(2026, 7, 20, 12, 0, 0))
+103 -32
View File
@@ -63,6 +63,7 @@ class DriveStatsData:
@dataclass
class _RouteStat:
name: str
date: datetime
distance_meters: float
duration: int
@@ -70,6 +71,7 @@ class _RouteStat:
attention_known: bool
clean: bool
undistracted: bool
time_source: str
def _json_object(value: Any) -> dict[str, Any]:
@@ -128,6 +130,66 @@ def _summary_has_data(summary: DriveSummary) -> bool:
return summary.drives > 0 or summary.distance > 0.0 or summary.hours > 0.0
def _route_counter(route_name: str) -> int | None:
prefix = route_name.split("--", 1)[0]
try:
return int(prefix, 16) if len(prefix) == 8 else None
except ValueError:
return None
def _repair_filesystem_route_dates(routes: list[_RouteStat]) -> None:
sequenced = [
(counter, route)
for route in routes
if (counter := _route_counter(route.name)) is not None
]
sequenced.sort(key=lambda item: item[0])
if sum(route.time_source == "log" for _, route in sequenced) < 2:
return
previous_anchors = []
previous_anchor = None
for counter, route in sequenced:
previous_anchors.append(previous_anchor)
if route.time_source == "log":
previous_anchor = (counter, route)
following_anchors = [None] * len(sequenced)
following_anchor = None
for index in range(len(sequenced) - 1, -1, -1):
counter, route = sequenced[index]
following_anchors[index] = following_anchor
if route.time_source == "log":
following_anchor = (counter, route)
for index, (counter, route) in enumerate(sequenced):
if route.time_source != "filesystem":
continue
previous = previous_anchors[index]
following = following_anchors[index]
if previous is None or following is None:
continue
previous_counter, previous_route = previous
following_counter, following_route = following
if previous_route.date > following_route.date or previous_route.date <= route.date <= following_route.date:
continue
if following_counter == counter + 1:
inferred = following_route.date - timedelta(seconds=route.duration + 5 * 60)
elif previous_counter == counter - 1:
inferred = previous_route.date + timedelta(seconds=previous_route.duration + 5 * 60)
else:
progress = (counter - previous_counter) / (following_counter - previous_counter)
inferred = previous_route.date + (following_route.date - previous_route.date) * progress
lower_bound = previous_route.date + timedelta(seconds=1)
upper_bound = following_route.date - timedelta(seconds=1)
route.date = min(max(inferred, lower_bound), upper_bound)
def _load_routes(galaxy_stats: dict[str, Any]) -> list[_RouteStat]:
raw_routes = galaxy_stats.get("routes", {})
if not isinstance(raw_routes, dict):
@@ -147,6 +209,7 @@ def _load_routes(galaxy_stats: dict[str, Any]) -> list[_RouteStat]:
distracted = max(0, int(_number(entry.get("distractedMoments", 0))))
unresponsive = max(0, int(_number(entry.get("unresponsiveMoments", 0))))
routes.append(_RouteStat(
name=str(route_name),
date=route_date,
distance_meters=max(0.0, _number(entry.get("distanceMeters", 0.0))),
duration=max(0, int(_number(entry.get("duration", 0)))),
@@ -154,7 +217,9 @@ def _load_routes(galaxy_stats: dict[str, Any]) -> list[_RouteStat]:
attention_known=attention_known,
clean=bool(entry.get("clean", unresponsive == 0)),
undistracted=bool(entry.get("undistracted", distracted == 0)),
time_source=str(entry.get("timeSource", "") or ""),
))
_repair_filesystem_route_dates(routes)
return sorted(routes, key=lambda route: route.date)
@@ -499,44 +564,45 @@ class DriveStatsDashboard:
cx = rect.x + rect.width / 2
cy = rect.y + rect.height / 2
color = PURPLE
thickness = 2.5
scale = min(rect.width, rect.height) / 64.0
thickness = 2.5 * scale
def line(x1: float, y1: float, x2: float, y2: float) -> None:
rl.draw_line_ex(rl.Vector2(x1, y1), rl.Vector2(x2, y2), thickness, color)
if index == 0:
line(cx - 13, cy, cx + 12, cy)
line(cx + 12, cy, cx + 5, cy - 7)
line(cx + 12, cy, cx + 5, cy + 7)
line(cx - 13 * scale, cy, cx + 12 * scale, cy)
line(cx + 12 * scale, cy, cx + 5 * scale, cy - 7 * scale)
line(cx + 12 * scale, cy, cx + 5 * scale, cy + 7 * scale)
elif index == 1:
rl.draw_circle_lines(int(cx), int(cy), 13, color)
rl.draw_circle_lines(int(cx), int(cy), 12, color)
line(cx - 7, cy, cx - 2, cy + 5)
line(cx - 2, cy + 5, cx + 8, cy - 7)
rl.draw_circle_lines(int(cx), int(cy), 13 * scale, color)
rl.draw_circle_lines(int(cx), int(cy), 12 * scale, color)
line(cx - 7 * scale, cy, cx - 2 * scale, cy + 5 * scale)
line(cx - 2 * scale, cy + 5 * scale, cx + 8 * scale, cy - 7 * scale)
elif index == 2:
line(cx - 13, cy - 12, cx - 13, cy + 12)
line(cx - 13, cy + 12, cx + 13, cy + 12)
line(cx - 9, cy + 6, cx - 2, cy - 2)
line(cx - 2, cy - 2, cx + 4, cy + 3)
line(cx + 4, cy + 3, cx + 13, cy - 8)
line(cx - 13 * scale, cy - 12 * scale, cx - 13 * scale, cy + 12 * scale)
line(cx - 13 * scale, cy + 12 * scale, cx + 13 * scale, cy + 12 * scale)
line(cx - 9 * scale, cy + 6 * scale, cx - 2 * scale, cy - 2 * scale)
line(cx - 2 * scale, cy - 2 * scale, cx + 4 * scale, cy + 3 * scale)
line(cx + 4 * scale, cy + 3 * scale, cx + 13 * scale, cy - 8 * scale)
elif index == 3:
points = (
(cx + 2, cy - 15), (cx - 10, cy + 2), (cx - 2, cy + 2),
(cx - 5, cy + 15), (cx + 11, cy - 5), (cx + 3, cy - 5),
(cx + 2 * scale, cy - 15 * scale), (cx - 10 * scale, cy + 2 * scale), (cx - 2 * scale, cy + 2 * scale),
(cx - 5 * scale, cy + 15 * scale), (cx + 11 * scale, cy - 5 * scale), (cx + 3 * scale, cy - 5 * scale),
)
for point_index, point in enumerate(points):
next_point = points[(point_index + 1) % len(points)]
line(point[0], point[1], next_point[0], next_point[1])
elif index == 4:
points = (
(cx, cy - 14), (cx + 12, cy - 9), (cx + 10, cy + 3),
(cx, cy + 14), (cx - 10, cy + 3), (cx - 12, cy - 9),
(cx, cy - 14 * scale), (cx + 12 * scale, cy - 9 * scale), (cx + 10 * scale, cy + 3 * scale),
(cx, cy + 14 * scale), (cx - 10 * scale, cy + 3 * scale), (cx - 12 * scale, cy - 9 * scale),
)
for point_index, point in enumerate(points):
next_point = points[(point_index + 1) % len(points)]
line(point[0], point[1], next_point[0], next_point[1])
line(cx - 5, cy, cx - 1, cy + 4)
line(cx - 1, cy + 4, cx + 6, cy - 4)
line(cx - 5 * scale, cy, cx - scale, cy + 4 * scale)
line(cx - scale, cy + 4 * scale, cx + 6 * scale, cy - 4 * scale)
else:
def sparkle(x: float, y: float, radius: float) -> None:
inner = radius * 0.22
@@ -550,9 +616,9 @@ class DriveStatsDashboard:
next_point = points[(point_index + 1) % len(points)]
line(point[0], point[1], next_point[0], next_point[1])
sparkle(cx + 3, cy + 2, 10)
sparkle(cx - 9, cy - 9, 5)
sparkle(cx + 12, cy - 10, 4)
sparkle(cx + 3 * scale, cy + 2 * scale, 10 * scale)
sparkle(cx - 9 * scale, cy - 9 * scale, 5 * scale)
sparkle(cx + 12 * scale, cy - 10 * scale, 4 * scale)
def _draw_fitted_centered(self, text: str, rect: rl.Rectangle, font_size: int, minimum_size: int, color: rl.Color) -> None:
size = font_size
@@ -685,19 +751,24 @@ class DriveStatsDashboard:
if row_index > 0:
rl.draw_line(int(rect.x + 24), int(row_y), int(rect.x + rect.width - 24), int(row_y), TRACK_COLOR)
icon_size = min(84.0, row_height - 44)
icon_size = min(120.0, row_height - 34)
icon_rect = rl.Rectangle(rect.x + 28, row_y + (row_height - icon_size) / 2, icon_size, icon_size)
self._draw_record_icon(record_index, icon_rect)
text_x = rect.x + 132
text_x = rect.x + 170
content_center_y = row_y + row_height / 2
title_pos = rl.Vector2(text_x, content_center_y - 56)
rl.draw_text_ex(self._font_medium, record.title, title_pos, 31, 0, MUTED_COLOR)
title_pos = rl.Vector2(text_x, content_center_y - 66)
rl.draw_text_ex(self._font_medium, record.title, title_pos, 38, 0, MUTED_COLOR)
value_pos = rl.Vector2(text_x, content_center_y - 8)
rl.draw_text_ex(self._font_bold, record.value, value_pos, 44, 0, TEXT_COLOR)
value_pos = rl.Vector2(text_x, content_center_y - 10)
rl.draw_text_ex(self._font_bold, record.value, value_pos, 56, 0, TEXT_COLOR)
detail_size = measure_text_cached(self._font_medium, record.detail, 27)
detail_x = max(text_x + 240, rect.x + rect.width - detail_size.x - 30)
detail_pos = rl.Vector2(detail_x, content_center_y - detail_size.y / 2 + 12)
rl.draw_text_ex(self._font_medium, record.detail, detail_pos, 27, 0, MUTED_COLOR)
detail_font_size = 34
detail_min_x = text_x + 250
detail_width = rect.x + rect.width - detail_min_x - 30
while detail_font_size > 29 and measure_text_cached(self._font_medium, record.detail, detail_font_size).x > detail_width:
detail_font_size -= 1
detail_size = measure_text_cached(self._font_medium, record.detail, detail_font_size)
detail_x = rect.x + rect.width - detail_size.x - 30
detail_pos = rl.Vector2(detail_x, content_center_y + 52)
rl.draw_text_ex(self._font_medium, record.detail, detail_pos, detail_font_size, 0, MUTED_COLOR)
@@ -1,5 +1,6 @@
import importlib.util
import json
import os
import sys
from pathlib import Path
@@ -457,6 +458,27 @@ def test_route_listing_prefers_segment_candidates_with_logs(tmp_path):
assert routes[0]["segments"][0]["path"] == standard_segment
def test_route_listing_uses_all_segment_times_when_segment_zero_was_touched(tmp_path):
route_start = utilities.datetime(2026, 7, 18, 7, 19, 0)
route_name = "000011e3--6e01289631"
for segment_num in range(2):
segment = tmp_path / f"{route_name}--{segment_num}"
segment.mkdir()
segment_end = route_start.timestamp() + (segment_num + 1) * 60
os.utime(segment, (segment_end, segment_end))
touched_segment = tmp_path / f"{route_name}--0"
touched_time = utilities.datetime(2026, 7, 20, 12, 8, 38).timestamp()
os.utime(touched_segment, (touched_time, touched_time))
routes = utilities._list_dashboard_routes([tmp_path])
assert routes[0]["startedAt"] == route_start
start, end = utilities._route_time_range(routes[0], 180)
assert start == "2026-07-18T07:19:00"
assert end == "2026-07-18T07:22:00"
def test_top_models_are_ranked_from_persisted_usage_not_favorites():
params = FakeParams({
"AvailableModels": "orion,vega,atlas,nova",
@@ -1337,6 +1359,52 @@ def test_invalid_filesystem_time_requests_reanalysis():
assert utilities._analysis_candidates([route], stats) == [route]
def test_corrected_filesystem_time_replaces_touched_persisted_time_without_losing_stats():
params = FakeParams({
utilities.DASHBOARD_PERSISTENT_STATS_PARAM: {
"routes": {
"000011e5--92dc4759b2": {
"date": "2026-07-20T11:33:49",
"endDate": "2026-07-20T12:08:38",
"distanceMeters": 45919.2,
"duration": 2089,
"engagedSeconds": 1200.0,
"model": "Pop Model V2",
"modifiedAt": 100.0,
"timeSource": utilities.DASHBOARD_TIME_SOURCE_FILESYSTEM,
"attentionKnown": True,
"analysisComplete": True,
"analysisVersion": utilities.DASHBOARD_ROUTE_ANALYSIS_VERSION,
},
},
},
})
corrected_shell = {
"name": "000011e5--92dc4759b2",
"date": "2026-07-19T04:55:02",
"endDate": "2026-07-19T05:30:02",
"distanceMeters": 0.0,
"duration": 2100,
"engagedSeconds": 0.0,
"model": "Unknown model",
"segmentCount": 35,
"routeModifiedAt": 100.0,
"timeSource": utilities.DASHBOARD_TIME_SOURCE_FILESYSTEM,
"attentionKnown": False,
"analysisComplete": False,
"analysisVersion": 0,
}
stats = utilities._update_dashboard_persistent_stats(params, [corrected_shell], wall_now=1000.0)
corrected = stats["routes"]["000011e5--92dc4759b2"]
assert corrected["date"] == "2026-07-19T04:55:02"
assert corrected["endDate"] == "2026-07-19T05:29:51"
assert corrected["distanceMeters"] == 45919.2
assert corrected["duration"] == 2089
assert corrected["model"] == "Pop Model V2"
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/"
+39 -10
View File
@@ -87,6 +87,7 @@ DASHBOARD_TIME_SOURCE_FILESYSTEM = "filesystem"
DASHBOARD_MIN_VALID_ROUTE_TIME = datetime(2026, 1, 1)
DASHBOARD_ROUTE_FUTURE_GRACE_SECONDS = 6 * 60 * 60
DASHBOARD_LOCAL_ROUTE_MAX_AGE_SECONDS = 45 * 24 * 60 * 60
DASHBOARD_ROUTE_TIME_REPAIR_THRESHOLD_SECONDS = 5 * 60
XOR_KEY = "s8#pL3*Xj!aZ@dWq"
@@ -992,6 +993,19 @@ def _select_dashboard_segment_candidate(candidates):
return next((candidate for candidate in candidates if _segment_has_dashboard_log(candidate)), candidates[0])
def _estimate_route_started_at(segments):
estimates = []
for segment in segments:
segment_num = max(0, _safe_int(segment.get("num", 0), 0))
# Segment directory mtimes normally land at the end of their one-minute segment.
estimate = _segment_mtime(segment.get("path")) - (segment_num + 1) * 60
parsed = _timestamp_to_dashboard_time(estimate, require_recent=True)
if parsed is not None:
estimates.append(parsed.timestamp())
# Dashboard analysis can touch a segment directory later, but cannot make it older.
return datetime.fromtimestamp(min(estimates)) if estimates else None
def _list_dashboard_routes(footage_paths, limit=DASHBOARD_ROUTE_SCAN_LIMIT):
routes = {}
@@ -1034,11 +1048,7 @@ def _list_dashboard_routes(footage_paths, limit=DASHBOARD_ROUTE_SCAN_LIMIT):
if not segments:
continue
first_segment = next((segment for segment in segments if segment["num"] == 0), segments[0])
try:
started_at = _timestamp_to_dashboard_time(first_segment["path"].stat().st_mtime, require_recent=True)
except OSError:
started_at = None
started_at = _estimate_route_started_at(segments)
route_infos.append({
"name": route["name"],
@@ -1418,14 +1428,16 @@ def _public_drive(drive, is_metric):
def _route_time_range(route_info, duration_seconds):
modified_at = _safe_float(route_info.get("modifiedAt", 0.0), 0.0)
duration_seconds = max(0.0, _safe_float(duration_seconds, 0.0))
started_at = route_info.get("startedAt")
if _dashboard_time_is_valid(started_at, require_recent=True):
end_time = started_at + timedelta(seconds=duration_seconds) if duration_seconds > 0.0 else None
return _jsonable_time(started_at), _jsonable_time(end_time)
modified_time = _timestamp_to_dashboard_time(modified_at, require_recent=True)
if modified_time is not None and duration_seconds > 0.0:
end_time = modified_time
start_time = end_time - timedelta(seconds=duration_seconds)
return _jsonable_time(start_time), _jsonable_time(end_time)
started_at = route_info.get("startedAt")
if _dashboard_time_is_valid(started_at, require_recent=True):
return _jsonable_time(started_at), _jsonable_time(modified_time) if modified_time is not None else ""
return "", ""
@@ -2414,6 +2426,19 @@ def _drive_time_reliable_for_persistence(drive):
return bool(date_text)
def _filesystem_route_time_changed(existing_entry, next_entry):
if (
str(existing_entry.get("timeSource", "") or "") != DASHBOARD_TIME_SOURCE_FILESYSTEM
or str(next_entry.get("timeSource", "") or "") != DASHBOARD_TIME_SOURCE_FILESYSTEM
):
return False
existing_date = _coerce_dashboard_time(existing_entry.get("date", ""))
next_date = _coerce_dashboard_time(next_entry.get("date", ""))
if existing_date is None or next_date is None:
return False
return abs((next_date - existing_date).total_seconds()) > DASHBOARD_ROUTE_TIME_REPAIR_THRESHOLD_SECONDS
def _update_dashboard_persistent_stats(params_obj, drives, wall_now):
stats = _load_dashboard_persistent_stats(params_obj)
before = json.dumps(stats, sort_keys=True, separators=(",", ":"))
@@ -2453,6 +2478,7 @@ def _update_dashboard_persistent_stats(params_obj, drives, wall_now):
next_entry["analysisVersion"] = DASHBOARD_ROUTE_ANALYSIS_VERSION
existing_entry = routes.get(route_name)
if isinstance(existing_entry, dict):
replace_filesystem_time = _filesystem_route_time_changed(existing_entry, next_entry)
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)
@@ -2471,11 +2497,14 @@ def _update_dashboard_persistent_stats(params_obj, drives, wall_now):
next_entry["distanceMeters"] = existing_distance
existing_duration = _safe_int(existing_entry.get("duration", 0), 0)
next_entry["duration"] = existing_duration if existing_attention_known else max(existing_duration, next_entry["duration"])
if existing_attention_known and str(existing_entry.get("date", "")).strip():
if existing_attention_known and str(existing_entry.get("date", "")).strip() and not replace_filesystem_time:
next_entry["date"] = str(existing_entry.get("date", "")).strip()
next_entry["timeSource"] = str(existing_entry.get("timeSource", "") or next_entry["timeSource"])
if str(existing_entry.get("endDate", "")).strip():
if str(existing_entry.get("endDate", "")).strip() and not replace_filesystem_time:
next_entry["endDate"] = str(existing_entry.get("endDate", "")).strip()
elif replace_filesystem_time:
corrected_start = _coerce_dashboard_time(next_entry.get("date", ""))
next_entry["endDate"] = _jsonable_time(corrected_start + timedelta(seconds=next_entry["duration"])) if corrected_start else ""
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))