# Setup Guide — Getting Started
## AI-Guided Cardio Thoracic Surgery Patient Intake System

**Audience:** Interns setting up for the first time
**Prerequisites:** Node 22+, Docker Desktop running

---

## 1. What You're Building

```
ai-nims/                     ← this repo (already cloned)
  frontend/                  ← React PWA — you will create this
  backend/                   ← FastAPI — you will create this
  docs/                      ← already exists, do not touch
  docker-compose.yml         ← you will create this
  .env.example               ← you will create this
```

One repo. One `docker compose up` to run everything.

---

## 2. Create the Folder Structure

From the repo root:

```bash
mkdir frontend backend
```

---

## 3. Frontend Setup (React PWA)

### 3.1 Scaffold the React app

```bash
cd frontend
npm create vite@latest . -- --template react-ts
```

When prompted — **Current directory** → confirm with `y`

### 3.2 Install dependencies

```bash
npm install
```

Then add the libraries:

```bash
# Routing
npm install react-router-dom

# HTTP client (calls to FastAPI)
npm install axios

# PWA support
npm install -D vite-plugin-pwa

# Styling
npm install -D tailwindcss @tailwindcss/vite
```

### 3.3 Configure Vite (Tailwind + PWA + dev proxy)

Replace `frontend/vite.config.ts` entirely:

```typescript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import { VitePWA } from 'vite-plugin-pwa'

export default defineConfig({
  plugins: [
    react(),
    tailwindcss(),
    VitePWA({
      registerType: 'autoUpdate',
      includeAssets: ['favicon.ico', 'icon-192.png', 'icon-512.png'],
      manifest: {
        name: 'AI NIMS — Patient Intake',
        short_name: 'AI NIMS',
        description: 'AI-guided cardio thoracic surgery patient intake',
        theme_color: '#ffffff',
        background_color: '#ffffff',
        display: 'standalone',
        orientation: 'portrait',
        start_url: '/',
        icons: [
          { src: 'icon-192.png', sizes: '192x192', type: 'image/png' },
          { src: 'icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'any maskable' }
        ]
      }
    })
  ],
  server: {
    port: 3000,
    proxy: {
      // In dev, /api/auth/login → FastAPI at /auth/login
      // In Docker, nginx does the same rewrite — always use /api prefix in axios
      '/api': {
        target: 'http://localhost:8000',
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
})
```

Replace everything in `frontend/src/index.css` with:

```css
@import "tailwindcss";
```

### 3.4 Add PWA icons

Add two PNG files to `frontend/public/`:
- `icon-192.png` — 192×192 px
- `icon-512.png` — 512×512 px

Use any placeholder for now. The PWA install prompt will not appear without these files.

### 3.5 Create the axios client

Create `frontend/src/api/client.ts`:

```typescript
import axios from 'axios'

// Base URL is /api — works in both dev (Vite proxy) and Docker (nginx proxy)
// Never hardcode http://localhost:8000 — it will break in Docker
const client = axios.create({
  baseURL: '/api',
  headers: { 'Content-Type': 'application/json' }
})

// Attach JWT from memory on every request
let _token: string | null = null

export function setToken(token: string | null) {
  _token = token
}

client.interceptors.request.use((config) => {
  if (_token) {
    config.headers.Authorization = `Bearer ${_token}`
  }
  return config
})

export default client
```

### 3.6 Create the Web Speech API hook

Create `frontend/src/hooks/useSpeechRecognition.ts`:

```typescript
import { useState, useRef, useCallback } from 'react'

type Language = 'ENGLISH' | 'HINDI' | 'TELUGU'

const LANG_CODE: Record<Language, string> = {
  ENGLISH: 'en-IN',
  HINDI: 'hi-IN',
  TELUGU: 'te-IN'
}

export function useSpeechRecognition(language: Language) {
  const [transcript, setTranscript] = useState('')
  const [listening, setListening] = useState(false)
  const [supported] = useState(() => 'SpeechRecognition' in window || 'webkitSpeechRecognition' in window)
  const recognitionRef = useRef<SpeechRecognition | null>(null)

  const startListening = useCallback(() => {
    if (!supported) return

    const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition
    const recognition = new SpeechRecognition()
    recognition.lang = LANG_CODE[language]
    recognition.interimResults = true
    recognition.continuous = false

    recognition.onresult = (event) => {
      const result = Array.from(event.results)
        .map((r) => r[0].transcript)
        .join('')
      setTranscript(result)
    }

    recognition.onend = () => setListening(false)
    recognition.onerror = () => setListening(false)

    recognitionRef.current = recognition
    recognition.start()
    setListening(true)
  }, [language, supported])

  const stopListening = useCallback(() => {
    recognitionRef.current?.stop()
    setListening(false)
  }, [])

  const reset = useCallback(() => setTranscript(''), [])

  return { transcript, listening, supported, startListening, stopListening, reset }
}
```

