From 3d43de5ab91fb75f3988db1e83268f60e90a6239 Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Thu, 3 Sep 2026 12:27:24 +0000 Subject: [PATCH] Longevity: il registro si semina da solo, e un cliente demo con un referto vero 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 in , il referto legge quelli). Quattro test lo presidiano. scripts/seed-demo.ts crea l'utente , 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) Claude-Session: https://claude.ai/code/session_01EndnceRA5WnA6rvA5iV9WL --- apps/platforms/package-lock.json | 20 +++ apps/platforms/package.json | 4 +- apps/platforms/scripts/seed-demo.ts | 122 ++++++++++++++++++ apps/platforms/src/lib/longevity/db.ts | 23 +++- .../tests/longevity/registro-seminato.test.ts | 51 ++++++++ 5 files changed, 218 insertions(+), 2 deletions(-) create mode 100644 apps/platforms/scripts/seed-demo.ts create mode 100644 apps/platforms/tests/longevity/registro-seminato.test.ts diff --git a/apps/platforms/package-lock.json b/apps/platforms/package-lock.json index b005346..d380987 100644 --- a/apps/platforms/package-lock.json +++ b/apps/platforms/package-lock.json @@ -26,6 +26,7 @@ "@types/bcryptjs": "^2.4.6", "@types/better-sqlite3": "^7.6.13", "marked": "^18.0.11", + "tsx": "^4.23.13", "typescript": "^6.0.3", "vitest": "^4.1.9" } @@ -6245,6 +6246,25 @@ "license": "0BSD", "optional": true }, + "node_modules/tsx": { + "version": "4.23.13", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", + "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/apps/platforms/package.json b/apps/platforms/package.json index 51cdb4f..c380f01 100644 --- a/apps/platforms/package.json +++ b/apps/platforms/package.json @@ -10,7 +10,8 @@ "start": "node ./dist/server/entry.mjs", "test": "vitest run", "create-user": "node scripts/create-user.mjs", - "sync-campus": "node scripts/sync-campus.mjs" + "sync-campus": "node scripts/sync-campus.mjs", + "seed-demo": "tsx scripts/seed-demo.ts" }, "dependencies": { "@astrojs/node": "11.0.1", @@ -31,6 +32,7 @@ "@types/bcryptjs": "^2.4.6", "@types/better-sqlite3": "^7.6.13", "marked": "^18.0.11", + "tsx": "^4.23.13", "typescript": "^6.0.3", "vitest": "^4.1.9" } diff --git a/apps/platforms/scripts/seed-demo.ts b/apps/platforms/scripts/seed-demo.ts new file mode 100644 index 0000000..1379da8 --- /dev/null +++ b/apps/platforms/scripts/seed-demo.ts @@ -0,0 +1,122 @@ +/** + * seed-demo.ts — un cliente dimostrativo con un referto vero da mostrare. + * + * COSA FA, in quest'ordine: + * 1. apre i tre database (li crea se mancano; il registro dei test si semina da sé, + * vedi src/lib/longevity/db.ts); + * 2. crea un utente con ruolo `cliente` in piattaforme.db; + * 3. crea il suo fascicolo — la riga con il nome in identity.db, quella senza nome in + * longevity.db — passando da `creaCliente`, l'unico varco fra le due; + * 4. compila il questionario per lui con risposte plausibili, attraverso `salvaDalForm`: + * cioè lo stesso percorso della pagina vera, consenso compreso. Da lì i punteggi + * vengono calcolati e congelati, ed è quello che il referto legge. + * + * COME SI USA: + * npm run seed-demo # utente "demo-cliente", password generata + * npm run seed-demo -- --utente nicola-demo --password sceltaDaTe + * npm run seed-demo -- --data 2026-09-03 # data della compilazione (default: oggi) + * percorsi: --utenti-db / --longevity-db / --identity-db, oppure le variabili + * DB_PATH / LONGEVITY_DB_PATH / IDENTITY_DB_PATH (stessi nomi del container). + * + * ⚠️ NON è idempotente per scelta: se l'utente esiste già lo dice e si ferma, senza + * toccare niente. Rifare la demo su dati esistenti significherebbe o duplicare un + * fascicolo o cancellarne uno, e nessuna delle due è una cosa che uno script deve + * decidere da solo. + * + * ⚠️ I dati sono INVENTATI e vanno detti tali a chi guarda: è una persona che non esiste. + * Il referto che ne esce non è un esempio clinico, è un esempio di interfaccia. + * + * ⚠️ Cosa mostrerà davvero il radar: il registro contiene oggi SOLO le 20 domande del + * questionario — i test fisici (handgrip, VO2max, plank, composizione corporea...) non + * hanno ancora una riga, arrivano col gestionale trainer. Quindi escono con un valore gli + * assi che il questionario copre almeno al 40%, e gli altri restano dichiarati + * insufficienti con l'elenco di cosa manca. Non è un difetto della demo: è lo stato del + * prodotto, ed è anche la regola di Nicola che si vede all'opera (annullare invece di + * stimare). + * + * DIPENDE DA: tsx (devDependency) — il codice del prodotto è TypeScript e questo script + * lo usa davvero invece di riscriverne una copia in SQL. + */ +import { randomBytes } from 'node:crypto'; +import { createLongevityDb, createIdentityDb } from '../src/lib/longevity/db'; +import { creaCliente, etaAllaData } from '../src/lib/longevity/anagrafica'; +import { salvaDalForm, campiDelQuestionario } from '../src/lib/longevity/vista'; +import { createDb } from '../src/lib/db'; +import { createUser } from '../src/lib/auth'; + +const argv = process.argv.slice(2); +const opt = (nome: string, def: string): string => { + const i = argv.indexOf(nome); + return i >= 0 && argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : def; +}; + +const utente = opt('--utente', 'demo-cliente'); +const password = opt('--password', randomBytes(9).toString('base64url')); +const nome = opt('--nome', 'Giulia'); +const cognome = opt('--cognome', 'Esempio'); +const nascita = opt('--nascita', '1990-05-14'); +const data = opt('--data', new Date().toISOString().slice(0, 10)); + +const utentiDb = createDb(opt('--utenti-db', process.env.DB_PATH ?? 'data/piattaforme.db')); +const longevity = createLongevityDb(opt('--longevity-db', process.env.LONGEVITY_DB_PATH ?? 'data/longevity.db')); +const identity = createIdentityDb(opt('--identity-db', process.env.IDENTITY_DB_PATH ?? 'data/identity.db')); + +const esistente = utentiDb.prepare('SELECT id FROM users WHERE username = ?').get(utente) as { id: number } | undefined; +if (esistente) { + console.error(`L'utente "${utente}" esiste già (id ${esistente.id}): non tocco niente.`); + console.error('Usa --utente per una demo nuova.'); + process.exit(1); +} + +/** + * Le risposte della persona inventata: una in salute ma non perfetta. Un questionario + * con tutti i valori al massimo darebbe un referto tutto verde, che è il modo migliore + * per non far vedere niente — né le curve, né la soglia del 40%, né cosa manca. + */ +const RISPOSTE: Record = { + // sonno + q_ore_sonno: 7, q_min_addorm: 20, q_risvegli: 1, q_riposato: 7, + q_sonnolenza_diurna: 2, q_caffeina: 3, + // energia e stress + q_energia_media: 7, q_calo_pomeridiano: 4, q_tensione: 3, q_sopraffatto: 4, + q_esaurimento: 1, q_controllo: 7, q_sicurezza_gestione: 8, q_pensieri_lavoro: 2, + // stile di vita + q_attivita: 3, q_alimentazione: 7, q_alcol_life: 4, q_sigarette: 0, + q_luce: 2, q_schermi: 30, +}; + +// Il registro è la fonte: se una domanda è stata aggiunta o rinominata, questo script deve +// dirlo invece di salvare un questionario a metà senza che si veda. +const campi = campiDelQuestionario(longevity, data); +const mancanti = campi.filter((c) => !(c.id in RISPOSTE)).map((c) => c.id); +const inventate = Object.keys(RISPOSTE).filter((id) => !campi.some((c) => c.id === id)); +if (mancanti.length || inventate.length) { + if (mancanti.length) console.error(`⚠️ domande del registro senza risposta qui: ${mancanti.join(', ')}`); + if (inventate.length) console.error(`⚠️ risposte che il registro non conosce: ${inventate.join(', ')}`); + console.error('Allinea RISPOSTE al registro prima di rifare la demo.'); + process.exit(1); +} + +const userId = createUser(utentiDb, utente, password, 'cliente'); +const code = creaCliente(identity, longevity, { + nome, cognome, sesso: 'F', data_nascita: nascita, user_id: userId, +}); + +const esito = salvaDalForm(longevity, { + client_code: code, + data, + eta: etaAllaData(nascita, data), + risposte: RISPOSTE, + consensoSanitario: 'sì', +}); + +if (!esito.ok) { + console.error(`Il questionario non è stato salvato: ${esito.errore}`); + process.exit(1); +} + +console.log(`utente ${utente} (ruolo cliente, id ${userId})`); +console.log(`password ${password}`); +console.log(`fascicolo ${code} — ${nome} ${cognome}, ${etaAllaData(nascita, data)} anni`); +console.log(`sessione ${esito.sessioneId}, questionario del ${data}, ${campi.length} risposte`); +console.log('\nEntra da /login e atterri sul referto: /longevity/io'); diff --git a/apps/platforms/src/lib/longevity/db.ts b/apps/platforms/src/lib/longevity/db.ts index 3aa81b7..703f592 100644 --- a/apps/platforms/src/lib/longevity/db.ts +++ b/apps/platforms/src/lib/longevity/db.ts @@ -1,6 +1,7 @@ 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. @@ -127,8 +128,28 @@ function apri(p: string, schema: string): Database.Database { 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 { - return apri(path ?? process.env.LONGEVITY_DB_PATH ?? 'data/longevity.db', SCHEMA_LONGEVITY); + 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 { diff --git a/apps/platforms/tests/longevity/registro-seminato.test.ts b/apps/platforms/tests/longevity/registro-seminato.test.ts new file mode 100644 index 0000000..e99bebe --- /dev/null +++ b/apps/platforms/tests/longevity/registro-seminato.test.ts @@ -0,0 +1,51 @@ +// Il registro dei test è dato di prodotto, non dato di un cliente: un database nuovo deve +// nascere con dentro le domande e i pesi. Fino al 03/09/2026 seedRegistro e seedPesi erano +// chiamati SOLO dai test, quindi ogni suite passava e il primo avvio vero apriva un +// questionario senza domande. Questo test guarda il database come lo trova l'applicazione. +import { describe, it, expect } from 'vitest'; +import { createLongevityDb } from '../../src/lib/longevity/db'; +import { campiDelQuestionario } from '../../src/lib/longevity/vista'; +import { MODEL_VERSION, seedRegistro, seedPesi } from '../../src/lib/longevity/registro'; + +const OGGI = new Date().toISOString().slice(0, 10); + +describe('un database longevity appena creato', () => { + it('ha il registro dei test già seminato, non vuoto', () => { + const db = createLongevityDb(':memory:'); + const n = (db.prepare('SELECT COUNT(*) AS n FROM registro_test').get() as { n: number }).n; + expect(n).toBeGreaterThan(0); + }); + + it('mostra le domande del questionario senza che nessuno semini a mano', () => { + const db = createLongevityDb(':memory:'); + const campi = campiDelQuestionario(db, OGGI); + expect(campi.length).toBeGreaterThan(0); + // I tre sotto-domini che il questionario alimenta oggi: se un giorno il registro + // perdesse una famiglia di domande, il referto continuerebbe a "funzionare" con un + // asse in meno e nessuno se ne accorgerebbe. + const sottoDomini = new Set(campi.map((c) => c.sottoDominio)); + expect(sottoDomini).toContain('questionario_sonno'); + expect(sottoDomini).toContain('questionario_energia_stress'); + expect(sottoDomini).toContain('questionario_lifestyle'); + }); + + it('ha i pesi del modello corrente, per assi e macro', () => { + const db = createLongevityDb(':memory:'); + const perLivello = db.prepare( + 'SELECT livello, COUNT(*) AS n FROM pesi WHERE model_version = ? GROUP BY livello' + ).all(MODEL_VERSION) as { livello: string; n: number }[]; + const mappa = Object.fromEntries(perLivello.map((r) => [r.livello, r.n])); + expect(mappa.asse).toBeGreaterThan(0); + expect(mappa.macro).toBeGreaterThan(0); + expect(mappa.fitness_age).toBeGreaterThan(0); + }); + + it('rilanciare la creazione non duplica niente (INSERT OR REPLACE)', () => { + const db = createLongevityDb(':memory:'); + const prima = (db.prepare('SELECT COUNT(*) AS n FROM registro_test').get() as { n: number }).n; + // Stessa semina, stesso database: è ciò che accade a ogni riavvio del container. + seedRegistro(db); seedPesi(db, MODEL_VERSION); + const dopo = (db.prepare('SELECT COUNT(*) AS n FROM registro_test').get() as { n: number }).n; + expect(dopo).toBe(prima); + }); +});