Include description in tasks polling ETag (#698)

* Include description in tasks polling ETag

* Align tasks endpoint headers with HTTP cache expectations
This commit is contained in:
Wirasm 2025-09-18 15:18:53 +03:00 committed by GitHub
parent 31cf56a685
commit 89fa9b4b49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -9,7 +9,8 @@ Handles:
""" """
import json import json
from datetime import datetime from datetime import datetime, timezone
from email.utils import format_datetime
from typing import Any from typing import Any
from fastapi import APIRouter, Header, HTTPException, Request, Response from fastapi import APIRouter, Header, HTTPException, Request, Response
@ -578,20 +579,45 @@ async def list_project_tasks(
tasks = result.get("tasks", []) tasks = result.get("tasks", [])
# Generate ETag from task data (excluding timestamps for consistency) # Generate ETag from task data (includes description and updated_at to drive polling invalidation)
etag_data = { etag_tasks: list[dict[str, object]] = []
"tasks": [{ last_modified_dt: datetime | None = None
"id": task.get("id"),
"title": task.get("title"), for task in tasks:
"status": task.get("status"), raw_updated = task.get("updated_at")
"task_order": task.get("task_order"), parsed_updated: datetime | None = None
"assignee": task.get("assignee"), if isinstance(raw_updated, datetime):
"priority": task.get("priority"), parsed_updated = raw_updated
"feature": task.get("feature") elif isinstance(raw_updated, str):
} for task in tasks], try:
"project_id": project_id, parsed_updated = datetime.fromisoformat(raw_updated.replace("Z", "+00:00"))
"count": len(tasks) except ValueError:
parsed_updated = None
if parsed_updated is not None:
parsed_updated = parsed_updated.astimezone(timezone.utc)
if last_modified_dt is None or parsed_updated > last_modified_dt:
last_modified_dt = parsed_updated
etag_tasks.append(
{
"id": task.get("id") or "",
"title": task.get("title") or "",
"status": task.get("status") or "",
"task_order": task.get("task_order") or 0,
"assignee": task.get("assignee") or "",
"priority": task.get("priority") or "",
"feature": task.get("feature") or "",
"description": task.get("description") or "",
"updated_at": (
parsed_updated.isoformat()
if parsed_updated is not None
else (str(raw_updated) if raw_updated else "")
),
} }
)
etag_data = {"tasks": etag_tasks, "project_id": project_id, "count": len(tasks)}
current_etag = generate_etag(etag_data) current_etag = generate_etag(etag_data)
# Check if client's ETag matches (304 Not Modified) # Check if client's ETag matches (304 Not Modified)
@ -599,14 +625,18 @@ async def list_project_tasks(
response.status_code = 304 response.status_code = 304
response.headers["ETag"] = current_etag response.headers["ETag"] = current_etag
response.headers["Cache-Control"] = "no-cache, must-revalidate" response.headers["Cache-Control"] = "no-cache, must-revalidate"
response.headers["Last-Modified"] = datetime.utcnow().isoformat() response.headers["Last-Modified"] = format_datetime(
last_modified_dt or datetime.now(timezone.utc)
)
logfire.debug(f"Tasks unchanged, returning 304 | project_id={project_id} | etag={current_etag}") logfire.debug(f"Tasks unchanged, returning 304 | project_id={project_id} | etag={current_etag}")
return None return None
# Set ETag headers for successful response # Set ETag headers for successful response
response.headers["ETag"] = current_etag response.headers["ETag"] = current_etag
response.headers["Cache-Control"] = "no-cache, must-revalidate" response.headers["Cache-Control"] = "no-cache, must-revalidate"
response.headers["Last-Modified"] = datetime.utcnow().isoformat() response.headers["Last-Modified"] = format_datetime(
last_modified_dt or datetime.now(timezone.utc)
)
logfire.debug( logfire.debug(
f"Project tasks retrieved | project_id={project_id} | task_count={len(tasks)} | etag={current_etag}" f"Project tasks retrieved | project_id={project_id} | task_count={len(tasks)} | etag={current_etag}"
@ -617,7 +647,7 @@ async def list_project_tasks(
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
logfire.error(f"Failed to list project tasks | error={str(e)} | project_id={project_id}") logfire.error(f"Failed to list project tasks | project_id={project_id}", exc_info=True)
raise HTTPException(status_code=500, detail={"error": str(e)}) raise HTTPException(status_code=500, detail={"error": str(e)})