# Project export: Triage

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: OpenAI Build Week
- Tagline: Triage uses Codex to sort scattered WhatsApp, email, and Classroom chaos into a clear Action queue and a ranked Study plan—you approve every action; it never decides alone.
- Devpost: https://devpost.com/software/triage-d2u63j
- GitHub: https://github.com/mohan27042007/Triage
- Video: https://www.youtube.com/embed/PS7LdBnxtM0?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Mohanarangan T R (47 commits)

## Devpost submission (written by the team)

### Inspiration

College communication does not arrive in one place. It is scattered across Gmail, Google Classroom, WhatsApp groups, files, and last-minute notices. The problem is not just volume—it is that some messages are compulsory, time-sensitive, and easy to miss: registrations, lab records, attendance forms, polls, deadlines, and reminders. At the same time, students receive question banks, unit notes, and assignment prompts without a clear way to decide what deserves attention first. I wanted to build something that treats student attention as limited and valuable. “Triage” comes from the medical practice of sorting incoming cases by urgency. The same idea fits student communication: identify what needs action now, what is useful for study, and what is just noise.

### What it does

Triage is a local-first AI student desk that turns scattered academic communication into clear next steps. It can ingest pasted notices, uploaded text files, Gmail messages, Google Classroom announcements/coursework, and representative WhatsApp-style demo messages. Each item is classified into one of three categories: Obligation — deadlines, registrations, forms, mandatory notices, and polls. Study Material — question banks, unit notes, and academic material. Noise — messages that do not require action or study. For obligations, Triage extracts explicit deadlines, identifies whether something is mandatory, groups work into Immediate, This Week, and Later, and shows it in an Action Queue. For study material, Triage compares question-bank material with unit notes and produces a ranked study outline. Assignment help is intentionally scaffolded: it breaks work into requirements, concepts, an approach, and test cases, without generating a ready-to-submit answer. Triage also includes a Human Review drawer. Nothing is sent or submitted externally. For completion polls and routine forms, it can draft an editable response such as “YES, completed,” but the student must copy it themselves. Approving an action only changes its status inside Triage.

### How we built it

Triage is built as a web application with a Python/FastAPI backend, SQLite persistence, and a custom HTML/CSS/JavaScript frontend. The backend uses the OpenAI API with structured JSON outputs to classify messages as Obligation, Study Material, or Noise, while extracting deadlines, mandatory status, and poll/form patterns. A separate study-planning flow ranks recurring topics from uploaded question-bank and unit-note text. Google integrations use read-only OAuth for Gmail and Google Classroom in the local setup. Uploaded text files are preserved locally so the original source material can be revisited. WhatsApp is represented by clearly labelled simulated group messages in the demo rather than an unreliable live bridge. The frontend is designed as a student command desk: a pulse-inspired navigation rail, Action Queue, ranked study cards, compact detail dialogs, deadline reminders, an Approval Drawer, light/dark/system themes, and reduced-motion support. I used Codex as my primary engineering collaborator throughout the project: implementing backend flows, iterating on UI behavior and navigation, auditing bugs, testing the local demo flow, and refining the project into a deployable experience. The frontend is deployed on Vercel and the FastAPI backend is deployed on Railway, while the local demo remains the most reliable environment for OAuth-backed source connections.

### Challenges we ran into

The hardest problem was not classification alone—it was making the product trustworthy. A student-assistant tool can easily become unsafe or misleading if it silently submits forms, sends group replies, or invents personal data. I had to draw a clear boundary: Triage can identify, prioritize, and draft, but it cannot act externally without the student. Google OAuth also introduced practical complexity. Gmail and Classroom need correct scopes, valid local tokens, and careful handling of deployment differences. The local version supports the intended read-only connection flow, while the hosted version is designed around a shared demo experience rather than pretending every personal integration is production-ready. WhatsApp was another deliberate challenge. A real group integration would depend on unofficial tooling and fragile sessions, so I chose an explicit simulated WhatsApp datasource for the demo. That kept the core experience stable and made the limitation visible rather than hidden. Finally, turning a dense dashboard into something usable required multiple UI iterations: horizontally navigable panels, reliable keyboard and arrow navigation, compact obligation cards, readable detail dialogs, and an approval flow that makes the human decision unmistakable.

