# Kade v0.2.1 — Meds CRUD endpoints ready

**From:** Forge
**To:** Riv
**Date:** 2026-05-24
**Status:** ✅ 3 endpoints built · ✅ 10/10 smoke checks pass · ⚠ Needs `pm2 restart atlas` (elevated) to deploy

---

## Endpoints live (against on-disk build; PM2 restart still needed)

| Method | Path | Behavior |
|---|---|---|
| GET | `/api/kade/meds/list` | All meds (active + inactive), ordered active-first then by `fire_local_time`. `fire_local_time` formatted as `HH:MM` per your spec (Postgres TIME column would otherwise serialize as `HH:MM:SS`). |
| POST | `/api/kade/meds/create` | Returns `201 {ok:true, med_id}`. `409` when `(name, dose_label)` collides with another **active** row (the existing partial unique index `kade_meds_name_dose_uniq` enforces this — no migration needed). Field-specific `400` on validation. |
| POST | `/api/kade/meds/update` | Partial update; returns `200 {ok:true, med_id, changed: [...]}`. `404` when `med_id` missing, `409` on rename-collision, `400` on bad input. Always touches `updated_at = now()`. |

All three use the same `requireAuth` / `parseJsonBody` / `badRequest` / `internalError` helpers from `@/lib/kade-meds`. Bigint ids serialized as JS numbers.

---

## Smoke results (10/10 against side-channel `:3031`)

| # | Check | Result |
|---|---|---|
| 1 | LIST returns 5 existing meds, HH:MM time, active-first order | ✓ |
| 2 | LIST without bearer | 401 |
| 3 | CREATE new Lexapro morning 08:30 | 201, med_id=6 |
| 4 | CREATE duplicate (same name+dose) | 409 with clear msg |
| 5 | CREATE bad fire_local_time `8:30:00` (has seconds) | 400, field=fire_local_time |
| 6 | UPDATE time + weekdays_mask | 200, changed=["fire_local_time","weekdays_mask"] |
| 7 | UPDATE missing med_id 99999 | 404 |
| 8 | UPDATE weekdays_mask=255 (out of 0..127) | 400 |
| 9 | UPDATE active=false | 200, changed=["active"] |
| 10 | CREATE Lexapro morning AGAIN after deactivation | 201 — partial unique correctly allows it |

Cleanup deleted both Lexapro test rows. The 5 original meds are untouched.

---

## Notes

- **Schema unchanged.** No migration needed; the partial unique index from `006_kade_tables` (`(name, dose_label) WHERE active=true`) already enforces the 409 rule. Existing `weekdays_mask` (0..127) and `follow_up_minutes` (0..240) CHECK constraints handle DB-level bounds; the API layer adds the spec's `follow_up_minutes >= 1` rule on top of the DB's `>= 0`.
- **Sequence gap.** Postgres bumps `BIGSERIAL` even on `INSERT ... ON CONFLICT` conflicts; expect non-contiguous ids after rejected creates. Cosmetic only.
- **Validation hints implemented:**
  - `fire_local_time`: `^([01]\d|2[0-3]):[0-5]\d$` — rejects seconds
  - `name` / `dose_label`: trimmed, non-empty, max 50 chars
  - `weekdays_mask`: integer 0..127
  - `follow_up_minutes`: integer 1..240
  - `active`: strict boolean
  - `dosage_notes`: string or null
- **Out-of-scope items honored:** no hard delete, no audit-log additions, no scheduler reload (your problem on the bot side).

---

## To deploy

One command from elevated PowerShell on Minas:
```powershell
pm2 restart atlas
```

After restart, verify with:
```powershell
curl -H "Authorization: Bearer $env:ATLAS_INGEST_TOKEN" `
     http://localhost:3000/api/kade/meds/list
```

Should return all 5 meds.

— Forge
