feat(coach,admin): finish the self-context and invite surfaces end to end

Invites move into their own admin section with a real ledger: status
badges, expiry, uses, a redemption history of who joined when (new
redemption table, revocation is now soft so history survives), and an
honest one-time link/QR moment. The assistant self-context grows
structured fields for conditions, allergies and focus — encrypted like
the free text — merges age and gender from the profile into one fenced
prompt block, and asks up to three targeted follow-up questions that
surface as tappable chips above the coach composer. A fourth nudge
trigger gently suggests a check-up when the self-context is stale and
the coach is actually in use.
This commit is contained in:
Marc-André Bombeck committed 2026-06-10 16:18:48 +02:00
1 parent 9fe5673251
commit 5b75184649
32 files changed
+2648 -423

No files matched your search

@@ -0,0 +1,53 @@
/**
* v1.16.0 — admin invite table: status-derivation precedence.
*
* revoked > exhausted > expired > active. Revocation must win even on
* a row that is also expired/exhausted, because the badge documents
* the admin's action; exhaustion beats expiry because "it was fully
* used" is the more informative fact.
*/
import { describe, expect, it } from "vitest";
import { deriveInviteStatus } from "../invite-tokens-section";
const NOW = new Date("2026-06-10T12:00:00Z");
const FUTURE = new Date(NOW.getTime() + 86_400_000).toISOString();
const PAST = new Date(NOW.getTime() - 86_400_000).toISOString();
describe("deriveInviteStatus", () => {
it("revoked wins over every other state", () => {
expect(
deriveInviteStatus(
{ uses: 5, maxUses: 1, expiresAt: PAST, revokedAt: PAST },
NOW,
),
).toBe("revoked");
});
it("exhausted beats expired", () => {
expect(
deriveInviteStatus(
{ uses: 1, maxUses: 1, expiresAt: PAST, revokedAt: null },
NOW,
),
).toBe("exhausted");
});
it("expired when past expiry with uses left", () => {
expect(
deriveInviteStatus(
{ uses: 0, maxUses: 1, expiresAt: PAST, revokedAt: null },
NOW,
),
).toBe("expired");
});
it("active otherwise", () => {
expect(
deriveInviteStatus(
{ uses: 0, maxUses: 1, expiresAt: FUTURE, revokedAt: null },
NOW,
),
).toBe("active");
});
});