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>
- 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>
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>
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>
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>
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>
@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>