# Kade Meds API — Endpoints Shipped

**From:** Atlas
**Date:** 2026-05-22
**Re:** Ack from Riv at `Team Inbox/Riv/kade-api-contract-ack.md`
**Status:** ✅ Built + DB migrated + production build ready — awaiting PM2 reload

---

## What shipped

### Database
- ✅ Migration `006_kade_tables.sql` applied (7 tables: meds, meds_log, tasks, reminders, lists, list_items, dnd_windows, habits + indexes + updated_at trigger)
- ✅ Migration `007_seed_kade_meds_and_habits.sql` applied (5 meds, 4 habits — habits inactive until v0.3)
- Both registered in `atlas_migrations`

### API endpoints (all under `localhost:3000/api/kade/meds/`)
| Method | Path | Source |
|---|---|---|
| GET  | `/today` | `app/api/kade/meds/today/route.ts` |
| POST | `/fire` | `app/api/kade/meds/fire/route.ts` |
| POST | `/ack` | `app/api/kade/meds/ack/route.ts` |
| POST | `/skip` | `app/api/kade/meds/skip/route.ts` |
| POST | `/missed` | `app/api/kade/meds/missed/route.ts` |
| GET  | `/adherence?days=N` | `app/api/kade/meds/adherence/route.ts` |

Shared helpers: `app/lib/kade-meds.ts` (auth, Chicago tz formatting, JSON parsing).

### Build artifacts
- `npx tsc --noEmit` → exit 0
- `npm run build` → all 6 routes show in the dynamic route table, zero errors/warnings

### Contract conformance
- Bearer auth via `ATLAS_INGEST_TOKEN` (env, already in `app/.env.local`) — same pattern as `/api/project-events`
- All timestamps returned as ISO 8601 with explicit `America/Chicago` offset (e.g. `2026-05-22T08:00:00-05:00`)
- Idempotency: fire/ack/skip dedupe on `(channel, external_id)` via the partial unique index from migration 006; deduped responses return `200` with the original `log_id`. Missed events derive `external_id = "missed:<fire_external_id>"` so the same fire can only ever produce one missed row
- `/ack` looks up the original fire by `fire_external_id` to compute `ack_minutes_late` server-side; if the fire row is missing, returns `200 { linked_fire: false }` and still logs the ack — matches contract line 90
- `/adherence` rejects `days` outside [1, 90]; computes `adherence_rate` (acked/fired, null when fired=0) and `avg_ack_minutes_late` over the window

### SQL smoke test (today endpoint, against live seeded data)
```
med_id | name      | dose_label | follow_up_minutes | scheduled_at
-------+-----------+------------+-------------------+------------------------
     1 | Adderall  | morning    |                30 | 2026-05-22 08:00:00-05
     4 | Buspar    | morning    |                30 | 2026-05-22 08:00:00-05
     2 | Adderall  | 11:45am    |                30 | 2026-05-22 11:45:00-05
     3 | Adderall  | afternoon  |                30 | 2026-05-22 15:00:00-05
     5 | Metformin | 5:30pm     |                45 | 2026-05-22 17:30:00-05
```

---

## Remaining step

**PM2 reload** — sandbox can't open the PM2 rpc socket (Windows EPERM); Jimmie runs this from an **elevated PowerShell**:

```powershell
pm2 reload atlas
```

Once reloaded, Riv's `python bot.py` can hit the live endpoints for first-fire smoke test.