> **Note:** Web Speech API only works on `localhost` or `https://`. It will silently fail on plain `http://` in production.

### 3.7 Frontend folder structure (target)

```
frontend/
  public/
    icon-192.png
    icon-512.png
    favicon.ico
  src/
    api/
      client.ts              ← axios instance (already created above)
      auth.ts                ← login(), logout() calls
      sessions.ts            ← sessions CRUD calls
      staff.ts               ← staff CRUD calls
    components/
      ui/                    ← reusable buttons, inputs, cards
    pages/
      LoginPage.tsx
      IntakeListPage.tsx
      IntakeSessionPage.tsx
      SummaryPage.tsx
      AccountManagementPage.tsx
    hooks/
      useSpeechRecognition.ts   ← already created above
    types/
      index.ts               ← shared TypeScript types (Session, Staff, etc.)
    App.tsx
    main.tsx
    index.css
  index.html
  vite.config.ts
  package.json
  tsconfig.json
  Dockerfile
  nginx.conf
```

### 3.8 Run the frontend (dev mode)

```bash
cd frontend
npm run dev
```

Opens at `http://localhost:3000`

---

## 4. Backend Setup (FastAPI)

### 4.1 Python version — read this before creating a venv

Your machine has Python 3.14. Some packages (notably `asyncpg`) do not yet have compiled wheels for 3.14 and will fail to install.

Check if Python 3.12 is available:

```bash
python3.12 --version
```

**If 3.12 is available** — use it explicitly:
```bash
cd backend
python3.12 -m venv .venv
```

**If only 3.14 is available** — skip local pip install entirely. Run the backend via Docker only (Option A in Section 8). The Dockerfile uses `python:3.12-slim` which is safe.

Always activate the venv before working on the backend:
```bash
source .venv/bin/activate      # Mac/Linux
# .venv\Scripts\activate       # Windows
```

### 4.2 Create requirements.txt

Create `backend/requirements.txt`:

```
fastapi==0.115.0
uvicorn[standard]==0.32.0
sqlalchemy[asyncio]==2.0.36
asyncpg==0.30.0
alembic==1.14.0
redis[asyncio]==5.2.0
anthropic==0.40.0
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
apscheduler==3.10.4
pydantic-settings==2.6.0
python-multipart==0.0.12
```

### 4.3 Install dependencies

```bash
pip install -r requirements.txt
```

### 4.4 Backend folder structure (target)

```
backend/
  app/
    __init__.py
    main.py                  ← FastAPI app, startup, APScheduler
    core/
      __init__.py
      config.py              ← all env vars via pydantic-settings
      auth.py                ← JWT sign/verify, bcrypt hash/verify
      deps.py                ← FastAPI dependencies (get_db, get_current_staff)
    api/
      __init__.py
      auth.py                ← POST /auth/login, POST /auth/logout
      staff.py               ← POST /staff, PATCH /staff/{id}
      sessions.py            ← all /sessions/* endpoints
    models/
      __init__.py            ← MUST import all model classes (see Section 7.3)
      department.py
      staff.py
      patient.py
      protocol.py
      session.py
      response.py
      summary.py
    schemas/
      __init__.py
      auth.py
      staff.py
      session.py
    services/
      __init__.py
      claude.py              ← get_next_question(), generate_summary()
      data_wipe.py           ← hourly APScheduler task
    db/
      __init__.py
      engine.py              ← SQLAlchemy async engine + session factory
      redis.py               ← redis[asyncio] client
  alembic/
    versions/
    env.py
  alembic.ini
  requirements.txt
  Dockerfile
  seed.py                    ← one-time data seeding script
  .env                       ← never commit this
```

### 4.5 Create router stubs first — before main.py can start

`main.py` imports routers from `app/api/`. Create these stub files before doing anything else, or the server will crash on start.

Create `backend/app/api/__init__.py` — empty file.

Create `backend/app/api/auth.py`:
```python
from fastapi import APIRouter
router = APIRouter()
```

