From 1db9db7d0313845183ac7b2fd0c1608e92f36c87 Mon Sep 17 00:00:00 2001 From: Oskar Kapala Date: Sun, 31 May 2026 22:10:50 +0200 Subject: [PATCH] fix(dashboard): read last_update from JSON content, not file mtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- services/agent-system/webui/web.py | 20 +++++++++++++++----- services/control-plane/src/operator_ui.py | 22 +++++++++++----------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/services/agent-system/webui/web.py b/services/agent-system/webui/web.py index d6a7fae..6833da5 100644 --- a/services/agent-system/webui/web.py +++ b/services/agent-system/webui/web.py @@ -68,12 +68,22 @@ def current_recommendations(): 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: - # Check for staleness - mtime = os.path.getmtime(WORLD_DIR / "runtime-summary.json") - summary["last_update"] = mtime - summary["stale"] = (time.time() - mtime) > 60 # Stale if older than 60s + last_update_val = summary.get("last_update") + if last_update_val: + try: + 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 diff --git a/services/control-plane/src/operator_ui.py b/services/control-plane/src/operator_ui.py index 67ced90..ce5def3 100644 --- a/services/control-plane/src/operator_ui.py +++ b/services/control-plane/src/operator_ui.py @@ -164,22 +164,22 @@ def current_recommendations(): 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: - # Check for staleness from the summary's own timestamp if available - # otherwise use file mtime - last_update_str = summary.get("last_update") - if last_update_str: + last_update_val = summary.get("last_update") + if last_update_val: try: - # Assuming ISO format from observer.py - last_update = datetime.fromisoformat(last_update_str.replace('Z', '+00:00')).timestamp() + 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(WORLD_DIR / "runtime-summary.json") + last_update = os.path.getmtime(path) else: - last_update = os.path.getmtime(WORLD_DIR / "runtime-summary.json") - + last_update = os.path.getmtime(path) 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