ADR-0030: Dashboard Routes Domain Decomposition¶
Status¶
Accepted
Context¶
src/dashboard_routes/_routes.py was 4,178 lines with 95 route handlers in a single create_router() function. Route handlers captured 17+ closure variables (config, event_bus, state, pr_manager, etc.). Navigation, testing, and modification were difficult.
A RouteContext dataclass already existed (ADR-0007) bundling all dependencies, but all routes remained in one file.
Decision¶
Pattern: register(router, ctx) functions in domain files¶
Extracted 70 route handlers into 7 domain-specific files, each exporting a register(router: APIRouter, ctx: RouteContext) -> None function:
| Module | Routes | Scope |
|---|---|---|
_epic_routes.py |
3 | Epic tracking and release |
_crates_routes.py |
9 | Crate CRUD, items, active, advance |
_hitl_routes.py |
9 | HITL management, human-input |
_control_routes.py |
13 | Pipeline control, admin tasks, bot-pr settings |
_metrics_routes.py |
12 | Metrics, insights, runs, artifacts |
_reports_routes.py |
5 | Bug report submission and tracking |
_state_routes.py |
19 | Runtimes, repos, filesystem, GitHub |
Why register() over include_router()¶
FastAPI's include_router with prefix mounting would change URL paths (e.g., /api/control/start becomes a sub-router concern). Since existing tests use find_endpoint(router, "/api/control/start") to locate handlers, changing the routing structure would break all 381 dashboard tests.
The register(router, ctx) pattern is simpler: each domain function decorates the shared router directly. No URL changes, no test changes, no prefix management. The domain files are just organizational — the runtime behavior is identical.
What stays in _routes.py¶
RouteContext dataclass, create_router() coordinator, issue history routes (complex cache infrastructure), core routes (healthz, pipeline, sessions, websocket, SPA catchall), and all shared helper functions. This reduced _routes.py from 4,178 to 2,019 lines (52% reduction).
Import structure¶
- Domain files import
RouteContextfromdashboard_routes._routes(direct, not via package) _routes.pyimports domain files only insidecreate_router()(local imports, deferred)__init__.pyre-exports public symbols from_routes.pyand_common.py- No circular import risk
Consequences¶
_routes.pyis now navigable — 2,019 lines with a clear table of contents via the_register_*calls increate_router()- Adding a new route domain: create
_<domain>_routes.py, add aregister()call increate_router() - All 381 existing tests pass without modification (except 2 monkeypatch target updates)
- Further decomposition of the remaining 25 core routes is possible using the same pattern