Commit Graph
20 Commits
Author SHA1 Message Date
lucianoandClaude Opus 4.7 9327e75395 feat: §5.2 batch — trend lineare, soglia allarme, guide misure, daily check-in
Quattro feature del PRD §5.2 Tracking peso e progressi.

§5.2.1 Trend regressione lineare + proiezione
- @ketopath/shared/tracking/weight-trend: linearWeightTrend() (least squares),
  projectGoalDate() con cap a 52 settimane e check di direzione coerente
- 9 unit test (slope/intercept, segno, cap)
- /tracking: terza statistica "Trend settimanale" + frase
  "Al ritmo attuale raggiungerai 75.0 kg il 24 luglio 2026"

§5.2.2 Soglia di allarme peso personalizzabile
- Profile.alertWeightKg @encrypted
- profileInputSchema accetta il campo, profile.routes lo persiste
- ProfileForm: nuovo input "Soglia di allarme peso (kg)"
- /tracking: banner pomodoro "Sei sopra la soglia" con suggerimento
  "settimana di riparazione" senza colpa

§5.2.3 Linee guida "come misurare bene"
- Disclosure inline in WeightEntryForm con istruzioni tecniche per
  girovita, fianchi, coscia, braccio + tip generale (ora del giorno,
  metro morbido, stessa procedura)

§5.2.4 Daily check-in indicatori soggettivi
- Nuovo modello DailyCheckIn (date, energy, sleep, hunger, mood, notes)
  con unique(userId, date) e migration
- shared dailyCheckInInputSchema (zod)
- API: GET /me/check-ins (last 30), GET /me/check-ins/today,
  POST /me/check-ins (upsert sul giorno)
- /tracking: card lampo in cima con 4 slider 1-10 + Salva/Aggiorna,
  10 secondi di compilazione

i18n: namespace Tracking esteso (weeklyTrend, projection, alertEyebrow/Message/Hint,
measurementsHelpToggle + measurementGuides{}, checkIn*, mood). Profile esteso
(alertWeightKg + hint).

Schema migrations: add_alert_weight_kg, add_daily_check_in.

81/81 unit test verdi (9 nuovi). Lint, typecheck verdi.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 23:50:05 +02:00
lucianoandClaude Opus 4.7 ea88ed8775 feat: §5.3 completion — stats, symptoms, milestones, pause, break-fast
Cinque feature che chiudono il PRD §5.3 (gestione digiuno).

§5.3.1 Streak + statistiche settimanali/mensili
- @ketopath/shared/tracking/fasting-stats: computeFastingStats(events) ritorna
  weeklyHours, monthlyHours, currentStreak, longestStreak, completionRate,
  totalSessions, completedSessions
- 6 unit test (currentStreak consecutivo, longestStreak storico, completionRate
  ignora IN_PROGRESS, range temporali)
- /fasting: la striscia di stat passa da 3 a 5 (settimanali, mensili, streak
  attuale, streak record, % completamento)

§5.3.2 Diario sintomi durante il digiuno
- updateFastSymptoms server action → PATCH /me/fast-events/:id { symptoms }
- SymptomsDiary component nel timer attivo: checkbox "Mal di testa" + 3 slider
  1-10 (Energia, Fame, Lucidità) + textarea note libere; "Salva" non
  interrompe il timer

§5.3.3 Notifiche per-fast
- FastEvent.notifiedMilestones String[] (idempotenza)
- Cron */5 * * * * `runFastingMilestonesJob` scansiona le sessioni IN_PROGRESS
  e invia push secondo le milestone:
  · hydration_2h / 4h / 6h — promemoria idratazione
  · electrolyte_18h — alert elettroliti per digiuni lunghi
  · ending_soon — 30 min prima della fine
- Skip automatico se l'utente ha fastingPausedUntil futuro
- Skip se l'utente non ha attivato `fastingMilestones` nelle preferenze
- Server.ts gestisce ora 2 ScheduledTask (weekly + fasting)

§5.3.4 Modalità giorno libero
- User.fastingPausedUntil DateTime?
- POST /me/fasting/pause con body { until?: ISO|null } — default: domani 06:00
- GET /me/fasting/pause ritorna { fastingPausedUntil, paused }
- /fasting: FastingPauseToggle bottone "Pausa per oggi" → banner con countdown +
  "Riprendi adesso" per terminare prima

§5.3.5 Pasto di rottura intelligente
- GET /me/fast-events/:id/break-fast-suggestions: solo per digiuni >= 24h,
  ritorna 3 ricette con prepMinutes ≤ 20, kcal ≤ 350, categoria
  COLAZIONE/SPUNTINO. Heuristica per "facile da digerire".
