--- id: CUST-WP-0018 type: workplan title: "State Hub — API Hardening & Code Quality" domain: custodian status: done owner: custodian topic_slug: custodian created: "2026-03-18" updated: "2026-03-18" state_hub_workstream_id: "c7777d8a-a796-4f72-b444-64cc14f77a58" --- # State Hub — API Hardening & Code Quality ## Summary Resolve all open medium/high technical debt items in the API and MCP server. These are small, targeted fixes: datetime correctness, missing filter params, MCP error handling, and a bundle of low-severity cleanup items. No schema migrations required. ## Resolves TD-CUST-006, TD-CUST-007, TD-CUST-008, TD-CUST-009, TD-CUST-010, TD-CUST-011, TD-CUST-012, TD-CUST-013, TD-CUST-015, TD-CUST-016, TD-CUST-017, TD-CUST-018, TD-CUST-019 TD-CUST-005 (N+1 selectin) deferred — not pressing at current scale. --- ## Tasks ### T01 — Fix deprecated datetime.utcnow() in MCP server ```task id: CUST-WP-0018-T01 status: done priority: high state_hub_task_id: "5045749c-22a5-4f37-81b1-6fc87ae7c580" ``` Replace `datetime.utcnow()` with `datetime.now(tz=timezone.utc)` throughout `state-hub/mcp_server/server.py`. Resolves TD-CUST-006 and TD-CUST-013 (mixed naive/aware datetime handling). One-line change with broad correctness impact. **Location:** `state-hub/mcp_server/server.py:343` (and any other occurrences) --- ### T02 — MCP HTTP helpers: catch exceptions and return user-friendly errors ```task id: CUST-WP-0018-T02 status: done priority: high state_hub_task_id: "8aadbaf8-d898-436e-8df0-7f095c916613" ``` Wrap `_get()`, `_post()`, `_patch()`, `_delete()` in a try/except that catches `httpx.HTTPError` and `Exception`, returning a structured error dict instead of letting the MCP tool crash with a stack trace. Resolves TD-CUST-015. **Location:** `state-hub/mcp_server/server.py:34–69` Pattern: ```python try: r = await client.get(url, ...) r.raise_for_status() return r.json() except httpx.HTTPStatusError as e: return {"error": f"API {e.response.status_code}: {e.response.text[:200]}"} except Exception as e: return {"error": f"Request failed: {e}"} ``` --- ### T03 — Decision write_log: warn when project path not found ```task id: CUST-WP-0018-T03 status: done priority: medium state_hub_task_id: "26f8d132-b2f4-4939-9497-a9ad64e0a73e" ``` `_write_project_log()` in `decisions.py` silently returns when the project path directory does not exist. Add a log warning so the caller knows the write was skipped. Resolves TD-CUST-012. **Location:** `state-hub/api/routers/decisions.py:147–196` --- ### T04 — Add priority and due_date filters to task list endpoint ```task id: CUST-WP-0018-T04 status: done priority: medium state_hub_task_id: "18da9d84-54a4-4028-8b8e-014d2b2f6ed6" ``` Add `priority: str | None` and `due_date_before: date | None` query params to `list_tasks()`. Resolves TD-CUST-007. **Location:** `state-hub/api/routers/tasks.py:14–30` --- ### T05 — Add owner and slug filters to workstream list endpoint ```task id: CUST-WP-0018-T05 status: done priority: medium state_hub_task_id: "488f448f-396d-4924-98a5-a2e84d4b1b95" ``` Add `owner: str | None` and `slug: str | None` query params to `list_workstreams()`. Resolves TD-CUST-008. **Location:** `state-hub/api/routers/workstreams.py:14–27` --- ### T06 — Add offset pagination to progress event list endpoint ```task id: CUST-WP-0018-T06 status: done priority: medium state_hub_task_id: "dd7e9da8-19fb-4b02-a100-972c582dbaa9" ``` Add `offset: int = 0` query param to `list_progress()` alongside existing `limit`. Resolves TD-CUST-010. **Location:** `state-hub/api/routers/progress.py:15–38` --- ### T07 — Low-severity cleanup bundle ```task id: CUST-WP-0018-T07 status: done priority: low state_hub_task_id: "b949805b-dd3e-43d6-89cc-631e3183f67c" ``` Bundle of small fixes (each a few lines): - **TD-CUST-009**: Document `decision_type` filter in OpenAPI schema for `list_decisions()` consistently with other filter params. - **TD-CUST-011**: Make `seed.py` upsert-based — update existing rows when field values change (use `ON CONFLICT ... DO UPDATE`). - **TD-CUST-016**: Normalise filter bar CSS class names across dashboard pages (`filter-search` vs `filter-owner` → use `filter-text-input` everywhere). - **TD-CUST-017**: Read CORS origins from `CORS_ORIGINS` env var (comma-separated), defaulting to `localhost:3000`; remove hard-coded list from `main.py`. - **TD-CUST-018**: Add a `ProgressEventCreate.detail` validator that at minimum rejects non-dict values (dict-or-None is sufficient). - **TD-CUST-019**: Add comment explaining the 30.5 avg-days-per-month constant in `decisions.md:99`. **Locations:** Various — see TD item descriptions for exact file:line refs.