Appearance
Flask Application
dps-code-api/app.py is the Flask application. It is primarily the composition and routing layer: it wires the environment, session, CORS, and route registration, and each route handler is a thin adapter — parse the request, apply gates, delegate to a service or shared operation, map the result to a response. Domain logic lives in the service modules under dps-code-api/.
Module layout at a glance
text
app.py routes, request handling, error mapping
├── environment.py per-request environment config + sessions
├── authz.py authentication/authorization gates
├── db_access.py low-level data access + serialization
├── request_limits.py body-size cap, input bounds
├── reports_service.py report actions/statuses (shared operations)
├── punishment_service.py / discipline_queue.py / action_queue.py
├── event_service.py canonical event stream interface
├── training_engine.py / training_service.py / training_runtime.py
├── agents_service.py / admin_service.py / health_service.py
├── oauth_service.py / contact_threads.py / erlc_relay.py
└── demo_seed.py demo-only dataset + resetapp.py re-exports the extracted helpers so existing callers and tests keep working; new code should import from the owning module directly.
Application setup
Flask(__name__)withSECRET_KEY(fallback),SESSION_COOKIE_HTTPONLY=True,SESSION_COOKIE_SECURE(default true),SESSION_COOKIE_SAMESITE=Lax, andMAX_CONTENT_LENGTH = 1 MiB.app.session_interface = EnvironmentSessionInterface()— environment-bound signed cookies (see Environments).- Boot fails fast when the production environment is not configured.
- An optional demo auto-reset scheduler starts as a daemon thread (
DEMO_AUTO_RESET_ENABLED) that can only touch the demo database. - The ER:LC integration requires
ERLC_RELAY_URLS+ERLC_RELAY_TOKEN(production) orERLC_SERVER_KEY(local dev) at import time.
Request handling pipeline
text
request
→ before_request: environment resolution (fail closed 503) + body-size check
→ route handler:
parse/validate input
authentication (verify_api_key or active_agent_from_session)
authorization (authorize_dashboard / require_admin / trainer gates)
row-level checks (supervisor_report_denied, is_assigned_agent)
service call → authoritative RPC or gated table write
serialize response (identifier scrubbing)
→ after_request: CORS for allowlisted origins
→ error handlers: 404 / 405 / 500 → JSON error contractEnvironment resolution
_require_request_environment runs before every request except the root liveness route. It resolves the environment from the trusted ingress hostname and returns {"success": false, "error": "API environment unavailable"} (503) when the host is unknown, the ingress assertion is invalid, or the environment is not configured. It also converts oversized bodies to the JSON error contract (413).
Authentication
- Browser sessions: the signed session cookie carries the agent's
discord_id.active_agent_from_session()(authz) fetches the liveagentsrow each request (cached per request), clears the session when the agent is missing or notactive/onboarding, and returns the row. - Bot/integration:
verify_api_key()compares theAuthorizationheader to the active environment'sAPI_KEYin constant time (cached per request).
Authorization
authorize_dashboard(permission)— API key (where a route allows the bot) or an active session with the required clearance; thereassign_docketpermission additionally requires Head Investigator rank or above.supervisor_report_denied(row)— row-level gate for supervisor-flagged reports.require_admin(permission, director_only=False)— browser-session only: admin allowlist membership + clearance + (for Director-only capabilities) Director rank.- Training gates in
training_runtime.py— browser session, trainer role, session-trainer ownership, trainee session membership.
Validation
Route handlers validate before touching the database: required fields, status enum membership, report-ID bounds (request_limits.validate_report_id), URL schemes (_validate_http_url), numeric bounds, and enum/whitelist membership. The transactional RPCs re-validate the critical values inside the database.
Service calls
Handlers delegate to shared operations, e.g.:
reports_service.perform_report_action(...)— the single application path for validate/invalidate/investigate/contact_reporter/claim; the mutation itself runs insiderpc_report_action.reports_service.conclude_investigation(...)— the single conclude path.punishment_service,discipline_queue,action_queue,admin_service— domain contracts and helpers.event_service.record_event / record_report_event— canonical event writes from application code (the transactional RPCs write their own events in the same transaction).
Error handling
- RPC errors are mapped tag → HTTP status (e.g.
REPORT_NOT_FOUND→ 404,STATUS_CONFLICT→ 409,ACTION_ALREADY_COMPLETED→ 409,DIRECTOR_REQUIRED→ 403). APIError(PostgREST) → 500 with the database message; unexpected exceptions → 500 "Unexpected server error".- Global handlers return the JSON contract for 404/405/500.
Response conventions
- Success:
{"success": true, ...}with a domain payload. - Failure:
{"success": false, "error": "<message>"}with an appropriate status. GET /actions/next(and queue pickups) return 204 with an empty body when there is nothing to do — an easy empty check for the bot.- Serializers are field whitelists (
serialize_*indb_access.py); raw DB rows are never dumped to the client. - Raw user IDs are scrubbed from display text (
_scrub_user_ids); identity lives in dedicated*_idfields. - Training mutations return slim
{"success": true}payloads; the frontend re-fetches authoritative state (session creation is the exception — it returns the full session).
What must NOT live in app.py
Per the project's organization rules (see Code Organization):
- Business rules belong in services (
reports_service,punishment_service,training_*, …). - Gating logic belongs in
authz.py/training_runtime.py. - Data access and serialization belong in
db_access.py/training_service.py. - Configuration belongs in
environment.py. - Authoritative multi-step mutations belong in the named RPCs.
If you find yourself adding a route that reaches for a table directly, first check whether an RPC already owns that mutation — and if not, whether it should (see RPC layer).