From 4bf45efcab338e052b3829d68d44e258893d7f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 28 Jun 2026 11:39:26 +0200 Subject: [PATCH] revert(insights): drop hydration logging Remove the in-app water-intake logging feature: the WATER_INTAKE measurement type, the per-user daily-goal field, the goal-ring card and its endpoint, and the related query keys, OpenAPI route, and locale strings. The feature is dropped from this release; the baseline-drift, breathing-screening, and labs-changes insight cards are unaffected, and the pre-existing TOTAL_BODY_WATER body-composition type stays intact. --- docs/api/openapi.yaml | 155 ---------- messages/de.json | 18 +- messages/en.json | 18 +- messages/es.json | 18 +- messages/fr.json | 18 +- messages/it.json | 18 +- messages/pl.json | 18 +- .../0209_v125_water_intake/migration.sql | 21 -- prisma/schema.prisma | 12 - src/app/api/hydration/route.ts | 111 ------- src/app/insights/page.tsx | 10 - src/components/insights/hydration-card.tsx | 288 ------------------ src/components/insights/hydration-ring.tsx | 121 -------- .../insights/insights-edit-mode.tsx | 1 - .../measurements/measurement-list-meta.ts | 4 - src/lib/__tests__/insights-layout.test.ts | 1 - .../measurement-type-enum-coverage.test.ts | 7 - src/lib/hydration/__tests__/hydration.test.ts | 72 ----- src/lib/hydration/hydration.ts | 80 ----- src/lib/insights-layout.ts | 1 - src/lib/insights/chart-tokens.ts | 3 - .../__tests__/apple-health-mapping.test.ts | 6 - src/lib/measurements/apple-health-mapping.ts | 5 - src/lib/measurements/categories.ts | 2 - src/lib/measurements/display-transform.ts | 2 - src/lib/openapi/registry.ts | 1 - src/lib/openapi/routes/hydration.ts | 102 ------- src/lib/openapi/routes/index.ts | 2 - src/lib/personal-records/pr-direction.ts | 5 - src/lib/query-keys/hydration.ts | 10 - src/lib/query-keys/index.ts | 4 - src/lib/validations/measurement.ts | 8 - 32 files changed, 6 insertions(+), 1136 deletions(-) delete mode 100644 prisma/migrations/0209_v125_water_intake/migration.sql delete mode 100644 src/app/api/hydration/route.ts delete mode 100644 src/components/insights/hydration-card.tsx delete mode 100644 src/components/insights/hydration-ring.tsx delete mode 100644 src/lib/hydration/__tests__/hydration.test.ts delete mode 100644 src/lib/hydration/hydration.ts delete mode 100644 src/lib/openapi/routes/hydration.ts delete mode 100644 src/lib/query-keys/hydration.ts diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index f7be19a22..6645c50c6 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -44,7 +44,6 @@ tags: - name: Illness - name: Admin - name: Meta - - name: Hydration paths: /api/cycle/day-logs: get: @@ -2088,7 +2087,6 @@ paths: - SLEEP_SCORE - BODY_TEMPERATURE_DEVIATION - RESILIENCE - - WATER_INTAKE - in: query name: from schema: @@ -2278,7 +2276,6 @@ paths: - SLEEP_SCORE - BODY_TEMPERATURE_DEVIATION - RESILIENCE - - WATER_INTAKE value: type: number measuredAt: @@ -5773,7 +5770,6 @@ paths: - SLEEP_SCORE - BODY_TEMPERATURE_DEVIATION - RESILIENCE - - WATER_INTAKE description: "The measurement type to read (single metric — no fan-out). Closed enum: an unknown type 422s." required: true description: "The measurement type to read (single metric — no fan-out). Closed enum: an unknown type 422s." @@ -8003,44 +7999,6 @@ paths: "401": *a1 "422": *a3 "429": *a4 - /api/hydration: - get: - tags: - - Hydration - summary: Today's hydration total vs goal - description: Sums the calling user's WATER_INTAKE measurements for the current local day and compares them against the - per-user goal (default 2000 ml). - responses: - "200": - description: Today's hydration summary. - content: - application/json: - schema: - $ref: "#/components/schemas/HydrationTodayEnvelope" - "401": *a1 - "422": *a3 - "429": *a4 - patch: - tags: - - Hydration - summary: Set the daily hydration goal - description: Updates the per-user daily hydration goal in millilitres. - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/HydrationGoalRequest" - responses: - "200": - description: Goal updated. - content: - application/json: - schema: - $ref: "#/components/schemas/HydrationGoalEnvelope" - "401": *a1 - "422": *a3 - "429": *a4 components: schemas: EncryptedExportRequest: @@ -9341,7 +9299,6 @@ components: - cycle-summary - signals - rhythm-events - - hydration - health-status - breathing - labs-changes @@ -9754,17 +9711,6 @@ components: - updatedAt description: Resumable module-tour progress point. `lastStopId` seeds the resume index; `status` is the running/terminal state. - HydrationGoalRequest: - type: object - properties: - goalMl: - type: integer - minimum: 250 - maximum: 8000 - description: Daily hydration goal in millilitres. - required: - - goalMl - description: Set the per-user daily hydration goal (ml). MedicationTreatmentClass: type: string enum: @@ -12366,7 +12312,6 @@ components: - SLEEP_SCORE - BODY_TEMPERATURE_DEVIATION - RESILIENCE - - WATER_INTAKE value: type: number unit: @@ -12683,7 +12628,6 @@ components: - SLEEP_SCORE - BODY_TEMPERATURE_DEVIATION - RESILIENCE - - WATER_INTAKE additionalProperties: type: array items: @@ -12761,7 +12705,6 @@ components: - SLEEP_SCORE - BODY_TEMPERATURE_DEVIATION - RESILIENCE - - WATER_INTAKE value: type: number description: Daily mean (spot metrics) or SUM (cumulative). @@ -12860,7 +12803,6 @@ components: - SLEEP_SCORE - BODY_TEMPERATURE_DEVIATION - RESILIENCE - - WATER_INTAKE description: Per-type daily series, keyed by MeasurementType. required: - series @@ -13094,7 +13036,6 @@ components: - SLEEP_SCORE - BODY_TEMPERATURE_DEVIATION - RESILIENCE - - WATER_INTAKE value: type: number unit: @@ -23378,101 +23319,6 @@ components: - progress additionalProperties: false description: The persisted completion flag and resume point after the write. - HydrationTodayEnvelope: - type: object - properties: - data: - $ref: "#/components/schemas/HydrationToday" - error: - type: "null" - meta: - type: object - properties: - requestId: - type: string - additionalProperties: false - required: - - data - - error - additionalProperties: false - HydrationToday: - type: object - properties: - date: - type: string - description: ISO-8601 start of the user's local day. - totalMl: - type: number - description: Today's summed intake in ml. - goalMl: - type: number - description: Effective daily goal in ml. - percent: - type: number - description: Progress toward the goal, capped at 100. - rawPercent: - type: number - description: Uncapped progress; can exceed 100. - met: - type: boolean - description: Whether the day's total reached the goal. - remainingMl: - type: number - description: Remaining ml to the goal (0 once met). - entries: - type: array - items: - type: object - properties: - id: - type: string - value: - type: number - description: Logged amount in millilitres. - measuredAt: - type: string - description: ISO-8601 instant the entry was logged. - required: - - id - - value - - measuredAt - additionalProperties: false - required: - - date - - totalMl - - goalMl - - percent - - rawPercent - - met - - remainingMl - - entries - additionalProperties: false - description: Today's water-intake total vs the user's daily goal. - HydrationGoalEnvelope: - type: object - properties: - data: - $ref: "#/components/schemas/HydrationGoalResponse" - error: - type: "null" - meta: - type: object - properties: - requestId: - type: string - additionalProperties: false - required: - - data - - error - additionalProperties: false - HydrationGoalResponse: - type: object - properties: - goalMl: - type: number - required: - - goalMl - additionalProperties: false MedicationScheduleOutput: type: object properties: @@ -23650,7 +23496,6 @@ components: - cycle-summary - signals - rhythm-events - - hydration - health-status - breathing - labs-changes diff --git a/messages/de.json b/messages/de.json index 52333ac05..b5e3eb798 100644 --- a/messages/de.json +++ b/messages/de.json @@ -574,8 +574,7 @@ "bulkDeleteSuccess": "{count} Messungen gelöscht", "bulkDeleteError": "Fehler beim Löschen der ausgewählten Messungen", "bulkDeleteConfirmTitle": "{count} Messungen löschen?", - "bulkDeleteConfirmBody": "Die {count} ausgewählten Messungen werden endgültig gelöscht.", - "typeWaterIntake": "Wasseraufnahme" + "bulkDeleteConfirmBody": "Die {count} ausgewählten Messungen werden endgültig gelöscht." }, "mood": { "title": "Stimmung", @@ -7404,20 +7403,5 @@ "glucoseHighDoctor": "Zwei aktuelle Messungen lagen bei {value} mg/dL oder höher. Miss erneut, trink ausreichend und kontaktiere möglichst bald deine Ärztin oder deinen Arzt, wenn es so hoch bleibt.", "glucoseHighEmergency": "Zwei aktuelle Messungen lagen bei {value} mg/dL oder höher und du hast Beschwerden angegeben. Bei Erbrechen, schneller Atmung oder Verwirrtheit sofort den Notruf wählen." } - }, - "hydration": { - "cardTitle": "Flüssigkeitszufuhr", - "editGoal": "Ziel bearbeiten", - "goalLabel": "Tagesziel (ml)", - "metMessage": "Du hast dein heutiges Ziel erreicht.", - "remainingMessage": "Noch {amount} ml heute.", - "add": "Hinzufügen", - "added": "{amount} ml hinzugefügt.", - "saveError": "Speichern fehlgeschlagen. Bitte erneut versuchen.", - "invalidAmount": "Gib eine Menge zwischen 1 und 5000 ml ein.", - "invalidGoal": "Gib ein Ziel zwischen 250 und 8000 ml ein.", - "goalSaved": "Ziel aktualisiert.", - "customPlaceholder": "Menge", - "customLabel": "Eigene Menge (ml)" } } diff --git a/messages/en.json b/messages/en.json index c47b3d0b7..8902daa12 100644 --- a/messages/en.json +++ b/messages/en.json @@ -574,8 +574,7 @@ "bulkDeleteSuccess": "{count} measurements deleted", "bulkDeleteError": "Error deleting the selected measurements", "bulkDeleteConfirmTitle": "Delete {count} measurements?", - "bulkDeleteConfirmBody": "The {count} selected measurements will be permanently deleted.", - "typeWaterIntake": "Water intake" + "bulkDeleteConfirmBody": "The {count} selected measurements will be permanently deleted." }, "mood": { "title": "Mood", @@ -7404,20 +7403,5 @@ "glucoseHighDoctor": "Two recent readings were {value} mg/dL or higher. Re-check, hydrate, and contact your doctor as soon as possible if it stays this high.", "glucoseHighEmergency": "Two recent readings were {value} mg/dL or higher and you noted symptoms. If you have vomiting, fast breathing, or confusion, call emergency services now." } - }, - "hydration": { - "cardTitle": "Hydration", - "editGoal": "Edit goal", - "goalLabel": "Daily goal (ml)", - "metMessage": "You've reached today's goal.", - "remainingMessage": "{amount} ml to go today.", - "add": "Add", - "added": "Added {amount} ml.", - "saveError": "Could not save. Please try again.", - "invalidAmount": "Enter an amount between 1 and 5000 ml.", - "invalidGoal": "Enter a goal between 250 and 8000 ml.", - "goalSaved": "Goal updated.", - "customPlaceholder": "Amount", - "customLabel": "Custom amount (ml)" } } diff --git a/messages/es.json b/messages/es.json index 6e03917c8..4cdd3886a 100644 --- a/messages/es.json +++ b/messages/es.json @@ -574,8 +574,7 @@ "bulkDeleteSuccess": "{count} mediciones eliminadas", "bulkDeleteError": "Error al eliminar las mediciones seleccionadas", "bulkDeleteConfirmTitle": "¿Eliminar {count} mediciones?", - "bulkDeleteConfirmBody": "Las {count} mediciones seleccionadas se eliminarán de forma permanente.", - "typeWaterIntake": "Ingesta de agua" + "bulkDeleteConfirmBody": "Las {count} mediciones seleccionadas se eliminarán de forma permanente." }, "mood": { "title": "Estado de ánimo", @@ -7404,20 +7403,5 @@ "glucoseHighDoctor": "Dos lecturas recientes fueron de {value} mg/dL o más. Vuelve a medir, hidrátate y contacta a tu médico cuanto antes si sigue así de alta.", "glucoseHighEmergency": "Dos lecturas recientes fueron de {value} mg/dL o más e indicaste síntomas. Si tienes vómitos, respiración rápida o confusión, llama a emergencias ya." } - }, - "hydration": { - "cardTitle": "Hidratación", - "editGoal": "Editar objetivo", - "goalLabel": "Objetivo diario (ml)", - "metMessage": "Has alcanzado el objetivo de hoy.", - "remainingMessage": "Faltan {amount} ml hoy.", - "add": "Añadir", - "added": "Añadidos {amount} ml.", - "saveError": "No se pudo guardar. Inténtalo de nuevo.", - "invalidAmount": "Introduce una cantidad entre 1 y 5000 ml.", - "invalidGoal": "Introduce un objetivo entre 250 y 8000 ml.", - "goalSaved": "Objetivo actualizado.", - "customPlaceholder": "Cantidad", - "customLabel": "Cantidad personalizada (ml)" } } diff --git a/messages/fr.json b/messages/fr.json index 1e40e6df6..8e50b7488 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -574,8 +574,7 @@ "bulkDeleteSuccess": "{count} mesures supprimées", "bulkDeleteError": "Erreur lors de la suppression des mesures sélectionnées", "bulkDeleteConfirmTitle": "Supprimer {count} mesures ?", - "bulkDeleteConfirmBody": "Les {count} mesures sélectionnées seront définitivement supprimées.", - "typeWaterIntake": "Apport en eau" + "bulkDeleteConfirmBody": "Les {count} mesures sélectionnées seront définitivement supprimées." }, "mood": { "title": "Humeur", @@ -7404,20 +7403,5 @@ "glucoseHighDoctor": "Deux mesures récentes étaient à {value} mg/dL ou plus. Remesure, hydrate-toi et contacte ton médecin dès que possible si cela reste aussi élevé.", "glucoseHighEmergency": "Deux mesures récentes étaient à {value} mg/dL ou plus et tu as signalé des symptômes. En cas de vomissements, de respiration rapide ou de confusion, appelle les secours immédiatement." } - }, - "hydration": { - "cardTitle": "Hydratation", - "editGoal": "Modifier l'objectif", - "goalLabel": "Objectif quotidien (ml)", - "metMessage": "Vous avez atteint l'objectif du jour.", - "remainingMessage": "Encore {amount} ml aujourd'hui.", - "add": "Ajouter", - "added": "{amount} ml ajoutés.", - "saveError": "Échec de l'enregistrement. Réessayez.", - "invalidAmount": "Saisissez une quantité entre 1 et 5000 ml.", - "invalidGoal": "Saisissez un objectif entre 250 et 8000 ml.", - "goalSaved": "Objectif mis à jour.", - "customPlaceholder": "Quantité", - "customLabel": "Quantité personnalisée (ml)" } } diff --git a/messages/it.json b/messages/it.json index c8b3d58a2..c22449a44 100644 --- a/messages/it.json +++ b/messages/it.json @@ -574,8 +574,7 @@ "bulkDeleteSuccess": "{count} misurazioni eliminate", "bulkDeleteError": "Errore durante l'eliminazione delle misurazioni selezionate", "bulkDeleteConfirmTitle": "Eliminare {count} misurazioni?", - "bulkDeleteConfirmBody": "Le {count} misurazioni selezionate verranno eliminate definitivamente.", - "typeWaterIntake": "Assunzione di acqua" + "bulkDeleteConfirmBody": "Le {count} misurazioni selezionate verranno eliminate definitivamente." }, "mood": { "title": "Umore", @@ -7404,20 +7403,5 @@ "glucoseHighDoctor": "Due misurazioni recenti erano di {value} mg/dL o superiori. Rimisura, idratati e contatta il medico al più presto se resta così alta.", "glucoseHighEmergency": "Due misurazioni recenti erano di {value} mg/dL o superiori e hai segnalato sintomi. In caso di vomito, respiro affannoso o confusione, chiama subito i soccorsi." } - }, - "hydration": { - "cardTitle": "Idratazione", - "editGoal": "Modifica obiettivo", - "goalLabel": "Obiettivo giornaliero (ml)", - "metMessage": "Hai raggiunto l'obiettivo di oggi.", - "remainingMessage": "Ancora {amount} ml oggi.", - "add": "Aggiungi", - "added": "Aggiunti {amount} ml.", - "saveError": "Salvataggio non riuscito. Riprova.", - "invalidAmount": "Inserisci una quantità tra 1 e 5000 ml.", - "invalidGoal": "Inserisci un obiettivo tra 250 e 8000 ml.", - "goalSaved": "Obiettivo aggiornato.", - "customPlaceholder": "Quantità", - "customLabel": "Quantità personalizzata (ml)" } } diff --git a/messages/pl.json b/messages/pl.json index 603de4300..69f50d7e6 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -574,8 +574,7 @@ "bulkDeleteSuccess": "Usunięto pomiary: {count}", "bulkDeleteError": "Błąd podczas usuwania wybranych pomiarów", "bulkDeleteConfirmTitle": "Usunąć pomiary ({count})?", - "bulkDeleteConfirmBody": "Wybrane pomiary ({count}) zostaną trwale usunięte.", - "typeWaterIntake": "Spożycie wody" + "bulkDeleteConfirmBody": "Wybrane pomiary ({count}) zostaną trwale usunięte." }, "mood": { "title": "Nastrój", @@ -7404,20 +7403,5 @@ "glucoseHighDoctor": "Dwa ostatnie pomiary wyniosły {value} mg/dL lub więcej. Zmierz ponownie, nawodnij się i jak najszybciej skontaktuj się z lekarzem, jeśli pozostaje tak wysoki.", "glucoseHighEmergency": "Dwa ostatnie pomiary wyniosły {value} mg/dL lub więcej i zgłosiłeś objawy. W razie wymiotów, przyspieszonego oddechu lub dezorientacji natychmiast zadzwoń po pomoc." } - }, - "hydration": { - "cardTitle": "Nawodnienie", - "editGoal": "Edytuj cel", - "goalLabel": "Cel dzienny (ml)", - "metMessage": "Osiągnięto dzisiejszy cel.", - "remainingMessage": "Jeszcze {amount} ml dzisiaj.", - "add": "Dodaj", - "added": "Dodano {amount} ml.", - "saveError": "Nie udało się zapisać. Spróbuj ponownie.", - "invalidAmount": "Podaj ilość od 1 do 5000 ml.", - "invalidGoal": "Podaj cel od 250 do 8000 ml.", - "goalSaved": "Cel zaktualizowany.", - "customPlaceholder": "Ilość", - "customLabel": "Własna ilość (ml)" } } diff --git a/prisma/migrations/0209_v125_water_intake/migration.sql b/prisma/migrations/0209_v125_water_intake/migration.sql deleted file mode 100644 index a3212ada1..000000000 --- a/prisma/migrations/0209_v125_water_intake/migration.sql +++ /dev/null @@ -1,21 +0,0 @@ --- v1.25 — hydration logging. --- --- Two additive changes, no backfill, no existing row touched: --- --- 1. `WATER_INTAKE` — a new MeasurementType: a manual water-intake counter in --- millilitres (ml). Cumulative within a day like ACTIVITY_STEPS — each --- entry adds to the day's running total, summed for the goal ring. --- DISTINCT from TOTAL_BODY_WATER (a scale body-composition reading in kg). --- Forward-only: Postgres cannot remove an enum value; idempotent via --- `IF NOT EXISTS`, so reruns are safe. --- --- 2. `users.hydration_goal_ml` — a nullable per-user daily hydration goal in --- millilitres. Null means the in-code default (2000 ml). Edited inline on --- the hydration card. --- --- The enum value is only added, never used in this same statement batch, so the --- Postgres "unsafe use of new enum value" restriction does not apply. - -ALTER TYPE "measurement_type" ADD VALUE IF NOT EXISTS 'WATER_INTAKE'; - -ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "hydration_goal_ml" INTEGER; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 85f64b7ff..5bbd4cb75 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -233,10 +233,6 @@ model User { // MDY pins month-day-year, YMD pins ISO yyyy-MM-dd. Display-time only — // canonical storage stays UTC instants. dateFormat DateFormatPreference @default(AUTO) @map("date_format") - // v1.25 — daily water-intake goal in millilitres for the hydration ring. - // Null = the in-code default (DEFAULT_HYDRATION_GOAL_ML, 2000 ml). Surfaced - // and edited inline on the hydration card; canonical storage stays ml. - hydrationGoalMl Int? @map("hydration_goal_ml") // AI provider selection for insights. Null = OPENAI fallback. aiProvider String? @map("ai_provider") // "OPENAI" | "ANTHROPIC" | "LOCAL" | "CHATGPT_OAUTH" aiModel String? @map("ai_model") @@ -1111,14 +1107,6 @@ enum MeasurementType { // `src/lib/oura/client.ts` (RESILIENCE_LEVELS). Stored as `source = OURA`. RESILIENCE // level (ordinal 1–5) — Oura `daily_resilience.level` (limited=1 … exceptional=5). - // ── v1.25 — hydration logging (additive) ── - // Manual water-intake counter in millilitres (ml). Cumulative within a day - // like ACTIVITY_STEPS / ACTIVE_ENERGY_BURNED — each entry adds to the day's - // running total; the daily total is summed for the goal ring. DISTINCT from - // TOTAL_BODY_WATER (a scale body-composition reading in kg). Manual entry - // only today (no device source maps to it), unit "ml", higher = more intake. - WATER_INTAKE // ml — manual hydration intake counter; daily total vs a user goal. - @@map("measurement_type") } diff --git a/src/app/api/hydration/route.ts b/src/app/api/hydration/route.ts deleted file mode 100644 index 0b056c59d..000000000 --- a/src/app/api/hydration/route.ts +++ /dev/null @@ -1,111 +0,0 @@ -/** - * v1.25 — hydration daily-goal surface. - * - * GET /api/hydration → today's summed water intake vs the user's goal. - * PATCH /api/hydration → set the per-user daily goal (ml). - * - * Logging itself rides the existing measurements create path (POST - * /api/measurements with `type: "WATER_INTAKE"`, unit "ml") — this route only - * reads the day's total and owns the goal. `userId` is always narrowed from - * `requireAuth()`; the goal is built field-by-field, never mass-assigned. - */ -import { NextRequest } from "next/server"; -import { z } from "zod/v4"; -import { - apiError, - apiSuccess, - returnAllZodIssues, - safeJson, -} from "@/lib/api-response"; -import { apiHandler, requireAuth } from "@/lib/api-handler"; -import { annotate } from "@/lib/logging/context"; -import { checkAnalyticsReadRateLimit } from "@/lib/rate-limit"; -import { prisma } from "@/lib/db"; -import { getUserTodayBounds } from "@/lib/tz/local-day"; -import { - MAX_HYDRATION_GOAL_ML, - MIN_HYDRATION_GOAL_ML, - resolveHydrationGoal, - summariseHydration, -} from "@/lib/hydration/hydration"; - -export const dynamic = "force-dynamic"; - -const goalPatchSchema = z.object({ - goalMl: z - .number() - .int() - .min(MIN_HYDRATION_GOAL_ML) - .max(MAX_HYDRATION_GOAL_ML), -}); - -export const GET = apiHandler(async () => { - const { user } = await requireAuth(); - - const rl = await checkAnalyticsReadRateLimit(user.id); - if (!rl.allowed) { - return apiError("Too many requests. Please retry later.", 429); - } - - const now = new Date(); - const { start, end } = getUserTodayBounds(now, user.timezone); - - const rows = await prisma.measurement.findMany({ - where: { - userId: user.id, - type: "WATER_INTAKE", - measuredAt: { gte: start, lte: end }, - }, - select: { id: true, value: true, measuredAt: true }, - orderBy: { measuredAt: "asc" }, - }); - - const totalMl = rows.reduce((sum, r) => sum + r.value, 0); - const summary = summariseHydration( - totalMl, - resolveHydrationGoal(user.hydrationGoalMl), - ); - - annotate({ - action: { name: "hydration.today.read" }, - meta: { entries: rows.length, met: summary.met }, - }); - - return apiSuccess({ - date: start.toISOString(), - ...summary, - entries: rows.map((r) => ({ - id: r.id, - value: r.value, - measuredAt: r.measuredAt.toISOString(), - })), - }); -}); - -export const PATCH = apiHandler(async (request: NextRequest) => { - const { user } = await requireAuth(); - - const { data: body, error: jsonError } = await safeJson(request, { - maxBytes: 1024, - }); - if (jsonError) return jsonError; - - const parsed = goalPatchSchema.safeParse(body); - if (!parsed.success) { - annotate({ - action: { name: "hydration.goal.validation-failed" }, - meta: { issue_count: parsed.error.issues.length }, - }); - return returnAllZodIssues(parsed.error, 422); - } - - const goalMl = parsed.data.goalMl; - await prisma.user.update({ - where: { id: user.id }, - data: { hydrationGoalMl: goalMl }, - }); - - annotate({ action: { name: "hydration.goal.update" }, meta: { goalMl } }); - - return apiSuccess({ goalMl }); -}); diff --git a/src/app/insights/page.tsx b/src/app/insights/page.tsx index 846068a61..2e90e60bc 100644 --- a/src/app/insights/page.tsx +++ b/src/app/insights/page.tsx @@ -127,15 +127,6 @@ const RhythmEventsCard = dynamic( })), { ssr: false }, ); -// v1.25 — hydration daily-goal ring. Always present (a goal ring at 0 / goal is -// still useful); it owns its own today-total query and the goal editor. -const HydrationCard = dynamic( - () => - import("@/components/insights/hydration-card").then((mod) => ({ - default: mod.HydrationCard, - })), - { ssr: false }, -); // v1.25 — baseline-drift card. Un-mounts when nothing is drifting; owns its own // read-only `/api/insights/health-status` query. const HealthStatusCard = dynamic( @@ -508,7 +499,6 @@ export default function InsightsPage() { ) : null, signals: , "rhythm-events": , - hydration: , "health-status": , breathing: , "labs-changes": , diff --git a/src/components/insights/hydration-card.tsx b/src/components/insights/hydration-card.tsx deleted file mode 100644 index 45e5c3919..000000000 --- a/src/components/insights/hydration-card.tsx +++ /dev/null @@ -1,288 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { Check, GlassWater, Pencil, Plus } from "lucide-react"; -import { toast } from "sonner"; - -import { useTranslations } from "@/lib/i18n/context"; -import { useAuth } from "@/hooks/use-auth"; -import { cn } from "@/lib/utils"; -import { ApiError, apiGet, apiPatch, apiPost } from "@/lib/api/api-fetch"; -import { - queryKeys, - invalidateKeys, - measurementDependentKeys, -} from "@/lib/query-keys"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { SectionHeading } from "@/components/insights/section-heading"; -import { HydrationRing } from "@/components/insights/hydration-ring"; -import { - HYDRATION_QUICK_ADD_ML, - MAX_HYDRATION_ENTRY_ML, - MAX_HYDRATION_GOAL_ML, - MIN_HYDRATION_ENTRY_ML, - MIN_HYDRATION_GOAL_ML, -} from "@/lib/hydration/hydration"; - -/** - * v1.25 — hydration daily-goal card. - * - * A calm goal tracker for the Insights overview: a goal ring (today's summed - * WATER_INTAKE vs the user's goal), quick-add buttons (+250 / +500 ml and a - * custom amount), and an inline goal editor. Logging rides the existing - * measurements create path (POST /api/measurements, type WATER_INTAKE), so a - * new entry busts `measurementDependentKeys`, which carries the hydration key — - * the ring refreshes in lockstep. The goal write goes to PATCH /api/hydration. - * - * Always present (a goal ring at 0 / goal is still useful), unlike the - * insufficient-history read cards which un-mount. - */ - -interface HydrationTodayResponse { - date: string; - totalMl: number; - goalMl: number; - percent: number; - rawPercent: number; - met: boolean; - remainingMl: number; - entries: { id: string; value: number; measuredAt: string }[]; -} - -interface HydrationCardProps { - enabled?: boolean; - className?: string; -} - -export function HydrationCard({ - enabled = true, - className, -}: HydrationCardProps) { - const { t } = useTranslations(); - const { isAuthenticated } = useAuth(); - const queryClient = useQueryClient(); - - const [busy, setBusy] = useState(false); - const [customMl, setCustomMl] = useState(""); - const [editingGoal, setEditingGoal] = useState(false); - const [goalDraft, setGoalDraft] = useState(""); - - const gated = enabled && isAuthenticated; - - const { data } = useQuery({ - queryKey: queryKeys.hydrationToday(), - queryFn: () => apiGet("/api/hydration"), - enabled: gated, - staleTime: 60_000, - }); - - async function logAmount(ml: number) { - if (busy || ml <= 0) return; - setBusy(true); - try { - await apiPost("/api/measurements", { - type: "WATER_INTAKE", - value: ml, - measuredAt: new Date().toISOString(), - }); - await invalidateKeys(queryClient, measurementDependentKeys); - toast.success(t("hydration.added", { amount: ml })); - } catch (err) { - toast.error( - err instanceof ApiError && err.message - ? err.message - : t("hydration.saveError"), - ); - } finally { - setBusy(false); - } - } - - async function logCustom() { - const ml = Math.round(Number(customMl)); - if ( - !Number.isFinite(ml) || - ml < MIN_HYDRATION_ENTRY_ML || - ml > MAX_HYDRATION_ENTRY_ML - ) { - toast.error(t("hydration.invalidAmount")); - return; - } - await logAmount(ml); - setCustomMl(""); - } - - async function saveGoal() { - const ml = Math.round(Number(goalDraft)); - if ( - !Number.isFinite(ml) || - ml < MIN_HYDRATION_GOAL_ML || - ml > MAX_HYDRATION_GOAL_ML - ) { - toast.error(t("hydration.invalidGoal")); - return; - } - setBusy(true); - try { - await apiPatch("/api/hydration", { goalMl: ml }); - await invalidateKeys(queryClient, [queryKeys.hydration()]); - setEditingGoal(false); - toast.success(t("hydration.goalSaved")); - } catch (err) { - toast.error( - err instanceof ApiError && err.message - ? err.message - : t("hydration.saveError"), - ); - } finally { - setBusy(false); - } - } - - const totalMl = data?.totalMl ?? 0; - const goalMl = data?.goalMl ?? 0; - const percent = data?.percent ?? 0; - const met = data?.met ?? false; - const remainingMl = data?.remainingMl ?? 0; - - return ( -
- { - setGoalDraft(String(goalMl || "")); - setEditingGoal(true); - }} - data-slot="hydration-edit-goal" - > -
- ); -} diff --git a/src/components/insights/hydration-ring.tsx b/src/components/insights/hydration-ring.tsx deleted file mode 100644 index 6c73c7c24..000000000 --- a/src/components/insights/hydration-ring.tsx +++ /dev/null @@ -1,121 +0,0 @@ -"use client"; - -import { useEffect, useId, useState } from "react"; - -import { cn } from "@/lib/utils"; -import { prefersReducedMotion } from "@/lib/charts/reduced-motion"; - -/** - * v1.25 — hydration daily-goal ring. - * - * A hand-rolled SVG arc gauge in the same idiom as `score-ring.tsx` (fixed - * `0 0 100 100` viewBox, ~9% round-cap stroke, a faint same-hue track, a - * subtle first-paint sweep gated on `prefers-reduced-motion`). It is purpose- - * built for a goal counter rather than a 0–100 band score: the arc fills to the - * percent of goal (capped at 100), the centre shows the day's total in ml, and - * the hue is a single calm water tone — never the green/yellow/red band - * semantics a low score would otherwise paint, so "not much yet" never reads as - * an alarm. The `` is real SVG JSX, not innerHTML. - */ - -const VIEW = 100; -const STROKE = 9; -const R = (VIEW - STROKE) / 2; -const C = 2 * Math.PI * R; -const CX = VIEW / 2; - -export interface HydrationRingProps { - /** Progress toward the goal, 0..100 (already capped by the caller). */ - percent: number; - /** Today's total in ml — the centred number. */ - totalMl: number; - /** The goal in ml — rendered under the total. */ - goalMl: number; - /** Whether the goal has been met (adds a met affordance). */ - met?: boolean; - /** Render size in px. */ - size?: number; - className?: string; -} - -export function HydrationRing({ - percent, - totalMl, - goalMl, - met = false, - size = 168, - className, -}: HydrationRingProps) { - const gid = useId(); - const clamped = Math.min( - 100, - Math.max(0, Number.isFinite(percent) ? percent : 0), - ); - const dash = (clamped / 100) * C; - - const reduced = prefersReducedMotion(); - const [armed, setArmed] = useState(false); - useEffect(() => { - if (reduced) return; - const raf = requestAnimationFrame(() => setArmed(true)); - return () => cancelAnimationFrame(raf); - }, [reduced]); - const filled = reduced || armed; - - return ( -
- -
- - {totalMl} - - {`/ ${goalMl} ml`} -
-
- ); -} diff --git a/src/components/insights/insights-edit-mode.tsx b/src/components/insights/insights-edit-mode.tsx index bf64ceea6..7d972157d 100644 --- a/src/components/insights/insights-edit-mode.tsx +++ b/src/components/insights/insights-edit-mode.tsx @@ -63,7 +63,6 @@ const SECTION_TITLE_KEYS: Record = { "cycle-summary": "cycle.insightsSummary.title", signals: "insights.derived.coincident.cardTitle", "rhythm-events": "insights.rhythmEvents.sectionTitle", - hydration: "hydration.cardTitle", "health-status": "insights.healthStatus.sectionTitle", breathing: "insights.breathingScreening.sectionTitle", "labs-changes": "insights.labsChanges.sectionTitle", diff --git a/src/components/measurements/measurement-list-meta.ts b/src/components/measurements/measurement-list-meta.ts index 56255d6b0..f27dd5ef2 100644 --- a/src/components/measurements/measurement-list-meta.ts +++ b/src/components/measurements/measurement-list-meta.ts @@ -35,7 +35,6 @@ import { Sun, PersonStanding, Waves, - GlassWater, type LucideIcon, } from "lucide-react"; @@ -55,7 +54,6 @@ export const MEASUREMENT_TYPE_LABEL_KEYS: Record = { HEART_RATE_VARIABILITY: "measurements.typeHeartRateVariability", RESTING_HEART_RATE: "measurements.typeRestingHeartRate", ACTIVE_ENERGY_BURNED: "measurements.typeActiveEnergyBurned", - WATER_INTAKE: "measurements.typeWaterIntake", FLIGHTS_CLIMBED: "measurements.typeFlightsClimbed", WALKING_RUNNING_DISTANCE: "measurements.typeWalkingRunningDistance", VO2_MAX: "measurements.typeVo2Max", @@ -142,7 +140,6 @@ export const MEASUREMENT_TYPE_ICONS: Record = { HEART_RATE_VARIABILITY: HeartPulse, RESTING_HEART_RATE: Heart, ACTIVE_ENERGY_BURNED: Flame, - WATER_INTAKE: GlassWater, FLIGHTS_CLIMBED: TrendingUp, WALKING_RUNNING_DISTANCE: Footprints, VO2_MAX: Gauge, @@ -266,7 +263,6 @@ export const MEASUREMENT_TYPE_COLORS: Record = { HEART_RATE_VARIABILITY: "bg-chart-5/20 text-chart-5", RESTING_HEART_RATE: "bg-chart-3/20 text-chart-3", ACTIVE_ENERGY_BURNED: "bg-chart-4/20 text-chart-4", - WATER_INTAKE: "bg-chart-2/20 text-chart-2", FLIGHTS_CLIMBED: "bg-chart-2/20 text-chart-2", WALKING_RUNNING_DISTANCE: "bg-chart-2/20 text-chart-2", VO2_MAX: "bg-chart-1/20 text-chart-1", diff --git a/src/lib/__tests__/insights-layout.test.ts b/src/lib/__tests__/insights-layout.test.ts index 0d216bed3..04dff7b0a 100644 --- a/src/lib/__tests__/insights-layout.test.ts +++ b/src/lib/__tests__/insights-layout.test.ts @@ -30,7 +30,6 @@ describe("insights-layout v2 — section id universe", () => { "cycle-summary", "signals", "rhythm-events", - "hydration", "health-status", "breathing", "labs-changes", diff --git a/src/lib/__tests__/measurement-type-enum-coverage.test.ts b/src/lib/__tests__/measurement-type-enum-coverage.test.ts index ceaf09f2c..a8ac5ef1c 100644 --- a/src/lib/__tests__/measurement-type-enum-coverage.test.ts +++ b/src/lib/__tests__/measurement-type-enum-coverage.test.ts @@ -98,8 +98,6 @@ const EXPECTED_TYPES = [ "BODY_TEMPERATURE_DEVIATION", // ── v1.19.0 — Oura resilience (ordinal-encoded level) ── "RESILIENCE", - // ── v1.25 — in-app water-intake logging (hydration goal ring) ── - "WATER_INTAKE", ] as const; describe("measurementTypeEnum coverage", () => { @@ -246,11 +244,6 @@ describe("measurementTypeEnum coverage", () => { // Insights / measurement-list surfaces and never belongs in the clinical // vitals PDF. See doctor-report-pdf-core.ts for the matching rationale. "RESILIENCE", - // v1.25 — water intake is an in-app lifestyle log (the hydration goal - // ring), not a measured clinical vital; it surfaces on the Insights - // overview and never belongs in the clinical vitals PDF. See - // doctor-report-pdf-core.ts for the matching exclusion rationale. - "WATER_INTAKE", ]); it("doctor-report PDF vital types cover the canonical enum minus documented exclusions", () => { diff --git a/src/lib/hydration/__tests__/hydration.test.ts b/src/lib/hydration/__tests__/hydration.test.ts deleted file mode 100644 index 8939919be..000000000 --- a/src/lib/hydration/__tests__/hydration.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { describe, it, expect } from "vitest"; - -import { - DEFAULT_HYDRATION_GOAL_ML, - MAX_HYDRATION_GOAL_ML, - MIN_HYDRATION_GOAL_ML, - resolveHydrationGoal, - summariseHydration, -} from "@/lib/hydration/hydration"; - -describe("resolveHydrationGoal", () => { - it("falls back to the default when unset", () => { - expect(resolveHydrationGoal(null)).toBe(DEFAULT_HYDRATION_GOAL_ML); - expect(resolveHydrationGoal(undefined)).toBe(DEFAULT_HYDRATION_GOAL_ML); - expect(resolveHydrationGoal(Number.NaN)).toBe(DEFAULT_HYDRATION_GOAL_ML); - }); - - it("clamps below the minimum", () => { - expect(resolveHydrationGoal(10)).toBe(MIN_HYDRATION_GOAL_ML); - }); - - it("clamps above the maximum", () => { - expect(resolveHydrationGoal(99999)).toBe(MAX_HYDRATION_GOAL_ML); - }); - - it("keeps an in-band value (rounded)", () => { - expect(resolveHydrationGoal(2500.4)).toBe(2500); - }); -}); - -describe("summariseHydration", () => { - it("empty day", () => { - const s = summariseHydration(0, 2000); - expect(s).toMatchObject({ - totalMl: 0, - goalMl: 2000, - percent: 0, - rawPercent: 0, - met: false, - remainingMl: 2000, - }); - }); - - it("partial day", () => { - const s = summariseHydration(1000, 2000); - expect(s.percent).toBe(50); - expect(s.rawPercent).toBe(50); - expect(s.met).toBe(false); - expect(s.remainingMl).toBe(1000); - }); - - it("goal met exactly", () => { - const s = summariseHydration(2000, 2000); - expect(s.percent).toBe(100); - expect(s.met).toBe(true); - expect(s.remainingMl).toBe(0); - }); - - it("exceeded — percent caps at 100, rawPercent does not", () => { - const s = summariseHydration(2500, 2000); - expect(s.percent).toBe(100); - expect(s.rawPercent).toBe(125); - expect(s.met).toBe(true); - expect(s.remainingMl).toBe(0); - }); - - it("floors a negative total at zero", () => { - const s = summariseHydration(-500, 2000); - expect(s.totalMl).toBe(0); - expect(s.met).toBe(false); - }); -}); diff --git a/src/lib/hydration/hydration.ts b/src/lib/hydration/hydration.ts deleted file mode 100644 index a16bce868..000000000 --- a/src/lib/hydration/hydration.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Hydration logging — pure helpers for the daily water-intake goal ring. - * - * Water intake is stored as `WATER_INTAKE` Measurement rows in millilitres - * (ml), one row per quick-add entry. The day's running total is the SUM of - * those rows (see `CUMULATIVE_HK_TYPES`); the goal ring compares that total - * against a per-user goal (`User.hydrationGoalMl`, null = the default). - * - * The math lives here, free of Prisma + React, so the ring states (empty / - * partial / met / exceeded) are unit-testable in isolation. - */ - -/** Default daily hydration goal when the user has not set their own (ml). */ -export const DEFAULT_HYDRATION_GOAL_ML = 2000; - -/** Bounds for a user-set goal — a plausible adult range, not a clinical limit. */ -export const MIN_HYDRATION_GOAL_ML = 250; -export const MAX_HYDRATION_GOAL_ML = 8000; - -/** The fixed quick-add amounts the card offers alongside a custom entry (ml). */ -export const HYDRATION_QUICK_ADD_ML = [250, 500] as const; - -/** Bounds for a single logged entry (ml) — mirrors the measurement clamp. */ -export const MIN_HYDRATION_ENTRY_ML = 1; -export const MAX_HYDRATION_ENTRY_ML = 5000; - -/** - * Resolve the effective goal from the (nullable) stored value: fall back to - * the default when unset, and clamp a stored value into the supported band so - * a stale out-of-range row never produces a broken ring. - */ -export function resolveHydrationGoal( - stored: number | null | undefined, -): number { - if (stored == null || !Number.isFinite(stored)) { - return DEFAULT_HYDRATION_GOAL_ML; - } - return Math.min( - MAX_HYDRATION_GOAL_ML, - Math.max(MIN_HYDRATION_GOAL_ML, Math.round(stored)), - ); -} - -export interface HydrationSummary { - /** Today's summed intake in ml. */ - totalMl: number; - /** The effective daily goal in ml. */ - goalMl: number; - /** Progress toward the goal, capped at 100 for the ring fill. */ - percent: number; - /** Uncapped progress (can exceed 100 once the goal is met). */ - rawPercent: number; - /** Whether the day's total has reached the goal. */ - met: boolean; - /** Remaining ml to the goal (0 once met). */ - remainingMl: number; -} - -/** - * Summarise a day's intake against a goal. `totalMl` is the already-summed - * day total; `goalMl` is the effective goal (run it through - * `resolveHydrationGoal` first). Negative totals are floored at 0. - */ -export function summariseHydration( - totalMl: number, - goalMl: number, -): HydrationSummary { - const total = Number.isFinite(totalMl) ? Math.max(0, Math.round(totalMl)) : 0; - const goal = resolveHydrationGoal(goalMl); - const rawPercent = goal > 0 ? Math.round((total / goal) * 100) : 0; - const percent = Math.min(100, rawPercent); - return { - totalMl: total, - goalMl: goal, - percent, - rawPercent, - met: total >= goal, - remainingMl: Math.max(0, goal - total), - }; -} diff --git a/src/lib/insights-layout.ts b/src/lib/insights-layout.ts index f3bff31be..1f7960440 100644 --- a/src/lib/insights-layout.ts +++ b/src/lib/insights-layout.ts @@ -113,7 +113,6 @@ export const INSIGHTS_SECTION_IDS = [ "cycle-summary", "signals", "rhythm-events", - "hydration", "health-status", "breathing", "labs-changes", diff --git a/src/lib/insights/chart-tokens.ts b/src/lib/insights/chart-tokens.ts index 06c669fb0..fca9edb1f 100644 --- a/src/lib/insights/chart-tokens.ts +++ b/src/lib/insights/chart-tokens.ts @@ -38,9 +38,6 @@ export const ALLOWED_CHART_TOKENS = [ // components ship in v1.5 alongside the iOS app surface. "metric:HEART_RATE_VARIABILITY", "metric:RESTING_HEART_RATE", - // v1.25 — daily water intake (cumulative ml), charted alongside the - // hydration goal ring on the overview. - "metric:WATER_INTAKE", "metric:ACTIVE_ENERGY_BURNED", "metric:FLIGHTS_CLIMBED", "metric:WALKING_RUNNING_DISTANCE", diff --git a/src/lib/measurements/__tests__/apple-health-mapping.test.ts b/src/lib/measurements/__tests__/apple-health-mapping.test.ts index ab5dbb6fd..0c0a7ef6b 100644 --- a/src/lib/measurements/__tests__/apple-health-mapping.test.ts +++ b/src/lib/measurements/__tests__/apple-health-mapping.test.ts @@ -77,12 +77,6 @@ const MEASUREMENT_TYPES_WITHOUT_HK_COUNTERPART = new Set([ // the Oura API (source = OURA), never from HealthKit: Apple ships no // resilience metric. No HK mapping by design. "RESILIENCE", - // v1.25 — water intake is logged in-app (the hydration goal ring writes - // WATER_INTAKE rows directly via POST /api/measurements). Apple's - // `HKQuantityTypeIdentifierDietaryWater` stays on the nutrition deferred - // hold (HK_QUANTITY_TYPE_DEFERRED), so the type carries no HK import mapping - // by design. - "WATER_INTAKE", ]); describe("APPLE_HEALTH_TYPE_MAP", () => { diff --git a/src/lib/measurements/apple-health-mapping.ts b/src/lib/measurements/apple-health-mapping.ts index 12a6d34aa..d6be7265a 100644 --- a/src/lib/measurements/apple-health-mapping.ts +++ b/src/lib/measurements/apple-health-mapping.ts @@ -706,11 +706,6 @@ export const CUMULATIVE_HK_TYPES: ReadonlySet = // daily total is the meaningful reduction (SUM), matching the other // cumulative HK counts. "FALL_COUNT", - // v1.25 — manual hydration intake. Not a HealthKit type, but its daily - // reduction is a SUM (running total of the day's entries), so it rides the - // same cumulative daily-sum path as the counts above rather than averaging - // individual sips. - "WATER_INTAKE", ]); /** diff --git a/src/lib/measurements/categories.ts b/src/lib/measurements/categories.ts index aaf1038f6..f9457adf2 100644 --- a/src/lib/measurements/categories.ts +++ b/src/lib/measurements/categories.ts @@ -89,8 +89,6 @@ export const MEASUREMENT_CATEGORIES: ReadonlyMap< // ── Activity (cumulative + fitness + mobility) ── ["ACTIVITY_STEPS", "activity"], ["ACTIVE_ENERGY_BURNED", "activity"], - // v1.25 — hydration intake; cumulative daily counter, groups with activity. - ["WATER_INTAKE", "activity"], ["FLIGHTS_CLIMBED", "activity"], ["WALKING_RUNNING_DISTANCE", "activity"], ["VO2_MAX", "activity"], diff --git a/src/lib/measurements/display-transform.ts b/src/lib/measurements/display-transform.ts index 39b058505..1bc059952 100644 --- a/src/lib/measurements/display-transform.ts +++ b/src/lib/measurements/display-transform.ts @@ -105,8 +105,6 @@ const INTEGER_DISPLAY_TYPES: ReadonlySet = new Set([ // grain for these day-cumulative counts. "ACTIVE_ENERGY_BURNED", "TIME_IN_DAYLIGHT", - // ml of water — whole millilitres are the meaningful display grain. - "WATER_INTAKE", ]); /** diff --git a/src/lib/openapi/registry.ts b/src/lib/openapi/registry.ts index afe62485e..147d50b24 100644 --- a/src/lib/openapi/registry.ts +++ b/src/lib/openapi/registry.ts @@ -77,7 +77,6 @@ const openApiBase: Pick< { name: "Illness" }, { name: "Admin" }, { name: "Meta" }, - { name: "Hydration" }, ], }; diff --git a/src/lib/openapi/routes/hydration.ts b/src/lib/openapi/routes/hydration.ts deleted file mode 100644 index 58dccb2f2..000000000 --- a/src/lib/openapi/routes/hydration.ts +++ /dev/null @@ -1,102 +0,0 @@ -/** - * OpenAPI route module — hydration daily-goal surface (v1.25). - * - * `GET /api/hydration` reads today's summed water intake vs the user's goal; - * `PATCH /api/hydration` sets the per-user daily goal. Logging rides the - * measurements create path (POST /api/measurements, type WATER_INTAKE), so it - * is documented there, not here. - * - * Part of the OpenAPI route table; aggregated in `./index.ts`. - */ -import { z } from "zod/v4"; -import type { ZodOpenApiObject } from "zod-openapi"; - -import { dataEnvelope, stdResponses } from "./shared"; - -const hydrationEntry = z.object({ - id: z.string(), - value: z.number().describe("Logged amount in millilitres."), - measuredAt: z.string().describe("ISO-8601 instant the entry was logged."), -}); - -const hydrationTodayResponse = z - .object({ - date: z.string().describe("ISO-8601 start of the user's local day."), - totalMl: z.number().describe("Today's summed intake in ml."), - goalMl: z.number().describe("Effective daily goal in ml."), - percent: z.number().describe("Progress toward the goal, capped at 100."), - rawPercent: z.number().describe("Uncapped progress; can exceed 100."), - met: z.boolean().describe("Whether the day's total reached the goal."), - remainingMl: z.number().describe("Remaining ml to the goal (0 once met)."), - entries: z.array(hydrationEntry), - }) - .meta({ - id: "HydrationToday", - description: "Today's water-intake total vs the user's daily goal.", - }); - -const hydrationGoalRequest = z - .object({ - goalMl: z - .number() - .int() - .min(250) - .max(8000) - .describe("Daily hydration goal in millilitres."), - }) - .meta({ - id: "HydrationGoalRequest", - description: "Set the per-user daily hydration goal (ml).", - }); - -const hydrationGoalResponse = z - .object({ goalMl: z.number() }) - .meta({ id: "HydrationGoalResponse" }); - -export const hydrationPaths: NonNullable = { - "/api/hydration": { - get: { - tags: ["Hydration"], - summary: "Today's hydration total vs goal", - description: - "Sums the calling user's WATER_INTAKE measurements for the current local day and compares them against the per-user goal (default 2000 ml).", - responses: { - "200": { - description: "Today's hydration summary.", - content: { - "application/json": { - schema: dataEnvelope( - hydrationTodayResponse, - "HydrationTodayEnvelope", - ), - }, - }, - }, - ...stdResponses, - }, - }, - patch: { - tags: ["Hydration"], - summary: "Set the daily hydration goal", - description: "Updates the per-user daily hydration goal in millilitres.", - requestBody: { - required: true, - content: { "application/json": { schema: hydrationGoalRequest } }, - }, - responses: { - "200": { - description: "Goal updated.", - content: { - "application/json": { - schema: dataEnvelope( - hydrationGoalResponse, - "HydrationGoalEnvelope", - ), - }, - }, - }, - ...stdResponses, - }, - }, - }, -}; diff --git a/src/lib/openapi/routes/index.ts b/src/lib/openapi/routes/index.ts index 0df9ab23c..ae44bef96 100644 --- a/src/lib/openapi/routes/index.ts +++ b/src/lib/openapi/routes/index.ts @@ -36,7 +36,6 @@ import { biomarkerPaths } from "./biomarkers"; import { devicePaths } from "./devices"; import { exportPaths } from "./export"; import { healthRecordPaths } from "./health-record"; -import { hydrationPaths } from "./hydration"; import { illnessPaths } from "./illness"; import { importPaths } from "./import"; import { insightsPaths } from "./insights"; @@ -85,7 +84,6 @@ export const openApiPaths: NonNullable = { ...biomarkerPaths, ...illnessPaths, ...onboardingPaths, - ...hydrationPaths, }; export const openApiComponents: NonNullable = { diff --git a/src/lib/personal-records/pr-direction.ts b/src/lib/personal-records/pr-direction.ts index 9671186b8..0058a5131 100644 --- a/src/lib/personal-records/pr-direction.ts +++ b/src/lib/personal-records/pr-direction.ts @@ -194,11 +194,6 @@ export function getPRDirection( // inputs — a derived composite with no clean goal axis, so it stays null // like the other device-derived scores. case "RESILIENCE": - // v1.25 — water intake is a cumulative daily counter surfaced as a - // goal ring + logging streak, not a single-reading achievement. A - // "biggest sip" record would be meaningless and the daily-total record - // is better served by the streak rail, so it stays null. - case "WATER_INTAKE": return null; } } diff --git a/src/lib/query-keys/hydration.ts b/src/lib/query-keys/hydration.ts deleted file mode 100644 index 867dce37a..000000000 --- a/src/lib/query-keys/hydration.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Query keys — hydration tree: today's water-intake total vs the daily goal - * (`/api/hydration`). Part of the centralized factory; aggregated in - * `./index.ts`. A `WATER_INTAKE` measurement write busts this key through the - * `measurementDependentKeys` bundle so the goal ring refreshes in lockstep. - */ -export const hydrationKeys = { - hydration: () => ["hydration"] as const, - hydrationToday: () => ["hydration", "today"] as const, -}; diff --git a/src/lib/query-keys/index.ts b/src/lib/query-keys/index.ts index 762f42536..d84854923 100644 --- a/src/lib/query-keys/index.ts +++ b/src/lib/query-keys/index.ts @@ -19,7 +19,6 @@ import { authKeys } from "./auth"; import { coachKeys } from "./coach"; import { cycleKeys } from "./cycle"; import { dashboardKeys } from "./dashboard"; -import { hydrationKeys } from "./hydration"; import { illnessKeys } from "./illness"; import { insightsKeys } from "./insights"; import { integrationKeys } from "./integrations"; @@ -47,7 +46,6 @@ export const queryKeys = { ...measurementReminderKeys, ...labKeys, ...illnessKeys, - ...hydrationKeys, }; /** @@ -88,8 +86,6 @@ export const measurementDependentKeys = [ ["sleep-night"] as const, // v1.17.0 — refresh the sleep-debt + chronotype read when sleep rows change. ["sleep-rhythm"] as const, - // v1.25 — refresh the hydration goal ring when a WATER_INTAKE row is added. - queryKeys.hydration(), ]; /** diff --git a/src/lib/validations/measurement.ts b/src/lib/validations/measurement.ts index 97a4876e0..2c11ba6f0 100644 --- a/src/lib/validations/measurement.ts +++ b/src/lib/validations/measurement.ts @@ -122,10 +122,6 @@ export const measurementTypeEnum = z.enum([ // into the numeric value (limited=1 … exceptional=5), unit `level`. Ingests // server-side as `source = OURA`. See `RESILIENCE_LEVELS` in `src/lib/oura/client`. "RESILIENCE", - // ── v1.25 — hydration logging (additive) ── - // Manual water-intake counter in millilitres. Cumulative within a day like - // ACTIVITY_STEPS; distinct from TOTAL_BODY_WATER (a scale reading in kg). - "WATER_INTAKE", ]); /** @@ -243,7 +239,6 @@ const unitMap: Record = { HEART_RATE_VARIABILITY: "ms", RESTING_HEART_RATE: "bpm", ACTIVE_ENERGY_BURNED: "kcal", - WATER_INTAKE: "ml", FLIGHTS_CLIMBED: "flights", WALKING_RUNNING_DISTANCE: "m", VO2_MAX: "mL/(kg·min)", @@ -399,9 +394,6 @@ export const VALUE_RANGES: Record = { // Active energy kcal/sample — a daily-rollup sample tops out around // 6–8000 kcal even for ultra-endurance days. ACTIVE_ENERGY_BURNED: { min: 0, max: 10000 }, - // Daily cumulative intake in ml; a single quick-add entry is small but the - // running daily total can reach several litres on an active day. - WATER_INTAKE: { min: 0, max: 20000 }, // Flights — vertical-feet/3 ≈ flights; cap generous. FLIGHTS_CLIMBED: { min: 0, max: 1000 }, // Walking/running distance per sample (metres). 200 km covers an