Document GET /api/measurements/series and GET /api/user/ai-provider in the
OpenAPI registry — both are consumed by the iOS client (charts and Coach
visibility gating) but were absent from the spec.
Break migration 0159's active-receipt dedup on (created_at, id) so an exact
created_at tie still leaves one survivor, keeping the unique index build from
aborting 23505 on affected tenants.
Catch P2002 in createReceipt: two concurrent first grants both reach create,
the loser re-reads the winning active receipt and returns it instead of a 500.
Two follow-ups to the active-receipt uniqueness work:
- Migration 0159 now revokes all-but-the-newest active row per
(user_id, kind) before building the partial unique index. Without the
pre-flight dedup the CREATE UNIQUE INDEX would abort on any tenant that
already carries duplicate active grants — exactly the tenants the fix
targets — and fail the deploy.
- createReceipt stamps the superseded row's revoked_at with a server
clock instead of the client-supplied signedAt. A backdated signedAt
would otherwise mark the prior row revoked before its own signed_at,
inverting the audit chain. The new row keeps the client signedAt.
Two concurrent web-grant heals could each pass the read-then-create gap
and insert a duplicate active ai_full receipt; the revoke path then had
to chase one row at a time. Wrap the web-grant mint in a Serializable
transaction with an in-transaction re-check, and add migration 0159 — a
partial unique index over (user_id, kind) WHERE revoked_at IS NULL — as
the structural backstop. createReceipt now supersedes any active row of
the same kind before appending, and revokeLatest flips revoked_at via a
single atomic updateMany, so the append-only audit chain is preserved
while at most one active row ever exists.