Create `backend/app/api/staff.py`:
```python
from fastapi import APIRouter
router = APIRouter()
```

Create `backend/app/api/sessions.py`:
```python
from fastapi import APIRouter
router = APIRouter()
```

Also create empty `__init__.py` files in every other package directory:
```bash
touch app/__init__.py
touch app/core/__init__.py
touch app/models/__init__.py
touch app/schemas/__init__.py
touch app/services/__init__.py
touch app/db/__init__.py
```

### 4.6 Create the entry point

Create `backend/app/main.py`:

```python
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
from apscheduler.schedulers.asyncio import AsyncIOScheduler

from app.api import auth, staff, sessions
from app.services.data_wipe import purge_expired_patients
from app.core.config import settings

scheduler = AsyncIOScheduler()

@asynccontextmanager
async def lifespan(app: FastAPI):
    scheduler.add_job(purge_expired_patients, "interval", hours=1)
    scheduler.start()
    yield
    scheduler.shutdown()

app = FastAPI(title="AI NIMS", version="1.0.0", lifespan=lifespan)

app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.ALLOWED_ORIGINS,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

app.include_router(auth.router, prefix="/auth", tags=["auth"])
app.include_router(staff.router, prefix="/staff", tags=["staff"])
app.include_router(sessions.router, prefix="/sessions", tags=["sessions"])

@app.get("/health")
async def health():
    return {"status": "ok"}
```

### 4.7 Create the config file

Create `backend/app/core/config.py`:

```python
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env")

    DATABASE_URL: str
    REDIS_URL: str
    JWT_SECRET: str
    JWT_EXPIRY_MINUTES: int = 30
    MAX_CONCURRENT_SESSIONS: int = 5
    SESSION_LOCK_TTL_SECONDS: int = 1800
    DATA_RETENTION_HOURS: int = 48
    BCRYPT_ROUNDS: int = 12
    ANTHROPIC_API_KEY: str
    CLAUDE_MODEL: str = "claude-sonnet-4-6"
    CLAUDE_TIMEOUT_SECONDS: int = 30
    ALLOWED_ORIGINS: list[str] = ["http://localhost:3000"]

settings = Settings()
```

### 4.8 Create the database engine

Create `backend/app/db/engine.py`:

```python
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase
from app.core.config import settings

class Base(DeclarativeBase):
    pass

engine = create_async_engine(settings.DATABASE_URL, echo=False)
AsyncSessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
```

`Base` is imported by every model. `AsyncSessionLocal` is imported by `deps.py`, `seed.py`, and `data_wipe.py`.

---

### 4.9 Create auth helpers

Create `backend/app/core/auth.py`:

```python
from passlib.context import CryptContext
from jose import jwt, JWTError
from datetime import datetime, timedelta, timezone
from app.core.config import settings

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def hash_pin(pin: str) -> str:
    return pwd_context.hash(pin)

def verify_pin(pin: str, hashed: str) -> bool:
    return pwd_context.verify(pin, hashed)

def create_access_token(staff_id: str, role: str, department_id: str) -> str:
    payload = {
        "sub": str(staff_id),
        "role": role,
        "departmentId": str(department_id),
        "iat": datetime.now(timezone.utc),
        "exp": datetime.now(timezone.utc) + timedelta(minutes=settings.JWT_EXPIRY_MINUTES),
    }
    return jwt.encode(payload, settings.JWT_SECRET, algorithm="HS256")

def decode_access_token(token: str) -> dict | None:
    try:
        return jwt.decode(token, settings.JWT_SECRET, algorithms=["HS256"])
    except JWTError:
        return None
```

`hash_pin()` is used in `seed.py`. `create_access_token()` and `decode_access_token()` are used in auth endpoints and `deps.py`.

---

### 4.10 Create FastAPI dependencies

Create `backend/app/core/deps.py`:

```python
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.engine import AsyncSessionLocal
from app.core.auth import decode_access_token

security = HTTPBearer()

async def get_db():
    async with AsyncSessionLocal() as session:
        yield session

async def get_current_staff(
    credentials: HTTPAuthorizationCredentials = Depends(security),
):
    payload = decode_access_token(credentials.credentials)
    if not payload:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid or expired token"
        )
    return payload  # { "sub": staffId, "role": role, "departmentId": departmentId }
```

Every protected endpoint adds `staff = Depends(get_current_staff)` to get the logged-in staff's JWT payload. `get_db()` provides the database session.

