fix(dashboard): read last_update from JSON content, not file mtime

operator_ui.py called .replace() on last_update without checking type —
an integer value (written by the materializer) raised AttributeError and
silently fell back to os.path.getmtime(), which was stuck at 5/29 after a
deploy with preserved timestamps. web.py had the same class of bug but
worse: it unconditionally replaced last_update with mtime, ignoring the
JSON field entirely. Both now branch on isinstance(str) and cast numeric
values directly to float, with mtime only as a last-resort fallback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Oskar Kapala 2026-05-31 22:10:50 +02:00
parent 52607a7cdd
commit 1db9db7d03
2 changed files with 26 additions and 16 deletions

View file

@ -68,12 +68,22 @@ def current_recommendations():
def current_summary(): def current_summary():
summary = read_json_file(WORLD_DIR / "runtime-summary.json", default={}) path = WORLD_DIR / "runtime-summary.json"
summary = read_json_file(path, default={})
if summary: if summary:
# Check for staleness last_update_val = summary.get("last_update")
mtime = os.path.getmtime(WORLD_DIR / "runtime-summary.json") if last_update_val:
summary["last_update"] = mtime try:
summary["stale"] = (time.time() - mtime) > 60 # Stale if older than 60s if isinstance(last_update_val, str):
last_update = datetime.fromisoformat(last_update_val.replace('Z', '+00:00')).timestamp()
else:
last_update = float(last_update_val)
except Exception:
last_update = os.path.getmtime(path)
else:
last_update = os.path.getmtime(path)
summary["last_update"] = last_update
summary["stale"] = (time.time() - last_update) > 60
return summary return summary

View file

@ -164,22 +164,22 @@ def current_recommendations():
def current_summary(): def current_summary():
summary = read_json_file(WORLD_DIR / "runtime-summary.json", default={}) path = WORLD_DIR / "runtime-summary.json"
summary = read_json_file(path, default={})
if summary: if summary:
# Check for staleness from the summary's own timestamp if available last_update_val = summary.get("last_update")
# otherwise use file mtime if last_update_val:
last_update_str = summary.get("last_update")
if last_update_str:
try: try:
# Assuming ISO format from observer.py if isinstance(last_update_val, str):
last_update = datetime.fromisoformat(last_update_str.replace('Z', '+00:00')).timestamp() last_update = datetime.fromisoformat(last_update_val.replace('Z', '+00:00')).timestamp()
else:
last_update = float(last_update_val)
except Exception: except Exception:
last_update = os.path.getmtime(WORLD_DIR / "runtime-summary.json") last_update = os.path.getmtime(path)
else: else:
last_update = os.path.getmtime(WORLD_DIR / "runtime-summary.json") last_update = os.path.getmtime(path)
summary["last_update"] = last_update summary["last_update"] = last_update
summary["stale"] = (time.time() - last_update) > 60 # Stale if older than 60s summary["stale"] = (time.time() - last_update) > 60
return summary return summary