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

+12 -2
View File
@@ -4903,7 +4903,8 @@
"symptomCategory": {
"physical": "Körperlich",
"emotional": "Emotional",
"digestive": "Verdauung"
"digestive": "Verdauung",
"custom": "Eigene"
},
"symptom": {
"cramps": "Krämpfe",
@@ -4920,7 +4921,16 @@
"foodCravings": "Heißhunger",
"nausea": "Übelkeit",
"diarrhea": "Durchfall",
"constipation": "Verstopfung"
"constipation": "Verstopfung",
"custom": {
"add": "Symptom hinzufügen",
"label": "Name",
"labelPlaceholder": "z. B. Schwindel",
"icon": "Symbol",
"limitReached": "Höchstzahl an eigenen Symptomen erreicht.",
"remove": "{label} entfernen",
"fallbackLabel": "Eigenes Symptom"
}
},
"prediction": {
"disclaimer": "Vorhersagen sind Schätzungen aus deinen erfassten Daten, keine medizinische Beratung und keine Verhütung. Die Genauigkeit steigt, je mehr Zyklen du erfasst."
+12 -2
View File
@@ -4903,7 +4903,8 @@
"symptomCategory": {
"physical": "Physical",
"emotional": "Emotional",
"digestive": "Digestive"
"digestive": "Digestive",
"custom": "Custom"
},
"symptom": {
"cramps": "Cramps",
@@ -4920,7 +4921,16 @@
"foodCravings": "Food cravings",
"nausea": "Nausea",
"diarrhea": "Diarrhea",
"constipation": "Constipation"
"constipation": "Constipation",
"custom": {
"add": "Add symptom",
"label": "Name",
"labelPlaceholder": "e.g. Dizziness",
"icon": "Icon",
"limitReached": "Custom symptom limit reached.",
"remove": "Remove {label}",
"fallbackLabel": "Custom symptom"
}
},
"prediction": {
"disclaimer": "Predictions are estimates from your logged data, not medical advice or contraception. Accuracy improves as you log more cycles."
+12 -2
View File
@@ -4903,7 +4903,8 @@
"symptomCategory": {
"physical": "Físico",
"emotional": "Emocional",
"digestive": "Digestivo"
"digestive": "Digestivo",
"custom": "Personalizado"
},
"symptom": {
"cramps": "Calambres",
@@ -4920,7 +4921,16 @@
"foodCravings": "Antojos",
"nausea": "Náuseas",
"diarrhea": "Diarrea",
"constipation": "Estreñimiento"
"constipation": "Estreñimiento",
"custom": {
"add": "Añadir síntoma",
"label": "Nombre",
"labelPlaceholder": "p. ej. Mareo",
"icon": "Icono",
"limitReached": "Se alcanzó el límite de síntomas personalizados.",
"remove": "Eliminar {label}",
"fallbackLabel": "Síntoma personalizado"
}
},
"prediction": {
"disclaimer": "Las predicciones son estimaciones basadas en tus datos registrados, no son consejo médico ni un método anticonceptivo. La precisión mejora a medida que registras más ciclos."
+12 -2
View File
@@ -4903,7 +4903,8 @@
"symptomCategory": {
"physical": "Physique",
"emotional": "Émotionnel",
"digestive": "Digestif"
"digestive": "Digestif",
"custom": "Personnalisé"
},
"symptom": {
"cramps": "Crampes",
@@ -4920,7 +4921,16 @@
"foodCravings": "Fringales",
"nausea": "Nausées",
"diarrhea": "Diarrhée",
"constipation": "Constipation"
"constipation": "Constipation",
"custom": {
"add": "Ajouter un symptôme",
"label": "Nom",
"labelPlaceholder": "ex. Vertige",
"icon": "Icône",
"limitReached": "Limite de symptômes personnalisés atteinte.",
"remove": "Supprimer {label}",
"fallbackLabel": "Symptôme personnalisé"
}
},
"prediction": {
"disclaimer": "Les prévisions sont des estimations issues de tes données enregistrées, et non un avis médical ou un moyen de contraception. La précision s'améliore à mesure que tu enregistres des cycles."
+12 -2
View File
@@ -4903,7 +4903,8 @@
"symptomCategory": {
"physical": "Fisico",
"emotional": "Emotivo",
"digestive": "Digestivo"
"digestive": "Digestivo",
"custom": "Personalizzato"
},
"symptom": {
"cramps": "Crampi",
@@ -4920,7 +4921,16 @@
"foodCravings": "Voglie alimentari",
"nausea": "Nausea",
"diarrhea": "Diarrea",
"constipation": "Stitichezza"
"constipation": "Stitichezza",
"custom": {
"add": "Aggiungi sintomo",
"label": "Nome",
"labelPlaceholder": "es. Vertigini",
"icon": "Icona",
"limitReached": "Limite di sintomi personalizzati raggiunto.",
"remove": "Rimuovi {label}",
"fallbackLabel": "Sintomo personalizzato"
}
},
"prediction": {
"disclaimer": "Le previsioni sono stime basate sui dati registrati, non sono un consiglio medico né un metodo contraccettivo. La precisione migliora man mano che registri più cicli."
+12 -2
View File
@@ -4903,7 +4903,8 @@
"symptomCategory": {
"physical": "Fizyczne",
"emotional": "Emocjonalne",
"digestive": "Trawienne"
"digestive": "Trawienne",
"custom": "Własne"
},
"symptom": {
"cramps": "Skurcze",
@@ -4920,7 +4921,16 @@
"foodCravings": "Zachcianki",
"nausea": "Nudności",
"diarrhea": "Biegunka",
"constipation": "Zaparcia"
"constipation": "Zaparcia",
"custom": {
"add": "Dodaj objaw",
"label": "Nazwa",
"labelPlaceholder": "np. Zawroty głowy",
"icon": "Ikona",
"limitReached": "Osiągnięto limit własnych objawów.",
"remove": "Usuń {label}",
"fallbackLabel": "Objaw własny"
}
},
"prediction": {
"disclaimer": "Prognozy to szacunki na podstawie zapisanych danych, a nie porada medyczna ani metoda antykoncepcji. Dokładność rośnie wraz z liczbą zapisanych cykli."