Commit Graph
16 Commits
Author SHA1 Message Date
lucianoandClaude Opus 4.7 bda96a3550 feat: catalogo+30, varianti, sostituti, reintroduzione Fase 2, free meal Fase 3 (PRD §5.1)
Quattro completamenti del PRD §5.1: catalogo, varianti/sostituti, fase 2,
free meal.

Catalogo ricette 16 → 46
- seed-recipes.ts: +30 ricette italiane keto bilanciate
  (8 colazione · 8 pranzo · 6 spuntino · 8 cena)
- seed-ingredients.ts: 33 → 55 ingredienti
  (verdura/carne/pesce/latticini/condimenti + categorie nuove `legumi` e
  `cereali` per Fase 2)
- seed.ts: ora fa upsert anche degli ingredienti pre-esistenti per
  propagare campi nuovi (es. phase2Week)

Varianti + sostituti
- Recipe.variants Json (array { name, description, kcalDelta? })
- RecipeIngredient.substitutes String[]
- /recipes/[id]: nuova sezione "Varianti suggerite" + riga "Sostituzioni:"
  sotto ogni ingrediente quando presenti

Reintroduzione progressiva Fase 2
- Ingredient.phase2Week (settimana minima di reintroduzione, NULL=sempre)
- User.phase2StartedAt: settato automaticamente quando si passa a TRANSITION
- @ketopath/shared/planner/phase-progression: currentPhase2Week,
  isRecipeAllowedForPhaseWeek, weeksSince
- plan.routes filtra fuori le ricette con ingredienti non ancora reintrodotti
- POST /me/profile/phase per avanzare di fase (idempotente sul timestamp)
- 12 unit test su phase-progression
- Esempi seed: lenticchie phase2Week=3, ceci=4, mela=5, pera=6, quinoa=7

Free Meal pianificato Fase 3
- MealSlot.isFreeMeal Boolean (default false)
- POST /me/meal-plans/slots/:id/free-meal toggle, valido solo in MAINTENANCE
- @ketopath/shared/planner/phase-progression: freeMealKcalAdjustment
  (compensazione clamp -200/+50 kcal per pasto sui rimanenti)
- /plan: bottone ☆ in ogni SlotCard quando phase=MAINTENANCE; lo slot
  marcato come pasto libero rende "Pasto libero pianificato" + ~750 kcal
  stimate; macro tracker ne tiene conto

Indicatori di fase nel piano
- /plan ora include currentPhase + phase2Week dalla API
- Header del piano: "Settimana N di Fase 2" o "Mantenimento — Fase 3"

i18n: namespaces Plan + Recipe arricchiti

Schema migration 20260429XXXXXX_add_meal_plan_extras.

66/66 unit test verdi (12 nuovi). Smoke verificato live: API ritorna
correctly i campi nuovi, UI rende badge fase, bottoni ☆/↻, free-meal slot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 23:18:47 +02:00
lucianoandClaude Opus 4.7 a583434c42 feat: storia diete, schedule allenamento, frequenza pasti (PRD §5.1)
Tre leve aggiuntive che migliorano la personalizzazione del piano dopo
le tre del commit precedente (BF%, deficit, condizioni mediche).

1. Storia delle diete precedenti (adattamento metabolico)
   - Profile.dietHistory ∈ {NONE,SOME,EXTENSIVE,SEVERE} cifrato at-rest
   - bmrAdjustForDietHistory: 0.97 / 0.93 / 0.85 — aggiustamento BMR per
     riflettere l'adattamento metabolico osservato in studi a lungo termine
     (Fothergill 2016 Biggest Loser, Rosenbaum 2008)
   - Select in ProfileForm con copy diretto sulla "questione yo-yo"

