Commit Graph
11 Commits
Author SHA1 Message Date
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 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 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 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 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 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 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 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 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