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

+18 -12
View File
@@ -1,11 +1,14 @@
/**
* DELETE /api/admin/invites/{id} — revoke a registration invite.
*
* v1.15.20 — hard delete: an invite row carries no health data and no
* plaintext secret, and the audit log already records mint + revoke, so
* keeping a tombstone would only grow the table. Idempotent — deleting
* an unknown id returns `{ deleted: false }` rather than 404 so a
* double-tap in the admin UI never surfaces an error.
* v1.16.0 — soft revocation (`revokedAt`), replacing the v1.15.20 hard
* delete: the admin table now renders redemption history per invite,
* and a deleted row would erase who was admitted through it. The
* consume path refuses a revoked invite like an expired one.
*
* Idempotent — revoking an unknown or already-revoked id returns
* `{ revoked: false }` rather than 404 so a double-tap in the admin UI
* never surfaces an error.
*/
import { prisma } from "@/lib/db";
import { apiHandler, requireAdmin } from "@/lib/api-handler";
@@ -20,11 +23,14 @@ export const DELETE = apiHandler(
const { user } = await requireAdmin();
const { id } = await params;
const { count } = await prisma.inviteToken.deleteMany({ where: { id } });
const deleted = count > 0;
const { count } = await prisma.inviteToken.updateMany({
where: { id, revokedAt: null },
data: { revokedAt: new Date() },
});
const revoked = count > 0;
if (deleted) {
await auditLog("admin.invite.deleted", {
if (revoked) {
await auditLog("admin.invite.revoked", {
userId: user.id,
ipAddress: getClientIp(req),
details: { inviteId: id },
@@ -32,10 +38,10 @@ export const DELETE = apiHandler(
}
annotate({
action: { name: "admin.invite.deleted" },
meta: { deleted },
action: { name: "admin.invite.revoked" },
meta: { revoked },
});
return apiSuccess({ deleted });
return apiSuccess({ revoked });
},
);
+11
View File
@@ -40,10 +40,21 @@ export const GET = apiHandler(async () => {
createdAt: true,
expiresAt: true,
usedAt: true,
revokedAt: true,
uses: true,
maxUses: true,
creator: { select: { id: true, username: true } },
consumer: { select: { id: true, username: true } },
// v1.16.0 — full per-signup history. `consumer` only carries the
// LAST account; multi-use invites list every admitted signup here.
redemptions: {
select: {
id: true,
redeemedAt: true,
user: { select: { id: true, username: true, email: true } },
},
orderBy: { redeemedAt: "desc" },
},
},
orderBy: { createdAt: "desc" },
});