- /fasting history: per le sessioni completate >= 24h aggiunge un disclosure
  "Suggerimenti per riprendere a mangiare" che fetcha on-demand e linka alle
  ricette

Schema migrations: add_fasting_paused_until, add_fast_event_milestones.

72/72 unit test verdi (6 nuovi). Lint, typecheck, build verdi.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 23:41:20 +02:00
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 2c11b73703 feat: macro tracker, cookingTime nel matching, rigenera singolo pasto (PRD §5.1)
Tre completamenti del PRD §5.1 lasciati indietro fino a oggi.

1. Macro tracker giornaliero
   - GET /me/meal-plans/current ora include proteinG/fatG/netCarbG/prepMinutes
     per ogni recipe selected e alternative
   - /plan: nuova summary card settimanale (medie kcal/proteine/grassi/carb
     sui giorni attivi)
   - Header per ogni giorno: "1390 KCAL · P 100 · G 92 · C 17"

2. cookingTime nel matchmaking
   - Nuovo prepMinutes su RecipeCandidate
   - MatchOptions accetta maxPrepMinutes (soft cap, non hard filter)
   - Score: penalty 0.01 per minuto di sforamento — preserva la diversità
     ma orienta le scelte
   - maxPrepMinutesFor(level): LOW=15, MEDIUM=30, HIGH=60
   - plan.routes deriva la soglia da Preferences.cookingTime

3. Rigenera singolo pasto
   - POST /me/meal-plans/slots/:slotId/regenerate
   - Riusa l'intero pipeline di calcolo (BMR, deficit, condizioni, training,
     protocollo, mealsPerDay, cookingTime) e ricalcola top5 per quello slot
   - Passa consumedSoFar (kcal/macros già scelti negli altri pasti del
     giorno) al matchmaking → coerenza dei macros giornalieri
   - Aggiunge la ricetta corrente al recentlyConsumedIds per forzare il
     cambio
   - UI: pulsante ↻ in ogni SlotCard, accessibile via aria-label

Smoke verificato live: 21 bottoni rigenera (3 pasti × 7 giorni), summary
"Media kcal/giorno 1397", header giornaliero "1390 KCAL · P 100 · G 92 · C 17".

54/54 unit test verdi. Lint, typecheck, build verdi.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 23:03:52 +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 feb54c1f9a feat: piano sensibile al protocollo digiuno (PRD §5.6 + §9.4)
Finora il fastingProtocol era una preferenza salvata ma ignorata dal
planner — ogni giorno generava 4 pasti. Ora cambia struttura per protocollo
e per giorno della settimana.

@ketopath/shared/planner
- protocolPlanForDay(protocol, dayOfWeek) → { share, kcalMultiplier }
- 14:10  → tutti e 4 i pasti, share default
- 16:8   → niente colazione: pranzo 50%, spuntino 10%, cena 40%
- 18:6   → niente colazione: pranzo 55%, spuntino 5%, cena 40%
- 20:4   → solo spuntino+cena (10/90)
- ESE 24h → mercoledì kcalMultiplier=0 (giorno saltato), altri default
- 5:2    → lunedì + giovedì kcalMultiplier=0.25, share 100% cena
- 7 unit test in protocol.test.ts

apps/api/plan
- POST /me/meal-plans: legge preferences.fastingProtocol, deriva il piano
  per ogni giorno, salta MealSlot con share=0 e giorni a kcalMultiplier=0
- GET /me/meal-plans/current: include fastingProtocol nella risposta
- baseDailyTarget restituito invariato per il client

apps/web
- /plan mostra "Finestra alimentare 16:8" sotto il titolo della settimana
- plan-week distingue i giorni "fasting" (digiuno completo) e "leggeri"
  (5:2 fasting day) con copy dedicato; rimuove totali calorici inappropriati
- i18n: Plan.protocolLabel + Plan.fastingDay/lightDay

Smoke tested: settato 16:8 da /profile, rigenerato il piano, verificato
che l'etichetta appaia e che ogni giorno abbia esattamente 3 pasti
(Pranzo / Spuntino / Cena, niente Colazione).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:27:49 +02:00
lucianoandClaude Opus 4.7 ded4215664 feat(web): onboarding ricco — goal step, notifiche, done celebrativo
- Step "L'obiettivo" iniziale (perdere peso / mantenere / energia):
  card-based, client-state, personalizza il copy degli step successivi
- Step "Promemoria": opt-in push notifications inline durante l'onboarding,
  riusa subscribe() del push-client; skippable
