feat(medications): an api tab, a stockable inventory and a next-due that tells the truth

The detail page gains a dedicated API tab, the full-edit button becomes
a lifecycle row that opens the editor, the danger zone drops its red
wash in favour of the shared card chrome, and the card menu's edit
entry leads to the detail page it describes. The inventory tab finally
records stock — packs and pens with optional expiry, plus corrections —
instead of pointing at an API. The card's next-dose line reads from the
canonical band model instead of a stale schedule window, which also
showed old dose times after a schedule change; the repair script learns
to reconcile drifted windows and tombstone pending rows parked on
abandoned anchors.
This commit is contained in:
Marc-André Bombeck committed 2026-06-10 20:48:46 +02:00
1 parent a2524b6d70
commit 258e2fd365
22 files changed
+1373 -221

No files matched your search

+30 -5
View File
@@ -1,7 +1,8 @@
# Medication intake repair
`scripts/repair-intake-anomalies.ts` repairs two known historic defects in
the medication intake ledger (`medication_intake_events`):
`scripts/repair-intake-anomalies.ts` repairs the known historic defects in
the medication intake ledger (`medication_intake_events`) and its schedule
metadata (`medication_schedules`):
1. **Duplicate dose-slot rows.** More than one live row on the same exact
`(user, medication, scheduled_for)` tuple — cross-source duplicates the
@@ -12,6 +13,20 @@ the medication intake ledger (`medication_intake_events`):
7 days before `scheduled_for` or more than 1 day in the future —
typically the residue of a mis-attributed edit before the v1.15.19
`taken_at` validation landed.
3. **Window / times drift (v1.16.1).** Schedule rows whose legacy
`window_start` / `window_end` no longer contains the canonical
`times_of_day` — typically a degenerate `07:00 / 07:00` point window
left behind by an old write path while the dose times moved on (e.g.
to `09:00 / 21:00`). The dose-band model never reads the window once
`times_of_day` exists, but the stale pair kept feeding every legacy
read (the pre-v1.16.1 cards painted "next intake 07:00" from it).
4. **Stale-anchor pending rows (v1.16.1).** Live pending rows
(`taken_at IS NULL`, not skipped, not auto-missed) whose
`scheduled_for` wall-clock HH:mm — evaluated in the user's timezone —
matches no current dose anchor of the medication (any schedule's
`times_of_day` entry; `window_start` for legacy rows without times).
These are reminder-minted slots on retired schedule times that linger
as phantom open doses in the ledger.
## When to use it
@@ -51,8 +66,10 @@ Use `pnpm dlx`, not bare `pnpm tsx` — the standalone image also strips
`tsx`.
The dry-run prints every duplicate group (which row would be kept, which
would be tombstoned) and a table of implausible rows (id, medication,
`scheduledFor`, `takenAt`, source, timestamps).
would be tombstoned), a table of implausible rows (id, medication,
`scheduledFor`, `takenAt`, source, timestamps), every schedule whose
window has drifted off its `times_of_day` (with the reconciled bounds it
would write), and a table of stale-anchor pending rows.
## Applying the fix
@@ -60,7 +77,7 @@ would be tombstoned) and a table of implausible rows (id, medication,
pnpm dlx --package pg --package tsx tsx scripts/repair-intake-anomalies.ts --fix
```
`--fix` does three things:
`--fix` does five things:
- **Duplicate groups** are collapsed to one winner per slot. Precedence:
an actioned row (taken or skipped) beats a pending one; between two
@@ -79,6 +96,14 @@ pnpm dlx --package pg --package tsx tsx scripts/repair-intake-anomalies.ts --fix
```
`--tombstone-implausible` is refused without `--fix`.
- **Drifted schedule windows are reconciled** to
`min/max(times_of_day)` so the legacy pair stops contradicting the
canonical dose times. Overnight windows (`window_end < window_start`)
encode a deliberate wrap and are never touched.
- **Stale-anchor pending rows are tombstoned** (same soft-delete shape
as the duplicate losers). A medication without any derivable anchor
(PRN-only / unscheduled) is skipped — there is nothing to compare
against.
- **Affected compliance rollups are recomputed** for every touched
`(user, medication, day)`. The script executes the same DISTINCT-slot
aggregation SQL the shared helper runs (a verbatim twin of