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
from datetime import datetime
from datetime import datetime, timezone
from email.utils import format_datetime
from typing import Any
from fastapi import APIRouter, Header, HTTPException, Request, Response
@ -578,20 +579,45 @@ async def list_project_tasks(
tasks = result.get("tasks", [])
# Generate ETag from task data (excluding timestamps for consistency)
etag_data = {
"tasks": [{
"id": task.get("id"),
"title": task.get("title"),
"status": task.get("status"),
"task_order": task.get("task_order"),
"assignee": task.get("assignee"),
"priority": task.get("priority"),
"feature": task.get("feature")
} for task in tasks],
"project_id": project_id,
"count": len(tasks)
# Generate ETag from task data (includes description and updated_at to drive polling invalidation)
etag_tasks: list[dict[str, object]] = []
last_modified_dt: datetime | None = None
for task in tasks:
raw_updated = task.get("updated_at")
parsed_updated: datetime | None = None
if isinstance(raw_updated, datetime):
parsed_updated = raw_updated
elif isinstance(raw_updated, str):
try:
parsed_updated = datetime.fromisoformat(raw_updated.replace("Z", "+00:00"))
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)
# Check if client's ETag matches (304 Not Modified)
@ -599,14 +625,18 @@ async def list_project_tasks(
response.status_code = 304
response.headers["ETag"] = current_etag
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}")
return None
# Set ETag headers for successful response
response.headers["ETag"] = current_etag
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"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:
raise
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)})