fix(events): normalize naive datetimes to UTC in HTTP route

All datetime parsing now uses _parse_dt() which adds UTC tzinfo when
none is present, matching the fix already applied in tools.py. This
prevents asyncpg errors when comparing naive datetimes against
TIMESTAMPTZ columns — the root cause of events not appearing in the
calendar view.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 00:00:43 -04:00
parent 3c38c04ad4
commit 968e536d3a
+14 -6
View File
@@ -1,7 +1,7 @@
"""Calendar events REST API."""
from __future__ import annotations
from datetime import datetime
from datetime import datetime, timezone
from quart import Blueprint, g, jsonify, request
@@ -11,6 +11,14 @@ import fabledassistant.services.events as events_svc
events_bp = Blueprint("events", __name__, url_prefix="/api/events")
def _parse_dt(value: str) -> datetime:
"""Parse ISO 8601 datetime string, ensuring UTC-awareness."""
dt = datetime.fromisoformat(value.replace("Z", "+00:00"))
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt
def _get_current_user_id() -> int:
return g.user.id
@@ -23,8 +31,8 @@ async def list_events():
if not date_from_str or not date_to_str:
return jsonify({"error": "from and to query params are required"}), 400
try:
date_from = datetime.fromisoformat(date_from_str)
date_to = datetime.fromisoformat(date_to_str)
date_from = _parse_dt(date_from_str)
date_to = _parse_dt(date_to_str)
except ValueError:
return jsonify({"error": "Invalid datetime format"}), 400
events = await events_svc.list_events(
@@ -42,8 +50,8 @@ async def create_event():
if not data.get("title") or not data.get("start_dt"):
return jsonify({"error": "title and start_dt are required"}), 400
try:
start_dt = datetime.fromisoformat(data["start_dt"])
end_dt = datetime.fromisoformat(data["end_dt"]) if data.get("end_dt") else None
start_dt = _parse_dt(data["start_dt"])
end_dt = _parse_dt(data["end_dt"]) if data.get("end_dt") else None
except ValueError:
return jsonify({"error": "Invalid datetime format"}), 400
event = await events_svc.create_event(
@@ -90,7 +98,7 @@ async def update_event(event_id: int):
for dt_field in ("start_dt", "end_dt"):
if dt_field in data and data[dt_field]:
try:
fields[dt_field] = datetime.fromisoformat(data[dt_field])
fields[dt_field] = _parse_dt(data[dt_field])
except ValueError:
return jsonify({"error": f"Invalid datetime for {dt_field}"}), 400
event = await events_svc.update_event(