---

### 4.11 Create the Redis client

Create `backend/app/db/redis.py`:

```python
from redis.asyncio import Redis
from app.core.config import settings

_redis: Redis | None = None

async def get_redis() -> Redis:
    global _redis
    if _redis is None:
        _redis = Redis.from_url(settings.REDIS_URL, decode_responses=True)
    return _redis

async def close_redis():
    global _redis
    if _redis:
        await _redis.aclose()
        _redis = None
```

Update `backend/app/main.py` lifespan to close Redis on shutdown:

```python
from app.db.redis import close_redis

@asynccontextmanager
async def lifespan(app: FastAPI):
    scheduler.add_job(purge_expired_patients, "interval", hours=1)
    scheduler.start()
    yield
    scheduler.shutdown()
    await close_redis()
```

In every endpoint that needs Redis: `redis = await get_redis()` — no global state leaks between requests.

---

### 4.12 Create the Protocol model

`app/models/protocol.py` is missing from the folder structure but imported by `seed.py` and `models/__init__.py`. Create `backend/app/models/protocol.py`:

```python
import uuid
from sqlalchemy import Column, Boolean, SmallInteger, ForeignKey, TIMESTAMP
from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.sql import func
from app.db.engine import Base

class Protocol(Base):
    __tablename__ = "protocols"

    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    department_id = Column(UUID(as_uuid=True), ForeignKey("departments.id"), nullable=False)
    version = Column(SmallInteger, nullable=False, default=1)
    question_bank = Column(JSONB, nullable=False)
    is_active = Column(Boolean, nullable=False, default=True)
    created_at = Column(TIMESTAMP(timezone=True), server_default=func.now(), nullable=False)
```

---

### 4.13 Create the data wipe service

Create `backend/app/services/data_wipe.py`:

```python
from sqlalchemy import text
from app.db.engine import AsyncSessionLocal

async def purge_expired_patients():
    async with AsyncSessionLocal() as session:
        await session.execute(
            text("DELETE FROM patients WHERE expires_at < NOW()")
        )
        await session.commit()
```

> CASCADE handles all child rows (sessions, responses, summaries) automatically.

### 4.14 Create the Dockerfile

Create `backend/Dockerfile`:

```dockerfile
FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# No --reload in Docker — file watching doesn't work inside containers
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
```

### 4.15 Run the backend (dev mode, without Docker)

```bash
cd backend
source .venv/bin/activate
uvicorn app.main:app --reload --port 8000
```

API available at `http://localhost:8000`
Auto-docs at `http://localhost:8000/docs`

---

## 5. Environment Variables

### 5.1 Create `.env.example` at repo root

```bash
# PostgreSQL
DATABASE_URL=postgresql+asyncpg://nims:nims_dev@localhost:5432/nims

# Redis
REDIS_URL=redis://localhost:6379

# JWT
JWT_SECRET=replace_with_64_char_random_string
JWT_EXPIRY_MINUTES=30

# Session config
MAX_CONCURRENT_SESSIONS=5
SESSION_LOCK_TTL_SECONDS=1800
DATA_RETENTION_HOURS=48
BCRYPT_ROUNDS=12

# Claude API
ANTHROPIC_API_KEY=sk-ant-replace-with-real-key
CLAUDE_MODEL=claude-sonnet-4-6
CLAUDE_TIMEOUT_SECONDS=30
```

### 5.2 Create your actual `.env`

```bash
cp .env.example backend/.env
# Now open backend/.env and fill in real values
```

Never commit `backend/.env`.

---

## 6. Docker Compose

Create `docker-compose.yml` at the repo root:

```yaml
services:
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    ports:
      - "3000:80"
    networks:
      - public

  fastapi:
    build:
      context: ./backend
      dockerfile: Dockerfile
    ports:
      - "8000:8000"
    env_file:
      - ./backend/.env
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - public
      - internal

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: nims
      POSTGRES_PASSWORD: nims_dev
      POSTGRES_DB: nims
    ports:
      - "5432:5432"      # exposed so local FastAPI (Option B) can reach it
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U nims"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - internal

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"      # exposed so local FastAPI (Option B) can reach it
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    networks:
      - internal

volumes:
  postgres_data:

networks:
  internal:
  public:
```

### Frontend Dockerfile

Create `frontend/Dockerfile`:

```dockerfile
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
```

### Frontend nginx config

Create `frontend/nginx.conf`:

```nginx
server {
    listen 80;
    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    # Strip /api prefix and forward to FastAPI — same as Vite proxy does in dev
    location /api/ {
        proxy_pass http://fastapi:8000/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}
```

---

## 7. Database Migrations (Alembic)

All Alembic commands must be run from the `backend/` directory with the venv active.

### 7.1 Initialise Alembic (one time only)

```bash
cd backend
source .venv/bin/activate
alembic init alembic
```

### 7.2 Edit `alembic/env.py`

Open `backend/alembic/env.py`. Find and replace these two sections:

**Replace the `target_metadata` line:**
```python
from app.models import Base, Department, Staff, Patient, IntakeSession, SessionContributor, QuestionResponse, PatientSummary, Protocol
target_metadata = Base.metadata
```

**Replace the `sqlalchemy.url` config block:**
```python
from app.core.config import settings
# Alembic uses sync driver — strip +asyncpg
config.set_main_option("sqlalchemy.url", settings.DATABASE_URL.replace("+asyncpg", ""))
```

### 7.3 Update `app/models/__init__.py`

This file must import every model class. If it's empty, Alembic autogenerate produces an empty migration with zero tables.

```python
from app.db.engine import Base
from app.models.department import Department
from app.models.staff import Staff
from app.models.patient import Patient
from app.models.session import IntakeSession, SessionContributor
from app.models.response import QuestionResponse
from app.models.summary import PatientSummary
from app.models.protocol import Protocol

__all__ = [
    "Base", "Department", "Staff", "Patient",
    "IntakeSession", "SessionContributor",
    "QuestionResponse", "PatientSummary", "Protocol"
]
```

### 7.4 Create and run the first migration

Make sure postgres is running first:
```bash
docker compose up postgres -d
```

Then from `backend/`:
```bash
cd backend
source .venv/bin/activate
alembic revision --autogenerate -m "initial schema"
alembic upgrade head
```

Run `alembic upgrade head` every time you change a model. Never edit a migration file that has already been applied.

---

## 8. Seed the Database

The app cannot function without initial data. You cannot log in without a staff row. A staff row requires a department. Run this once after migrations.

Create `backend/seed.py`:

```python
import asyncio
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.engine import AsyncSessionLocal
from app.models.department import Department
from app.models.staff import Staff
from app.models.protocol import Protocol
from app.core.auth import hash_pin
import uuid

CT_SURGERY_QUESTION_BANK = [
    {"id": "q_chief_complaint", "text": "What brings the patient in today?", "category": "CHIEF_COMPLAINT", "required": True},
    {"id": "q_chest_pain", "text": "Does the patient have chest pain? If yes, describe onset, duration, and character.", "category": "CARDIAC", "required": True},
    {"id": "q_breathlessness", "text": "Does the patient have breathlessness? At rest or on exertion?", "category": "RESPIRATORY", "required": True},
    {"id": "q_palpitations", "text": "Does the patient experience palpitations or irregular heartbeat?", "category": "CARDIAC", "required": True},
    {"id": "q_syncope", "text": "Has the patient had any episodes of fainting or near-fainting?", "category": "CARDIAC", "required": True},
    {"id": "q_cardiac_history", "text": "Any previous cardiac conditions, surgeries, or interventions?", "category": "HISTORY", "required": True},
    {"id": "q_medications", "text": "What medications is the patient currently taking?", "category": "MEDICATIONS", "required": True},
    {"id": "q_allergies", "text": "Does the patient have any known allergies, especially to medications?", "category": "HISTORY", "required": True},
    {"id": "q_smoking", "text": "Does the patient smoke or have a history of smoking?", "category": "LIFESTYLE", "required": True},
    {"id": "q_family_history", "text": "Any family history of heart disease or sudden cardiac death?", "category": "HISTORY", "required": True},
    {"id": "q_investigations", "text": "Has the patient had any recent ECG, Echo, or blood tests? What were the findings?", "category": "INVESTIGATIONS", "required": True},
    {"id": "q_diabetes_hypertension", "text": "Does the patient have diabetes or hypertension?", "category": "HISTORY", "required": True},
]

async def seed():
    async with AsyncSessionLocal() as session:
        # Department
        dept = Department(
            id=uuid.uuid4(),
            name="Cardio Thoracic Surgery",
            llm_system_prompt=(
                "You are a clinical intake assistant for the Cardio Thoracic Surgery department. "
                "Your role is to guide medical staff through patient intake by asking one question at a time. "
                "Use the question bank as a checklist of topics to cover. Ask follow-up questions where clinically relevant. "
                "When all required topics are adequately covered, respond with exactly: INTAKE_COMPLETE\n"
                "Rules: Never diagnose. Never recommend treatment. Never classify emergencies. "
                "Always respond in English regardless of the language used in patient answers. "
                "Keep questions clear and simple — they will be read aloud to patients."
            ),
            llm_summary_prompt=(
                "You are a clinical documentation assistant. Generate a structured patient case summary "
                "for a Cardiothoracic Surgeon based on the intake conversation provided. "
                "Format with clearly labelled sections: Chief Complaint, Cardiac Symptoms, Respiratory Symptoms, "
                "Cardiac History, Current Medications, Allergies, Lifestyle, Family History, Investigations. "
                "Include all captured information. Do not add information not present in the conversation. "
                "Do not include diagnostic conclusions or treatment recommendations."
            ),
            is_active=True,
        )
        session.add(dept)
        await session.flush()

        # Protocol
        protocol = Protocol(
            id=uuid.uuid4(),
            department_id=dept.id,
            version=1,
            question_bank=CT_SURGERY_QUESTION_BANK,
            is_active=True,
        )
        session.add(protocol)

        # Admin staff account
        admin = Staff(
            id=uuid.uuid4(),
            name="Admin",
            role="ADMIN",
            department_id=dept.id,
            email="admin@hospital.com",
            pin_hash=hash_pin("000000"),   # change immediately after first login
            is_active=True,
        )
        session.add(admin)

        await session.commit()
        print("Seeded: CT Surgery department, protocol, admin account")
        print("Admin login: admin@hospital.com / PIN: 000000")
        print("Change the admin PIN immediately via PATCH /staff/{id}")

asyncio.run(seed())
```