- Step "Pronti a partire" arricchito:
  · auto-genera il primo piano in background al primo render
  · copy che cambia in base al goal scelto
  · 3 next-cards (Cucina, Digiuno, Tracking) per orientare l'utente
  · banner "Aggiungi alla Home" condizionale per iOS Safari non-standalone
    (necessario per ricevere push su iPhone, vedi ADR 0003)
- Smart-jump aggiornato per la nuova sequenza (5 step invece di 3)
- i18n: namespace Onboarding esteso con goal/notifications/iOS install copy

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:18:45 +02:00
lucianoandClaude Opus 4.7 624152b29b feat: preferences, exclusion-aware planner, /onboarding (PRD §5.1, §3)
Preferences (PRD §5.1)
- @ketopath/shared: zod schemas + EXCLUSION_GROUPS, CUISINE_TAGS,
  COOKING_TIMES, FASTING_PROTOCOL_VALUES
- apps/api: GET/PATCH /me/preferences (exclusions, cuisinePreferences,
  cookingTime, fastingProtocol)
- apps/web: PreferencesForm chip-based + PreferencesSection wired into
  /profile so existing users can edit
- Plan generation now aggregates Ingredient.exclusionGroups → Recipe
  exclusionTags so matchMeals filters out excluded recipes

Onboarding (PRD §3)
- /onboarding multi-step (profile → preferences → done) with smart-jump:
  starts at the first incomplete step
- ProfileForm gains optional onSaved/saveLabel for reuse outside /profile
- Redirect chain updated: /welcome accept → /onboarding (was /profile);
  /plan, /tracking, /shopping redirect missing profiles to /onboarding
- /profile remains the editing surface for returning users

i18n
- New Onboarding + Preferences namespaces (it)

Smoke tested end-to-end: selected fish exclusion, regenerated the plan,
verified Salmone/Branzino/Tonno are filtered out and unaffected recipes
(Frittata, Pollo, Mandorle, Polpette) remain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:14:50 +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 b79340efb7 feat(web): fasting page — live timer, metabolic phase, history (PRD §5.3)
New /fasting route. Editorial layout matching the rest of the app
("CAPITOLO VII / Digiuno intermittente.").

When no active session:
- "Pronto a iniziare" eyebrow + italic call-to-action
- Protocol Select (16:8 default; full set FASTING_PROTOCOLS, hours derived
  from PROTOCOL_DEFAULT_MINUTES)
- "Avvia digiuno" CTA → server action that POSTs to /me/fast-events
- Right column: Roman-numeral guide (I-IV) for the four most common protocols
  with title + description from messages.json

When a session is active (status=IN_PROGRESS, no endedAt):
- Live timer in massive monospace clamp(3rem,9vw,6.25rem), tabular numerals,
  ticking once per second via setInterval
- Italic remaining-time line ("Mancano HH:MM:SS") or "Obiettivo raggiunto."
  once the protocol target is reached
- Hairline progress bar; ink while running, pomodoro after target
- "Concludi adesso" / "Concludi" CTA → PATCH with status=COMPLETED
- "Annulla sessione" ghost link → PATCH with status=ABORTED (with confirm())
- Right column: "FASE METABOLICA" with current phase title in pomodoro and
  italic description (postprandial 0-4h / glycogenolysis 4-12h / lipolysis
  12-18h / ketogenesis 18-24h / autophagy 24h+, PRD §5.3)
- "INIZIATO" and "OBIETTIVO" stats in mono

History below:
- Three big mono stats: completed sessions count, total hours, total sessions
- Last 12 events listed with protocol label in pomodoro italic, italian
  short date, status eyebrow, duration in mono

Home gains a fourth NavItem ("Digiuno — Digiuno intermittente").

Verified end-to-end in Chrome: started a 16:8 → live ticking timer at
00:00:04, "Mancano 15:59:55", "Post-prandiale" phase shown.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 17:15:17 +02:00
lucianoandClaude Opus 4.7 4e00c131b9 feat(web): tracking page — weight entries with sparkline + history (PRD §5.2)
New /tracking route in the editorial language:
- "CAPITOLO VI / Pesata e misure." masthead
- 7/5 split: form left, STORICO right

Form (left, "NUOVA RILEVAZIONE"):
- Date + weight required, with the latest weight pre-filled to encourage
  consistency between weeks
- Optional misure: girovita / fianchi / coscia / braccio (cm), grouped under
  a "MISURE (CM) — OPZIONALI" eyebrow
- Subjective indicators 1–10: energia / sonno / fame
- Italic "Registrato." (oliva) confirmation after a successful POST
- Server action calls POST /me/weight-entries through API_URL with the
  user's session cookie; sparkline + history revalidate via
  revalidatePath('/tracking')

