feat(api): CUST-WP-0018 — API hardening & code quality

T01: Fix datetime.utcnow() → datetime.now(tz=timezone.utc) in MCP server
T02: Wrap _get/_post/_patch/_delete with try/except; return error dicts
T03: Log warnings when write_log skips missing project path
T04: Add priority + due_date_before filters to GET /tasks/
T05: Add owner + slug filters to GET /workstreams/
T06: Add offset param to GET /progress/ for proper pagination
T07: Low-severity bundle:
  - CORS origins from CORS_ORIGINS env var (TD-017)
  - seed.py upsert domains+topics on re-run (TD-011)
  - normalise filter bar CSS → filter-text-input everywhere (TD-016)
  - add 30.5 avg-days-per-month comment in decisions.md (TD-019)
  - TD-009, TD-018 already resolved by existing code

Closes CUST-WP-0018.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-18 02:17:04 +01:00
parent cb2c4f9a0c
commit 2d0ce8f943
11 changed files with 98 additions and 40 deletions

View File

@@ -9,7 +9,7 @@ import json
import os
import re
import sys
from datetime import datetime
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from uuid import UUID
@@ -41,34 +41,54 @@ def _client() -> httpx.Client:
def _get(path: str, params: dict | None = None) -> Any:
if not path.endswith("/"):
path = path + "/"
with _client() as c:
r = c.get(path, params={k: v for k, v in (params or {}).items() if v is not None})
r.raise_for_status()
return r.json()
try:
with _client() as c:
r = c.get(path, params={k: v for k, v in (params or {}).items() if v is not None})
r.raise_for_status()
return r.json()
except httpx.HTTPStatusError as e:
return {"error": f"API {e.response.status_code}: {e.response.text[:300]}"}
except Exception as e:
return {"error": f"Request failed: {e}"}
def _post(path: str, body: dict) -> Any:
if not path.endswith("/"):
path = path + "/"
with _client() as c:
r = c.post(path, json={k: v for k, v in body.items() if v is not None})
r.raise_for_status()
return r.json()
try:
with _client() as c:
r = c.post(path, json={k: v for k, v in body.items() if v is not None})
r.raise_for_status()
return r.json()
except httpx.HTTPStatusError as e:
return {"error": f"API {e.response.status_code}: {e.response.text[:300]}"}
except Exception as e:
return {"error": f"Request failed: {e}"}
def _patch(path: str, body: dict) -> Any:
if not path.endswith("/"):
path = path + "/"
with _client() as c:
r = c.patch(path, json={k: v for k, v in body.items() if v is not None})
r.raise_for_status()
return r.json()
try:
with _client() as c:
r = c.patch(path, json={k: v for k, v in body.items() if v is not None})
r.raise_for_status()
return r.json()
except httpx.HTTPStatusError as e:
return {"error": f"API {e.response.status_code}: {e.response.text[:300]}"}
except Exception as e:
return {"error": f"Request failed: {e}"}
def _delete(path: str) -> None:
with _client() as c:
r = c.delete(path)
r.raise_for_status()
try:
with _client() as c:
r = c.delete(path)
r.raise_for_status()
except httpx.HTTPStatusError as e:
return {"error": f"API {e.response.status_code}: {e.response.text[:300]}"}
except Exception as e:
return {"error": f"Request failed: {e}"}
# ---------------------------------------------------------------------------
@@ -526,7 +546,7 @@ def resolve_decision(
"decision_type": "made",
"rationale": rationale,
"decided_by": decided_by,
"decided_at": datetime.utcnow().isoformat() + "Z",
"decided_at": datetime.now(tz=timezone.utc).isoformat(),
})
_post("/progress", {
"topic_id": decision.get("topic_id"),