2. Schedule allenamento (kcal extra training-day)
   - Preferences.trainingDays Int[] (0=Lun..6=Dom)
   - Preferences.trainingType ∈ {CARDIO,STRENGTH,MIXED,SPORT}
   - Preferences.sessionMinutes Int?
   - extraKcalForSession: equazione MET (Ainsworth 2011) con MET prevalenti
     8/5/6/7 a tipo. Output arrotondato a 25 kcal per evitare false-precisioni
   - Plan generation aggiunge l'extra solo nei training days
   - Chips Lun-Dom + select tipo + input durata in PreferencesForm

3. Frequenza pasti preferita
   - Preferences.mealsPerDay Int? (1..4)
   - mealShareForFrequency: 1=solo cena, 2=pranzo+cena, 3=no spuntino, 4=default
   - Quando mealsPerDay è impostato e non c'è fastingProtocol attivo,
     sostituisce la share del protocollo (il digiuno IF ha precedenza)
   - Slot a share=0 saltati alla creazione (niente colazione/spuntino "vuoti")

Schema migration 20260429220XXX_add_lifestyle_fields.

Smoke verificato via API: con mealsPerDay=3 e nessun protocollo IF, ogni
giorno del piano ha esattamente 3 pasti (Colazione+Pranzo+Cena, niente
Spuntino). 54/54 unit test verdi (11 nuovi).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:56:41 +02:00
lucianoandClaude Opus 4.7 9401497f03 feat: composizione corporea, deficit dinamico, condizioni mediche (PRD §5.1)
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>
2026-04-29 22:44:16 +02:00
lucianoandClaude Opus 4.7 bb48357179 feat: web push notifications, mobile-ready (PRD §5.6)
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>
2026-04-29 21:58:35 +02:00
lucianoandClaude Opus 4.7 ad1e4ecaeb feat: recipe detail + weekly shopping list (PRD §5.1, §5.4)
Recipe detail
- Seed catalog of 33 italian ingredients with macros, allergens, avg price.
- Each seed recipe now declares its RecipeIngredient links (qty + unit).
- /recipes/[id] page: editorial layout with prep meta, ingredients list,
  macros block, preparation, chef's note. Plan-week meal names linkable.

Shopping list
- GET /me/shopping-list aggregates RecipeIngredient quantities of the
  ACTIVE plan, grouped by ingredient category, sorted alphabetically.
- /shopping page: total items, estimated cost, per-category checklist
  with check-off persisted in localStorage per plan id.
- Home gets a "Spesa" nav item; /plan links the list when a plan exists.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 21:13:00 +02:00
lucianoandClaude Opus 4.7 4b6c3f6ae9 feat: V1 tracking endpoints + meal plan generation (PRD §5.1, §5.2, §5.3)
@ketopath/shared:
- tracking/schema.ts — Zod schemas for WeightEntry input (weight, optional
  measurements/notes/energy/sleep/hunger, photo URLs) and FastEvent start /
  update, plus PROTOCOL_DEFAULT_MINUTES table
- planner/macros.ts — macrosForPhase(): protein 1.6-1.8g/kg, netCarb 25/60/120g
  by phase, fat = remainder (PRD §9.3)
- planner/matchmaking.ts — pure matchMeals() that filters by exclusion tags,
  phase compatibility and meal category, then scores by euclidean distance
  from the meal's macro target with a 1.5 penalty for recently-consumed recipes;
  DEFAULT_MEAL_SHARE constant (25/35/10/30 %)
- 5 new unit tests cover exclusion, phase, recency, topN, category mismatch

apps/api:
- modules/tracking/weight.routes.ts — GET /me/weight-entries (last 60),
  POST /me/weight-entries with upsert on (userId, date); encrypted fields
  serialised to/from JSON strings
- modules/tracking/fast.routes.ts — GET, POST (start) and PATCH (update) on
  fast events; symptoms stored as encrypted JSON
- modules/plan/plan.routes.ts — POST /me/meal-plans:
  - reads profile + preferences, computes BMR (Mifflin-St Jeor), TDEE, daily
    kcal target with the phase-dependent deficit, then macros via macrosForPhase
  - upserts a MealPlan rooted at Monday-of-this-week, wipes prior slots, and
    fills 28 slots running matchMeals per (day, meal) with a recent-2-day
    rolling exclusion list to keep variety
  - selected recipe + 4 alternatives per slot