History (right, "STORICO"):
- Inline SVG sparkline of the user's weight series with a terracotta dot
  marking the latest reading; ink-soft date axis labels under the line
- Two big mono stats: "PESO ATTUALE" and "VARIAZIONE" (oliva when negative,
  pomodoro when positive, ink at zero)
- Newest 8 entries listed with hairline rules between rows
- Italian italic empty-state copy when no entries exist yet

Home gains a third NavItem ("Tracking — Pesata & misure").

Verified manually with a fresh user signup → disclaimer → profile → tracking:
sparkline / "PESO ATTUALE 76.0 kg" / "VARIAZIONE 0.0 kg" / list row "29 apr 76.0 kg".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 16:53:04 +02:00
lucianoandClaude Opus 4.7 59a311cfc9 feat(web): editorial slow-food redesign across the whole UI
A complete visual + tonal pass on every page. Replaces the previous shadcn
slate+emerald defaults with an Italian editorial aesthetic — refined slow-food
magazine crossed with a luxury cookbook. Mobile-first, premium, distinctly
Italian, no generic AI-wellness vibes.

Tokens:
- New palette: carta (cream parchment), ink (warm black), oliva (deep olive),
  pomodoro (terracotta), oro (aged gold), rule (hairline). All exposed as
  HSL CSS variables and as Tailwind colors. Dark mode = oxblood/cream.
- Type stack: Fraunces (variable, optical-sizing auto, italic + roman) for
  display / Inter Tight for body / JetBrains Mono with tnum + zero for
  numerals. Replaces Inter-only setup.
- Font feature settings on body and .font-display for ligatures, opsz
  and stylistic alternates.
- Subtle SVG paper grain on body::before (multiply blend, 4.5% opacity).
- 2px radius default (editorial print, not soft web).

Component primitives:
- Button — mono uppercase 11px tracking-widest, sharp corners, hover
  inverts ink/cream. Variants tuned for the new palette.
- Input — bottom-only hairline, Fraunces 18px text, italic placeholders.
- Label — tiny mono caps, ink-soft.
- Card — top + bottom hairlines only, no shadow; Fraunces title and italic
  description; CardFooter has its own top-rule.
- Select — same hairline-bottom trigger as Input; SelectContent panel uses
  a hard 6px offset shadow for an editorial print stamp feel.

New component:
- Masthead — top-of-page newspaper header with thick top-rule, KP wordmark
  with terracotta dot, "ANNO I · N. 0X" issue label, today's date in Italian.

Pages:
- / — Massive Fraunces "Una keto italiana, sostenibile." with terracotta
  dot, italic Fraunces tagline, right-column nav with category eyebrows
  (CUCINA / PROFILO) and arrow rule. Bottom: "Tre principi" with roman
  numerals I/II/III. Disclaimer in italic.
- /sign-in — "CAPITOLO II / Bentornato in cucina." split layout.
- /sign-up — "CAPITOLO III / Inizia il tuo percorso." Italian copy refresh.
- /welcome — Editor's-letter layout with the four PRD §14.3 points as a
  Roman-numeral list; custom hand-drawn check icon for the consent boxes.
- /profile — 7/5 split: form on the left (anagrafica + peso group), summary
  on the right with massive mono numerals (BMR, TDEE, multiplier) and italic
  "kcal" units; empty-state copy until the profile is saved.
- /plan — "CAPITOLO V / Il tuo piano." Italian-formatted week range. Each
  day has a Roman-numeral mark (I-VII), Fraunces day name, mono kcal total.
  Each meal slot uses Roman numerals (I-IV) for the meal index.
- Cookie banner — refreshed to match (italic Fraunces, "PRIVACY" eyebrow,
  outline button).

Italian copy refresh:
- Headline: "Una keto italiana, sostenibile" (was "KetoPath")
- Tagline: narrative arc instead of marketing line
- CTAs: "Iscriviti" / "Accedi" (was "Inizia gratis" / "Scopri come funziona")
- Greeting: "Buongiorno, {name}." (was "Accesso effettuato come ...")
- Profile: weight columns become Iniziale / Attuale / Obiettivo under a
  "Peso (chilogrammi)" group eyebrow.

E2E updated to match: home looks for the new headline and CTA labels;
profile uses the new "Come ti chiami" / "Iniziale / Attuale / Obiettivo"
labels and asserts numeric values separately from the "kcal" unit span.

