Commit Graph
63 Commits
Author SHA1 Message Date
lucianoandClaude Opus 4.7 0722abc070 feat(ui): dashboard ribilanciata + trial banner d'impatto + masthead minimale
Tre fix di vestizione dopo il primo "sguardo dal vivo" della dashboard loggata:

1. **Masthead minimale**: rimosse "ANNO I · N. 01" e data corrente che davano un sapore "magazine vintage" stridente con la nuova typography Geist. Ora solo logo a sx + (opzionale) etichetta sezione a dx, divider sottile.

2. **TrialBanner d'impatto**: era un avviso sussurrato (bordo dashed oro su crema). Adesso card scura full-width con mesh-blob oro animato, numero giorni in mega 4xl/5xl con accento oro, CTA bottone solido oro→pomodoro al hover. Diventa il primo focus visivo della dashboard, coerente col fatto che è la conversion principale.

3. **Dashboard bilanciata**: hero "Piano della settimana" allargata da `md:col-span-6` a `md:col-span-7` + rotazione ridotta da `-rotate-1` a `-rotate-[0.5deg]` (meno dramma). La nav resta su 5 col da col-start-8, ma con un `pl-2` chiude lo spazio fra hero e prima voce, eliminando il "buco" centrale di aria. Rimossi anche chapter marker gigante "I" e vertical text "Continua la lettura" che rinforzavano il sapore magazine.

Lint/typecheck/build verdi in locale.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:00:40 +02:00
lucianoandClaude Opus 4.7 38f28e015b docs: documenta NEXT_PUBLIC_APP_URL e setup Google OAuth nel runbook
Due colpevoli scoperti durante il deploy live:
- `NEXT_PUBLIC_APP_URL` mancava nel runbook ed in .env.example. Il client di Better Auth (lib/auth-client.ts) la usa come baseURL — senza, fallback a localhost:3000 → CORS error sul click di "Continua con Google". Va inlined al build (è NEXT_PUBLIC_*), quindi modificarla richiede rebuild.
- Setup Google OAuth: il bottone è SSG (prerendered al build), quindi aggiungere le env GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET non basta — serve rebuild perché `enabledSocialProviders` è valutato a build-time.

Aggiunta una sezione 4.3.bis dedicata al setup Google con i punti critici evidenziati.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 14:52:53 +02:00
lucianoandClaude Opus 4.7 6a5559fa0a fix(billing): tratta env vars vuote come unset (soft-degradation)
Le `STRIPE_*` env vars in ADR 0004 sono opzionali — quando il provider non è ancora configurato l'app deve continuare a funzionare. Lo schema Zod però usava `.string().min(1).optional()` che fallisce con stringhe vuote: `STRIPE_SECRET_KEY=` in un .env diventa `process.env.STRIPE_SECRET_KEY = ''`, presente ma length=0, quindi rifiutato.