### Accomplishments we're proud of

Built a working end-to-end student triage flow: ingest, classify, persist, prioritize, review. Implemented structured AI classification with categories, deadlines, mandatory status, and poll/form detection. Built a human-in-the-loop Approval Drawer instead of automating external submissions. Added editable, copy-only drafts for completion polls and simple forms. Created a ranked Study Plan that focuses on structure and priorities rather than generating academic answers. Added an Assignment Scaffolding Assistant that helps students plan work without completing it for them. Implemented Gmail and Google Classroom sync paths with read-only OAuth for the local workflow. Added file upload archiving for uploaded text material. Built a polished, responsive dashboard with a custom pulse rail, urgency grouping, deadline reminders, themes, and reduced-motion support. Deployed the frontend and backend while retaining a dependable local demo path. Prepared believable, explicitly simulated demo data so the product can be demonstrated without exposing personal messages.

### What we learned

We learned that useful student automation is less about doing everything automatically and more about making the right information visible at the right time. Structured outputs matter. Asking an AI model for a label is not enough; the application needs reliable fields for category, deadline, requirement level, reasoning, and safe handling of uncertainty. We also learned that “human in the loop” has to be visible in the product, not just stated in documentation. The Approval Drawer, editable drafts, and no-send boundary are central to the experience because they make the student’s control concrete. Finally, we learned that honest product scope improves a demo. Simulated WhatsApp data is better than claiming a fragile live integration, and a local OAuth workflow is better than presenting a hosted connection as complete when it still needs per-user production authorization.

### What's next

Next, Triage will move from a strong demo into a fuller multi-user product: Per-user hosted Google OAuth instead of local demo tokens. Durable hosted storage replacing local SQLite for production accounts. Real, permissioned source synchronization with clearer connection health and retry handling. Attachment ingestion and archiving beyond uploaded text files. Safer notification and reminder workflows. A production-ready WhatsApp integration path, only if it can be made reliable and policy-compliant. More robust routine-form assistance using user-provided profile information, always staged for review and never auto-submitted. Better source-level filtering, search, and an archive/history view. Accessibility and mobile refinements for students using Triage as a daily tool. The core principle will remain unchanged: Triage can organize attention and draft the next step, but the student keeps control of every real-world action.

## README (from the GitHub repository)

# Triage

> Sort what college sends into action, study, and calm—without sending or submitting anything for the student.

Triage is a local-first AI student desk for scattered academic communication. It classifies incoming text as an **Obligation**, **Study Material**, or **Noise** item; turns obligations into a deadline-aware Action Queue; builds ranked study outlines from question-bank and unit-note text; and keeps every change behind explicit human review.

