From f096807ed6ba3ffc76a3c36b0ea6c4457cab4a1b Mon Sep 17 00:00:00 2001 From: "Adriano (goldrake)" Date: Sat, 26 Sep 2026 12:58:17 +0000 Subject: [PATCH] longevity: schema misure con verifica/verificata_da/verificata_at + migrazione Aggiunge le tre colonne allo schema (db.ts) e una funzione migra() idempotente che le aggiunge via ALTER TABLE a un longevity.db esistente (PRAGMA table_info per capire cosa manca): CREATE TABLE IF NOT EXISTS non tocca una tabella gia' creata, e in produzione un longevity.db esiste dal 03/09/2026. misureValideAlla (motore/index.ts) lascia entrare anche le misure fuori range confermate (verifica = 'confermata'), non solo quelle nel range. Refs #7 Co-Authored-By: Claude Sonnet 5 --- apps/platforms/src/lib/longevity/db.ts | 38 +++++++++++++++++++ .../src/lib/longevity/motore/index.ts | 14 ++++--- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/apps/platforms/src/lib/longevity/db.ts b/apps/platforms/src/lib/longevity/db.ts index 703f592..8621879 100644 --- a/apps/platforms/src/lib/longevity/db.ts +++ b/apps/platforms/src/lib/longevity/db.ts @@ -54,6 +54,13 @@ CREATE TABLE IF NOT EXISTS misure ( fonte TEXT NOT NULL CHECK (fonte IN ('manuale','questionario','wellness_tower','vald','calibre','stress_index')), fuori_range INTEGER NOT NULL DEFAULT 0, + -- Verifica di una misura fuori range (issue #7): il trainer la conferma (entra nello + -- score come le altre, vedi misureValideAlla in motore/index.ts) o la scarta (resta + -- esclusa, per sempre). NULL = non ancora verificata. Su un longevity.db creato PRIMA + -- di questa modifica queste tre colonne NON ci sono: le aggiunge migra(), qui sotto. + verifica TEXT CHECK (verifica IN ('confermata','scartata')), + verificata_da TEXT, + verificata_at TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_misure_cliente_test ON misure (client_code, test_id); @@ -128,6 +135,36 @@ function apri(p: string, schema: string): Database.Database { return db; } +/** + * Migrazione idempotente per un `longevity.db` aperto PRIMA di questa modifica (issue #7): + * `CREATE TABLE IF NOT EXISTS` (sopra) non aggiunge colonne a una tabella che esiste già, e + * in produzione un longevity.db esiste dal 03/09/2026 (VPS, + * /opt/docker/insanitylab-apps/apps/platforms/data/longevity.db) — la stessa trappola già + * documentata sul vincolo di `score`, qui sotto. Senza questa funzione le tre colonne di + * verifica resterebbero assenti in produzione mentre ogni database creato da zero (ogni + * test, e un domani un secondo deploy) le avrebbe già: un disallineamento silenzioso fra + * ambienti, non un errore che si vede subito. + * `PRAGMA table_info` legge le colonne DAVVERO presenti sul file aperto, non lo schema + * dichiarato sopra: per ognuna delle tre assente, un `ALTER TABLE ... ADD COLUMN` la + * aggiunge (nullable, quindi le righe esistenti restano valide contro il CHECK — NULL non + * fa mai fallire un CHECK). Su un database che le ha già il controllo salta tutto: girarla + * a ogni apertura (come seedRegistro/seedPesi) è sicuro. + */ +function migra(db: Database.Database): void { + const colonne = new Set( + (db.prepare(`PRAGMA table_info(misure)`).all() as { name: string }[]).map((c) => c.name) + ); + if (!colonne.has('verifica')) { + db.exec(`ALTER TABLE misure ADD COLUMN verifica TEXT CHECK (verifica IN ('confermata','scartata'))`); + } + if (!colonne.has('verificata_da')) { + db.exec(`ALTER TABLE misure ADD COLUMN verificata_da TEXT`); + } + if (!colonne.has('verificata_at')) { + db.exec(`ALTER TABLE misure ADD COLUMN verificata_at TEXT`); + } +} + /** * ⚠️ Il registro dei test e i pesi si seminano QUI, all'apertura del database. * @@ -147,6 +184,7 @@ function apri(p: string, schema: string): Database.Database { */ export function createLongevityDb(path?: string): Database.Database { const db = apri(path ?? process.env.LONGEVITY_DB_PATH ?? 'data/longevity.db', SCHEMA_LONGEVITY); + migra(db); seedRegistro(db); seedPesi(db, MODEL_VERSION); return db; diff --git a/apps/platforms/src/lib/longevity/motore/index.ts b/apps/platforms/src/lib/longevity/motore/index.ts index 2e072ad..d367869 100644 --- a/apps/platforms/src/lib/longevity/motore/index.ts +++ b/apps/platforms/src/lib/longevity/motore/index.ts @@ -40,8 +40,8 @@ * applicarla, e restituisce `null` se il registro (dato modificabile senza * migrazioni) ha una riga incompleta, invece del punteggio pieno che dava * `lerp` con estremi indefiniti; e la lettura delle misure esclude - * `fuori_range = 1` (§9 della spec), in attesa della colonna di conferma - * (punto aperto in §12). + * `fuori_range = 1` (§9 della spec) salvo conferma esplicita del trainer + * (`misure.verifica = 'confermata'`, issue #7: `verificaMisura` in `misure.ts`). */ import type Database from 'better-sqlite3'; @@ -156,9 +156,11 @@ export const FINESTRA_GIORNI: number | null = null; * * Stessa esclusione di `fuori_range = 1` che c'era prima nella lettura diretta (§9 della * spec): una misura fuori dal range dichiarato non entra, né dalla sessione corrente né da - * una precedente. La colonna della conferma non esiste ancora (§12 della spec, issue #7): - * quando ci sarà, questo filtro andrà rilassato per lasciar passare le misure confermate, - * non tolto del tutto. + * una precedente — A MENO che il trainer l'abbia confermata (`verifica = 'confermata'`, + * colonna aggiunta dall'issue #7, scritta da `verificaMisura` in `misure.ts`): da quel + * momento in poi entra nello score come qualunque altra misura, qui e in ogni sessione + * successiva che la eredita. `verifica = 'scartata'`, o mai verificata (NULL), resta + * esclusa come prima. */ export function misureValideAlla(db: Database.Database, sessioneId: number): Map { const sessione = db.prepare( @@ -170,7 +172,7 @@ export function misureValideAlla(db: Database.Database, sessioneId: number): Map `SELECT m.test_id AS test_id, m.valore_num AS valore_num, s2.data AS data FROM misure m JOIN sessioni s2 ON s2.id = m.sessione_id - WHERE m.client_code = ? AND m.fuori_range = 0 AND m.valore_num IS NOT NULL + WHERE m.client_code = ? AND (m.fuori_range = 0 OR m.verifica = 'confermata') AND m.valore_num IS NOT NULL AND (s2.data < ? OR (s2.data = ? AND s2.id <= ?)) ORDER BY s2.data DESC, s2.id DESC, m.id DESC` ).all(sessione.client_code, sessione.data, sessione.data, sessione.id) as