Run it:
```bash
cd backend
source .venv/bin/activate
python seed.py
```

---

## 9. Run Everything

### Option A — Docker (closest to production)

```bash
# From repo root
docker compose up --build
```

Then seed the database:
```bash
docker compose exec fastapi python seed.py
```

- Frontend: `http://localhost:3000`
- FastAPI: `http://localhost:8000`
- API docs: `http://localhost:8000/docs`

### Option B — Local dev (faster iteration, use this daily)

**Terminal 1 — PostgreSQL + Redis**
```bash
docker compose up postgres redis
```

**Terminal 2 — FastAPI**
```bash
cd backend
source .venv/bin/activate
uvicorn app.main:app --reload --port 8000
```

**Terminal 3 — React**
```bash
cd frontend
npm run dev
```

First time only — seed after Terminal 2 is running:
```bash
cd backend && source .venv/bin/activate && python seed.py
```

---

## 10. Verify Everything Works

```bash
# FastAPI health
curl http://localhost:8000/health
# → {"status":"ok"}

# API docs (test all endpoints interactively)
open http://localhost:8000/docs

# Frontend
open http://localhost:3000
```

Test login with the seeded admin account:
```bash
curl -X POST http://localhost:8000/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@hospital.com","pin":"000000"}'
# → {"accessToken":"...","staff":{...}}
```

---

## 11. Update `.gitignore`

Add to the existing `.gitignore` at repo root:

```
# Python
backend/.venv/
backend/.env
backend/__pycache__/
backend/**/__pycache__/
backend/*.pyc

# Node
frontend/node_modules/
frontend/dist/

# OS
.DS_Store
```

---

## 12. First Steps for Each Intern

### Intern 1 (FastAPI backend)

1. Set up venv (Section 4.1 — check Python version first)
2. Create all `__init__.py` stubs and router stubs (Section 4.5) — do this before anything else
3. Create `app/core/config.py` (Section 4.7) and `backend/.env`
4. Create `app/db/engine.py` — SQLAlchemy async engine and `AsyncSessionLocal` (Section 4.8)
5. Create `app/core/auth.py` — bcrypt and JWT helpers (Section 4.9)
6. Create `app/core/deps.py` — `get_db` and `get_current_staff` (Section 4.10)
7. Create `app/db/redis.py` — Redis client (Section 4.11)
8. Create all models in `app/models/` matching [db-architecture.md](db-architecture.md); update `models/__init__.py` (Section 7.3)
9. Run Alembic migrations (Section 7) — verify all tables exist
10. Run seed script (Section 8) — verify admin login works via curl
11. Build `POST /auth/login` and `POST /auth/logout` in `app/api/auth.py`
12. Build `POST /staff` and `PATCH /staff/{id}` in `app/api/staff.py`
13. Build all `/sessions/*` endpoints in `app/api/sessions.py`
14. Wire Redis locks and concurrency enforcement