Aggiunto `emptyAsUndefined` che traduce stringhe vuote → `undefined`. Il check di `isBillingConfigured()` rimane invariato (accetta solo valori non-empty). Sblocca il boot del backend con env Stripe vuote in produzione.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 14:04:16 +02:00
lucianoandClaude Opus 4.7 e76074869b fix(web): typecheck stricti per build di produzione
Quattro fix di tipi che emergono solo durante `next build` (con Next.js che esegue typecheck stretto su tutta l'app), pre-esistenti su main:

- `fast-timer.tsx`: l'omitted-vs-undefined di `exactOptionalPropertyTypes`
  rifiuta `{other: undefined}` per un campo opzionale. Costruisco l'oggetto
  symptoms condizionalmente.
- `plan-week.tsx`: `slots[0]` con `noUncheckedIndexedAccess` è `Slot |
  undefined`. Optional chaining `slots[0]?.meal`.
- `profile-form.tsx` e `weight-entry-form.tsx`: il mismatch Zod 3 + RHF
  + `exactOptionalPropertyTypes` sul tipo del Resolver è noto e non sistemabile
  senza upgrade. Cast locale del solo zodResolver, runtime invariato.

Lint pulito, 99/99 test verdi, `next build` ok in locale.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 13:49:52 +02:00
lucianoandClaude Opus 4.7 d98f95b7ca fix(billing): build su VPS — ExtendedPrismaClient, apiVersion Stripe
Tre fix che emergono solo durante il build di produzione (typecheck stretto, niente tsx watch lazy):

- `packages/db` ora ri-esporta Subscription / BillingWebhookEvent / SubscriptionStatus / BillingInterval / Achievement, così non serve importare da `@prisma/client` direttamente da apps/api (che non ce l'ha tra le deps).
- `apps/api/src/modules/billing/{service,webhook.routes}.ts` usano ExtendedPrismaClient (il client post-`$extends` di prisma-field-encryption) invece di PrismaClient base. Il client esteso ha tipi diversi per via dell'estensione, e la tipizzazione stretta lo richiede.
- `apiVersion` di Stripe portata a `2026-04-22.dahlia` (la versione corrente in stripe@22.x). La precedente `2025-09-30.clover` era già obsoleta nei tipi.

Lint pulito, 99/99 test verdi, build api ok in locale.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 13:43:24 +02:00
lucianoandClaude Opus 4.7 11d73eae69 docs: runbook deploy completo su VPS Virtualmin
Guida step-by-step per deployare KetoPath sul server `lamiadieta.luzaonline.net`: preparazione VPS, PostgreSQL 15+ con backup, env, build, PM2, reverse proxy Apache + Let's Encrypt sui due sottodomini (web + api), webhook Stripe pubblico, operazioni quotidiane, troubleshooting. Riusabile come runbook per futuri deploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 11:51:57 +02:00
lucianoandClaude Opus 4.7 f455cbe499 feat(billing): Stripe + abbonamento Pro con trial 30gg (ADR 0004)
Chiude la decisione "payment provider" aperta in CLAUDE.md.

**Modello**: free 30gg post-signup (no carta richiesta) → Pro mensile €9,90 / annuale €89. Allinea il paywall al confine fra fase INTENSIVE e TRANSITION (PRD §5.1), quando l'utente ha già visto i primi risultati. Dopo la scadenza l'app non si "spegne": storico restano consultabili (sola lettura), ma generazione piani / nuove pesate / digiuni / foto / export richiedono abbonamento attivo.

**Schema**: nuova tabella `subscriptions` (1:1 con users) con stati TRIALING/ACTIVE/PAST_DUE/CANCEL_AT_PERIOD_END/CANCELED/EXPIRED + `billing_webhook_events` per idempotenza dei retry Stripe.

**Backend** (`apps/api/src/modules/billing/`):
- `GET /me/billing/status` — snapshot + derived (kind, isPro, trialDaysRemaining)
- `POST /me/billing/checkout` — crea Stripe Checkout Session (subscription mode + Stripe Tax + tax_id_collection)
- `POST /me/billing/portal` — Customer Portal Session
- `POST /webhooks/stripe` — raw body, firma HMAC, idempotenza per `event.id`, dispatch su `customer.subscription.*`, `checkout.session.completed`, `invoice.payment_failed`
- Plugin `requirePro()` (402 payment_required) applicato a 9 rotte: meal-plans CRUD, weight-entries POST, check-ins POST, fast-events POST/PATCH, fasting/pause POST, export.pdf (plan+tracking)

**Shared** (`@ketopath/shared/billing/pro-status`):
- `isProActive(snap)` — verifica live (gestisce anche TRIALING con `trialEndsAt` passato in caso di cron in ritardo)
- `deriveProStatus(snap)` — kind + isPro + trialDaysRemaining + accessEndsAt per l'UI
- `computeTrialEndsAt(signupAt, days=30)`
- 11 unit test

**Frontend** (`apps/web/src/app/[locale]/billing/`):
- Pagina `/billing` editoriale (capitolo VIII) con StatusBlock per ogni kind, BillingActionsBar client (transitions, redirect a Stripe), 3 benefits
- `<TrialBanner>` in SignedInDashboard (3 stati: trial in corso oro / scaduto pomodoro / past_due pomodoro)
- Nav item "Abbonamento" (chapter VI) nel grid asimmetrico
- i18n IT completo (`Billing` namespace)

**Soft-degradation**: env Stripe (`STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, `STRIPE_PRICE_ID_*`, `BILLING_RETURN_URL`) sono tutte opzionali. Senza configurazione i route billing rispondono 503 e il banner trial mostra "pagamenti non ancora attivati" — utenti in trial continuano a usare l'app.

99/99 test verdi, lint pulito su tutto il monorepo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 11:27:06 +02:00
lucianoandClaude Opus 4.7 41b3646325 feat(ui): layout edge-to-edge + font Geist su tutta l'app
- Font: Geist (Vercel) sostituisce Fraunces/Inter Tight; GeistMono al posto di JetBrains Mono. `--font-display`, `--font-sans`, `--font-mono` mappati tutti su Geist via alias CSS — vocabolario tipografico unificato sans-serif moderno.
- Layout: rimosso `mx-auto max-w-7xl` da 7 pagine app (root, onboarding, profile, plan, tracking, fasting, shopping, recipe detail). Sostituito con `w-full px-6 sm:px-10 lg:px-16 xl:px-24` — edge-to-edge, ma respiro su 4K. Welcome/sign-in/sign-up restano stretti (testo lungo / form login).
- Tailwind: fontFamily fallback display da `Georgia, serif` a `system-ui` sans. Rimosse le OpenType features di Fraunces (`cv11`, `dlig`) — Geist usa `ss01`/`ss03`.
- Cleanup: rimosso `docs/~\$D_KetoPath.docx` (Word lockfile residuo).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 11:04:40 +02:00
lucianoandClaude Opus 4.7 d467e212cb feat(ui): editorial off-grid restyling — landing + tutte le pagine app
Direzione visiva "asimmetrico/off-grid" applicata in modo coerente:
titoli mega che sbordano dal viewport (bleed-left), chapter markers
giganti in stroke trasparente come art di sfondo, eyebrow verticale
sul bordo destro, mesh-blob fluttuanti, cursor-glow che segue il mouse.

- Landing marketing rifatta da zero: hero cinema con mockup ruotato in
  absolute, marquee infinito di keyword, stats sparse a quote diverse,
  bento di feature ruotate, mockup browser 3D che sborda a destra,
  CTA finale dark con "?" gigante.
- Dashboard loggata: greeting bleed-left, hero card "/plan" ruotata,
  nav asimmetrica con chapter markers per ogni voce.
- /plan, /onboarding, /profile, /tracking, /fasting, /shopping: stesso
  trattamento chrome (titolo bleed, chapter marker, atmosfera mesh-blob),
  contenuti funzionali invariati.

Componenti nuovi:
- CursorGlow (client) — alone radiale che segue il mouse, easing morbido
- EditorialHeader (server) — header riusabile con chapter+eyebrow+title
- EditorialAtmosphere (server) — preset di mesh-blob per pagina

CSS utilities: text-mega, text-chapter, text-stroke, bleed-left/right,
btn-gradient, glow-ring, tilt-3d, animate-marquee, animate-float-{x,y,z},
animate-ring-trace. Tutte rispettano prefers-reduced-motion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:43:47 +02:00
lucianoandClaude Opus 4.7 d17c5ef869 feat: §6 Gruppo B residuo — pannello macros, esclusioni granulari, achievement
- Plan: pannello macros laterale "oggi" con consumati/pianificati vs target (kcal/P/F/C). Sticky su lg, in-flow su mobile. Profile/derived ora include kcalTarget e i macro target (BMR aggiustato per condizioni e diet history).
- Preferenze: nuova sezione "Ingredienti specifici da evitare" con ricerca debounced sul catalogo (cap 50). Schema Preferences.bannedIngredientIds + matchmaking che scarta ricette contenenti ingredient bandito (test unit).
- Achievement: tabella Achievement (userId+key unique), 4 traguardi iniziali (first_weigh_in, first_plan, first_fast_complete, ten_meals_consumed). Service idempotente eval+persist agganciato a 4 trigger (weight POST, plan POST, fast PATCH→COMPLETED, slot consumed). Push notification best-effort all'unlock. Pannello badge nel profilo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:45:30 +02:00
lucianoandClaude Opus 4.7 3b895191f5 feat: §6 quick wins (Gruppo B) — store mode, extras, tutorial digiuno, pesata di partenza
- Shopping: modalità "al supermercato" (font/touch più grandi, persiste in localStorage); articoli extra ad personam (aggiunta/rimozione, persisteno per piano).
- Preferenze: tutorial lampo del digiuno intermittente (disclosure con 3 punti + disclaimer medico) sopra il selettore protocollo.
- Onboarding: nuovo step IV "Pesata di partenza" tra condizioni e preferenze, salva via saveWeightEntry server action.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 00:44:57 +02:00
lucianoandClaude Opus 4.7 4633f42bc5 feat: §6 quick wins (Gruppo A) — 5 quality-of-life che chiudono gap user-flow
A1. Notifiche per-pasto (PRD §6.3)
- NotificationSettings.mealReminders (default off)
- 3 ScheduledTask aggiuntivi: 13:00 pranzo, 16:00 spuntino, 19:00 cena
  Europe/Rome
- runMealReminderJob: skip se utente in pausa, altrimenti push "Ora di
  pranzo: vedi cosa c'è nel piano di oggi" → /plan
- Toggle in /profile sezione notifiche

A2. Auto-start digiuno a fine cena (PRD §6.3)
- POST /me/meal-plans/slots/:id/consumed: se è il "pasto finale" del
  giorno (CENA o ultimo dello schedule IF) e l'utente ha preferences.
  fastingProtocol senza FastEvent IN_PROGRESS → crea automaticamente
  un nuovo digiuno col protocollo preferito
- isMealLastOfDay helper (rank-based: COL=0, PRA=1, SPU=2, CENA=3)
- Risposta include autoStartedFast: { id } | null

A3. Riassunto piano nel done step onboarding (PRD §6.1.9)
- StepDone fetcha il profilo dopo regenerate
- Card 4-colonne con Fase corrente + durata, BMR, TDEE, aderenza target
- i18n: phaseLabel + phaseDuration

A4. "% del percorso" in /tracking (PRD §6.4.5)
- WeightHistory accetta startKg, calcola (start-current)/(start-goal)
  clampato 0-100
- 4° card nel summary: "Del percorso · 65%"

A5. Lista spesa: spuntati in fondo + totale rimanente (PRD §6.5.4-5)
- ShoppingChecklist sort items con consumed=true in fondo,
  alfabetico tra non-spuntati
- "Restano €X" mostrato accanto al "n di total" — solo non-spuntati

i18n: namespace Notifications, Onboarding, Tracking, Shopping estesi.

81/81 unit test verdi. Lint, typecheck verdi.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 00:35:35 +02:00
lucianoandClaude Opus 4.7 60a3d072f6 feat: GDPR (export+delete) + catalogo ricette 46→96
GDPR (PRD §14, art. 17 + 20)
- GET /me/export.json: dump completo dei dati personali
  (User, Profile, Preferences, WeightEntry, FastEvent, MealPlan+slots,
  DailyCheckIn, DeviceToken). I campi @encrypted vengono decifrati a runtime
  dall'extension Prisma. Risposta con Content-Disposition attachment.
- DELETE /me: cancellazione account con cascade automatico. Per gli
  account email/password richiede conferma password (verifica via
  auth.api.signInEmail). Account OAuth-only: solo sessione attiva.
- Proxy Next /api/gdpr-export per same-origin + cookie sessione.
- /profile: nuova sezione "I tuoi dati (GDPR)" in fondo con due azioni
  (esporta + elimina) e modale di conferma password per la cancellazione.

Catalogo ricette 46 → 96 (target 100)
- 50 ricette nuove italiane keto/low-carb bilanciate per categoria:
  12 colazioni, 14 pranzi, 12 spuntini, 12 cene
- Macros realistici, ingredienti dal seed esistente (no nuovi ingredienti
  necessari)
- Run db:seed: ingredients +0 (catalog 55), recipes +49 (catalog 96),
  recipe-ingredient links: 323

i18n: namespace Profile esteso con dataEyebrow/Title/Subtitle, dataExport*,
dataDelete* (incluso typed errors map), working.

81/81 unit test verdi. Lint, typecheck verdi.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 00:24:53 +02:00
lucianoandClaude Opus 4.7 d8f2a17888 test(e2e): rinnova suite Playwright per i flussi attuali
Lo schema dei test era fermo all'epoca pre-onboarding. Aggiornati e
ampliati a 4 spec, 8 test totali, tutti verdi:

- e2e/auth.spec.ts (esistente, già passante): sign-up/out/in
- e2e/home.spec.ts (esistente): smoke landing
- e2e/onboarding.spec.ts (nuovo, sostituisce profile.spec.ts):
  signup → /welcome → /onboarding → goal → profile → conditions →
  verifica BMR/TDEE persistiti su /profile (Michele 49a, 76 kg, 170 cm,
  sedentario → 1583 / 1899 kcal)
- e2e/plan.spec.ts (nuovo): genera piano, verifica presenza colazione/
  pranzo/cena, header con kcal·P·G·C, bottoni "Segna come consumato"
- e2e/fasting-tracking.spec.ts (nuovo): avvia 16:8 + diario sintomi +
  abort; inserisci pesata e verifica che appaia nella history

8 passed (9.6s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 00:15:40 +02:00
lucianoandClaude Opus 4.7 42a6ec57e2 fix(api): export PDF a 0 byte — bufferizza chunks pdfkit prima di send
Il pattern \`reply.send(doc)\` con uno stream pdfkit ha una race con fastify:
il body può chiudersi prima che pdfkit emetta tutti i chunks dopo \`doc.end()\`.

Ora colleziono i chunks in memoria, attendo \`doc.end()\` via Promise sull'evento
\`end\`, e mando il Buffer completo con \`Content-Length\` corretto. Applicato a
entrambi gli export (piano settimanale + tracking).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 00:07:11 +02:00
lucianoandClaude Opus 4.7 e015abff33 feat: export PDF del piano settimanale
GET /me/meal-plans/export.pdf restituisce un PDF stampabile del piano
ACTIVE: 7 giorni × pasti con macros giornalieri + per-pasto + tempo
preparazione, gestione free-meal e giorni di digiuno completo, ultima
pagina con lista spesa aggregata per reparto.

- apps/api/.../plan/export.routes.ts: pdfkit, layout A4 con paginazione
  automatica e footer disclaimer su tutte le pagine
- apps/web/.../api/plan-export: proxy Next.js per same-origin + cookie
- apps/web/.../plan/page.tsx: aggiunto link "Esporta PDF ↗" accanto a
  "Lista della spesa" nell'header del piano

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 00:04:19 +02:00
lucianoandClaude Opus 4.7 83bab7b6d5 feat: §5.2 finale — aderenza piano + export PDF
§5.2.5 Aderenza al piano alimentare
- MealSlot.consumed Boolean + consumedAt DateTime? (migration)
- POST /me/meal-plans/slots/:id/consumed (toggle, idempotente con body opzionale)
- @ketopath/shared/planner/adherence: computeAdherence() su slot dei giorni
  passati, free-meal contati come "rispettati"
- /plan: bottone ✓ in ogni SlotCard (oliva quando attivo) + 5° card nel
  summary settimanale "Aderenza: X% (n/m)"

§5.2.6 Export PDF per medico/nutrizionista
- pdfkit (Node, no headless Chrome) + @types/pdfkit
- GET /me/tracking/export.pdf: profilo, storico peso (30 entries),
  piano corrente con aderenza, footer con disclaimer medico
- Proxy Next.js /api/tracking-export per same-origin + cookie sessione
- /tracking: link "Esporta PDF per medico/nutrizionista" sotto subtitle

Schema migration: add_meal_slot_consumed.

81/81 unit test verdi. Lint, typecheck verdi.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 00:01:28 +02:00
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 c37dc5d717 feat(seed): popola Recipe.variants e RecipeIngredient.substitutes
- DEFAULT_SUBSTITUTES_BY_INGREDIENT in seed-recipes.ts: ~50 mappature
  riusabili (pesci interscambiabili, latticini lactose-free, manzo→tacchino,
  pollo→tacchino, frutta secca, condimenti, legumi inter-cambiabili)
- 12 ricette signature ora dichiarano variants reali (1-2 ciascuna):
  • Frittata di spinaci → senza lattosio / più saziante
  • Yogurt greco con noci → vegana / con frutti di bosco
  • Uova strapazzate con pancetta → senza maiale / con verdure
  • Insalata di pollo, avocado e pomodorini → vegetariana / con feta e olive
  • Salmone al forno con asparagi → versione fredda / sgombro
  • Bistecca con zucchine → tacchino / con melanzane
  • Insalatona con tonno e olive → Niçoise / sgombro
  • Branzino al cartoccio → mediterranea / orata
  • Polpette di pollo e ricotta → in sugo / tacchino-spinaci
  • Tagliata con rucola → con pomodorini / bresaola
  • Caprese → con avocado / con prosciutto crudo
  • Pancake proteici, Smoothie verde, Tagliolini di zucchine,
    Insalata di lenticchie, Quinoa con pollo (varianti analoghe)
- seed.ts ora propaga r.variants al campo Recipe.variants e applica
  ing.substitutes (override) o DEFAULT_SUBSTITUTES_BY_INGREDIENT[name]
  alle righe RecipeIngredient

Smoke verificato live: /recipes/<frittata> mostra "VARIANTI SUGGERITE"
con "Versione senza lattosio (-40 kcal)" e "Più saziante (+90 kcal)";
gli ingredienti hanno la riga "Sostituzioni: bietole, Grana Padano…".

Catalog: 46 ricette, 164 link RecipeIngredient, ~50 mappature substitutes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 23:31:33 +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 cf7361f735 feat(web): PWA — manifest, icone, meta iOS
- src/app/manifest.ts (Next 14 file convention) → /manifest.webmanifest
- public/icons/icon.svg + icon-maskable.svg (vettoriali, palette editoriale)
- scripts/generate-icons.mjs (sharp): genera icon-192/512.png,
  apple-touch-icon-180.png, icon-maskable-512.png, badge-72.png
- layout.tsx: viewport con themeColor light/dark, manifest link,
  appleWebApp meta (capable + title), icons (svg + png + apple touch)
- .eslintrc: ignora scripts Node out-of-project; lint-staged: .mjs format-only

Display standalone, lang it, theme #221d18 (ink) / bg #f6f0df (carta).
Sblocca "Installa app" su Chrome/Edge/Android e "Aggiungi alla Home" su
iOS Safari — quest'ultimo è pre-requisito per ricevere le push su iOS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:03:19 +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 cdfc685855 chore: fix typecheck on monorepo
- regeneratePlan: switch return type to Promise<void> so it satisfies the
  React form action contract.
- fast.routes PATCH: omit undefined keys instead of passing them, to
  comply with Prisma + exactOptionalPropertyTypes.
- weight-entry-form: cast field value/defaultValues to bypass z.input
  reporting `Date` for `z.coerce.date()` (zod 3.25 still emits the
  target type, not unknown).

pnpm typecheck, lint, test now all green across the monorepo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 21:16:08 +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 e7158abbad feat(web): refactor profile to shadcn Form + Select primitives
- Add shadcn Form helpers (Form, FormField, FormItem, FormLabel, FormControl,
  FormMessage, FormDescription) on top of react-hook-form's Controller
- Add shadcn Select wrapping @radix-ui/react-select with Trigger / Content /
  Item / Value, lucide chevron + check icons
- Profile form now wraps fields in <Form>...<FormField> blocks; gender and
  activityLevel use the new Select with placeholder, the rest use Input;
  validation errors land in <FormMessage> per field automatically

Playwright config:
- webServer is now an array — Playwright boots both api (:4000) and web (:3000)
  before the suite, so server actions that proxy to API_URL work in CI/local
- profile.spec.ts updated for the new flow: post-signup goes through /welcome
  to accept the medical disclaimer, then through Select primitives via
  combobox click + role=option for Sesso and Livello di attività

Verified: pnpm test:e2e — 4/4 specs pass (home + 2 auth + profile).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 16:15:21 +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 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 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 f954be610b docs(adr): 0002 encryption at-rest plan for art. 9 GDPR data
Picks application-level field encryption via prisma-field-encryption
(AES-256-GCM, master key from KMS) as the primary defence, combined with
volume encryption on the production Postgres host as defence in depth.

Documents:
- which fields are sensitive now (Profile.weight*, Profile.targetDate) and
  which arrive in V1 (WeightEntry, FastEvent, ProgressPhoto)
- which fields stay in clear text and why (email/login, age/gender for
  aggregate analytics, height/activity for plan generation)
- alternatives rejected: pgcrypto (key in queries), volume-only (no app-level
  protection), client-side E2E (kills BMR/TDEE server-side calculation)
- consequences and the implementation roadmap for a follow-up PR

Status: proposed — must be implemented before opening V1 to public users.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:38:01 +02:00
lucianoandClaude Opus 4.7 02af6685cf ci: add GitHub Actions workflow for lint/typecheck/test/build
.github/workflows/ci.yml runs on push to main and on PRs. Single job with
a Postgres 15 service container, applies migrations via prisma migrate
deploy, then lint / format:check / typecheck / unit tests / api+web build.

Concurrency group cancels superseded runs on the same ref. Node version is
read from .nvmrc and pnpm cache is used to keep installs fast.

BETTER_AUTH_SECRET is supplied as a repo secret; falls back to a known
test value when the secret is not set so external forks can still run CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:37:50 +02:00
lucianoandClaude Opus 4.7 e931132342 fix(web): switch i18n.ts to await requestLocale (next-intl 3.22+)
The `locale` parameter passed to getRequestConfig is deprecated; next-intl
now provides `requestLocale` as a Promise. Resolve it at the top of the
callback, fall back to defaultLocale when the requested one isn't supported,
and return both locale and messages.

Removes the per-request deprecation warning in dev/E2E logs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:37:39 +02:00
lucianoandClaude Opus 4.7 4349c395e3 test(web): e2e profile flow with BMR/TDEE assertions
- e2e/profile.spec.ts: fresh user signs up, opens /profile via the home CTA,
  fills the Michele PRD persona values (49, MALE, 170cm, 76kg, SEDENTARY)
  and asserts the summary panel shows BMR 1583 kcal and TDEE 1899 kcal
- profile-form.tsx: each Field receives an explicit `id` so <Label htmlFor>
  binds correctly — Playwright's getByLabel and screen-reader navigation
  both rely on this association

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:37:30 +02:00
lucianoandClaude Opus 4.7 5b38ddc41d feat: wire Sentry, conditional on DSN env var
apps/web (@sentry/nextjs):
- sentry.client.config.ts / sentry.server.config.ts / sentry.edge.config.ts
  initialize Sentry only when NEXT_PUBLIC_SENTRY_DSN (or SENTRY_DSN on the
  server) is set; tracesSampleRate 0.1
- next.config.mjs wraps the existing config with withSentryConfig only when a
  DSN is present; source-map upload stays disabled until SENTRY_AUTH_TOKEN is
  provided
- .env.example documents NEXT_PUBLIC_SENTRY_DSN and the optional auth token

apps/api (@sentry/node):
- src/lib/sentry.ts initializes Sentry at module load when SENTRY_DSN is set
- server.ts imports sentry.ts as the very first side-effect so early-boot
  errors (env validation, plugin registration) reach Sentry
- env schema gains optional SENTRY_DSN (URL)
- app.ts registers an errorHandler that captures the exception with the
  authenticated user when SENTRY_DSN is set; logs and re-sends as before

Build-time housekeeping:
- profile route: targetDate ?? null on create to satisfy Prisma's input shape
  under exactOptionalPropertyTypes
- profile form: useForm receives defaultValues only when initial is provided
  (spread instead of `defaultValues: undefined`); Field error prop typed
  `string | undefined` for exactOptionalPropertyTypes

Without a DSN both apps run unchanged (Sentry is inert).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:25:08 +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 ac2227fdec feat(web): adopt shadcn/ui primitives for auth surface
Add shadcn/ui plumbing:
- components.json (style: new-york, baseColor: slate, primary: green ~142 71%)
- src/lib/utils.ts with cn() helper (clsx + tailwind-merge)
- Tailwind config switched to CSS variables theming with primary/destructive/
  muted/card etc. tokens, dark-mode class, tailwindcss-animate plugin
- src/styles/globals.css declares :root and .dark variable palettes

Initial component set under src/components/ui:
- Button (cva variants: default/destructive/outline/secondary/ghost/link, asChild)
- Input
- Label (Radix Label)
- Card + CardHeader/Title/Description/Content/Footer

Refactor existing auth surface to the new primitives:
- sign-in / sign-up pages wrap form in a Card with Title/Description (+ footer
  for sign-up disclaimer)
- sign-in-form / sign-up-form use Input, Label, Button; tokens replace bespoke
  emerald/slate classes; divider and "Continua con Google" use the same Button
- Home page CTAs become Button asChild around Link; SignOutButton uses Button
- Hide-Google logic still works: enabledSocialProviders is now
  ReadonlyArray<SocialProvider> for ergonomic .includes() checks

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:09:59 +02:00
lucianoandClaude Opus 4.7 646b98cccf feat(auth): hide Google button when OAuth env is missing
- Export enabledSocialProviders from @ketopath/auth: ['google'] when
  GOOGLE_CLIENT_ID/SECRET are both set, [] otherwise
- sign-in and sign-up pages read enabledSocialProviders server-side and pass
  googleEnabled to their forms; the divider and button disappear when false
- No changes to the Better Auth instance — the Google provider is still
  conditionally registered, this just keeps the UI honest about it

docs/runbooks/google-oauth-setup.md walks through the Google Cloud Console
flow end-to-end (project, consent screen, credentials, redirect URIs, env
injection, verification, troubleshooting, prod considerations).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 13:55:04 +02:00
lucianoandClaude Opus 4.7 c27e807c43 test(auth): add unit env tests and E2E sign-up/sign-in flow
Unit tests on @ketopath/auth (added to the Vitest workspace):
- readAuthEnv accepts a valid configuration
- rejects secret < 32 chars and non-URL BETTER_AUTH_URL
- preserves Google credentials when both vars are set

Playwright e2e/auth.spec.ts:
- happy path: sign-up creates a user, redirects home with welcome message,
  sign-out clears session, sign-in with the same credentials restores it
- error path: invalid credentials surface a role="alert" message
- each test uses a unique generated email to avoid DB collisions

home.spec.ts: links instead of buttons (CTAs are now next/link).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 13:52:11 +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 31eea207ba feat(auth): add @ketopath/auth package wrapping Better Auth
- New workspace packages/auth exporting:
  - auth: Better Auth server instance using Prisma adapter on @ketopath/db
  - makeAuthClient: React client factory parameterised by baseURL
  - readAuthEnv: Zod-validated env reader (BETTER_AUTH_SECRET min 32 chars,
    BETTER_AUTH_URL, optional GOOGLE_CLIENT_ID/SECRET)
- emailAndPassword enabled with min 8 chars, requireEmailVerification: false
  for MVP (re-enable in V1, see ADR 0001)
- Google provider conditionally registered when both env vars are present —
  keeps the package usable before OAuth configuration is delivered
- 7-day sessions, 24h rolling refresh, cookie prefix 'ketopath'

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:48:51 +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 e9f1a55a5b docs(adr): adopt Better Auth, replacing Clerk
Reasons (full ADR in docs/decisions/0001-auth-provider.md):
- KetoPath targets EU users with Art. 9 GDPR data; Clerk on US infra
  raises Schrems II concerns
- Free, self-hostable, stays in our Postgres
- TypeScript-first, fits Next.js + Fastify + Prisma stack

CLAUDE.md updated:
- Tech stack: Better Auth on EU Postgres
- "Cosa NON fare mai": no manual password hashing
- References: Better Auth docs link

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:44:21 +02:00
lucianoandClaude Opus 4.7 72d47da453 feat(api): integrate Prisma client with /db/health endpoint
- Add @ketopath/db and fastify-plugin to apps/api
- prismaPlugin decorates the Fastify instance with prisma and disconnects
  on app close, with FastifyInstance type augmented in src/types/fastify.d.ts
- DATABASE_URL is now required by env validation
- New module modules/db with GET /db/health running SELECT 1 via Prisma
- dev script switched to tsx --env-file=.env for env loading

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:38:04 +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 a3109d2a6c test: setup Vitest workspace and Playwright with first nutrition tests
Vitest:
- Workspace at root (vitest.workspace.ts) running per-package configs
- packages/shared/vitest.config.ts with v8 coverage and 70% thresholds
- Root scripts: test / test:watch / test:coverage

Nutrition module in @ketopath/shared:
- calculateBmr (Mifflin-St Jeor) + calculateTdee with activity multipliers
- ACTIVITY_MULTIPLIERS constants matching CLAUDE.md domain knowledge
- 14 unit tests covering Michele/Laura PRD personas, all sex variants,
  every activity level, and input validation (100% coverage on src/nutrition)

Playwright in apps/web:
- chromium-only project, webServer auto-boot of pnpm dev
- e2e/home.spec.ts smoke test asserts Italian copy and disclaimer

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:32:55 +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 8625d85a87 feat(api): scaffold Fastify server with health route
- ESM Fastify 4 with Pino logger (pretty-print in dev only)
- Plugins: helmet, cors (allowlist via CORS_ORIGINS), rate-limit, sensible
- Zod-validated environment config in src/config/env.ts
- Modular structure under src/modules with /health route
- tsx watch for dev, tsc for build, dist/server.js as production entry

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:19:04 +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
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