2026-05-17 05:49:29 +02:00
|
|
|
import asyncio
|
2026-06-04 08:25:31 +02:00
|
|
|
import logging
|
Add state-hub v0.1 — local-first state service for the Custodian
Implements the first live layer of the Custodian cognitive infrastructure:
PostgreSQL schema, FastAPI REST API, FastMCP stdio server, and Observable
Framework telemetry dashboard.
- state-hub/: full stack (docker-compose, FastAPI, Alembic, MCP server, dashboard)
- 5 DB tables: topics, workstreams, tasks, decisions, progress_events
- 11 MCP tools + 5 resources registered in .mcp.json
- Observable dashboard: Overview, Workstreams, Decisions, Progress pages
- CLAUDE.md: session protocol (get_state_summary / add_progress_event ritual)
- ~/.claude/CLAUDE.md: global cross-project reference to the hub
- scripts/pull_image.py: WSL2 TLS-resilient Docker image downloader
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 17:47:49 +01:00
|
|
|
import uuid
|
2026-05-02 12:01:45 +02:00
|
|
|
import socket
|
perf(api): CUST-WP-0041 — DB indexes, TTL caches, noload on list endpoints
- Migration t7o8p9q0r1s2: indexes on tasks.status, tasks(workstream_id,status),
workstreams.status, sbom_snapshots(repo_id,snapshot_at)
- workplan-index: 30 s TTL cache + ?refresh param (4171 ms → 16 ms on hit)
- /state/summary: 15 s TTL cache, bypassed on Cache-Control: no-cache
- /topics/: noload(workstreams, decisions, progress_events) (2382 ms → 115 ms)
- /domains/: noload(topics, repos, goals) (2252 ms → 39 ms)
- /repos/: noload(goals) (2222 ms → 599 ms first / fast on repeat)
- conftest: reset TTL caches between tests to prevent bleed-through
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 11:12:17 +02:00
|
|
|
import time
|
2026-06-06 00:42:00 +02:00
|
|
|
from datetime import datetime, timezone
|
2026-05-02 12:01:45 +02:00
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Any
|
Add state-hub v0.1 — local-first state service for the Custodian
Implements the first live layer of the Custodian cognitive infrastructure:
PostgreSQL schema, FastAPI REST API, FastMCP stdio server, and Observable
Framework telemetry dashboard.
- state-hub/: full stack (docker-compose, FastAPI, Alembic, MCP server, dashboard)
- 5 DB tables: topics, workstreams, tasks, decisions, progress_events
- 11 MCP tools + 5 resources registered in .mcp.json
- Observable dashboard: Overview, Workstreams, Decisions, Progress pages
- CLAUDE.md: session protocol (get_state_summary / add_progress_event ritual)
- ~/.claude/CLAUDE.md: global cross-project reference to the hub
- scripts/pull_image.py: WSL2 TLS-resilient Docker image downloader
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 17:47:49 +01:00
|
|
|
|
2026-05-18 01:31:36 +02:00
|
|
|
import yaml
|
2026-06-04 08:25:31 +02:00
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response, status
|
Add state-hub v0.1 — local-first state service for the Custodian
Implements the first live layer of the Custodian cognitive infrastructure:
PostgreSQL schema, FastAPI REST API, FastMCP stdio server, and Observable
Framework telemetry dashboard.
- state-hub/: full stack (docker-compose, FastAPI, Alembic, MCP server, dashboard)
- 5 DB tables: topics, workstreams, tasks, decisions, progress_events
- 11 MCP tools + 5 resources registered in .mcp.json
- Observable dashboard: Overview, Workstreams, Decisions, Progress pages
- CLAUDE.md: session protocol (get_state_summary / add_progress_event ritual)
- ~/.claude/CLAUDE.md: global cross-project reference to the hub
- scripts/pull_image.py: WSL2 TLS-resilient Docker image downloader
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 17:47:49 +01:00
|
|
|
from sqlalchemy import select
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
|
|
|
|
from api.database import get_session
|
2026-05-17 05:49:29 +02:00
|
|
|
from api.events import EventEnvelope, publish_event
|
2026-05-02 12:01:45 +02:00
|
|
|
from api.models.managed_repo import ManagedRepo
|
2026-06-22 13:52:13 +02:00
|
|
|
from api.models.workplan import Workplan
|
|
|
|
|
from api.schemas.workplan import (
|
2026-07-04 00:42:56 +02:00
|
|
|
WorkplanBindingsSync,
|
2026-06-22 13:52:13 +02:00
|
|
|
WorkplanCreate,
|
|
|
|
|
WorkplanRead,
|
|
|
|
|
WorkplanUpdate,
|
2026-05-02 00:21:14 +02:00
|
|
|
)
|
2026-06-22 13:52:13 +02:00
|
|
|
from api.services.lifecycle import transition_workplan_status
|
2026-07-10 13:31:57 +02:00
|
|
|
from api.services.legacy_compat import retire_legacy_route
|
2026-05-18 01:31:36 +02:00
|
|
|
from api.workplan_status import (
|
2026-06-22 13:52:13 +02:00
|
|
|
is_supported_workplan_status,
|
|
|
|
|
normalize_workplan_status,
|
2026-05-18 01:31:36 +02:00
|
|
|
ready_review_status,
|
|
|
|
|
)
|
Add state-hub v0.1 — local-first state service for the Custodian
Implements the first live layer of the Custodian cognitive infrastructure:
PostgreSQL schema, FastAPI REST API, FastMCP stdio server, and Observable
Framework telemetry dashboard.
- state-hub/: full stack (docker-compose, FastAPI, Alembic, MCP server, dashboard)
- 5 DB tables: topics, workstreams, tasks, decisions, progress_events
- 11 MCP tools + 5 resources registered in .mcp.json
- Observable dashboard: Overview, Workstreams, Decisions, Progress pages
- CLAUDE.md: session protocol (get_state_summary / add_progress_event ritual)
- ~/.claude/CLAUDE.md: global cross-project reference to the hub
- scripts/pull_image.py: WSL2 TLS-resilient Docker image downloader
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 17:47:49 +01:00
|
|
|
|
2026-06-04 08:25:31 +02:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
Add state-hub v0.1 — local-first state service for the Custodian
Implements the first live layer of the Custodian cognitive infrastructure:
PostgreSQL schema, FastAPI REST API, FastMCP stdio server, and Observable
Framework telemetry dashboard.
- state-hub/: full stack (docker-compose, FastAPI, Alembic, MCP server, dashboard)
- 5 DB tables: topics, workstreams, tasks, decisions, progress_events
- 11 MCP tools + 5 resources registered in .mcp.json
- Observable dashboard: Overview, Workstreams, Decisions, Progress pages
- CLAUDE.md: session protocol (get_state_summary / add_progress_event ritual)
- ~/.claude/CLAUDE.md: global cross-project reference to the hub
- scripts/pull_image.py: WSL2 TLS-resilient Docker image downloader
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 17:47:49 +01:00
|
|
|
router = APIRouter(prefix="/workstreams", tags=["workstreams"])
|
2026-06-04 08:25:31 +02:00
|
|
|
workplan_router = APIRouter(prefix="/workplans", tags=["workplans"])
|
Add state-hub v0.1 — local-first state service for the Custodian
Implements the first live layer of the Custodian cognitive infrastructure:
PostgreSQL schema, FastAPI REST API, FastMCP stdio server, and Observable
Framework telemetry dashboard.
- state-hub/: full stack (docker-compose, FastAPI, Alembic, MCP server, dashboard)
- 5 DB tables: topics, workstreams, tasks, decisions, progress_events
- 11 MCP tools + 5 resources registered in .mcp.json
- Observable dashboard: Overview, Workstreams, Decisions, Progress pages
- CLAUDE.md: session protocol (get_state_summary / add_progress_event ritual)
- ~/.claude/CLAUDE.md: global cross-project reference to the hub
- scripts/pull_image.py: WSL2 TLS-resilient Docker image downloader
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 17:47:49 +01:00
|
|
|
|
perf(api): CUST-WP-0041 — DB indexes, TTL caches, noload on list endpoints
- Migration t7o8p9q0r1s2: indexes on tasks.status, tasks(workstream_id,status),
workstreams.status, sbom_snapshots(repo_id,snapshot_at)
- workplan-index: 30 s TTL cache + ?refresh param (4171 ms → 16 ms on hit)
- /state/summary: 15 s TTL cache, bypassed on Cache-Control: no-cache
- /topics/: noload(workstreams, decisions, progress_events) (2382 ms → 115 ms)
- /domains/: noload(topics, repos, goals) (2252 ms → 39 ms)
- /repos/: noload(goals) (2222 ms → 599 ms first / fast on repeat)
- conftest: reset TTL caches between tests to prevent bleed-through
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 11:12:17 +02:00
|
|
|
_INDEX_CACHE: dict[str, Any] | None = None
|
|
|
|
|
_INDEX_CACHE_AT: float = 0.0
|
|
|
|
|
_INDEX_TTL = 30.0
|
2026-06-06 00:42:00 +02:00
|
|
|
_INDEX_REFRESH_TASK: asyncio.Task | None = None
|
|
|
|
|
_INDEX_LAST_ERROR: str | None = None
|
perf(api): CUST-WP-0041 — DB indexes, TTL caches, noload on list endpoints
- Migration t7o8p9q0r1s2: indexes on tasks.status, tasks(workstream_id,status),
workstreams.status, sbom_snapshots(repo_id,snapshot_at)
- workplan-index: 30 s TTL cache + ?refresh param (4171 ms → 16 ms on hit)
- /state/summary: 15 s TTL cache, bypassed on Cache-Control: no-cache
- /topics/: noload(workstreams, decisions, progress_events) (2382 ms → 115 ms)
- /domains/: noload(topics, repos, goals) (2252 ms → 39 ms)
- /repos/: noload(goals) (2222 ms → 599 ms first / fast on repeat)
- conftest: reset TTL caches between tests to prevent bleed-through
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 11:12:17 +02:00
|
|
|
|
2026-06-04 08:25:31 +02:00
|
|
|
_COMPLETED_WORKPLAN_EVENT = "org.statehub.workplan.completed"
|
|
|
|
|
|
Add state-hub v0.1 — local-first state service for the Custodian
Implements the first live layer of the Custodian cognitive infrastructure:
PostgreSQL schema, FastAPI REST API, FastMCP stdio server, and Observable
Framework telemetry dashboard.
- state-hub/: full stack (docker-compose, FastAPI, Alembic, MCP server, dashboard)
- 5 DB tables: topics, workstreams, tasks, decisions, progress_events
- 11 MCP tools + 5 resources registered in .mcp.json
- Observable dashboard: Overview, Workstreams, Decisions, Progress pages
- CLAUDE.md: session protocol (get_state_summary / add_progress_event ritual)
- ~/.claude/CLAUDE.md: global cross-project reference to the hub
- scripts/pull_image.py: WSL2 TLS-resilient Docker image downloader
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 17:47:49 +01:00
|
|
|
|
2026-05-02 12:01:45 +02:00
|
|
|
def _repo_path(repo: ManagedRepo) -> Path | None:
|
|
|
|
|
hostname = socket.gethostname()
|
|
|
|
|
candidates = []
|
|
|
|
|
host_paths = repo.host_paths or {}
|
|
|
|
|
if host_paths.get(hostname):
|
|
|
|
|
candidates.append(host_paths[hostname])
|
|
|
|
|
if repo.local_path:
|
|
|
|
|
candidates.append(repo.local_path)
|
|
|
|
|
for raw in candidates:
|
|
|
|
|
path = Path(raw).expanduser()
|
|
|
|
|
if path.is_dir():
|
|
|
|
|
return path
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _frontmatter(path: Path) -> dict[str, Any]:
|
|
|
|
|
try:
|
|
|
|
|
text = path.read_text(encoding="utf-8")
|
|
|
|
|
except OSError:
|
|
|
|
|
return {}
|
|
|
|
|
if not text.startswith("---\n"):
|
|
|
|
|
return {}
|
|
|
|
|
end = text.find("\n---", 4)
|
|
|
|
|
if end == -1:
|
|
|
|
|
return {}
|
|
|
|
|
|
2026-05-18 01:31:36 +02:00
|
|
|
try:
|
|
|
|
|
return yaml.safe_load(text[4:end].strip()) or {}
|
|
|
|
|
except yaml.YAMLError:
|
|
|
|
|
return {}
|
2026-05-02 12:01:45 +02:00
|
|
|
|
|
|
|
|
|
2026-06-04 08:25:31 +02:00
|
|
|
def _legacy_key(method: str, route: str) -> str:
|
|
|
|
|
return f"rest_api:{method} {route}"
|
|
|
|
|
|
|
|
|
|
|
2026-06-22 13:52:13 +02:00
|
|
|
async def _list_workplans(
|
2026-06-04 08:25:31 +02:00
|
|
|
*,
|
|
|
|
|
topic_id: uuid.UUID | None,
|
|
|
|
|
repo_id: uuid.UUID | None,
|
|
|
|
|
repo_goal_id: uuid.UUID | None,
|
|
|
|
|
status_filter: str | None,
|
|
|
|
|
owner: str | None,
|
|
|
|
|
slug: str | None,
|
|
|
|
|
session: AsyncSession,
|
2026-06-22 13:52:13 +02:00
|
|
|
) -> list[Workplan]:
|
|
|
|
|
q = select(Workplan)
|
Add state-hub v0.1 — local-first state service for the Custodian
Implements the first live layer of the Custodian cognitive infrastructure:
PostgreSQL schema, FastAPI REST API, FastMCP stdio server, and Observable
Framework telemetry dashboard.
- state-hub/: full stack (docker-compose, FastAPI, Alembic, MCP server, dashboard)
- 5 DB tables: topics, workstreams, tasks, decisions, progress_events
- 11 MCP tools + 5 resources registered in .mcp.json
- Observable dashboard: Overview, Workstreams, Decisions, Progress pages
- CLAUDE.md: session protocol (get_state_summary / add_progress_event ritual)
- ~/.claude/CLAUDE.md: global cross-project reference to the hub
- scripts/pull_image.py: WSL2 TLS-resilient Docker image downloader
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 17:47:49 +01:00
|
|
|
if topic_id:
|
2026-06-22 13:52:13 +02:00
|
|
|
q = q.where(Workplan.topic_id == topic_id)
|
2026-03-02 23:39:17 +01:00
|
|
|
if repo_id:
|
2026-06-22 13:52:13 +02:00
|
|
|
q = q.where(Workplan.repo_id == repo_id)
|
feat(goals): add domain/repo goal tracking and update_workstream MCP tool
- Migration c5d6e7f8a9b0: domain_goals and repo_goals tables, repo_goal_id FK on workstreams
- DomainGoal: one active per domain (partial unique index), status active/archived/superseded
- RepoGoal: integer priority, status active/paused/completed/archived, optional domain_goal_id link
- WorkstreamUpdate schema and router extended with repo_goal_id and repo_goal_id filter
- 6 new MCP goal tools: create_domain_goal, get_domain_goals, activate_domain_goal, create_repo_goal, get_repo_goals, update_repo_goal
- update_workstream MCP tool: patch any subset of workstream fields (title, description, owner, due_date, repo_goal_id, status)
- get_domain_summary extended with goal_guidance (needs_workplan, alignment_warnings) signals
- Dashboard goals.md page and docs/goals.md reference page
- CLAUDE.md template updated to act on goal_guidance signals at session start
- CUST-WP-0010 workplan for this feature
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 00:15:29 +01:00
|
|
|
if repo_goal_id:
|
2026-06-22 13:52:13 +02:00
|
|
|
q = q.where(Workplan.repo_goal_id == repo_goal_id)
|
2026-06-04 08:25:31 +02:00
|
|
|
if status_filter:
|
2026-06-22 13:52:13 +02:00
|
|
|
normalised_status = normalize_workplan_status(status_filter)
|
|
|
|
|
if not is_supported_workplan_status(status_filter):
|
2026-06-04 08:25:31 +02:00
|
|
|
raise HTTPException(status_code=422, detail=f"Unsupported workplan status '{status_filter}'")
|
2026-06-22 13:52:13 +02:00
|
|
|
q = q.where(Workplan.status == normalised_status)
|
2026-03-18 02:17:04 +01:00
|
|
|
if owner:
|
2026-06-22 13:52:13 +02:00
|
|
|
q = q.where(Workplan.owner == owner)
|
2026-03-18 02:17:04 +01:00
|
|
|
if slug:
|
2026-06-22 13:52:13 +02:00
|
|
|
q = q.where(Workplan.slug == slug)
|
2026-05-04 11:45:24 +02:00
|
|
|
q = q.order_by(
|
2026-06-22 13:52:13 +02:00
|
|
|
Workplan.planning_priority.asc().nullslast(),
|
|
|
|
|
Workplan.planning_order.asc().nullslast(),
|
|
|
|
|
Workplan.updated_at.desc(),
|
2026-05-04 11:45:24 +02:00
|
|
|
)
|
Add state-hub v0.1 — local-first state service for the Custodian
Implements the first live layer of the Custodian cognitive infrastructure:
PostgreSQL schema, FastAPI REST API, FastMCP stdio server, and Observable
Framework telemetry dashboard.
- state-hub/: full stack (docker-compose, FastAPI, Alembic, MCP server, dashboard)
- 5 DB tables: topics, workstreams, tasks, decisions, progress_events
- 11 MCP tools + 5 resources registered in .mcp.json
- Observable dashboard: Overview, Workstreams, Decisions, Progress pages
- CLAUDE.md: session protocol (get_state_summary / add_progress_event ritual)
- ~/.claude/CLAUDE.md: global cross-project reference to the hub
- scripts/pull_image.py: WSL2 TLS-resilient Docker image downloader
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 17:47:49 +01:00
|
|
|
result = await session.execute(q)
|
|
|
|
|
return list(result.scalars().all())
|
|
|
|
|
|
|
|
|
|
|
2026-06-06 00:42:00 +02:00
|
|
|
async def _build_workplan_index(session: AsyncSession) -> dict[str, Any]:
|
2026-05-02 12:01:45 +02:00
|
|
|
result = await session.execute(
|
|
|
|
|
select(ManagedRepo).where(ManagedRepo.status == "active").order_by(ManagedRepo.slug)
|
|
|
|
|
)
|
|
|
|
|
index: dict[str, Any] = {}
|
|
|
|
|
for repo in result.scalars().all():
|
|
|
|
|
root = _repo_path(repo)
|
|
|
|
|
if root is None:
|
|
|
|
|
continue
|
|
|
|
|
for directory, archived in (
|
|
|
|
|
(root / "workplans", False),
|
|
|
|
|
(root / "workplans" / "archived", True),
|
|
|
|
|
):
|
|
|
|
|
if not directory.is_dir():
|
|
|
|
|
continue
|
|
|
|
|
for path in sorted(directory.glob("*.md")):
|
|
|
|
|
data = _frontmatter(path)
|
2026-06-22 13:52:13 +02:00
|
|
|
workplan_id = data.get("state_hub_workstream_id") or data.get("state_hub_workplan_id")
|
|
|
|
|
if not workplan_id:
|
2026-05-02 12:01:45 +02:00
|
|
|
continue
|
2026-06-22 13:52:13 +02:00
|
|
|
file_status = normalize_workplan_status(data.get("status", ""))
|
2026-05-18 01:31:36 +02:00
|
|
|
review = (
|
|
|
|
|
ready_review_status(
|
|
|
|
|
root,
|
|
|
|
|
data.get("reviewed_against_commit"),
|
|
|
|
|
data.get("context_paths"),
|
|
|
|
|
)
|
|
|
|
|
if file_status == "ready"
|
|
|
|
|
else None
|
|
|
|
|
)
|
2026-06-22 13:52:13 +02:00
|
|
|
index[str(workplan_id)] = {
|
2026-05-02 12:01:45 +02:00
|
|
|
"filename": path.name,
|
|
|
|
|
"relative_path": str(path.relative_to(root)),
|
|
|
|
|
"repo_slug": repo.slug,
|
|
|
|
|
"archived": archived,
|
2026-05-18 01:31:36 +02:00
|
|
|
"status": file_status or None,
|
|
|
|
|
"needs_review": bool(review and review.needs_review),
|
|
|
|
|
"health_labels": ["needs_review"] if review and review.needs_review else [],
|
2026-05-02 12:01:45 +02:00
|
|
|
}
|
2026-07-04 00:42:56 +02:00
|
|
|
await _merge_db_backing_index(session, index)
|
2026-06-06 00:42:00 +02:00
|
|
|
return {"workplans": index, "workstreams": index}
|
|
|
|
|
|
|
|
|
|
|
2026-07-04 00:42:56 +02:00
|
|
|
async def _merge_db_backing_index(session: AsyncSession, index: dict[str, Any]) -> None:
|
|
|
|
|
"""Fill index gaps from DB-backed file bindings synced by fix-consistency."""
|
|
|
|
|
result = await session.execute(
|
|
|
|
|
select(Workplan, ManagedRepo.slug)
|
|
|
|
|
.join(ManagedRepo, Workplan.repo_id == ManagedRepo.id)
|
|
|
|
|
.where(Workplan.backing_filename.isnot(None))
|
|
|
|
|
)
|
|
|
|
|
for wp, repo_slug in result.all():
|
|
|
|
|
key = str(wp.id)
|
|
|
|
|
if key in index:
|
|
|
|
|
continue
|
|
|
|
|
index[key] = {
|
|
|
|
|
"filename": wp.backing_filename,
|
|
|
|
|
"relative_path": wp.backing_relative_path,
|
|
|
|
|
"repo_slug": repo_slug,
|
|
|
|
|
"archived": bool(wp.backing_archived),
|
|
|
|
|
"status": normalize_workplan_status(wp.status) if wp.status else None,
|
|
|
|
|
"needs_review": False,
|
|
|
|
|
"health_labels": [],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _invalidate_workplan_index_cache() -> None:
|
|
|
|
|
global _INDEX_CACHE, _INDEX_CACHE_AT
|
|
|
|
|
_INDEX_CACHE = None
|
|
|
|
|
_INDEX_CACHE_AT = 0.0
|
|
|
|
|
|
|
|
|
|
|
2026-06-06 00:42:00 +02:00
|
|
|
def _index_with_meta(*, stale: bool, refresh_in_progress: bool) -> dict[str, Any]:
|
|
|
|
|
age = time.monotonic() - _INDEX_CACHE_AT if _INDEX_CACHE_AT else None
|
|
|
|
|
return {
|
|
|
|
|
**(_INDEX_CACHE or {"workplans": {}, "workstreams": {}}),
|
|
|
|
|
"_meta": {
|
|
|
|
|
"generated_at": _INDEX_CACHE.get("_meta", {}).get("generated_at") if _INDEX_CACHE else None,
|
|
|
|
|
"stale": stale,
|
|
|
|
|
"cache_age_seconds": round(age, 3) if age is not None else None,
|
|
|
|
|
"refresh_in_progress": refresh_in_progress,
|
|
|
|
|
"last_error": _INDEX_LAST_ERROR,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _refresh_workplan_index_background() -> None:
|
|
|
|
|
global _INDEX_CACHE, _INDEX_CACHE_AT, _INDEX_LAST_ERROR
|
|
|
|
|
from api.database import async_session_factory
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
async with async_session_factory() as session:
|
|
|
|
|
index = await _build_workplan_index(session)
|
|
|
|
|
index["_meta"] = {
|
|
|
|
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
|
|
|
"stale": False,
|
|
|
|
|
"cache_age_seconds": 0.0,
|
|
|
|
|
"refresh_in_progress": False,
|
|
|
|
|
"last_error": None,
|
|
|
|
|
}
|
|
|
|
|
_INDEX_CACHE = index
|
|
|
|
|
_INDEX_CACHE_AT = time.monotonic()
|
|
|
|
|
_INDEX_LAST_ERROR = None
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
_INDEX_LAST_ERROR = str(exc)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ensure_index_refresh_started() -> None:
|
|
|
|
|
global _INDEX_REFRESH_TASK
|
|
|
|
|
if _INDEX_REFRESH_TASK is not None and not _INDEX_REFRESH_TASK.done():
|
|
|
|
|
return
|
|
|
|
|
_INDEX_REFRESH_TASK = asyncio.create_task(_refresh_workplan_index_background())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _workplan_index(
|
|
|
|
|
*,
|
|
|
|
|
refresh: bool,
|
|
|
|
|
session: AsyncSession,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
"""Map file-backed workplan ids to their local workplan filenames."""
|
|
|
|
|
global _INDEX_CACHE, _INDEX_CACHE_AT, _INDEX_LAST_ERROR
|
|
|
|
|
cache_age = time.monotonic() - _INDEX_CACHE_AT if _INDEX_CACHE_AT else None
|
|
|
|
|
if not refresh and _INDEX_CACHE is not None and cache_age is not None and cache_age < _INDEX_TTL:
|
|
|
|
|
refresh_running = _INDEX_REFRESH_TASK is not None and not _INDEX_REFRESH_TASK.done()
|
|
|
|
|
return _index_with_meta(stale=False, refresh_in_progress=refresh_running)
|
|
|
|
|
|
|
|
|
|
if not refresh and _INDEX_CACHE is not None:
|
|
|
|
|
_ensure_index_refresh_started()
|
|
|
|
|
return _index_with_meta(stale=True, refresh_in_progress=True)
|
|
|
|
|
|
|
|
|
|
index = await _build_workplan_index(session)
|
|
|
|
|
index["_meta"] = {
|
|
|
|
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
|
|
|
"stale": False,
|
|
|
|
|
"cache_age_seconds": 0.0,
|
|
|
|
|
"refresh_in_progress": False,
|
|
|
|
|
"last_error": None,
|
|
|
|
|
}
|
|
|
|
|
_INDEX_CACHE = index
|
perf(api): CUST-WP-0041 — DB indexes, TTL caches, noload on list endpoints
- Migration t7o8p9q0r1s2: indexes on tasks.status, tasks(workstream_id,status),
workstreams.status, sbom_snapshots(repo_id,snapshot_at)
- workplan-index: 30 s TTL cache + ?refresh param (4171 ms → 16 ms on hit)
- /state/summary: 15 s TTL cache, bypassed on Cache-Control: no-cache
- /topics/: noload(workstreams, decisions, progress_events) (2382 ms → 115 ms)
- /domains/: noload(topics, repos, goals) (2252 ms → 39 ms)
- /repos/: noload(goals) (2222 ms → 599 ms first / fast on repeat)
- conftest: reset TTL caches between tests to prevent bleed-through
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 11:12:17 +02:00
|
|
|
_INDEX_CACHE_AT = time.monotonic()
|
2026-06-06 00:42:00 +02:00
|
|
|
_INDEX_LAST_ERROR = None
|
perf(api): CUST-WP-0041 — DB indexes, TTL caches, noload on list endpoints
- Migration t7o8p9q0r1s2: indexes on tasks.status, tasks(workstream_id,status),
workstreams.status, sbom_snapshots(repo_id,snapshot_at)
- workplan-index: 30 s TTL cache + ?refresh param (4171 ms → 16 ms on hit)
- /state/summary: 15 s TTL cache, bypassed on Cache-Control: no-cache
- /topics/: noload(workstreams, decisions, progress_events) (2382 ms → 115 ms)
- /domains/: noload(topics, repos, goals) (2252 ms → 39 ms)
- /repos/: noload(goals) (2222 ms → 599 ms first / fast on repeat)
- conftest: reset TTL caches between tests to prevent bleed-through
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 11:12:17 +02:00
|
|
|
return _INDEX_CACHE
|
2026-05-02 12:01:45 +02:00
|
|
|
|
|
|
|
|
|
2026-06-22 13:52:13 +02:00
|
|
|
async def _create_workplan(
|
2026-06-04 08:25:31 +02:00
|
|
|
*,
|
2026-06-22 13:52:13 +02:00
|
|
|
body: WorkplanCreate,
|
2026-06-04 08:25:31 +02:00
|
|
|
session: AsyncSession,
|
2026-06-22 13:52:13 +02:00
|
|
|
) -> Workplan:
|
|
|
|
|
wp = Workplan(**body.model_dump())
|
|
|
|
|
session.add(wp)
|
Add state-hub v0.1 — local-first state service for the Custodian
Implements the first live layer of the Custodian cognitive infrastructure:
PostgreSQL schema, FastAPI REST API, FastMCP stdio server, and Observable
Framework telemetry dashboard.
- state-hub/: full stack (docker-compose, FastAPI, Alembic, MCP server, dashboard)
- 5 DB tables: topics, workstreams, tasks, decisions, progress_events
- 11 MCP tools + 5 resources registered in .mcp.json
- Observable dashboard: Overview, Workstreams, Decisions, Progress pages
- CLAUDE.md: session protocol (get_state_summary / add_progress_event ritual)
- ~/.claude/CLAUDE.md: global cross-project reference to the hub
- scripts/pull_image.py: WSL2 TLS-resilient Docker image downloader
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 17:47:49 +01:00
|
|
|
await session.commit()
|
2026-06-22 13:52:13 +02:00
|
|
|
await session.refresh(wp)
|
|
|
|
|
return wp
|
Add state-hub v0.1 — local-first state service for the Custodian
Implements the first live layer of the Custodian cognitive infrastructure:
PostgreSQL schema, FastAPI REST API, FastMCP stdio server, and Observable
Framework telemetry dashboard.
- state-hub/: full stack (docker-compose, FastAPI, Alembic, MCP server, dashboard)
- 5 DB tables: topics, workstreams, tasks, decisions, progress_events
- 11 MCP tools + 5 resources registered in .mcp.json
- Observable dashboard: Overview, Workstreams, Decisions, Progress pages
- CLAUDE.md: session protocol (get_state_summary / add_progress_event ritual)
- ~/.claude/CLAUDE.md: global cross-project reference to the hub
- scripts/pull_image.py: WSL2 TLS-resilient Docker image downloader
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 17:47:49 +01:00
|
|
|
|
|
|
|
|
|
2026-06-22 13:52:13 +02:00
|
|
|
async def _get_workplan(
|
2026-06-04 08:25:31 +02:00
|
|
|
*,
|
2026-06-22 13:52:13 +02:00
|
|
|
workplan_id: uuid.UUID,
|
2026-06-04 08:25:31 +02:00
|
|
|
session: AsyncSession,
|
2026-06-22 13:52:13 +02:00
|
|
|
) -> Workplan:
|
|
|
|
|
wp = await session.get(Workplan, workplan_id)
|
|
|
|
|
if wp is None:
|
2026-06-04 08:25:31 +02:00
|
|
|
raise HTTPException(status_code=404, detail="Workplan not found")
|
2026-06-22 13:52:13 +02:00
|
|
|
return wp
|
Add state-hub v0.1 — local-first state service for the Custodian
Implements the first live layer of the Custodian cognitive infrastructure:
PostgreSQL schema, FastAPI REST API, FastMCP stdio server, and Observable
Framework telemetry dashboard.
- state-hub/: full stack (docker-compose, FastAPI, Alembic, MCP server, dashboard)
- 5 DB tables: topics, workstreams, tasks, decisions, progress_events
- 11 MCP tools + 5 resources registered in .mcp.json
- Observable dashboard: Overview, Workstreams, Decisions, Progress pages
- CLAUDE.md: session protocol (get_state_summary / add_progress_event ritual)
- ~/.claude/CLAUDE.md: global cross-project reference to the hub
- scripts/pull_image.py: WSL2 TLS-resilient Docker image downloader
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 17:47:49 +01:00
|
|
|
|
|
|
|
|
|
2026-06-22 13:52:13 +02:00
|
|
|
async def _update_workplan(
|
2026-06-04 08:25:31 +02:00
|
|
|
*,
|
2026-06-22 13:52:13 +02:00
|
|
|
workplan_id: uuid.UUID,
|
|
|
|
|
body: WorkplanUpdate,
|
2026-06-04 08:25:31 +02:00
|
|
|
session: AsyncSession,
|
2026-06-22 13:52:13 +02:00
|
|
|
) -> Workplan:
|
|
|
|
|
wp = await session.get(Workplan, workplan_id)
|
|
|
|
|
if wp is None:
|
2026-06-04 08:25:31 +02:00
|
|
|
raise HTTPException(status_code=404, detail="Workplan not found")
|
2026-05-23 18:42:32 +02:00
|
|
|
update_data = body.model_dump(exclude_unset=True)
|
|
|
|
|
status_update = update_data.pop("status", None)
|
2026-06-22 13:52:13 +02:00
|
|
|
prev_status = wp.status
|
2026-05-23 18:42:32 +02:00
|
|
|
for field, value in update_data.items():
|
2026-06-22 13:52:13 +02:00
|
|
|
setattr(wp, field, value)
|
2026-05-23 18:42:32 +02:00
|
|
|
if status_update is not None:
|
2026-06-22 13:52:13 +02:00
|
|
|
transition_workplan_status(wp, status_update)
|
Add state-hub v0.1 — local-first state service for the Custodian
Implements the first live layer of the Custodian cognitive infrastructure:
PostgreSQL schema, FastAPI REST API, FastMCP stdio server, and Observable
Framework telemetry dashboard.
- state-hub/: full stack (docker-compose, FastAPI, Alembic, MCP server, dashboard)
- 5 DB tables: topics, workstreams, tasks, decisions, progress_events
- 11 MCP tools + 5 resources registered in .mcp.json
- Observable dashboard: Overview, Workstreams, Decisions, Progress pages
- CLAUDE.md: session protocol (get_state_summary / add_progress_event ritual)
- ~/.claude/CLAUDE.md: global cross-project reference to the hub
- scripts/pull_image.py: WSL2 TLS-resilient Docker image downloader
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 17:47:49 +01:00
|
|
|
await session.commit()
|
2026-06-22 13:52:13 +02:00
|
|
|
await session.refresh(wp)
|
2026-05-17 05:49:29 +02:00
|
|
|
|
2026-06-22 13:52:13 +02:00
|
|
|
if normalize_workplan_status(prev_status) != "finished" and wp.status == "finished":
|
|
|
|
|
await _publish_completion_events(wp, session)
|
2026-05-17 05:49:29 +02:00
|
|
|
|
2026-06-22 13:52:13 +02:00
|
|
|
return wp
|
Add state-hub v0.1 — local-first state service for the Custodian
Implements the first live layer of the Custodian cognitive infrastructure:
PostgreSQL schema, FastAPI REST API, FastMCP stdio server, and Observable
Framework telemetry dashboard.
- state-hub/: full stack (docker-compose, FastAPI, Alembic, MCP server, dashboard)
- 5 DB tables: topics, workstreams, tasks, decisions, progress_events
- 11 MCP tools + 5 resources registered in .mcp.json
- Observable dashboard: Overview, Workstreams, Decisions, Progress pages
- CLAUDE.md: session protocol (get_state_summary / add_progress_event ritual)
- ~/.claude/CLAUDE.md: global cross-project reference to the hub
- scripts/pull_image.py: WSL2 TLS-resilient Docker image downloader
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 17:47:49 +01:00
|
|
|
|
|
|
|
|
|
2026-06-22 13:52:13 +02:00
|
|
|
async def _archive_workplan(
|
2026-06-04 08:25:31 +02:00
|
|
|
*,
|
2026-06-22 13:52:13 +02:00
|
|
|
workplan_id: uuid.UUID,
|
2026-06-04 08:25:31 +02:00
|
|
|
session: AsyncSession,
|
2026-06-22 13:52:13 +02:00
|
|
|
) -> Workplan:
|
|
|
|
|
wp = await session.get(Workplan, workplan_id)
|
|
|
|
|
if wp is None:
|
2026-06-04 08:25:31 +02:00
|
|
|
raise HTTPException(status_code=404, detail="Workplan not found")
|
2026-06-22 13:52:13 +02:00
|
|
|
transition_workplan_status(wp, "archived")
|
Add state-hub v0.1 — local-first state service for the Custodian
Implements the first live layer of the Custodian cognitive infrastructure:
PostgreSQL schema, FastAPI REST API, FastMCP stdio server, and Observable
Framework telemetry dashboard.
- state-hub/: full stack (docker-compose, FastAPI, Alembic, MCP server, dashboard)
- 5 DB tables: topics, workstreams, tasks, decisions, progress_events
- 11 MCP tools + 5 resources registered in .mcp.json
- Observable dashboard: Overview, Workstreams, Decisions, Progress pages
- CLAUDE.md: session protocol (get_state_summary / add_progress_event ritual)
- ~/.claude/CLAUDE.md: global cross-project reference to the hub
- scripts/pull_image.py: WSL2 TLS-resilient Docker image downloader
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 17:47:49 +01:00
|
|
|
await session.commit()
|
2026-06-22 13:52:13 +02:00
|
|
|
await session.refresh(wp)
|
|
|
|
|
return wp
|
2026-06-04 08:25:31 +02:00
|
|
|
|
|
|
|
|
|
2026-06-22 13:52:13 +02:00
|
|
|
async def _publish_completion_events(wp: Workplan, session: AsyncSession) -> None:
|
2026-06-04 08:25:31 +02:00
|
|
|
workplan_envelope = EventEnvelope.new(
|
|
|
|
|
_COMPLETED_WORKPLAN_EVENT,
|
|
|
|
|
attributes={
|
2026-06-22 13:52:13 +02:00
|
|
|
"workplan_id": str(wp.id),
|
|
|
|
|
"slug": wp.slug,
|
|
|
|
|
"title": wp.title,
|
|
|
|
|
"topic_id": str(wp.topic_id) if wp.topic_id else None,
|
|
|
|
|
"repo_id": str(wp.repo_id) if wp.repo_id else None,
|
|
|
|
|
"repo_goal_id": str(wp.repo_goal_id) if wp.repo_goal_id else None,
|
2026-06-04 08:25:31 +02:00
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
asyncio.create_task(publish_event(_COMPLETED_WORKPLAN_EVENT, workplan_envelope))
|
|
|
|
|
|
|
|
|
|
|
2026-07-10 13:31:57 +02:00
|
|
|
@router.get("/", status_code=status.HTTP_410_GONE)
|
2026-06-04 08:25:31 +02:00
|
|
|
async def list_workstreams(
|
|
|
|
|
request: Request,
|
|
|
|
|
response: Response,
|
|
|
|
|
topic_id: uuid.UUID | None = None,
|
|
|
|
|
repo_id: uuid.UUID | None = None,
|
|
|
|
|
repo_goal_id: uuid.UUID | None = None,
|
|
|
|
|
status: str | None = None,
|
|
|
|
|
owner: str | None = None,
|
|
|
|
|
slug: str | None = None,
|
|
|
|
|
session: AsyncSession = Depends(get_session),
|
2026-07-10 13:31:57 +02:00
|
|
|
) -> None:
|
|
|
|
|
await retire_legacy_route(
|
2026-06-04 08:25:31 +02:00
|
|
|
session=session,
|
|
|
|
|
request=request,
|
|
|
|
|
response=response,
|
|
|
|
|
interface_key=_legacy_key("GET", "/workstreams/"),
|
|
|
|
|
replacement_ref="/workplans/",
|
2026-07-10 13:31:57 +02:00
|
|
|
detail="Legacy GET /workstreams/ retired; use GET /workplans/",
|
2026-06-04 08:25:31 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-06-22 13:52:13 +02:00
|
|
|
@workplan_router.get("/", response_model=list[WorkplanRead])
|
2026-06-04 08:25:31 +02:00
|
|
|
async def list_workplans(
|
|
|
|
|
topic_id: uuid.UUID | None = None,
|
|
|
|
|
repo_id: uuid.UUID | None = None,
|
|
|
|
|
repo_goal_id: uuid.UUID | None = None,
|
|
|
|
|
status: str | None = None,
|
|
|
|
|
owner: str | None = None,
|
|
|
|
|
slug: str | None = None,
|
|
|
|
|
session: AsyncSession = Depends(get_session),
|
2026-06-22 13:52:13 +02:00
|
|
|
) -> list[Workplan]:
|
|
|
|
|
return await _list_workplans(
|
2026-06-04 08:25:31 +02:00
|
|
|
topic_id=topic_id,
|
|
|
|
|
repo_id=repo_id,
|
|
|
|
|
repo_goal_id=repo_goal_id,
|
|
|
|
|
status_filter=status,
|
|
|
|
|
owner=owner,
|
|
|
|
|
slug=slug,
|
|
|
|
|
session=session,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-07-09 00:43:08 +02:00
|
|
|
@router.get("/workplan-index", status_code=status.HTTP_410_GONE)
|
2026-06-04 08:25:31 +02:00
|
|
|
async def workplan_index(
|
|
|
|
|
request: Request,
|
|
|
|
|
response: Response,
|
|
|
|
|
refresh: bool = Query(False, description="Force cache invalidation"),
|
|
|
|
|
session: AsyncSession = Depends(get_session),
|
2026-07-09 00:43:08 +02:00
|
|
|
) -> None:
|
2026-07-10 13:31:57 +02:00
|
|
|
await retire_legacy_route(
|
2026-06-04 08:25:31 +02:00
|
|
|
session=session,
|
|
|
|
|
request=request,
|
|
|
|
|
response=response,
|
|
|
|
|
interface_key=_legacy_key("GET", "/workstreams/workplan-index"),
|
2026-07-10 13:31:57 +02:00
|
|
|
replacement_ref="/workplans/index",
|
2026-07-09 00:43:08 +02:00
|
|
|
detail="Legacy GET /workstreams/workplan-index retired; use GET /workplans/index",
|
2026-06-04 08:25:31 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@workplan_router.get("/index")
|
|
|
|
|
async def workplan_index_preferred(
|
|
|
|
|
refresh: bool = Query(False, description="Force cache invalidation"),
|
|
|
|
|
session: AsyncSession = Depends(get_session),
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
return await _workplan_index(refresh=refresh, session=session)
|
|
|
|
|
|
|
|
|
|
|
2026-07-04 00:42:56 +02:00
|
|
|
@workplan_router.put("/index/bindings")
|
|
|
|
|
async def sync_workplan_bindings(
|
|
|
|
|
body: WorkplanBindingsSync,
|
|
|
|
|
session: AsyncSession = Depends(get_session),
|
|
|
|
|
) -> dict[str, int]:
|
|
|
|
|
"""Upsert workstation workplan file bindings for remote API index fallback."""
|
|
|
|
|
synced_at = datetime.now(timezone.utc)
|
|
|
|
|
updated = 0
|
2026-08-30 22:38:54 +02:00
|
|
|
requested_ids = {entry.workplan_id for entry in body.bindings}
|
|
|
|
|
rows = await session.execute(select(Workplan).where(Workplan.id.in_(requested_ids)))
|
|
|
|
|
workplans = {workplan.id: workplan for workplan in rows.scalars().all()}
|
2026-07-04 00:42:56 +02:00
|
|
|
for entry in body.bindings:
|
2026-08-30 22:38:54 +02:00
|
|
|
wp = workplans.get(entry.workplan_id)
|
2026-07-04 00:42:56 +02:00
|
|
|
if wp is None:
|
|
|
|
|
continue
|
|
|
|
|
wp.backing_filename = entry.filename
|
|
|
|
|
wp.backing_relative_path = entry.relative_path
|
|
|
|
|
wp.backing_archived = entry.archived
|
|
|
|
|
wp.backing_synced_at = synced_at
|
|
|
|
|
updated += 1
|
|
|
|
|
await session.commit()
|
|
|
|
|
_invalidate_workplan_index_cache()
|
|
|
|
|
return {"updated": updated, "received": len(body.bindings)}
|
|
|
|
|
|
|
|
|
|
|
2026-07-10 13:31:57 +02:00
|
|
|
@router.post("/", status_code=status.HTTP_410_GONE)
|
2026-06-04 08:25:31 +02:00
|
|
|
async def create_workstream(
|
|
|
|
|
request: Request,
|
|
|
|
|
response: Response,
|
2026-06-22 13:52:13 +02:00
|
|
|
body: WorkplanCreate,
|
2026-06-04 08:25:31 +02:00
|
|
|
session: AsyncSession = Depends(get_session),
|
2026-07-10 13:31:57 +02:00
|
|
|
) -> None:
|
|
|
|
|
await retire_legacy_route(
|
2026-06-04 08:25:31 +02:00
|
|
|
session=session,
|
|
|
|
|
request=request,
|
|
|
|
|
response=response,
|
|
|
|
|
interface_key=_legacy_key("POST", "/workstreams/"),
|
|
|
|
|
replacement_ref="/workplans/",
|
2026-07-10 13:31:57 +02:00
|
|
|
detail="Legacy POST /workstreams/ retired; use POST /workplans/",
|
2026-06-04 08:25:31 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-06-22 13:52:13 +02:00
|
|
|
@workplan_router.post("/", response_model=WorkplanRead, status_code=status.HTTP_201_CREATED)
|
2026-06-04 08:25:31 +02:00
|
|
|
async def create_workplan(
|
2026-06-22 13:52:13 +02:00
|
|
|
body: WorkplanCreate,
|
2026-06-04 08:25:31 +02:00
|
|
|
session: AsyncSession = Depends(get_session),
|
2026-06-22 13:52:13 +02:00
|
|
|
) -> Workplan:
|
|
|
|
|
return await _create_workplan(body=body, session=session)
|
2026-06-04 08:25:31 +02:00
|
|
|
|
|
|
|
|
|
2026-07-10 13:31:57 +02:00
|
|
|
@router.get("/{workstream_id}", status_code=status.HTTP_410_GONE)
|
2026-06-04 08:25:31 +02:00
|
|
|
async def get_workstream(
|
|
|
|
|
request: Request,
|
|
|
|
|
response: Response,
|
|
|
|
|
workstream_id: uuid.UUID,
|
|
|
|
|
session: AsyncSession = Depends(get_session),
|
2026-07-10 13:31:57 +02:00
|
|
|
) -> None:
|
|
|
|
|
await retire_legacy_route(
|
2026-06-04 08:25:31 +02:00
|
|
|
session=session,
|
|
|
|
|
request=request,
|
|
|
|
|
response=response,
|
|
|
|
|
interface_key=_legacy_key("GET", "/workstreams/{workstream_id}"),
|
|
|
|
|
replacement_ref="/workplans/{workplan_id}",
|
2026-07-10 13:31:57 +02:00
|
|
|
detail="Legacy GET /workstreams/{workstream_id} retired; use GET /workplans/{workplan_id}",
|
2026-06-04 08:25:31 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-06-22 13:52:13 +02:00
|
|
|
@workplan_router.get("/{workplan_id}", response_model=WorkplanRead)
|
2026-06-04 08:25:31 +02:00
|
|
|
async def get_workplan(
|
|
|
|
|
workplan_id: uuid.UUID,
|
|
|
|
|
session: AsyncSession = Depends(get_session),
|
2026-06-22 13:52:13 +02:00
|
|
|
) -> Workplan:
|
|
|
|
|
return await _get_workplan(workplan_id=workplan_id, session=session)
|
2026-06-04 08:25:31 +02:00
|
|
|
|
|
|
|
|
|
2026-07-10 13:31:57 +02:00
|
|
|
@router.patch("/{workstream_id}", status_code=status.HTTP_410_GONE)
|
2026-06-04 08:25:31 +02:00
|
|
|
async def update_workstream(
|
|
|
|
|
request: Request,
|
|
|
|
|
response: Response,
|
|
|
|
|
workstream_id: uuid.UUID,
|
2026-06-22 13:52:13 +02:00
|
|
|
body: WorkplanUpdate,
|
2026-06-04 08:25:31 +02:00
|
|
|
session: AsyncSession = Depends(get_session),
|
2026-07-10 13:31:57 +02:00
|
|
|
) -> None:
|
|
|
|
|
await retire_legacy_route(
|
2026-06-04 08:25:31 +02:00
|
|
|
session=session,
|
|
|
|
|
request=request,
|
|
|
|
|
response=response,
|
|
|
|
|
interface_key=_legacy_key("PATCH", "/workstreams/{workstream_id}"),
|
|
|
|
|
replacement_ref="/workplans/{workplan_id}",
|
2026-07-10 13:31:57 +02:00
|
|
|
detail="Legacy PATCH /workstreams/{workstream_id} retired; use PATCH /workplans/{workplan_id}",
|
2026-06-04 08:25:31 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-06-22 13:52:13 +02:00
|
|
|
@workplan_router.patch("/{workplan_id}", response_model=WorkplanRead)
|
2026-06-04 08:25:31 +02:00
|
|
|
async def update_workplan(
|
|
|
|
|
workplan_id: uuid.UUID,
|
2026-06-22 13:52:13 +02:00
|
|
|
body: WorkplanUpdate,
|
2026-06-04 08:25:31 +02:00
|
|
|
session: AsyncSession = Depends(get_session),
|
2026-06-22 13:52:13 +02:00
|
|
|
) -> Workplan:
|
|
|
|
|
return await _update_workplan(workplan_id=workplan_id, body=body, session=session)
|
2026-06-04 08:25:31 +02:00
|
|
|
|
|
|
|
|
|
2026-07-08 23:36:58 +02:00
|
|
|
@router.delete("/{workstream_id}", status_code=status.HTTP_410_GONE)
|
2026-06-04 08:25:31 +02:00
|
|
|
async def archive_workstream(
|
|
|
|
|
request: Request,
|
|
|
|
|
response: Response,
|
|
|
|
|
workstream_id: uuid.UUID,
|
|
|
|
|
session: AsyncSession = Depends(get_session),
|
2026-07-08 23:36:58 +02:00
|
|
|
) -> None:
|
2026-07-10 13:31:57 +02:00
|
|
|
await retire_legacy_route(
|
2026-06-04 08:25:31 +02:00
|
|
|
session=session,
|
|
|
|
|
request=request,
|
|
|
|
|
response=response,
|
|
|
|
|
interface_key=_legacy_key("DELETE", "/workstreams/{workstream_id}"),
|
2026-07-10 13:31:57 +02:00
|
|
|
replacement_ref="DELETE /workplans/{workplan_id}",
|
2026-07-08 23:36:58 +02:00
|
|
|
detail="Legacy DELETE /workstreams/{workstream_id} retired; use DELETE /workplans/{workplan_id}",
|
2026-06-04 08:25:31 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-06-22 13:52:13 +02:00
|
|
|
@workplan_router.delete("/{workplan_id}", response_model=WorkplanRead)
|
2026-06-04 08:25:31 +02:00
|
|
|
async def archive_workplan(
|
|
|
|
|
workplan_id: uuid.UUID,
|
|
|
|
|
session: AsyncSession = Depends(get_session),
|
2026-06-22 13:52:13 +02:00
|
|
|
) -> Workplan:
|
2026-08-30 22:38:54 +02:00
|
|
|
return await _archive_workplan(workplan_id=workplan_id, session=session)
|