TB-25 — Portal Router Consolidation
Status: Plan only. Approved direction pending.
Origin: Session 47 audit (2026-04-17).
Goal: Reduce 32 portal_*.py routers in /opt/unlikely-api/routes/ (22,811 LOC) to 8 domain-grouped routers while preserving every HTTP path the frontend depends on.
Proposed target architecture (8 routers)
| # | Target file | Absorbs | Risk | Notes |
|---|---|---|---|---|
| A | portal_auth.py | unchanged | — | Self-contained. No merge. |
| B | portal_projects_core.py | portal_projects + portal_status + portal_cert_board + portal_temp | MED | portal_status is misnamed (dependency health, not project status). portal_temp may be dead code — grep frontend first, delete if unused. |
| C | portal_workflow.py | intake_requests + sow_gate + sched_approve + schedule_changes + calendar_drag + cancel + holding_pool + escalations + sla | HIGH | Nine files, 4742 LOC, lateral dependencies. Prefer a routes/workflow/ subpackage over a single monolith. |
| D | portal_messaging.py | messages + notifications + chat + rfi | HIGH | portal_chat.py alone is 1524 LOC with WebSocket state. Prefer routes/messaging/ subpackage. |
| E | portal_billing.py | cert_invoice + payment_recon + reconciliation | MED-HIGH | Touches money. Defer to dedicated sprint. |
| F | portal_field.py | inspections + failed_inspections + missed_appointments + return_visits (portal half) | MED | _record_missed_flag is imported by 3 files — lift to services/ first. |
| G | portal_files.py | unchanged | — | Self-contained. No merge. |
| H | portal_admin.py | actions + audit + reporting + dashboard + ssi | LOW | Recommended first merge. |
| I | portal_friday.py | unchanged | — | Split candidate, not merge. _TOOL_EXECUTORS / _ALL_TOOLS consumed by friday_engine.py. |
Recommended first merge: portal_admin
Scope: 5 files → 1. 25 endpoints. 747 LOC. Zero cross-module coupling.
Files absorbed: portal_actions.py, portal_audit.py, portal_reporting.py, portal_dashboard.py, portal_ssi.py.
Why first
- No other file in the codebase imports from any of these five.
- Mount prefixes are disjoint (
/portal/actions,/portal/audit,/portal/reporting,/portal/dashboard,/portal/ssi) — no route-path conflicts. - No WebSockets, no background workers, no shared state.
- Small enough to review in one PR.
Step-by-step
- Create
routes/portal_admin.pywith 5 namedAPIRouter()instances (pattern already used byportal_return_visits.py):actions_router = APIRouter() audit_router = APIRouter() reporting_router = APIRouter() dashboard_router = APIRouter() ssi_router = APIRouter() - Copy handlers verbatim from each source file, replacing the single
routerreference with the appropriate named router. All paths, models, dependencies, response types unchanged. - Consolidate imports at top of the new file (dedupe).
- Update
main.pyimports (lines 106, 107, 109, 117, 144):from routes.portal_admin import ( actions_router as portal_actions_router, audit_router as portal_audit_router, reporting_router as portal_reporting_router, dashboard_router as portal_dashboard_router, ssi_router as portal_ssi_router, ) - Delete 5 old files.
- Rebuild:
cd /opt/unlikely-api && docker compose up -d --build unlikely-api && docker compose restart worker scheduler. - Smoke test each path from the portal (no path changes — pure regression check):
GET /portal/actions/summary- one endpoint each from /portal/audit, /portal/reporting, /portal/dashboard
GET /portal/ssi/submissionsGET /openapi.json— confirm 25 endpoints still present.
- Commit + push per standing policy.
Rollback
git revert <merge commit> restores exact prior state — change is additive-then-subtractive with no path renames.
Deferred / high-risk
- portal_friday (3611 LOC). Split candidate, not merge. Extract tool definitions into
services/friday_tools.pyfirst. - portal_projects (2551 LOC). Core read surface. Split out site-visit and flag endpoints rather than absorb more.
- portal_chat (1524 LOC, WebSocket, push subs). Use a
routes/messaging/subpackage — never merge into a single file. - portal_cert_invoice + portal_payment_recon (2537 LOC combined). Money. Dedicated sprint.
- Workflow cluster (9 files, 4742 LOC, lateral deps). Needs its own effort.
- portal_missed_appointments exports
_record_missed_flag(used by trip_charge, whatsapp, supabase_write_service). Lift toservices/missed_appointments.pyfirst, then merge the route shell. - portal_intake_requests exports
create_intake_request_from_confirm(used by intake.py). Same lift-to-services pattern first.
Other observations
- Route layer leaks into service responsibilities in 4 places — any serious consolidation should lift these to
services/first, then collapse the route file. portal_status.pyis misnamed (dependency health, not project status). Consider renaming toportal_health.pyor folding into/monitorin a follow-up pass.portal_temp.py’s two endpoints (/temp-report,/temp-report-2025) may be dead code — grep the portal before absorbing.- Mount-prefix convention is inconsistent: 6 files share
/portalwith per-route paths while the rest have dedicated sub-prefixes. Fix only in a dedicated API-versioning sprint — never during a consolidation pass. portal_return_visits.pyis the template for how consolidated files should export multiple named routers.