- GET /me/meal-plans/current returns the active plan with selected/alternatives

@ketopath/db:
- prisma/seed-recipes.ts — 16 italian keto recipes (4 per meal type) with
  estimated macros per serving and phase compatibility
- prisma/seed.ts — idempotent insertion (skip on existing name match)

Verified end-to-end with sign-up → create profile (Michele PRD persona) →
generate plan: 28 slots filled, daily target 1399 kcal / 137 P / 83 F / 25 C,
matchmaking distributes 4 different breakfasts across the first 4 days as
the recent-consumed penalty kicks in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 16:07:56 +02:00
lucianoandClaude Opus 4.7 0d2d1f49c7 feat(db): migration 20260429135619 — onboarding flag, V1 health tables, recipes
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>
2026-04-29 15:56:49 +02:00
lucianoandClaude Opus 4.7 f6423844da feat: medical disclaimer onboarding + V1 + recipe schema (no migration yet)
Schema changes (require a single migration to apply):
- User gains disclaimerAcceptedAt — null until the user accepts the medical
  disclaimer, blocks /profile until set (PRD §14.3)
- New WeightEntry (PRD §5.2) — weight/measurements/notes encrypted at rest,
  energy/sleep/hunger left in the clear so we can produce aggregate analytics
- New FastEvent (PRD §5.3) — symptoms/notes encrypted, protocol/status/timer
  in the clear; indexed on (userId, startedAt)
- New Ingredient / Recipe / RecipeIngredient (PRD §5.1) — italian keto recipe
  database with macros and exclusion groups, ready for the matchmaking algo
- New MealPlan / MealSlot — generated weekly plan with selected recipe and
  alternatives per (day, meal)
- New enums: FastStatus, MealCategory, Difficulty, MealPlanStatus

Web — disclaimer flow:
- /welcome page (server component, requires auth) lists the 4 PRD-mandated
  points and surfaces the 3 mandatory checkboxes; submit triggers a server
  action that stamps disclaimerAcceptedAt and redirects to /profile
- /profile redirects to /welcome whenever disclaimerAcceptedAt is null,
  so the disclaimer becomes a hard prerequisite for any sensitive page
- New Italian copy under Welcome.* in messages/it.json
- @ketopath/db added to apps/web dependencies (was indirect via @ketopath/auth)

@ketopath/db re-exports the new models and enums.

Run the migration before testing: `pnpm db:migrate --name onboarding_v1_recipes`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 15:53:15 +02:00
lucianoandClaude Opus 4.7 c85179659a feat(db): migration 20260429130352 — alter Profile encrypted columns to text
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>
2026-04-29 15:05:42 +02:00
lucianoandClaude Opus 4.7 9555022143 feat(db): encrypt Profile health fields at rest (ADR 0002)
Wire prisma-field-encryption AES-256-GCM extension on the shared Prisma
client and annotate the four sensitive columns on Profile with @encrypted:
- weightStartKg / weightCurrentKg / weightGoalKg (Decimal → String)
- targetDate (DateTime @db.Date → String, ISO YYYY-MM-DD)

Other Profile fields stay in clear text per ADR 0002 (age, gender,
heightCm, activityLevel) — they're needed for plan generation and
aggregate analytics, and are not strongly identifying on their own.

apps/api profile.routes.ts:
- serialize() now reads the columns as strings and parses them back to
  numbers for BMR/TDEE; targetDate is already an ISO string from the DB
- the upsert stringifies numeric inputs and slices the date to YYYY-MM-DD

Env wiring:
- packages/db, apps/api, apps/web .env.example all document
  PRISMA_FIELD_ENCRYPTION_KEY (k1.aesgcm256.<base64url>) — must match
  across every process that hits the DB
- key generation snippet documented inline

