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