**Live demo:** [triage-27.vercel.app](https://triage-27.vercel.app) · **Backend health check:** [Railway API](https://triage-production-b91f.up.railway.app/health)

## What is implemented

- Paste text or upload `.txt`, selectable-text `.pdf`, and `.docx` files for classification. PDFs with no selectable text (such as scanned pages) are rejected rather than silently guessed.
- A unified, source-labelled live stream of recently classified Gmail, Classroom, manual-upload, and WhatsApp-demo items. It refreshes every 30 seconds while the app tab is open; this is local polling, not a webhook feed.
- A dedicated Archive / History panel with local search and source, classification, and open/done filters; retained files remain downloadable from matching records.
- Structured OpenAI classification with category, evidence-based reason, explicit deadline, mandatory/optional status, and poll/form detection.
- Action Queue grouped into **Immediate**, **This Week**, and **Later**.
- Detail dialogs and a review-first **Mark done** workflow.
- Approval Drawer with editable copy-only drafts for completion polls and routine forms. Optional profile details stay in the student's browser and are matched only to explicit, allow-listed form fields; Triage never invents a value or submits anything externally.
- Study Plan that ranks topics from a question bank and unit notes, with topic outlines rather than generated answers.
- Assignment Scaffolding that returns requirements, concepts, an approach, and test cases—not a submittable solution.
- Read-only Gmail and Google Classroom sync with persisted last-sync outcomes, clear setup/failure states, and explicit retry controls after Google OAuth setup.
- Clearly labelled representative WhatsApp demo data; there is no live WhatsApp integration.
- Authenticated archiving and download of uploaded `.txt`, `.pdf`, and `.docx` files plus newly synced Gmail attachments and accessible Classroom Drive files (up to 20 MB each). Local development uses disk; hosted deployments can use private S3-compatible storage.
- Shared demo-password gate, in-memory sessions, in-app deadline reminders with per-item snooze and optional browser notifications, keyboard/pulse-rail navigation, a `Ctrl/Cmd + K` command palette, theme controls, and reduced-motion support.

## Product boundaries

Triage is intentionally review-first:

- It does **not** submit forms, send WhatsApp messages, post replies, or make external changes.
- It does **not** invent personal details for form fields.
- Routine form drafts use only details the student explicitly saves in their browser and only for matching supported labels; those details are never sent to the API or classifier.
- It does **not** produce complete academic submissions. Assignment help is planning and self-checking support only.
- Gmail and Classroom access is read-only and is currently supported through the local OAuth workflow.

## V2 guardrails

Before autonomous source processing is introduced, Triage maintains a synthetic-only
classification regression corpus and explicit quality gates for obligation recall.
See `docs/V2_EVALUATION_GUARDRAILS.md` for the evaluation command, thresholds, secret
handling, retention target, and incident runbook. Real user messages must never be
added to the committed corpus.

## V2 workspace foundation

Hosted accounts now receive one personal workspace and an `individual` membership.
Current owner-scoped behavior remains compatible while `workspace_id` is written for
new hosted records. See `docs/V2_WORKSPACE_FOUNDATION.md` for the additive migration
behavior and staging verification steps.

## V2 PostgreSQL migrations

Hosted PostgreSQL schema changes now use ordered, recorded, advisory-lock-protected
migrations. See `docs/V2_POSTGRES_MIGRATIONS.md` for the staging-first deployment,
verification, and forward-only rollback procedure.

## V2 source connections

Gmail and Classroom now have server-persisted, pause/resume-able connection records
with workspace scope and sync health. See `docs/V2_SOURCE_CONNECTIONS.md`; this does
not enable background syncing yet.

## V2 source deduplication

Imported items now deduplicate by workspace, provider, and provider item ID, so equal
IDs from Gmail and Classroom do not collide. See `docs/V2_SOURCE_DEDUPLICATION.md`
for the data-preserving migration and staging checks.

## V2 durable job queue

Hosted source work now has a durable, leased PostgreSQL job model with idempotency
and bounded retries. It does not schedule or autonomously sync sources yet; see
`docs/V2_DURABLE_JOB_QUEUE.md` for the worker and deployment contract.

## V2 external scheduling

Source schedules now run through a short-lived hosted PostgreSQL cron command that
only enqueues jobs; it never syncs during an API request. See
`docs/V2_EXTERNAL_SCHEDULING.md` for the Railway deployment boundary.

## V2 connector abstraction

Gmail and Classroom manual sync now use a shared normalized connector contract while
remaining read-only and user-requested. See `docs/V2_CONNECTOR_ABSTRACTION.md` for
the explicit cursor boundary and staging verification steps.

## V2 autonomous Google pilot

An opt-in, workspace-allowlisted worker can now read explicitly selected Gmail or
Classroom sources only after the synthetic evaluation gate passes. See
`docs/V2_AUTONOMOUS_GOOGLE_PILOT.md` before configuring any pilot worker.

## V2 policy routing

New items receive deterministic, persisted review reasons instead of an LLM confidence
gate. See `docs/V2_POLICY_ROUTING.md` for the rules and explicit sender-metadata limit.

## V2 operational controls

Hosted workspaces now have redacted audit outcomes, an autonomous-work kill switch,
and an approval-gated operational retention command. See
`docs/V2_AUDIT_RETENTION_KILL_SWITCH.md` before enabling any maintenance run.

## Stack

| Layer | Current implementation |
| --- | --- |
| Frontend | Vanilla HTML, CSS, and JavaScript |
| Backend | Python + FastAPI + Uvicorn |
| AI | OpenAI Responses API using `gpt-5.6-luna` structured JSON outputs |
| Local persistence | SQLite |
| Google sources | Gmail API and Google Classroom API via read-only OAuth |
| Hosting | Vercel frontend + Railway FastAPI backend |

## Run locally

### 1. Configure the backend

From the repository root:

```powershell
cd backend
py -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
Copy-Item .env.example .env
```

Set these values in `backend/.env`:

```dotenv
OPENAI_API_KEY=your_openai_api_key
DEMO_PASSWORD=a_shared_demo_password
```

Start the API:

```powershell
uvicorn main:app --reload
```

The API runs at `http://localhost:8000` and exposes interactive documentation at `http://localhost:8000/docs`.

### 2. Serve the frontend

In a second terminal:

```powershell
cd frontend
py -m http.server 3000
```

Open `http://localhost:3000`, then sign in with the shared `DEMO_PASSWORD`.

### 3. Load the local video-demo data (optional)

The seed script is additive and idempotent: it creates five clearly simulated obligation cards, one poll-response draft in Human Review, and one assignment-scaffold example. It does not delete or reset existing data.

```powershell
cd backend
.\.venv\Scripts\python.exe seed_demo_video_data.py
```

## Google source setup (local desktop mode)

Create a Google Cloud **Desktop app** OAuth client and save it as `backend/credentials.json`. Then run:

```powershell
cd backend
.\.venv\Scripts\python.exe setup_google_auth.py
```

The browser consent flow writes the local refresh t

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 77 recognized source files, 570 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- React (technology) — claimed on Devpost, not found in the code
- Tailwind CSS (technology) — claimed on Devpost, not found in the code
- AI coding agent: Codex — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (87 of 87)

```
.agents/skills/superdesign/agents/openai.yaml
.agents/skills/superdesign/INIT.md
.agents/skills/superdesign/references/INIT.md
.agents/skills/superdesign/references/SUPERDESIGN.md
.agents/skills/superdesign/SKILL.md
.agents/skills/superdesign/SUPERDESIGN.md
.gitignore
.superdesign/design-system.md
.superdesign/init/components.md
.superdesign/init/extractable-components.md
.superdesign/init/layouts.md
.superdesign/init/pages.md
.superdesign/init/routes.md
.superdesign/init/theme.md
AGENTS.md
api/config.js
backend/.env.example
backend/assignment_helper.py
backend/attachment_archive.py
backend/autonomous_google_worker.py
backend/classifier.py
backend/classroom_sync.py
backend/database.py
backend/document_ingestion.py
backend/evaluation.py
backend/evaluations/corpus_v1.json
backend/generate_vapid_keys.py
backend/gmail_sync.py
backend/google_client.py
backend/hosted_auth.py
backend/main.py
backend/policy_routing.py
backend/postgres_migrations.py
backend/railway.toml
backend/rate_limit.py
backend/reminder_schedule.py
backend/requirements.txt
backend/retention_maintenance.py
backend/run_classification_evaluation.py
backend/setup_google_auth.py
backend/source_connectors.py
backend/source_ingestion.py
backend/sync_jobs.py
backend/sync_scheduler.py
backend/sync_worker.py
backend/test_attachment_archiving.py
backend/test_autonomous_google_worker.py
backend/test_data_safeguards.py
backend/test_document_ingestion.py
backend/test_evaluation.py
backend/test_operational_controls.py
backend/test_policy_routing.py
backend/test_postgres_migrations.py
backend/test_rate_limit.py
backend/test_reminder_schedule.py
backend/test_source_connections.py
backend/test_source_connectors.py
backend/test_source_dedupe.py
backend/test_source_health.py
backend/test_sync_jobs.py
backend/test_workspace_foundation.py
backend/whatsapp_demo_data.py
backend/workspace_foundation.py
docs/BUILD_LOG.md
docs/V2_AUDIT_RETENTION_KILL_SWITCH.md
docs/V2_AUTONOMOUS_GOOGLE_PILOT.md
docs/V2_CONNECTOR_ABSTRACTION.md
docs/V2_DURABLE_JOB_QUEUE.md
docs/V2_EVALUATION_GUARDRAILS.md
docs/V2_EXTERNAL_SCHEDULING.md
docs/V2_POLICY_ROUTING.md
docs/V2_POSTGRES_MIGRATIONS.md
docs/V2_REVISED_PLAN.md
docs/V2_SOURCE_CONNECTIONS.md
docs/V2_SOURCE_DEDUPLICATION.md
docs/V2_WORKSPACE_FOUNDATION.md
frontend/app.js
frontend/index.html
frontend/service-worker.js
frontend/styles.css
LICENSE
PROJECT.md
README.md
skills-lock.json
test folders/notes.txt
test folders/questions.txt
vercel.json
```

### Dependencies

- backend/requirements.txt: boto3, cryptography, fastapi, google-api-python-client, google-auth-httplib2, google-auth-oauthlib, openai, psycopg[binary], PyMuPDF, python-docx, python-dotenv, python-multipart, pywebpush, uvicorn

### Recent commits (newest first)

- feat(v2): add audit retention and kill switch
- feat(v2): add policy routing and review
- feat(v2): add gated autonomous Google pilot
- refactor(v2): add source connector abstraction
- infra(v2): schedule durable worker runs
- feat(v2): add leased sync jobs and worker runner
- fix(v2): scope source dedupe by workspace and provider
- Updated Build logs till Day 9p
- feat(v2): persist workspace source connections
- feat(v2): add versioned postgres migrations
- feat(v2): add personal workspaces and memberships
- docs: add v2 evaluation and safety baseline
- Day 9p: Add source connection health
- Day 9o: Add data safeguards
- Day 9n: Improve responsive accessibility
- Day 9m: Harden production safeguards
- Day 9l: Add durable deadline reminders
- Day 9k: Add PDF and DOCX ingestion
- Day 9j: Add durable attachment storage
- Day 9i: Handle Google OAuth scope normalization

## Key source files (fetched from GitHub, selected and truncated for size)

### AGENTS.md

```markdown
# Local data safety

- Treat `backend/triage.db` and every database file as protected user data.
- Never delete, overwrite, reset, recreate, or otherwise modify a database as a side effect of cleanup, testing, or an unrelated task.
- Before any destructive database operation, stop and obtain the user's separate, explicit approval. Create a dated backup first when an existing database is available.

```

### PROJECT.md

```markdown
# Triage — Project Overview

## One-line pitch

Triage is a review-first AI student desk that turns scattered college communication into an Action Queue, a ranked Study Plan, and clear human decisions.

## The problem

Students receive important academic information across Gmail, Google Classroom, WhatsApp groups, files, and informal notices. Some messages demand immediate action—registration deadlines, attendance forms, completion polls, lab records, and project checkpoints—while others are useful only for study or are simply noise. The cost is not just inbox overload; it is missed obligations and time spent manually deciding what matters.

Question banks and unit notes create a related problem. Students often have to manually compare material to infer which topics recur and are most worth revising. Assignment prompts can be equally ambiguous, but giving students a finished answer would undermine learning.

## The solution

Triage triages incoming academic information into three structured categories:

- **Obligation**: deadlines, forms, registrations, notices, and polls.
- **Study Material**: question banks, unit notes, and assessment preparation.
- **Noise**: messages that do not need action or study time.

Obligations are grouped into **Immediate**, **This Week**, and **Later**, with explicit deadlines and requirement status. Study material becomes a ranked outline generated from question-bank and unit-note text. Assignment help is deliberately limited to requirements, concepts, approach steps, and test cases.

The key safety mechanism is Human Review. Triage can draft a copy-only response for a completion poll or routine form, but it does not send it, submit a form, or invent a student's personal details. Students may save optional details in their browser—such as their name or roll number—which are deterministically matched only to explicit supported fields in a form draft. Those details never leave the browser. The student can edit and copy the draft themselves; reviewing a form draft does not mark an obligation complete.

## Current implementation

### Ingestion and classification

- Manual text paste plus `.txt`, selectable-text PDF, and DOCX uploads for classification and study planning. Image-only/scanned PDFs are intentionally rejected rather than OCR-guessed.
- Read-only Gmail and Google Classroom sync with persisted last-sync outcomes, clear setup/failure states, and explicit retry controls.
- Clearly marked representative WhatsApp demo data, not a live WhatsApp connection.
- A unified source-labelled stream of recently classified items, refreshed by the open browser every 30 seconds.
- OpenAI structured-output classification with category, reason, deadline, mandatory status, and poll/form detection.
- SQLite/Postgres persistence, source IDs for deduplication, an additive schema-migration ledger, and a private attachment archive: uploaded text files, newly synced Gmail attachments, and accessible Classroom Drive files are retained u
[truncated — 3988 more characters]
```

### backend/requirements.txt

```
fastapi
uvicorn
python-multipart
openai
python-dotenv
google-auth-oauthlib
google-auth-httplib2
google-api-python-client
cryptography
psycopg[binary]
boto3
PyMuPDF
python-docx
pywebpush

```

### backend/main.py

```python
"""FastAPI server for the local-first Triage classification and Action Queue."""

import os
import secrets
from datetime import date, timedelta
from pathlib import Path

from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, RedirectResponse, Response
from dotenv import load_dotenv
from oauthlib.oauth2.rfc6749.errors import OAuth2Error

load_dotenv()

from attachment_archive import (
    MAX_ARCHIVE_BYTES,
    archive_attachment,
    archived_file_exists,
    original_filename_from_archive,
    read_archived_file,
)
from assignment_helper import scaffold_assignment
from classifier import (
    build_study_plan,
    classify,
    draft_poll_or_form_response,
    draft_routine_form_response,
)
from document_ingestion import extract_document_text
from google_client import TOKEN_PATH
from hosted_auth import (
    authorization_url,
    complete_authorization,
    configuration_error as hosted_auth_configuration_error,
    dispatch_due_reminders,
    has_google_connection,
    initialize as initialize_hosted_auth,
    is_enabled as hosted_auth_enabled,
    push_public_configuration,
    remove_push_subscription,
    revoke_session,
    save_push_subscription,
    session_user,
    workspace_for_user,
)
from rate_limit import RateLimiter
from reminder_schedule import parse_deadline
from source_ingestion import ingest_source_changes
from whatsapp_demo_data import WHATSAPP_DEMO_MESSAGES, WHATSAPP_DEMO_SOURCE
from database import (
    create_assignment_help,
    create_item,
    create_pending_action,
    enable_source_connection,
    approve_pending_action,
    get_item,
    get_item_by_source_id,
    has_items_from_source,
    get_assignment_history,
    get_archived_attachments,
    export_owner_data,
    get_open_obligations,
    get_history_items,
    get_recent_items,
    get_pending_actions,
    get_source_sync_status,
    get_audit_events,
    get_source_connections,
    get_study_plan,
    initialize_database,
    is_workspace_kill_switch_enabled,
    is_source_connection_paused,
    record_source_connection_outcome,
    reject_pending_action,
    record_source_sync,
    replace_study_plan,
    set_source_connection_state,
    set_workspace_kill_switch,
    record_audit_event,
    DEFAULT_OWNER_ID,
)

app = FastAPI(title="Triage API", version="0.1.0")
DEMO_PASSWORD = os.getenv("DEMO_PASSWORD", "")
VALID_SESSION_TOKENS: set[str] = set()
ARCHIVE_DIRECTORY = Path(__file__).with_name("archive")
MAX_CLASSIFICATION_TEXT_CHARS = 5_000
MAX_STUDY_DOCUMENT_TEXT_CHARS = 30_000
DEFAULT_CORS_ORIGINS = "http://localhost:3000,http://127.0.0.1:3000"
CORS_ORIGINS = [
    origin.strip()
    for origin in os.getenv("CORS_ORIGINS", DEFAULT_CORS_ORIGINS).split(",")
    if origin.strip()
]
REMINDER_DISPATCH_SECRET = os.getenv("REMINDER_DISPATCH_SECRET", "").strip()
TRUST_PROXY_HEADERS = os.getenv("TRUST_PROXY_HEADERS", "").lower() == "true"
RATE_LIMIT_ENABLED = os.getenv("RATE_LIMIT_ENABLED", "true").lower() != "false"
RATE_LIMITER = RateLimiter()

initialize_database()
initialize_hosted_auth()


@app.middleware("http")
async def require_demo_auth(request: Request, call_next):
    """Require an in-memory demo token for all non-public API routes."""
    public_paths = {
        "/health", "/auth/login", "/auth/config", "/auth/google/start", "/auth/google/callback",
        "/internal/reminders/dispatch",
    }
    if request.method == "OPTIONS":
        return await call_next(request)

    if request.url.path in public_paths:
        if not _allow_request(request, _public_rate_limit(request.url.path)):
            return _too_many_requests()
        return await call_next(request)

    authorization = request.headers.get("authorization", "")
    scheme, _, token = authorization.partition(" ")
    if scheme.lower() != "bearer":
        return JSONResponse(status_code=401, content={"detail": "Authentication required."})
    if hosted_auth_enabled():
        owner_id = session_user(token)
        if not owner_id:
            return JSONResponse(status_code=401, content={"detail": "Authentication required."})
        request.state.owner_id = owner_id
        workspace_id = workspace_for_user(owner_id)
        if workspace_id is None:
            return JSONResponse(status_code=503, content={"detail": "Workspace setup is incomplete. Please try again."})
        request.state.workspace_id = workspace_id
        request.state.session_token = token
        if not _allow_request(request, _authenticated_rate_limit(request.url.path), owner_id):
            return _too_many_requests()
        return await call_next(request)
    if token not in VALID_SESSION_TOKENS:
        return JSONResponse(status_code=401, content={"detail": "Authentication required."})
    request.state.owner_id = DEFAULT_OWNER_ID
    request.state.workspace_id = None
    request.state.session_token = token
    if not _allow_request(request, _authenticated_rate_limit(request.url.path), DEFAULT_OWNER_ID):
        return _too_many_requests()
    return await call_next(request)


@app.middleware("http")
async def add_hardening_headers(request: Request, call_next):
    """Avoid caching private API output and set baseline browser protections."""
    response = await call_next(request)
    response.headers.setdefault("Cache-Control", "no-store")
    response.headers.setdefault("X-Content-Type-Options", "nosniff")
    response.headers.setdefault("X-Frame-Options", "DENY")
    response.headers.setdefault("Referrer-Policy", "no-referrer")
    response.headers.setdefault("Permissions-Policy", "camera=(), microphone=(), geolocation=()")
    return response


app.add_middleware(
    CORSMiddleware,
    allow_origins=CORS_ORIGINS,
    allow_credentials=False,
    allow_methods=["*"],
    allow_headers=["*"],
)


@app.get("/health")
def health() -> dict[str, str]:
    return {"status": "ok"}


def _client_key(request: Request) -> str:
    if TRUST_PROXY_HEADERS:
        for
[truncated — 26619 more characters]
```

### api/config.js

```javascript
module.exports = (request, response) => {
  const apiBaseUrl = process.env.TRIAGE_API_BASE_URL || "";
  response.setHeader("Content-Type", "application/javascript; charset=utf-8");
  response.setHeader("Cache-Control", "no-store");
  response.status(200).send(`window.TRIAGE_API_BASE_URL = ${JSON.stringify(apiBaseUrl)};`);
};

```

### backend/run_classification_evaluation.py

```python
"""Run the synthetic Triage classification corpus against the configured model."""

from __future__ import annotations

import json

from classifier import classify
from evaluation import calculate_metrics, load_corpus, quality_gate_failures


def main() -> None:
    corpus = load_corpus()
    predictions = {
        item["id"]: classify(item["text"])
        for item in corpus["items"]
    }
    metrics = calculate_metrics(corpus, predictions)
    failures = quality_gate_failures(metrics)
    print(json.dumps(metrics, indent=2, sort_keys=True))
    if failures:
        raise SystemExit("Evaluation gate failed:\n- " + "\n- ".join(failures))
    print("Evaluation gate passed.")


if __name__ == "__main__":
    main()

```

### backend/sync_scheduler.py

```python
"""Short-lived scheduler command that only enqueues durable source jobs."""

from __future__ import annotations

import argparse
import json

import database
from sync_jobs import enqueue_due_sync_jobs


def main() -> int:
    parser = argparse.ArgumentParser(description="Enqueue due durable Triage source-sync jobs.")
    parser.add_argument("--run-once", action="store_true", help="Enqueue currently due jobs and exit.")
    arguments = parser.parse_args()
    if not arguments.run_once:
        parser.error("--run-once is required.")
    if not database.USING_POSTGRES:
        print(json.dumps({"status": "blocked", "reason": "External scheduling requires hosted PostgreSQL."}))
        return 0
    print(json.dumps(enqueue_due_sync_jobs()))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

```

### backend/test_rate_limit.py

```python
"""Unit tests for the process-local request limiter; no app/database import."""

import unittest

from rate_limit import RateLimiter


class RateLimiterTests(unittest.TestCase):
    def test_blocks_after_limit_then_recovers_after_window(self):
        limiter = RateLimiter()
        self.assertTrue(limiter.allow("login:ip", 2, 60, now=0))
        self.assertTrue(limiter.allow("login:ip", 2, 60, now=10))
        self.assertFalse(limiter.allow("login:ip", 2, 60, now=20))
        self.assertTrue(limiter.allow("login:ip", 2, 60, now=61))

    def test_keys_are_independent(self):
        limiter = RateLimiter()
        self.assertTrue(limiter.allow("a", 1, 60, now=0))
        self.assertFalse(limiter.allow("a", 1, 60, now=1))
        self.assertTrue(limiter.allow("b", 1, 60, now=1))


if __name__ == "__main__":
    unittest.main()

```

### backend/rate_limit.py

```python
"""Small, dependency-free, process-local fixed-window rate limiter.

It is intentionally a safety net for the current Railway deployment, not a
replacement for a shared edge/WAF limit in a multi-replica production setup.
"""

from __future__ import annotations

import time
from collections import deque


class RateLimiter:
    def __init__(self) -> None:
        self._attempts: dict[str, deque[float]] = {}

    def allow(self, key: str, limit: int, window_seconds: int, now: float | None = None) -> bool:
        current = time.monotonic() if now is None else now
        attempts = self._attempts.setdefault(key, deque())
        threshold = current - window_seconds
        while attempts and attempts[0] <= threshold:
            attempts.popleft()
        if len(attempts) >= limit:
            return False
        attempts.append(current)
        return True


```

### backend/generate_vapid_keys.py

```python
"""Print a VAPID key pair for Railway Web Push configuration.

Run locally once. Store the output only in Railway variables; do not commit it
or add it to a local .env file tracked by Git.
"""

import base64

from cryptography.hazmat.primitives import serialization
from py_vapid import Vapid


def _base64url(value: bytes) -> str:
    return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii")


def main() -> None:
    vapid = Vapid()
    vapid.generate_keys()
    private_key = vapid.private_key.private_bytes(
        serialization.Encoding.Raw,
        serialization.PrivateFormat.Raw,
        serialization.NoEncryption(),
    )
    public_key = vapid.public_key.public_bytes(
        serialization.Encoding.X962,
        serialization.PublicFormat.UncompressedPoint,
    )
    print(f"VAPID_PUBLIC_KEY={_base64url(public_key)}")
    print(f"VAPID_PRIVATE_KEY={_base64url(private_key)}")


if __name__ == "__main__":
    main()

```

[64 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]