### Intern 2 (React PWA frontend)

1. Scaffold frontend, run `npm run dev`, confirm Vite default page at `localhost:3000`
2. The `client.ts` and `useSpeechRecognition.ts` are already provided above — copy them in
3. Set up React Router in `App.tsx` with 5 page placeholders
4. Build `LoginPage.tsx` — calls `POST /api/auth/login`, saves token via `setToken()`, redirects to intake list
5. Build `IntakeListPage.tsx` — calls `GET /api/sessions`, renders a session list
6. Build `IntakeSessionPage.tsx` — patient info form → Q&A loop → completion screen (3 states in one page)
7. Wire `useSpeechRecognition` into the Q&A screen with language selector (English / Hindi / Telugu)
8. Build `SummaryPage.tsx` — render summary text, Ask More button
9. Build `AccountManagementPage.tsx` — create/update staff form

### Intern 3 (Claude integration)

1. Get Claude API key from TL, verify it works (Section 14.1)
2. Create `app/services/claude.py` with `get_next_question()` and `generate_summary()`
3. Read [llm-prompt-design.md](llm-prompt-design.md) — CT Surgery system prompt and summary prompt are defined there; use them as-is to start
4. Wire `get_next_question()` into `POST /sessions/{id}/respond`
5. Wire `generate_summary()` into `POST /sessions/{id}/complete`
6. Test INTAKE_COMPLETE signal — confirm all required question_bank topics are covered before it fires
7. Test `is_flagged` scan — confirm non-substantive answers ("NA", "no", "doesn't know") get flagged

---

## 13. Key Reference Docs

| What | Where |
|---|---|
| All API contracts (request/response shapes) | [trd-milestone-1.md](trd-milestone-1.md) |
| Database tables and columns | [db-architecture.md](db-architecture.md) |
| System flow and sequence diagrams | [technical-architecture.md](technical-architecture.md) |
| What to build and acceptance criteria | [implementation-roadmap.md](implementation-roadmap.md) |
| Claude prompt design | [llm-prompt-design.md](llm-prompt-design.md) |
| Full product requirements | [prd-milestone-1.md](prd-milestone-1.md) |

---

## 14. Beyond Setup — Decisions and Actions Required

These are not code tasks. They are decisions, procurements, or configurations that must happen before specific milestones. If they are missed, features will silently fail or be undeployable.

---

### 14.1 Claude API Key — PTL action, needed before Sprint 2

**Who:** PTL procures, TL hands key to Intern 3, Intern 3 configures
**Blocks:** All Claude integration work — `get_next_question()`, `generate_summary()`, Ask More

The entire AI-INTEGRATION sprint cannot start without this.

