diff --git a/chelation_logger.py b/chelation_logger.py index e71fd19..f1cb855 100644 --- a/chelation_logger.py +++ b/chelation_logger.py @@ -57,11 +57,11 @@ def __init__( Initialize logger. Args: - log_path: Path to log file (default: chelation_debug.jsonl) + log_path: Path to log file (default: chelation_events.jsonl) console_level: Logging level for console output file_level: (Unused - kept for backward compatibility) """ - self.log_path = log_path or Path("chelation_debug.jsonl") + self.log_path = log_path or Path("chelation_events.jsonl") self.start_time = time.time() self.operation_stack = [] # Track nested operations diff --git a/dashboard_server.py b/dashboard_server.py index 0f69086..6deaea7 100644 --- a/dashboard_server.py +++ b/dashboard_server.py @@ -1154,12 +1154,9 @@ def filter_events( # Generic event_type field filter filtered = [e for e in filtered if e.get("event_type") == event_type] - # Sort by timestamp (most recent first) - filtered = sorted( - filtered, - key=lambda e: e.get("timestamp", 0), - reverse=True - ) + # Sort by timestamp (most recent first). ISO strings and missing values + # must not be compared with each other as str and int. + filtered = sorted(filtered, key=_event_sort_value, reverse=True) # Apply limit (0 means no rows; clamped above at _MAX_API_LIMIT) if limit is not None: @@ -1168,6 +1165,25 @@ def filter_events( return filtered +def _event_sort_value(event: Dict[str, Any]) -> float: + """Numeric sort key. Naive ISO datetimes are UTC. Missing values sort as 0.""" + stamp = event.get("timestamp", None) + if isinstance(stamp, bool): + return 0.0 + if isinstance(stamp, (int, float)): + return float(stamp) + if isinstance(stamp, str) and stamp: + text = stamp + if not (text.endswith("Z") or len(text) >= 6 and text[-6] in "+-" and text[-3] == ":"): + text = text + "Z" + try: + parsed = datetime.fromisoformat(text.replace("Z", "+00:00")) + except ValueError: + return 0.0 + return parsed.timestamp() + return 0.0 + + def _first_present(payload: Dict[str, Any], keys: List[str]) -> Any: for key in keys: if key in payload: diff --git a/test_event_log_contract.py b/test_event_log_contract.py new file mode 100644 index 0000000..12c3c5d --- /dev/null +++ b/test_event_log_contract.py @@ -0,0 +1,31 @@ +"""Default event-log path and mixed timestamp sorting.""" + +import unittest +from pathlib import Path + +import dashboard_server +from chelation_logger import ChelationLogger + + +class TestEventLogContract(unittest.TestCase): + def test_default_log_path_matches_dashboard_events_file(self): + logger = ChelationLogger(console_level="ERROR") + self.assertEqual(logger.log_path, Path("chelation_events.jsonl")) + self.assertEqual(dashboard_server.LOG_FILE_PATH, "chelation_events.jsonl") + explicit = ChelationLogger(log_path=Path("custom.jsonl"), console_level="ERROR") + self.assertEqual(explicit.log_path, Path("custom.jsonl")) + + def test_filter_iso_timestamp_does_not_crash_when_timestamp_missing(self): + rows = [ + {"timestamp": "2026-09-24T00:00:00", "query_snippet": "a"}, + {"message": "no ts"}, + ] + result = dashboard_server.filter_events(rows) + self.assertEqual(len(result), 2) + self.assertEqual(result[0]["timestamp"], "2026-09-24T00:00:00") + self.assertEqual(result[0]["query_snippet"], "a") + self.assertEqual(result[1]["message"], "no ts") + + +if __name__ == "__main__": + unittest.main()