All 4 specs pass. Verified visually at 1280×900 and 375×812.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 16:44:55 +02:00
lucianoandClaude Opus 4.7 f8eaf2f493 feat(web): weekly meal plan UI with regenerate + per-slot swap
apps/api:
- New PATCH /me/meal-plans/slots/:slotId — accepts { recipeId } and validates
  ownership + that the new recipe is one of the slot's existing alternatives
  (or already selected). Returns the updated slot with the new selected recipe.

apps/web — /plan route:
- Server component runs the same auth+disclaimer+profile guard chain as
  /profile, then redirects to /profile if no profile exists yet (you can't
  generate a plan without macros)
- actions.ts exposes three server actions: fetchCurrentPlan, regeneratePlan
  and swapSlotRecipe; all proxy to api with the user's session cookie and
  revalidatePath('/plan') after writes
- plan-week.tsx renders day-by-day sections (Lun→Dom) with a 4-card meal
  row (1/2/4 columns at sm/lg breakpoints), recipe name + kcal, daily kcal
  totals, expandable list of alternatives with one-click "Scegli"
- "Genera piano" CTA when there's no plan yet, "Rigenera" when there is one
- Italian copy under Plan.* with day, meal and kcal-total interpolations
- Home gains a "Vedi il piano settimanale" CTA next to "Completa profilo"

Verified end-to-end in Chrome:
- generate plan → 7 sections (Lun-Dom), each with 4 meal cards and per-day
  kcal sum (e.g. Lunedì 1510 kcal = 360+540+150+460)
- recently-consumed penalty visible: Mon/Tue/Wed have different breakfasts
- swap: clicked "Vedi 3 alternative" on Mon Colazione → "Scegli" on
  "Uova strapazzate" → card refreshed to that recipe and 410 kcal

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 16:23:50 +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 50d68fc522 feat(web): privacy-first cookie banner
- New CookieBanner client component anchored at the bottom of the locale
  layout, dismissed via Button click and persisted via localStorage key
  ketopath:cookie-notice-ack (no cookie set for the dismissal itself)
- Italian copy: only essential cookies for authentication/session, no
  analytics or third-party trackers (consistent with the ADR 0001 stance
  and CLAUDE.md "Privacy first" non-negotiable)
- Layout body now uses bg-background / text-foreground tokens so dark mode
  toggling will work the day we add it

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 15:48:32 +02:00
lucianoandClaude Opus 4.7 1d904484e6 feat: profile flow — Zod schema, /me/profile API, /profile page with BMR/TDEE
@ketopath/shared — new module profile/schema:
- profileInputSchema (Zod): age 18-110, gender, height 120-230 cm, weights
  35-300 kg, activityLevel, optional targetDate
- GENDERS / ACTIVITY_LEVELS string-literal arrays for UI iteration
- Re-exported from @ketopath/shared

apps/api — new module modules/profile:
- PUT /me/profile: requireAuth, validates body via profileInputSchema, upserts
  the row, returns the saved profile + derived { bmr, tdee, activityMultiplier }
  (BMR/TDEE computed via @ketopath/shared)
- GET /me/profile: requireAuth, returns the same shape, 404 when missing
- Decimal columns serialised back as numbers

apps/web — new /profile page:
- src/app/[locale]/profile/page.tsx (server): redirects unauthenticated users
  to /sign-in, calls fetchProfile to hydrate the form
- profile-form.tsx (client): react-hook-form + zodResolver bound to the same
  shared schema, native styled selects for gender/activityLevel until shadcn
  Select arrives, post-submit panel showing BMR/TDEE/multiplier
- actions.ts: server actions saveProfile / fetchProfile that proxy the call
  to API_URL via cookie passthrough (no CORS, no exposed token)
- Home page gains a "Completa il tuo profilo" CTA when signed in
- API_URL env var added to .env.example
- Italian copy in messages/it.json under Profile

Verified end-to-end with the "Michele" PRD persona (49, M, 170cm, 76kg, SEDENTARY):
BMR = 1583 kcal, TDEE = 1899 kcal, multiplier 1.2 — matches the unit tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:20:07 +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 94eeb1d817 feat(web): scaffold Next.js 14 app with Tailwind and next-intl
- App Router under src/app/[locale] with locale 'it' as default
- next-intl middleware and getRequestConfig with setRequestLocale
- Tailwind CSS with shared font variable and content from packages/ui
- ESLint via @ketopath/eslint-config/nextjs, tsconfig via @ketopath/tsconfig/nextjs
- Inter font, base layout, home page with translated copy and disclaimer

Tweaks to shared eslint-config:
- Disable consistent-type-imports for .cjs files (next parser is not @typescript-eslint)
- Configure import resolver to discover tsconfig in apps/* and packages/*
- Drop *.config.* ignore patterns (overrides handle them with env.node)

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