Tre leve per migliorare l'accuratezza del piano oltre Mifflin-St Jeor.
1. Composizione corporea (US Navy + Katch-McArdle)
- Profile: neckCm/waistCm/hipsCm opzionali in input
- bodyFatPct calcolato lato server con la formula US Navy metrica
(Hodgdon-Beckett 1984) e cifrato at-rest come dato sanitario
- BMR usa Katch-McArdle (FFM-based) quando bodyFatPct è noto, Mifflin
altrimenti — più accurato del 5-10% sui casi fuori-norma
- Test: estimateBodyFatPercentageUSNavy + calculateBmrKatchMcArdle
2. Deficit dinamico
- Profile.targetWeeklyLossKg (kg/settimana, opzionale)
- computeDailyKcalTarget calcola il deficit da kg/sett ×7700/7 con due cap
di sicurezza: 30% TDEE (Schoenfeld 2014) e 1% peso/sett (Helms 2014)
- plan.routes lo usa al posto del -500/-200 hard-coded
- Test: 6 casi (mantenimento, transizione, intensiva, cap peso, cap TDEE,
default mancante)
3. Condizioni mediche strutturate
- Profile.medicalConditions (array JSON cifrato)
- 11 condizioni: 7 adattabili (tiroide → BMR -10%, diabete II, IBS,
dislipidemia, ipertensione, reni → cap proteine 0.8 g/kg, fegato →
1.0 g/kg) + 4 escludenti (gravidanza, allattamento, diabete I, disturbi
alimentari)
- PATCH /me/profile/conditions per salvataggio mirato dall'onboarding
- hasExcludingCondition: plan.routes restituisce 409 medical_block se
attiva una condizione incompatibile
Onboarding allargato a 6 step: aggiunto "Condizioni mediche" tra Profile
e Preferences. Step labels e numeri rinumerati.
Schema migration 20260429203451_add_profile_health_fields.
44/44 unit test verdi.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ADR 0003: VAPID self-hosted today, DeviceToken model agnostic to
platform so iOS/Android (Expo/APNs/FCM) plug in as new senders later.
Backend (apps/api/src/modules/notifications)
- sender.ts: NotificationSender interface, WebPushSender via VAPID
- notifications.routes.ts: GET /me/notifications/config, POST/DELETE
/me/device-tokens, PATCH /me/notifications/settings, POST /me/notifications/test
- scheduler.ts: node-cron Mon 09:00 Europe/Rome for weekly weigh-in
reminder; auto-cleanup of expired tokens on 404/410
- env: VAPID_PUBLIC_KEY/PRIVATE_KEY/SUBJECT (all optional → push gracefully off)
Frontend
- public/sw.js minimal (push + notificationclick)
- lib/notifications/push-client.ts: subscribe / unsubscribe / getCurrentSubscription
- profile/notifications-{actions,panel}.tsx: editorial panel with toggles,
device list, "send test", per-device removal
- pushReady requires both permission AND active subscription (covers the
case where the user revoked the SW but kept the browser permission)
Schema
- DeviceToken { userId, platform, endpoint, p256dh, auth, token, userAgent,
createdAt, lastSeenAt } with unique(userId, endpoint)
- ExtendedPrismaClient type exported from @ketopath/db
- NotificationSettings zod schema in @ketopath/shared
Tooling
- lint-staged: split .js out of eslint glob so service worker is only
formatted (it lives outside the TS project)
i18n
- Notifications namespace (it) with typed error keys
Smoke tested: POST /me/device-tokens 201, POST /me/notifications/test 200,
real push delivered to a macOS Chrome device.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generated by `prisma migrate dev --name onboarding_v1_recipes`.
Adds:
- users.disclaimer_accepted_at (TIMESTAMP NULL) for the medical disclaimer flow
- weight_entries, fast_events with their indexes and FK cascade
- ingredients, recipes, recipe_ingredients with category index on recipes
- meal_plans, meal_slots with composite uniqueness on (planId, dayOfWeek, meal)
- 4 new enums: FastStatus, MealCategory, Difficulty, MealPlanStatus
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generated by `prisma migrate dev --name encrypt_profile_fields`.
ALTER TABLE "profiles" sets weight_start_kg / weight_current_kg /
weight_goal_kg / target_date to TEXT. Existing rows keep their decimal
representation as plaintext text (e.g. "76.00") and prisma-field-encryption
re-encrypts them on the next write — both ciphertext (v1.aesgcm256.<keyId>.
<iv>.<ciphertext>) and legacy plaintext are decrypted transparently on read,
so no app-level fallback was needed.
Verified end-to-end:
- pnpm test:e2e — all 4 specs pass, including profile signup → submit →
BMR 1583 / TDEE 1899 round-trip
- raw SQL on profiles shows ciphertext for new rows; older rows stay in clear
text until their next update
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- User extended with emailVerified, name, image (Better Auth fields)
- Session: signed session tokens with expiry, ip and user agent
- Account: credential rows for email+password (provider 'credential') and
OAuth providers (e.g. Google) — password hash stored on the credential row
- Verification: single-use tokens for email verification, password reset,
and magic links
- Re-export new types from @ketopath/db
Migration 20260429104721_add_auth_tables run against local Postgres.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generated by `prisma migrate dev --name init` against the local Postgres.
Creates the 6 enums and the 3 tables with their unique indexes and
foreign keys (profiles.user_id and preferences.user_id with ON DELETE CASCADE).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>