feat(cycle): user-defined custom symptoms

Mirror the v1.13 custom-mood-tag CRUD for cycle symptoms. A user can mint
their own symptoms beyond the 15 seeded ones, picked from a label + icon
popover via a dashed ghost-chip in the log-day sheet's symptom grid.

- POST/GET /api/cycle/symptoms/custom and PATCH/DELETE on /[key]: mint a
  custom:<uuid> key, encrypt the free-text label at rest (AES-256-GCM),
  per-user cap, owner-scoped, gated behind requireCycleEnabled. Soft-hide by
  default, hard-purge on ?purge=true.
- The day-log write already resolves a symptom key against the catalogue plus
  the caller's own rows, so a custom: key links by construction.
- The log-day sheet fetches the user's customs and merges them under a new
  custom category in the chip grid; the ghost-chip + popover reuse the chip
  styling and the Lucide icon set.
- Seed the global custom symptom category (migration 0134).
- The purge route (DELETE /api/cycle/all) now drops the user's custom symptom
  rows too, so no intent-revealing label survives a purge.
- The label is never logged: only the icon + counts reach annotate/auditLog.
- i18n: cycle.symptom.custom.* across all six locales.
This commit is contained in:
Marc-André Bombeck committed 2026-06-06 16:54:43 +02:00
1 parent 81ec98fc97
commit 985e197db4
18 files changed
+1233 -59

No files matched your search

+7
View File
@@ -37,6 +37,12 @@ export const DELETE = apiHandler(async (request: NextRequest) => {
const dayLogs = await tx.cycleDayLog.deleteMany({
where: { userId: user.id },
});
// Per-user custom symptoms carry an intent-revealing free-text label
// (encrypted at rest). A purge that promises "nothing reproductive
// persists" must drop them too; their links cascade off the row delete.
const customSymptoms = await tx.cycleSymptom.deleteMany({
where: { userId: user.id },
});
const predictions = await tx.cyclePrediction.deleteMany({
where: { userId: user.id },
});
@@ -79,6 +85,7 @@ export const DELETE = apiHandler(async (request: NextRequest) => {
});
return {
dayLogs: dayLogs.count,
customSymptoms: customSymptoms.count,
predictions: predictions.count,
cycles: cycles.count,
auditRows: auditRows.count,