Migration is intentionally NOT in this commit: needs to be created against
a live Postgres instance and applied. The fields change Decimal/Date → text
so prisma migrate dev will require a USING cast — see the follow-up commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 15:03:24 +02:00
lucianoandClaude Opus 4.7 5f17f95d6d feat(api): add Better Auth plugin and protected GET /me endpoint
- authPlugin runs preHandler that turns Fastify request headers into a Headers
  object, calls auth.api.getSession, and decorates request.user / request.session
- requireAuth() preHandler short-circuits with 401 when not signed in
- New module modules/me with GET /me returning the authenticated user/session
- env validates BETTER_AUTH_SECRET (≥32) and BETTER_AUTH_URL — must match web
- Restored .js extensions in shared packages so NodeNext-resolution consumers
  (api) typecheck cleanly; Next webpack now uses extensionAlias to map .js → .ts
- Re-enabled NodeNext for packages/auth and packages/db tsconfigs

Verified end-to-end:
- POST /api/auth/sign-in/email on web returns session cookie
- GET /me on api with the cookie returns 200 + user/session
- GET /me without the cookie returns 401

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 13:50:14 +02:00
lucianoandClaude Opus 4.7 0139b13fc6 feat(web): integrate Better Auth with sign-in/sign-up pages
Web app integration:
- /api/auth/[...all] route exposes Better Auth handler
- src/lib/auth.ts: server helper getServerSession() reading cookies via headers()
- src/lib/auth-client.ts: browser auth client built on shared makeAuthClient
- (auth) route group with sign-in and sign-up pages, both with email+password
  form and "Continua con Google" button (Google flow active when env is set)
- Home page becomes async, shows signed-in user name with sign-out button or
  routes to sign-up/sign-in CTAs
- Italian copy in messages/it.json under Auth.SignIn / Auth.SignUp
- transpilePackages includes @ketopath/auth
- .env.example: BETTER_AUTH_SECRET, BETTER_AUTH_URL, DATABASE_URL, Google placeholders

Build tooling:
- Drop .js extensions from internal package imports so Next webpack can
  transpile them; switch packages/db tsconfig from NodeNext to Bundler
  resolution to keep TS happy (same as packages/auth)

Verified end-to-end via browser:
- /sign-up creates user (Postgres rows in users + accounts), session cookie set,
  redirect to / shows "Accesso effettuato come Mario Test"
- /sign-out clears session, page falls back to anonymous CTAs
- /sign-in with the same credentials restores session

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 13:01:59 +02:00
lucianoandClaude Opus 4.7 79d4943c00 feat(db): add Better Auth tables (Session, Account, Verification)
- 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>
2026-04-29 12:47:40 +02:00
lucianoandClaude Opus 4.7 d309d06970 feat(db): add initial migration creating users, profiles, preferences
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>
2026-04-29 12:35:14 +02:00
lucianoandClaude Opus 4.7 ff004cdf8d feat(db): scaffold Prisma schema with User, Profile, Preferences
Initial schema covers only the 3 base entities from PRD §8 — additional
models (MealPlan, Recipe, WeightEntry, FastEvent, ShoppingList, etc.)
will be added alongside their respective features.

- Postgres datasource via DATABASE_URL
- Enums: Role, Gender, ActivityLevel, Phase, CookingTime, FastingProtocol
- Singleton PrismaClient export from @ketopath/db
- Root scripts: db:generate / db:migrate / db:studio / db:seed / db:format
- Seed stub at prisma/seed.ts
- Ignore .claude/settings.local.json (per-user CLI permissions)

The first migration must be run by the developer:
  pnpm db:migrate --name init

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:28:43 +02:00
lucianoandClaude Opus 4.7 87ca5af765 chore: initial monorepo scaffold
Setup pnpm workspaces with apps/{web,api} placeholders and shared
packages: tsconfig, eslint-config, shared, ui, db. Includes Prettier,
ESLint, Husky pre-commit, lint-staged, EditorConfig, VSCode settings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:10:08 +02:00