18b0877026
seedRegistro/seedPesi esistevano dal 22/08 ed erano chiamati SOLO dai test: su un database nuovo il registro restava vuoto, campiDelQuestionario non trovava niente e la pagina del questionario si apriva SENZA DOMANDE. Non era un problema della demo, era il primo avvio in produzione - lo stesso difetto gia' visto in questo ramo (una funzione corretta che nessuno chiama), con la stessa correzione: agganciarla dove non si puo' dimenticare, cioe' alla creazione del database. Entrambe le semine sono INSERT OR REPLACE, quindi rilanciarle e' a vuoto, e i punteggi gia' emessi non cambiano (sono congelati nella tabella "score", che e' quello che il referto legge). Quattro test lo presidiano. scripts/seed-demo.ts crea l'utente con ruolo "cliente", il fascicolo (passando da creaCliente, l'unico varco fra identity e longevity) e compila il questionario attraverso salvaDalForm - lo stesso percorso della pagina vera, consenso compreso - cosi' i punteggi vengono calcolati e congelati come in produzione. Si ferma se l'utente esiste gia': rifare la demo su dati esistenti vuol dire duplicare o cancellare un fascicolo, e non e' una decisione da script. Verificato girando l'app: Energy 74,5/100 con copertura 50%, Performance e Recovery dichiarati insufficienti (5% e 35%) con l'elenco di cosa manca, Fitness Age assente. E' la regola di Nicola all'opera - annullare invece di stimare - ed e' lo stato vero del prodotto: nel registro ci sono solo le 20 domande del questionario, i test fisici arrivano col gestionale trainer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EndnceRA5WnA6rvA5iV9WL
172 lines
7.6 KiB
TypeScript
172 lines
7.6 KiB
TypeScript
import Database from 'better-sqlite3';
|
|
import { mkdirSync } from 'node:fs';
|
|
import { dirname } from 'node:path';
|
|
import { seedRegistro, seedPesi, MODEL_VERSION } from './registro';
|
|
|
|
/**
|
|
* Sotto questa copertura di peso un elemento calcolato e' 'insufficiente' e non ha valore.
|
|
* Vale a ogni livello E dentro il questionario: soglia unica, confermata da Nicola il
|
|
* 22/08 — due criteri diversi sarebbero due cose da spiegare e da ricordare.
|
|
*/
|
|
export const COPERTURA_MINIMA = 0.4;
|
|
|
|
const SCHEMA_LONGEVITY = `
|
|
CREATE TABLE IF NOT EXISTS soggetti (
|
|
client_code TEXT PRIMARY KEY,
|
|
sesso TEXT NOT NULL CHECK (sesso IN ('M','F')),
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE TABLE IF NOT EXISTS registro_test (
|
|
test_id TEXT PRIMARY KEY,
|
|
etichetta TEXT NOT NULL,
|
|
unita TEXT,
|
|
tipo_valore TEXT NOT NULL CHECK (tipo_valore IN ('num','txt')),
|
|
curva TEXT,
|
|
params TEXT,
|
|
asse TEXT,
|
|
sotto_dominio TEXT,
|
|
range_min REAL,
|
|
range_max REAL,
|
|
attivo_da TEXT NOT NULL,
|
|
attivo_a TEXT,
|
|
note TEXT
|
|
);
|
|
CREATE TABLE IF NOT EXISTS sessioni (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
client_code TEXT NOT NULL REFERENCES soggetti(client_code),
|
|
data TEXT NOT NULL,
|
|
tipo TEXT NOT NULL CHECK (tipo IN ('checkup','questionario')),
|
|
eta_alla_data INTEGER,
|
|
operatore TEXT,
|
|
note TEXT,
|
|
quest_version TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_sessioni_cliente ON sessioni (client_code, data DESC);
|
|
CREATE TABLE IF NOT EXISTS misure (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
sessione_id INTEGER NOT NULL REFERENCES sessioni(id) ON DELETE CASCADE,
|
|
client_code TEXT NOT NULL REFERENCES soggetti(client_code),
|
|
test_id TEXT NOT NULL REFERENCES registro_test(test_id),
|
|
valore_num REAL,
|
|
valore_txt TEXT,
|
|
unita TEXT,
|
|
fonte TEXT NOT NULL CHECK (fonte IN
|
|
('manuale','questionario','wellness_tower','vald','calibre','stress_index')),
|
|
fuori_range INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_misure_cliente_test ON misure (client_code, test_id);
|
|
CREATE INDEX IF NOT EXISTS idx_misure_sessione ON misure (sessione_id);
|
|
CREATE TABLE IF NOT EXISTS pesi (
|
|
model_version TEXT NOT NULL,
|
|
livello TEXT NOT NULL CHECK (livello IN ('asse','macro','fitness_age')),
|
|
contenitore TEXT NOT NULL,
|
|
elemento TEXT NOT NULL,
|
|
peso REAL NOT NULL,
|
|
PRIMARY KEY (model_version, livello, contenitore, elemento)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS profilo_note (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
client_code TEXT NOT NULL REFERENCES soggetti(client_code),
|
|
sessione_id INTEGER REFERENCES sessioni(id),
|
|
campo_id TEXT NOT NULL,
|
|
testo TEXT NOT NULL,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE TABLE IF NOT EXISTS score (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
client_code TEXT NOT NULL REFERENCES soggetti(client_code),
|
|
sessione_id INTEGER NOT NULL REFERENCES sessioni(id),
|
|
tipo TEXT NOT NULL CHECK (tipo IN ('asse','macro','fitness_age')),
|
|
elemento TEXT NOT NULL,
|
|
valore REAL,
|
|
copertura REAL NOT NULL,
|
|
stato TEXT NOT NULL CHECK (stato IN ('ok','insufficiente')),
|
|
quest_version TEXT,
|
|
model_version TEXT NOT NULL,
|
|
calcolato_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
-- Il tipo Punteggio protegge il codice ma non la tabella: l'interfaccia leggera'
|
|
-- 'score' direttamente, dove quel tipo non c'e' piu'. Questo vincolo lo riscrive
|
|
-- a livello di dato: o lo stato e' 'ok' e il valore c'e', o e' 'insufficiente' e
|
|
-- il valore e' NULL, mai un numero scritto sotto uno stato che lo nega.
|
|
-- ATTENZIONE al limite, che non e' quello che sembra: lo schema qui sopra gira con
|
|
-- CREATE TABLE IF NOT EXISTS, che su un file gia' esistente e' un'operazione a vuoto.
|
|
-- Quindi un longevity.db creato prima di questa modifica NON ha questo vincolo, e
|
|
-- continuera' a non averlo finche' qualcuno non lo migra a mano.
|
|
-- (Non e' un limite di SQLite: la versione imbarcata da better-sqlite3 e' la 3.53,
|
|
-- che ALTER TABLE ... ADD CONSTRAINT lo supporta. E' che qui una migrazione non c'e'.
|
|
-- Verificato il 22/08: la CLI di sistema e' la 3.37 e non lo supporta, quindi provarlo
|
|
-- da riga di comando da' un errore fuorviante.)
|
|
-- Oggi nessun longevity.db esiste ancora, quindi il caso e' teorico: lo diventa il
|
|
-- giorno del primo deploy, ed e' quello il momento di ricordarselo.
|
|
CHECK ((stato = 'ok' AND valore IS NOT NULL) OR (stato = 'insufficiente' AND valore IS NULL))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_score_cliente ON score (client_code, calcolato_at DESC);
|
|
`;
|
|
|
|
const SCHEMA_IDENTITY = `
|
|
CREATE TABLE IF NOT EXISTS clienti (
|
|
client_code TEXT PRIMARY KEY,
|
|
user_id INTEGER,
|
|
nome TEXT NOT NULL,
|
|
cognome TEXT NOT NULL,
|
|
data_nascita TEXT,
|
|
email TEXT,
|
|
telefono TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_clienti_user ON clienti (user_id);
|
|
`;
|
|
|
|
function apri(p: string, schema: string): Database.Database {
|
|
if (p !== ':memory:') mkdirSync(dirname(p), { recursive: true });
|
|
const db = new Database(p);
|
|
db.pragma('journal_mode = WAL');
|
|
db.pragma('foreign_keys = ON');
|
|
db.exec(schema);
|
|
return db;
|
|
}
|
|
|
|
/**
|
|
* ⚠️ Il registro dei test e i pesi si seminano QUI, all'apertura del database.
|
|
*
|
|
* Fino al 03/09/2026 `seedRegistro` e `seedPesi` esistevano, erano giusti, ed erano
|
|
* chiamati **solo dai test**: su un database nuovo il registro restava vuoto, quindi
|
|
* `campiDelQuestionario` non trovava niente e la pagina del questionario si apriva
|
|
* **senza domande**. Non era un difetto della demo: era il primo avvio in produzione.
|
|
* È lo stesso difetto già visto una volta in questo ramo — «una funzione corretta che
|
|
* nessuno chiama» — e la correzione è la stessa: agganciarla dove non si può dimenticare.
|
|
*
|
|
* Entrambe le semine sono `INSERT OR REPLACE`, quindi rilanciarle è a vuoto; e il
|
|
* registro è **dato di prodotto** (le domande e i pesi decisi con Nicola), non dato di
|
|
* un cliente: riallinearlo a ogni avvio è voluto, così una domanda modificata in
|
|
* `registro.ts` entra col rilascio invece che con una migrazione a mano.
|
|
* ⚠️ I punteggi già emessi non cambiano: sono congelati in `score` e il referto legge
|
|
* quelli, mai un ricalcolo (vedi `refertoDi`).
|
|
*/
|
|
export function createLongevityDb(path?: string): Database.Database {
|
|
const db = apri(path ?? process.env.LONGEVITY_DB_PATH ?? 'data/longevity.db', SCHEMA_LONGEVITY);
|
|
seedRegistro(db);
|
|
seedPesi(db, MODEL_VERSION);
|
|
return db;
|
|
}
|
|
|
|
export function createIdentityDb(path?: string): Database.Database {
|
|
return apri(path ?? process.env.IDENTITY_DB_PATH ?? 'data/identity.db', SCHEMA_IDENTITY);
|
|
}
|
|
|
|
// Connessione singola per processo, come getDb() in src/lib/db.ts: gli endpoint chiedono
|
|
// una connessione già aperta invece di aprirne una nuova a ogni richiesta.
|
|
// ⚠️ Qui c'è SOLO longevity (il database senza nomi: può usarlo chiunque). Un singleton
|
|
// analogo per identity non si esporta da qui — vive privato dentro anagrafica.ts, l'unico
|
|
// modulo autorizzato a tenerlo: un controllo testuale file-per-file non vede una giunzione
|
|
// che passa da un re-export su piu' file, quindi la connessione condivisa a identity non
|
|
// deve essere raggiungibile da fuori quel modulo, punto.
|
|
let longevitySingleton: Database.Database | null = null;
|
|
|
|
export function getLongevityDb(): Database.Database {
|
|
if (!longevitySingleton) longevitySingleton = createLongevityDb();
|
|
return longevitySingleton;
|
|
}
|