fix(kanban): task_age() tolerates ISO-8601 timestamps

Prevents ValueError crash in dashboard get_board() when a task has
an ISO timestamp (e.g. "2026-05-10T15:00:00Z") instead of a unix epoch
int. Adds _to_epoch() helper that normalises both formats.
This commit is contained in:
Interstellar-code 2026-05-18 20:17:59 -07:00 committed by Teknium
parent ca8126bd53
commit d8ad431de8

View file

@ -4756,26 +4756,44 @@ def board_stats(conn: sqlite3.Connection) -> dict:
} }
def _safe_int(val: Optional[str]) -> Optional[int]: def _to_epoch(val) -> Optional[int]:
"""Parse a timestamp field to int, returning None on garbage like '%s'.""" """Normalise a timestamp to unix epoch seconds.
Accepts ints (pass-through), numeric strings, and ISO-8601 strings.
Returns ``None`` for ``None`` / empty values.
"""
if val is None: if val is None:
return None return None
try: if isinstance(val, int):
return val
if isinstance(val, float):
return int(val) return int(val)
except (ValueError, TypeError): s = str(val).strip()
if not s:
return None
try:
return int(s)
except ValueError:
pass
# ISO-8601 fallback (e.g. '2026-05-10T15:00:00Z')
try:
from datetime import datetime, timezone
dt = datetime.fromisoformat(s.replace("Z", "+00:00"))
return int(dt.timestamp())
except (ValueError, OSError):
return None return None
def task_age(task: Task) -> dict: def task_age(task: Task) -> dict:
"""Return age metrics for a single task. All values are seconds or None.""" """Return age metrics for a single task. All values are seconds or None."""
now = int(time.time()) now = int(time.time())
created = _safe_int(task.created_at) _c = _to_epoch(task.created_at)
started = _safe_int(task.started_at) _s = _to_epoch(task.started_at)
completed = _safe_int(task.completed_at) _co = _to_epoch(task.completed_at)
age_since_created = now - created if created else None age_since_created = now - _c if _c is not None else None
age_since_started = now - started if started else None age_since_started = now - _s if _s is not None else None
time_to_complete = ( time_to_complete = (
completed - (started or created) if completed else None _co - (_s or _c) if _co is not None else None
) )
return { return {
"created_age_seconds": age_since_created, "created_age_seconds": age_since_created,