mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-22 09:42:10 +08:00
home cleanup
This commit is contained in:
@@ -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))
|
||||
|
||||
|
||||
@@ -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/"
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user