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>
This commit is contained in:
lucianoandClaude Opus 4.7 committed 2026-04-29 23:50:05 +02:00
1 parent ea88ed8775
commit 9327e75395
18 files changed
+627 -8

No files matched your search

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "profiles" ADD COLUMN "alert_weight_kg" TEXT;
@@ -0,0 +1,24 @@
-- CreateTable
CREATE TABLE "daily_check_ins" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"date" DATE NOT NULL,
"energy" INTEGER,
"sleep" INTEGER,
"hunger" INTEGER,
"mood" INTEGER,
"notes" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "daily_check_ins_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "daily_check_ins_user_id_date_idx" ON "daily_check_ins"("user_id", "date");
-- CreateIndex
CREATE UNIQUE INDEX "daily_check_ins_user_id_date_key" ON "daily_check_ins"("user_id", "date");
-- AddForeignKey
ALTER TABLE "daily_check_ins" ADD CONSTRAINT "daily_check_ins_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+28
View File
@@ -100,6 +100,7 @@ model User {
sessions Session[]
accounts Account[]
weightEntries WeightEntry[]
dailyCheckIns DailyCheckIn[]
fastEvents FastEvent[]
mealPlans MealPlan[]
deviceTokens DeviceToken[]
@@ -187,6 +188,10 @@ model Profile {
// PRD §5.1 — % grasso corporeo stimata (US Navy) per BMR Katch-McArdle.
/// @encrypted
bodyFatPct String? @map("body_fat_pct")
// PRD §5.2 — soglia di allarme peso. Quando il peso corrente la supera,
// mostriamo un banner di "settimana di riparazione" in /tracking.
/// @encrypted
alertWeightKg String? @map("alert_weight_kg")
// PRD §5.1 — storia delle diete restrittive precedenti, per stimare
// l'adattamento metabolico (Fothergill 2016, Rosenbaum 2008). Categoriale:
// NONE / SOME / EXTENSIVE / SEVERE. Cifrato perché può alludere a
@@ -228,6 +233,29 @@ model Preferences {
// PRD §5.2 — pesata settimanale, misure, indicatori soggettivi.
// I campi sanitari sono cifrati at-rest (ADR 0002).
// PRD §5.2 — check-in lampo quotidiano (10s): solo indicatori soggettivi.
// Distinto da WeightEntry (settimanale, con peso e misure).
model DailyCheckIn {
id String @id @default(cuid())
userId String @map("user_id")
date DateTime @db.Date
energy Int? // 1..10
sleep Int? // 1..10
hunger Int? // 1..10
mood Int? // 1..10
/// @encrypted
notes String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([userId, date])
@@index([userId, date])
@@map("daily_check_ins")
}
model WeightEntry {
id String @id @default(cuid())
userId String @map("user_id")