**PTL action:**
1. Create an account at [console.anthropic.com](https://console.anthropic.com)
2. Generate an API key under API Keys
3. Hand the key to Intern 3 securely (not over chat/email in plain text)

**Intern 3 action:**
```bash
# Add to backend/.env
ANTHROPIC_API_KEY=sk-ant-...

# Verify it works before Sprint 2 starts
cd backend
source .venv/bin/activate
python -c "
import anthropic
client = anthropic.Anthropic()
msg = client.messages.create(
    model='claude-sonnet-4-6',
    max_tokens=50,
    messages=[{'role': 'user', 'content': 'Say: API key works'}]
)
print(msg.content[0].text)
"
```

If this prints a response, Claude integration can begin. If it throws `AuthenticationError`, the key is wrong.

---

### 14.2 HTTPS for Hospital Deployment — needed before go-live

**Who:** TL decides approach, Intern 3 implements, Hospital IT provides cert if needed
**Blocks:** Web Speech API on hospital tablets — it silently refuses to activate on plain `http://`

Web Speech API works on:
- `localhost` — always (local dev only)
- `https://` — always
- `http://` — never (Chrome blocks it without exception)

This means: the app will work perfectly during development on your laptop but will appear broken on hospital tablets the moment you deploy to a non-localhost URL over HTTP.

**Options — pick one:**

| Option | Effort | When to use |
|---|---|---|
| Self-signed certificate | 30 min | Internal hospital network, tablets can trust the cert manually |
| Let's Encrypt (Certbot) | 1–2 hours | If the server has a public domain name |
| Hospital IT provides cert | varies | If the hospital has an existing SSL infrastructure |

**Minimum nginx config for HTTPS (self-signed, for testing):**

Generate the cert:
```bash
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout nginx-selfsigned.key \
  -out nginx-selfsigned.crt \
  -subj "/CN=ai-nims.local"
```

Update `frontend/nginx.conf`:
```nginx
server {
    listen 80;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    ssl_certificate     /etc/nginx/ssl/nginx-selfsigned.crt;
    ssl_certificate_key /etc/nginx/ssl/nginx-selfsigned.key;

    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location /api/ {
        proxy_pass http://fastapi:8000/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}
```

Mount the cert in `docker-compose.yml` under the `frontend` service:
```yaml
frontend:
  volumes:
    - ./nginx-selfsigned.crt:/etc/nginx/ssl/nginx-selfsigned.crt
    - ./nginx-selfsigned.key:/etc/nginx/ssl/nginx-selfsigned.key
  ports:
    - "443:443"
    - "80:80"
```

On each hospital tablet: open the URL once in Chrome → accept the security warning → mic will work from that point on.

> Do not commit `nginx-selfsigned.key` to git. Add it to `.gitignore`.

---

### 14.3 Git Branching Convention — agree on Day 1

**Blocks:** Nothing technically, but without this interns will push directly to `main` and overwrite each other

**Rules:**

| Branch | Who | Purpose |
|---|---|---|
| `main` | protected — PR only | Stable, always deployable |
| `feature/backend-*` | Intern 1 | All FastAPI work |
| `feature/frontend-*` | Intern 2 | All React work |
| `feature/claude-*` | Intern 3 | Claude integration |

**Day 1 setup — do this before writing any code:**
```bash
# Protect main on GitHub:
# Settings → Branches → Add rule → main → Require PR before merging

# Each intern creates their first branch
git checkout -b feature/backend-foundation    # Intern 1
git checkout -b feature/frontend-scaffold     # Intern 2
git checkout -b feature/claude-setup          # Intern 3
```

**Commit message format** (keep it simple):
```
feat: add POST /auth/login endpoint
fix: correct Redis TTL not refreshing on /respond
chore: add models/__init__.py imports
```

**PR rule:** All PRs need at least one review before merging to `main`. No PR sits open longer than 24 hours — merge or close it.

---

### 14.4 CI/CD Pipeline — set up by end of Sprint 1

**Who:** Intern 1
**Blocks:** Nothing in Sprint 1, but catching broken builds early saves Sprint 3 and 4

For MVP, a minimal GitHub Actions pipeline is enough — just confirm the Docker images build and the health check passes.

Create `.github/workflows/ci.yml` at the repo root:

```yaml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build Docker images
        run: docker compose build

      - name: Start services
        run: docker compose up -d
        env:
          DATABASE_URL: postgresql+asyncpg://nims:nims_dev@localhost:5432/nims
          REDIS_URL: redis://localhost:6379
          JWT_SECRET: ci_test_secret_64_chars_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          CLAUDE_MODEL: claude-sonnet-4-6
          CLAUDE_TIMEOUT_SECONDS: 30

      - name: Wait for FastAPI
        run: |
          for i in $(seq 1 10); do
            curl -sf http://localhost:8000/health && break
            sleep 3
          done

      - name: Health check
        run: curl -sf http://localhost:8000/health

      - name: Tear down
        run: docker compose down
```

**Add the API key as a GitHub secret:**
- Go to repo → Settings → Secrets and variables → Actions → New secret
- Name: `ANTHROPIC_API_KEY`
- Value: the actual key

This gives you: broken Docker build = red CI = no merge to `main`. That's enough for MVP.

---

### Summary — who does what before each sprint

| Before | Action | Owner |
|---|---|---|
| Sprint 1 Day 1 | Set up git branch rules on GitHub, create feature branches | All interns |
| Sprint 1 Day 1 | Procure Claude API key | PTL |
| Sprint 1 Day 3 | Intern 3 verifies Claude API key works (Section 14.1) | Intern 3 |
| Sprint 1 end | CI/CD pipeline running on main | Intern 1 |
| Sprint 3 start | Decide on HTTPS approach with hospital IT | TL + PTL |
| Before go-live | HTTPS configured and tested on one hospital tablet | Intern 3 |
