Compare commits

..

87 Commits

Author SHA1 Message Date
Adriano Dal Pastro 13d8cdd137 Il sito lascia le piattaforme: separazione fusa in main
Fuso `feat/split-apps` (fermo dal 03/09 e mai portato in main, mentre il container
delle piattaforme girava gia' da li'): il sito e le piattaforme erano online in due
copie, con database diversi e ruoli divergenti — `nicola` era `admin` di la' e
`piattaforme` di qua, e la promozione di ieri valeva solo su una delle due.

Adesso `insanitylab.it/campus`, `/piattaforme` e `/login` fanno 404: le aree
riservate stanno solo su `piattaforme.tielogic.xyz`. Il pannello `/admin` resta.
Nel database del sito restano `admin` ed `editor`; `nicola` e `adriano` sono usciti.

Il lavoro di oggi e' passato intatto: edizione `settembre-2026` coi cinque
pacchetti, i 73 tag, il countdown al 19/09 e la correzione del sabato. Verificato
sull'immagine nuova prima di metterla, chiedendo la pagina a quattro date.

Tre cose che lo split del 03/09 aveva lasciato indietro, trovate eseguendo:

- **`Dockerfile` del sito copiava ancora `campus-content`**, spostato sotto
  `apps/platforms`: il build falliva. Nel sito non c'era piu' un solo riferimento a
  quella cartella - solo la riga del Dockerfile. Rimossa.
- **`migra-utenti.mjs --togli-dal-sito` era un no-op silenzioso**: il `continue`
  sugli utenti gia' presenti a destinazione saltava anche la cancellazione
  dall'origine. Siccome copiare e ripulire sono due passi separati apposta, l'opzione
  non funzionava proprio nel caso per cui esiste. Ora chi e' gia' di la' esce di qua
  lo stesso, e il riepilogo dice quanti ne ha tolti.
- **`.env` non e' in git**, quindi il merge non lo ha spostato sotto `apps/sito/`.
  L'`up` e' fallito dopo che il `down` aveva gia' tolto il container: **9,7 secondi
  di sito giu'**, misurati. La prova con `docker run` non lo aveva visto perche' non
  legge `env_file`. Il rimedio - `docker compose config` prima dello swap - e' scritto
  nel README del sito, che prima non diceva come si fa un deploy.

Sito 149 test verdi, piattaforme 202.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPKFABRT3y26dFNMquF3CS
2026-09-04 10:18:00 +00:00
Adriano Dal Pastro 33091be802 Merge remote-tracking branch 'origin/feat/split-apps' into merge-split
# Conflicts:
#	apps/sito/compose.yaml
2026-09-04 10:09:29 +00:00
Adriano Dal Pastro faf7647a5f La promo di settembre entra in produzione prima della mezzanotte
Gli Opening Bundles scadono stasera alle 23:59:59 e dopo di loro l'elenco delle
edizioni era vuoto: senza questo merge, dal 5 mattina la pagina avrebbe mostrato
"Nessuna promozione in corso" con le promo del cliente vive.

Merge autorizzato da Adriano il 04/09/2026 dopo aver visto l'anteprima.

Lo scambio a mezzanotte non richiede altri interventi: la pagina legge l'edizione
valida nell'istante della richiesta, e il nuovo container va in produzione adesso
con dentro entrambe le edizioni.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPKFABRT3y26dFNMquF3CS
2026-09-04 09:35:14 +00:00
Adriano Dal Pastro 2b71cbb49e L'inaugurazione e' di sabato, e la fascia morta fra 700 e 701 px
Due rilievi della revisione, uno dei quali fuori dalla promo.

**`eventi.astro`: "venerdi' 19 settembre 2026"**, ma il 19/09/2026 e' **sabato**
(`calendari.ts:136` lo diceva gia' giusto). E' la meta description della pagina
dell'inaugurazione: e' cio' che si legge nell'anteprima di un link su WhatsApp,
che e' il canale con cui questo cliente parla ai suoi. Una riga, ma pubblica e
falsa.

**`.cards-5` fra 700 e 701 px non era coperta da nessuna regola**: la fascia
intermedia aveva `and (min-width: 701px)`, e a larghezze frazionarie - uno zoom
del browser dà 700,5 px - non valeva ne' quella ne' il blocco <=700, quindi le
cinque card tornavano a sei colonne dentro ~570 px. Tolto il `min-width`: ci
pensa il blocco <=700, che viene dopo e a pari specificita' vince.

Tolta anche la riga ridondante `.cards-5 { 1fr }` nel blocco <=700: `.lp .cards`
copre gia' tutte le griglie a una colonna, ed e' lo stesso meccanismo per cui
`.cards-3` funziona senza una regola sua.

Restano non toccati, perche' verificati corretti: la virgola di "composizione
corporea, e analisi HRV" e' **nella creativa** (a fine riga, nel layout su tre
righe), e le code esplicative delle card 1 e 2 vengono dai testi di agosto
gia' approvati dal cliente, non dalle creative compresse per il formato social.

186 test verdi.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPKFABRT3y26dFNMquF3CS
2026-09-04 06:53:30 +00:00
Adriano Dal Pastro f914d0699a La promo di settembre subentra ai bundle il 5, cinque pacchetti
Gli Opening Bundles scadono il 04/09 alle 23:59:59 e dopo di loro non c'era
nessuna edizione: dal 5 il sito avrebbe mostrato "Nessuna promozione in corso"
mentre le promo erano vive. Adriano ha consegnato le sei creative il 03/09.

Edizione `settembre-2026` (5 -> 19 settembre, chiude il giorno
dell'inaugurazione): tornano i pacchetti da 5 incontri, con in piu' il
Check-up avanzato, che e' una valutazione singola e per questo dice
"1 valutazione" invece di "5 incontri". Prezzi 250/150/130/80/70.

Due cose che la struttura non prevedeva:

- **Nessun prezzo barrato.** Le creative danno il solo prezzo promo, e i
  listini pieni di agosto (300/220/140/100) sono numeri che nessuno ha
  riconfermato - riportarli accanto ai nuovi sarebbe stato inventarli.
  `price-old` diventa opzionale nella CardSpec (`priceOld`) invece di essere
  un tag vuoto: cosi' la spec dichiara che quella card non ne ha, e il test
  lo pretende solo dove c'e'. Agosto e i bundle sono marcati `priceOld: true`,
  quindi restano sotto controllo come prima.

- **Cinque card non stanno in una griglia da due o da tre** senza lasciare un
  buco in fondo. `.cards-5` va a sei colonne con span due, cosi' le ultime due
  si spostano al centro; a due card per riga sotto i 1000px, a una sotto i 700.

Testi ripresi dall'edizione di agosto, gia' approvata dal cliente, cambiando
solo cio' che le date rendevano falso: la FAQ delle class diceva "gli orari
saranno pubblicati entro Settembre" e adesso e' settembre, quindi rimanda al
calendario che esiste. La FAQ 7 sul Check-up e' nuova e va fatta leggere a
Nicola prima della pubblicazione.

186 test verdi, `astro check` invariato (i 3 errori della pagina promo sono
preesistenti e non toccati).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPKFABRT3y26dFNMquF3CS
2026-09-04 06:43:50 +00:00
AdrianoDev a59b4e0c0e crea-fascicolo: aprire il fascicolo di chi ha gia' un account
"Anche un admin deve avere il suo referto" (Adriano). Il codice lo permetteva
gia': il referto si aggancia all'utente, non al ruolo. Mancava il modo di
creare la riga che lega utente e fascicolo senza passare da seed-demo, che
inventa anche la persona e le risposte.

Non indovina niente: --sesso e' obbligatorio perche' entra nelle curve dei
punteggi, la data di nascita si puo' omettere e allora l'eta' resta vuota -
meglio un campo vuoto di una data plausibile e falsa. Si ferma se quell'utente
ha gia' un fascicolo: due fascicoli per la stessa persona sono due storie
parallele, e nessuna e' quella giusta.

Il fascicolo nasce vuoto: il referto dice "non hai ancora compilato il
questionario" finche' la persona non lo compila, che e' il percorso vero.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EndnceRA5WnA6rvA5iV9WL
2026-09-03 13:02:54 +00:00
AdrianoDev c8a6b318cb Dopo il login si atterra sulla scelta della piattaforma
Chiesto da Adriano: si entra e si sceglie dove andare, anche quando la scelta
e' una sola. Prima ogni ruolo saltava dritto nella sua sezione.

Non basta cambiare landingFor: /piattaforme era riservato ai ruoli admin e
piattaforme, quindi mandarci un cliente avrebbe prodotto un CICLO di redirect
- il middleware nega la pagina, rimanda a landingFor, landingFor rimanda dove
il middleware nega. L'indice diventa la casa comune (aperto a ogni ruolo),
mentre Stress Index, che sta sotto lo stesso percorso, resta riservato: la sua
regola viene prima, perche' vince la prima che combacia.

L'indice mostra a ognuno solo le sezioni che puo' aprire, quindi la scelta non
espone mai una porta chiusa.

Un test nuovo presidia l'invariante che rende impossibile il ciclo: la
destinazione di landingFor deve essere accessibile a TUTTI i ruoli.

Verificato in produzione: il cliente demo atterra sulla scelta e vede la sola
card Longevity, adriano vede Campus e Stress Index, e da li' il percorso
prosegue fino al referto.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EndnceRA5WnA6rvA5iV9WL
2026-09-03 12:53:32 +00:00
AdrianoDev 62b19b21d9 Il link del sito all'area riservata punta all'host che esiste
Era rimasto apps.insanitylab.it, che e' l'indirizzo definitivo ma non ha
ancora un record su Aruba: alla fusione il menu del sito avrebbe portato a un
errore di DNS, e nessun test se ne sarebbe accorto perche' il link esce dal
dominio e non lo serve nessuna rotta interna.

Ora punta a piattaforme.tielogic.xyz, con un test che lo presidia.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EndnceRA5WnA6rvA5iV9WL
2026-09-03 12:45:17 +00:00
AdrianoDev 89d2186e69 Il vecchio sottodominio esce anche dal compose del ramo
insanitylab.tielogic.xyz e' stato tolto dal DNS: una regola Traefik che lo
nomina fa fallire una sfida ACME a ogni tentativo, ed e' cio' che Let's
Encrypt limita. Resta il redirect di www.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EndnceRA5WnA6rvA5iV9WL
2026-09-03 12:41:53 +00:00
AdrianoDev f5fac89c1b Via dal router il vecchio sottodominio, tolto dal DNS
Adriano ha rimosso il record insanitylab.tielogic.xyz il 03/09/2026. Una
regola Traefik che nomina un host inesistente non e' inerte: il router chiede
un certificato per i nomi della sua regola, e la sfida ACME su un nome che
non risolve fallisce a ogni tentativo - proprio cio' che Let's Encrypt limita.

Resta il redirect di www verso l'apex, che e' l'unico ancora vero.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EndnceRA5WnA6rvA5iV9WL
2026-09-03 12:41:52 +00:00
AdrianoDev 15dbe6a397 Le aree riservate hanno il loro host: piattaforme.tielogic.xyz
Creato da Adriano sulla zona tielogic.xyz (Hostinger). Sostituisce il prestito
di insanitylab.tielogic.xyz, durato un'ora: quell'host e' tornato al suo 308
verso l'apex e il sito e' di nuovo esattamente com'era.

allowedDomains, router del container e router di redirect del sito cambiano
insieme: e' la terna che decide di chi e' un host, e sfasarla lascia un
indirizzo senza padrone o un login che risponde 403.

Verificato in HTTPS: certificato emesso per il nome nuovo, login e referto
funzionanti, insanitylab.it 200 e i due redirect al loro posto.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EndnceRA5WnA6rvA5iV9WL
2026-09-03 12:39:29 +00:00
AdrianoDev 665c598462 Il vecchio sottodominio torna al suo redirect
Adriano ha creato piattaforme.tielogic.xyz, che e' il nome giusto per le aree
riservate: il prestito di insanitylab.tielogic.xyz e' durato un'ora e finisce
qui. Il sito torna esattamente come stamattina - apex, www e vecchio
sottodominio - e le piattaforme stanno su un host loro.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EndnceRA5WnA6rvA5iV9WL
2026-09-03 12:39:29 +00:00
AdrianoDev a7b81e2ff4 Le aree riservate partono da insanitylab.tielogic.xyz
Deciso da Adriano il 03/09/2026: il dominio del cliente resta su Aruba e non
si tocca, le piattaforme partono da un sottodominio nostro. Scelto l'host che
risolveva gia' e aveva gia' il certificato - fino a oggi faceva 308 verso
l'apex del sito - invece di aspettare un record DNS nuovo.

Tre modifiche coordinate, che vanno insieme o l'host resta senza padrone:
- allowedDomains dell'app: aggiunto l'host temporaneo accanto a quello
  definitivo. Senza, dietro Traefik il CSRF di Astro respinge ogni POST con
  403 e il login sembra rifiutare le credenziali.
- router del container: Host(insanitylab.tielogic.xyz)
- compose del sito: quell'host esce dal router di redirect (www continua)

Quando il record A "apps" esistera' su Aruba si tengono doppi per un po',
invece di spostare di netto un indirizzo che qualcuno ha gia' salvato.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EndnceRA5WnA6rvA5iV9WL
2026-09-03 12:36:33 +00:00
AdrianoDev 4103376eb1 Il vecchio sottodominio esce dal redirect: serve alle aree riservate
insanitylab.tielogic.xyz faceva 308 verso l'apex dal 12/07. Dal 03/09/2026
ospita le aree riservate (Campus, Stress Index, Longevity) mentre si aspetta
il record A di apps.insanitylab.it su Aruba: il dominio del cliente resta
dov'e', le piattaforme partono da un sottodominio nostro.

www.insanitylab.it continua a reindirizzare come prima.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EndnceRA5WnA6rvA5iV9WL
2026-09-03 12:34:59 +00:00
AdrianoDev 18b0877026 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 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
2026-09-03 12:27:48 +00:00
AdrianoDev 6301f521fd Separazione: le aree riservate diventano un'app propria (apps/platforms)
Campus, Stress Index e Longevity escono dal sito e diventano
apps.insanitylab.it, con utenti, sessioni, login e database propri.
Deciso da Adriano il 03/09/2026: una repo sola, due applicazioni
autosufficienti, cosi' che ognuna resti estraibile.

Misura che ha guidato il taglio: le tre aree condividevano col sito
SOLO l'identita' - ~170 righe fra auth.ts, middleware.ts e safe-next.ts.
Layout, librerie e dati erano gia' disgiunti.

Lato piattaforme:
- ruoli piattaforme/cliente/trainer/admin, senza quelli redazionali
- canAccessPath con default CHIUSO (nel sito era aperto: li' le rotte non
  elencate erano di chiunque fosse loggato, qui ogni sezione e' di qualcuno)
- tutta l'app protetta, si elencano le eccezioni invece dei prefissi protetti
- /api risponde 401/403, non un redirect verso una pagina di login
- tre database distinti: utenti, dati clinici senza nomi, mappatura coi nomi

Lato sito:
- ROLES tornano i tre redazionali; via le regole e il login pubblico
- il login del pannello RESPINGE un account emigrato invece di aprirgli una
  sessione: senza, landingFor lo mandava su /admin, il middleware glielo
  negava, e si formava un ciclo di redirect che sembra un guasto del sito
- l'header non mostra piu' un menu delle piattaforme: un link che esce

Verificato: sito 20 file / 146 test, piattaforme 26 file / 197 test, entrambi
i build passano, e l'app girata davvero in locale - il ruolo piattaforme entra
in Campus e viene rimbalzato da Longevity, il cliente il contrario.

Non ancora in produzione: manca il record DNS di apps.insanitylab.it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EndnceRA5WnA6rvA5iV9WL
2026-09-03 11:21:12 +00:00
AdrianoDev 510d7ca4ec Struttura: il sito esistente si sposta sotto apps/sito
Primo passo della separazione sito / piattaforme decisa da Adriano il 03/09:
due applicazioni autosufficienti, senza workspace di root, cosi' che ognuna
resti un progetto Astro completo ed estraibile.

Qui non cambia niente del codice: solo la radice. I pattern del .gitignore
che avevano una barra erano ancorati alla radice e dopo lo spostamento non
avrebbero piu' coperto niente (data/*.db*, uploads/*): resi validi a
qualunque profondita'.

Suite: 45 file, 347 test verdi prima e dopo lo spostamento.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EndnceRA5WnA6rvA5iV9WL
2026-09-03 11:05:54 +00:00
AdrianoDev c03edccd8e Merge remote-tracking branch 'origin/main' into feat/split-apps
# Conflicts:
#	.gitignore
2026-09-03 11:03:09 +00:00
Adriano Dal Pastro b75e12de86 Calendario: alzato il min-width a 780px, che il layout fisso rende necessario
Coda della modifica precedente, e senza questa quella era una regressione sul
telefono. A colonne uguali la piu' stretta deve contenere la parola piu'
lunga; in layout automatico il browser allargava da solo le colonne piene e
stringeva quelle vuote, quindi il problema non si vedeva.

Il conto, misurato sul Montserrat 400 che il sito serve davvero (non
stimato): REFORMER a .7rem con letter-spacing .1em e' 75,4 px, mentre la
colonna a 720px ne offriva 72,7 di spazio utile — 2,7 in meno. La parola
sarebbe uscita dalla cella proprio dove il minimo si vede, cioe' sul
telefono, che quella tabella la fa scorrere in orizzontale apposta.

A 780px la colonna da' 81,3 px, ~6 di margine. In stampa il conto era gia'
buono (corpo .62rem e padding ridotto: 66,8 px in 77,9).

Il conto e' scritto nel CSS accanto alla regola: chi tocca padding, corpo o
spaziatura deve rifarlo.

185 test verdi.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 08:57:41 +00:00
Adriano Dal Pastro 718ca499c4 Calendario: le sette colonne dei giorni tutte della stessa larghezza
Chiesto da Adriano. La tabella era in layout automatico, quindi il browser
dimensionava ogni colonna sul contenuto: il sabato, che porta la nota «solo
il 12 settembre», veniva largo quasi il doppio, e i giorni senza class si
stringevano. Una griglia oraria con le colonne di misure diverse si legge
male proprio dove serve — nell'incrocio fra un giorno e un'ora.

`table-layout: fixed` piu' una larghezza sulla sola colonna delle ore
(64px): tutto il resto viene diviso in parti uguali fra le sette dei giorni.

E' lo stesso criterio che il PDF applica da sempre — la' le colonne sono
`(larghezzaUtile - larghezzaOra) / GIORNI.length` — quindi adesso le due
versioni dello stesso foglio si somigliano invece di divergere.

La colonna delle ore resta piu' stretta dei giorni, come nel PDF: ci sta
dentro «12.15» e nient'altro.

185 test verdi.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 08:55:14 +00:00
Adriano Dal Pastro 4f031cf571 Calendario: niente class il venerdi' dall'1 al 18 settembre
Chiesto da Adriano l'01/09. Tolte le tre celle del venerdi' dal calendario
'settembre-1-18': MOBILITY alle 8.30, REFORMER alle 9.15 e alle 10.00.

Erano tutte di mattina, quindi in quel periodo il venerdi' resta una colonna
completamente vuota. Resta VUOTA e non barrata: togliere le class non vuol
dire che la sala sia chiusa, e nessuno l'ha detto — il dato distingue le due
cose apposta (`chiusi`), e sceglierne una che non ci e' stata comunicata
sarebbe inventare un'informazione.

⚠️ L'edit e' confinato al primo calendario. Le tre righe del venerdi' sono
IDENTICHE, carattere per carattere, in 'settembre-21-in-poi' — dove il
venerdi' deve restare — quindi una sostituzione sul file intero ne avrebbe
prese sei invece di tre. Verificato dopo: dal 21 settembre il venerdi' ha
ancora PILATES 7.45, MOBILITY 8.30, REFORMER 9.15 e 10.00.

Aggiunto un commento nel dato, perche' confrontando le due griglie il buco
sembra una trascrizione monca e il prossimo che passa lo "corregge".

185 test verdi.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 08:51:13 +00:00
Adriano 6ba8794770 Fuori dal versionamento la cache di Python
Nel repo ora c'e' anche Python — lo script che rigenera la locandina, e i
riferimenti dei test longevity — e ogni esecuzione lascia dietro __pycache__.
Comparivano come file non tracciati a ogni git status, senza essere codice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 13:16:29 +02:00
Adriano Dal Pastro 0ea1cb5e77 Eventi: la citazione di chiusura fra virgolette e piu' grande
Caporali «» attorno alla frase — sono le virgolette italiane, e su un corsivo
di quel corpo reggono meglio delle doppie curve.

Corpo da clamp(1.05rem, 2.4vw, 1.3rem) a clamp(1.35rem, 3.4vw, 1.9rem): ~+45%
sia sul minimo che sul massimo, quindi cresce anche su mobile. Interlinea da
1.8 a 1.5, altrimenti a quel corpo le due righe galleggiavano.

Il <br /> che spezza la frase in due versi resta dov'era.
2026-08-28 11:12:33 +00:00
Adriano 7f96b95e04 Eventi: via il piano dai blocchi, la locandina scende in fondo
Tre ritocchi chiesti dal cliente: il "Primo piano" sotto "Prova i nostri
servizi" spariva il messaggio piu' che aiutare, il download della locandina
sta meglio a fine lettura che nell'apertura, e il titolo del programma va
a capo dopo "Come si svolge".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 11:55:14 +02:00
Adriano 13d73f90a8 Eventi: i test seguono la data separata e l'URL stampato nel QR
Il test dell'inaugurazione cercava la data dentro il titolo, ma i due sono
stati separati in tag distinti il 19 agosto: era rosso da allora, anche in
produzione.

Il nuovo test tiene insieme le tre cose che devono restare d'accordo, perche'
il QR delle locandine affisse non si corregge piu': la rotta servita dal sito,
l'URL cucito nel codice e il PDF che la pagina offre in download.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 11:43:13 +02:00
Adriano 7fd3961469 Eventi: la landing dell'In-Sanity Hour, con la locandina da scaricare
La pagina dell'inaugurazione del 19 settembre: il programma completo della
giornata, gli slot di prova del mattino e del pomeriggio, la festa serale.

Il blocco del titolo è il ritaglio della locandina vera, non una sua
riproduzione: i caratteri originali non li abbiamo, e ricostruirli a occhio
è già stato provato senza riuscirci. Il resto della pagina usa la tipografia
del sito e i colori campionati dalla locandina, così volantino e schermo si
riconoscono come la stessa cosa.

La locandina scaricabile è ricalcata in vettoriale dallo stesso JPEG — tre
colori piatti, quindi ogni forma diventa un tracciato — e monta il QR code
definitivo verso questa pagina, un filo più piccolo dell'originale per
lasciargli attorno il margine muto che lo standard richiede.

I testi non passano più dal pannello contenuti: la pagina è in codice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 11:41:47 +02:00
Adriano 1d6bdd0b3d longevity: revisione finale — il motore ora si calcola, il consenso è atomico, il codice cliente è tipato fuori dal corpo
Sei correzioni dalla revisione finale del 22/08, ognuna con test scritto prima:

1. salvaDalForm calcolava mai i punteggi (calcolaSessione/salvaScore) dopo il
   salvataggio: un cliente compilava venti domande e vedeva un referto vuoto.
   Ora avviene nella stessa transazione del salvataggio.
2. Il corpo della richiesta dell'endpoint questionario è tipato
   (CorpoQuestionario): client_code non esiste in quel tipo, non è più
   prendibile per errore. Il vecchio test a lista di nomi vietati era vacuo
   (cercava body./corpo., il file usa data.) — sostituito con un test
   comportamentale end-to-end sull'endpoint vero.
3. Il consenso: aggiunto il test sul ramo affermativo (non coperto, vacuo
   quanto il punto 2); la nota di consenso ora è nella stessa transazione dei
   dati sanitari; ha una versione propria (CONSENT_VERSION) invece di quella
   del questionario.
4. La prima apertura del referto ora spiega invece di sembrare rotta: Fitness
   Age con nota quando manca, spiegazione del radar prima del grafico,
   copertura sempre visibile, invito a fare i test in sala col trainer,
   numeri formattati all'italiana.
5. Aggiunto il link per ricompilare il questionario nella pagina del referto
   (prima spariva appena esisteva una compilazione).
6. Il test anti-query ora scandisce tutte le pagine sotto src/pages/longevity/,
   non due nomi a mano. Il test anti-giunzione identity/longevity resta con lo
   stesso limite di sempre (createIdentityDb è esportata per creaCliente) —
   corretto solo il commento perché lo dichiari onestamente.

Report con comandi e output letterali:
.superpowers/sdd/2026-08-22-longevity-interfaccia/review-finale-report.md
2026-08-22 15:17:47 +02:00
Adriano a56398ea02 longevity: il controllo sui colori guarda tutto il foglio, non solo lg-score
Filtrava le regole sul selettore ("solo quelle con lg-score nel nome"): un elenco
di quali regole guardare invecchia appena nasce una classe nuova, ed e' successo
subito con .lg-radar__voce-nome, .lg-radar__intro, .lg-macro__nota (Task 3), mai
controllate perche' non contengono quella stringa. Tolto il filtro sul selettore:
ora si scandisce ogni regola del foglio (che e' interamente prefissato lg- per
costruzione), con un elenco di eccezioni dichiarate (selettore + valore + motivo)
per gli usi legittimi di un colore fuori lista (il #fff del testo sul bottone
pieno d'accento). Verificato rosso con un colore-giudizio iniettato sia su una
classe del radar sia su una del macro-score, verde ripristinato in entrambi i casi.
2026-08-22 14:43:57 +02:00
Adriano 6630be7a02 longevity: la prova sul congelamento ora discrimina davvero il ricalcolo
Il peso mutato prima era l'unico sotto-dominio del suo asse (questionario_lifestyle,
1.00 in Stile di Vita & Sonno): in una media pesata a un solo elemento disponibile
il peso si semplifica sia al numeratore sia al denominatore, quindi il test restava
verde anche se refertoDi avesse ricalcolato invece di leggere gli score congelati.
Sostituita con il peso di un sotto-dominio ASSENTE in un asse a piu' elementi con
copertura parziale non nulla (hrv in Recupero & Sistema Nervoso): li' un ricalcolo
sposta davvero la copertura oltre soglia. Verificato rosso col ricalcolo forzato,
verde ripristinato.
2026-08-22 14:32:43 +02:00
Adriano 7f01ee0b45 longevity: il referto del cliente, e il radar che dice cosa non sa 2026-08-22 14:28:29 +02:00
Adriano 8c980750c0 longevity: la connessione a identity non e' piu' raggiungibile fuori da anagrafica.ts
Il revisore ha dimostrato che il test anti-giunzione si aggira senza malizia:
tre file (uno riesporta la connessione a identity, uno quella a longevity, un
terzo le importa da entrambi e le usa insieme) e nessuno dei tre contiene
singolarmente le due parole cercate. Un controllo testuale file-per-file non
puo' vedere una giunzione che attraversa piu' file - quindi la risposta non
e' un test piu' furbo, e' rendere la cosa impossibile invece di sorvegliarla.

- db.ts: rimossa l'esportazione di getIdentityDb. Resta solo getLongevityDb
  (il database senza nomi, puo' usarlo chiunque) e createIdentityDb (serve ai
  test per aprire database in memoria - li' la giunzione e' voluta ed
  evidente, il problema era il singleton condiviso, non la fabbrica).
- anagrafica.ts: getIdentityDb torna dentro, privata (non esportata) e usata
  solo da codicePerUtenteLoggato. Verificato che il bypass a tre file del
  revisore ora fallisce gia' a tsc --noEmit ('has no exported member named
  getIdentityDb'), non solo a un test che lo sorveglia.
- tests/longevity/anagrafica.test.ts: il test anti-giunzione resta come rete
  secondaria, con un commento che dichiara onestamente cosa non vede (una
  giunzione via re-export su piu' file) invece di promettere una garanzia che
  non mantiene.

Due correzioni minori dalla stessa revisione:
- api/longevity/questionario.ts: salvaDalForm (via salvaCompilazione) lancia
  se una risposta porta un test_id sconosciuto al registro; ora e' avvolta in
  try/catch e risponde 400 pulito invece di un 500 generico, come gia' fa
  src/pages/api/contact.ts per l'invio email.
- components/longevity/Questionario.tsx: un campo con tipo_valore='txt' nel
  registro ora rende un input testuale (non piu' sempre "number"), e in
  invio i campi testuali non vengono convertiti in NaN e scartati in
  silenzio - restano fuori esplicitamente, con un commento che dice perche'.
2026-08-22 14:12:43 +02:00
Adriano f0629db3c4 longevity: l'endpoint del questionario apre solo longevity, e il test anti-giunzione diventa strutturale
L'endpoint /api/longevity/questionario apriva anche identity (getIdentityDb)
per risolvere il client_code, violando la garanzia della spec (§4): un solo
modulo, anagrafica.ts, tiene insieme le due connessioni. La logica di
giunzione era gia' al posto giusto (chiamava codicePerUtente, che sta in
anagrafica.ts) ma l'APERTURA delle connessioni no.

- anagrafica.ts: nuova codicePerUtenteLoggato(userId), apre lei stessa
  identity via getIdentityDb() (e' il modulo autorizzato) e delega a
  codicePerUtente. L'endpoint chiama solo questa: apre esclusivamente
  longevity, per salvare.
- tests/longevity/anagrafica.test.ts: il test anti-giunzione cercava due nomi
  letterali (createIdentityDb/createLongevityDb) e non vedeva getIdentityDb/
  getLongevityDb, nati dopo di lui - la stessa lezione del semaforo nel Task 1
  (proteggere il nome invece della cosa). Ora e' strutturale: segnala
  qualunque file (esclusi anagrafica.ts e db.ts) che contenga sia "Identity"
  sia "Longevity", in qualunque forma. Verificato con un file finto sotto
  src/ che apriva entrambe con i nomi nuovi: il test diventa rosso, poi
  torna verde dopo la cancellazione.
2026-08-22 14:01:33 +02:00
Adriano f5ca841d92 longevity: il questionario in pagina, e il consenso che si puo negare davvero
Sostituisce il prototipo del cliente (salvava anche con "No" al consenso
sanitario, perche' controllava solo che il campo non fosse vuoto). Qui il
consenso si verifica PRIMA di scrivere qualunque dato: se non e' affermativo
non si apre nessuna sessione, e la risposta lo dice. Il rifiuto si registra
comunque fra le note di profilo, con esito e versione del questionario: va
dimostrato tanto quanto un assenso.

- src/lib/longevity/vista.ts: campiDelQuestionario (dal registro, non da una
  lista nel codice) e salvaDalForm (consenso prima di tutto, poi delega a
  salvaCompilazione).
- src/pages/api/longevity/questionario.ts: il client_code arriva SEMPRE da
  codicePerUtente sulla sessione, mai dal corpo della richiesta.
- src/lib/longevity/db.ts: getLongevityDb/getIdentityDb, connessioni singole
  per processo (come getDb() del sito), cosi' l'endpoint non apre una nuova
  connessione a ogni richiesta.
- src/components/longevity/Questionario.tsx + pages/longevity/questionario.astro:
  isola React a quattro blocchi (le tre aree di domande + note/consenso), la
  pagina non interroga mai il database da sola.
2026-08-22 13:55:16 +02:00
Adriano 2ea822a84d longevity: test anti-semaforo strutturale (colori ammessi, non nomi vietati) e rotta verificata sul contenuto 2026-08-22 13:43:09 +02:00
Adriano 34c66ca438 longevity: il guscio della piattaforma, senza semaforo sui punteggi 2026-08-22 13:33:21 +02:00
Adriano 779aef32e6 docs: piano dell'interfaccia — questionario e referto del cliente 2026-08-22 13:29:54 +02:00
Adriano 0e9b081a79 spec: cosa ha scoperto la costruzione del motore 2026-08-22 11:02:26 +02:00
Adriano 42a039ae6b schema: il limite del vincolo e' la migrazione che non c'e', non SQLite 2026-08-22 11:01:45 +02:00
Adriano 82a4cd2000 test: chiude l'errore di tipi pre-esistente in middleware.test.ts
Cast di FakeContext a onRequest via 'unknown' invece che diretto: i
due tipi non si sovrappongono abbastanza per tsc (FakeContext è un
doppio minimale, non un APIContext completo). Il typecheck è il
meccanismo di sicurezza di questo motore: uno rosso in permanenza è un
cancello che nessuno guarda più.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XhLbMQ1q7wHwJSykgXRwQF
2026-08-22 10:49:42 +02:00
Adriano c1c868622f test: le sei tabelle dei sollevamenti sorvegliate, e il caso misto
- motore-fisici.test.ts confrontava i tiers letti dal file di
  riferimento, non le costanti TIERS_* del codice: ventiquattro numeri
  senza custode. Ora identifica la tabella dagli argomenti
  dell'oracolo ma usa le costanti vere per calcolare, quindi una
  costante alterata non combacia più con nessun caso.
- motore-caso-reale.test.ts: aggiunto il caso end-to-end mancante, un
  asse valido (Composizione Corporea, via due righe aggiunte al
  registro) e sei insufficienti — oggi la suite copriva solo "tutto
  insufficiente".
- test-fisici.ts: commento in testa che dichiara le venti curve non
  collegate al motore (vocabolario delle curve senza nomi per loro,
  applicaCurva senza modo di ricevere età/sesso) — aggancio è lavoro
  del piano degli import, non "una modifica ai dati" come la spec
  lascia intendere.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XhLbMQ1q7wHwJSykgXRwQF
2026-08-22 10:49:35 +02:00
Adriano 469251366e motore: chiude i tre buchi laterali della barriera dell'insufficiente
- eta_alla_data NULL non usa più il ripiego a zero: senza età la
  Fitness Age è null, non un'età di forma negativa dichiarata valida.
  Gli assi e i macro-score restano calcolabili (non dipendono dall'età).
- applicaCurva valida i parametri richiesti da ciascuna curva prima di
  applicarla: un registro con params vuoti/incompleti dà null, mai il
  punteggio pieno che dava lerp con estremi indefiniti.
- calcolaSessione esclude le misure fuori_range=1 (§9 della spec): non
  entrano nello score finché non esiste la colonna di conferma (§12,
  punto aperto).
- leggiScore(db, sessioneId): legge la tabella score ritipata come
  Punteggio, con l'ultimo calcolo per tipo+elemento esplicito nella
  query (MAX(id)), non l'ordine naturale delle righe — salvaScore non
  sovrascrive di proposito (la storia degli score si tiene, discende
  dal congelamento della §6).
- documentata la seconda divergenza dall'oracolo, mai scritta finora:
  la Fitness Age esclude gli assi insufficienti, l'oracolo li include
  comunque. Comportamento giusto, ma cambia il numero (25,0 contro
  34,8 sullo stesso profilo parziale).
- nuovo test che prova che i pesi di due model_version diverse restano
  separati (pesiDi filtra su model_version, non li fonde).

Ogni fix verificato in TDD (test rosso prima, verde dopo) e ri-rotto a
mano per confermare che discrimina davvero (vedi report).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XhLbMQ1q7wHwJSykgXRwQF
2026-08-22 10:49:25 +02:00
Adriano c4524e3a73 schema: la tabella score rifiuta un valore sotto uno stato che lo nega
CHECK ((stato='ok' AND valore NOT NULL) OR (stato='insufficiente' AND
valore NULL)): il tipo Punteggio protegge il codice, non la tabella, e
l'interfaccia leggerà 'score' direttamente. Idempotente sui database
nuovi (CREATE TABLE IF NOT EXISTS); un longevity.db già esistente NON
riceve il vincolo, SQLite non lo permette via ALTER TABLE.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XhLbMQ1q7wHwJSykgXRwQF
2026-08-22 10:49:12 +02:00
Adriano d22f88ad71 longevity: la prova sul profilo con una sola area misurata
Task 6 - la prova sul caso reale documentato dal cliente (dati sintetici,
nessuna persona reale). Il test smontava un difetto nel task 5: RisultatoSessione
esponeva fitnessAge come Punteggio invece che number|null (come lo produce
calcolaFitnessAge in cascata.ts), quindi un fitnessAge insufficiente non era mai
`null`. Corretto in motore/index.ts, con fitnessAgeCopertura a fianco per non
perdere l'informazione che salvaScore scrive in `score`.
2026-08-22 10:07:28 +02:00
Adriano ac8b377f84 longevity test: motivazione corretta per il caso 'test disattivato', e media verificata numericamente
Il commento del test 'un test disattivato non entra nel calcolo' diceva
che q_attivita fosse l'unica voce del sotto-dominio questionario_lifestyle:
falso, ne ha sei (q_attivita, q_alimentazione, q_sigarette, q_alcol_life,
q_luce, q_schermi). Il motivo vero per cui il test funziona e' che il
fixture ne compila solo una delle sei, quindi disattivarla svuota il
sotto-dominio. Corretto il commento a dire il motivo vero, con nota su
cosa succederebbe se il fixture cambiasse.

Aggiunto anche il test mancante sulla regola della media: q_attivita(3)=65
e q_alimentazione(6)=60, stesso sotto-dominio, verificano che il valore
dell'asse "Stile di Vita & Sonno" sia la loro media (62.5), diversa sia
dalla somma (125) sia da ciascun addendo (65, 60) — nessun test esistente
controllava il VALORE della media, solo lo stato.
2026-08-22 10:02:59 +02:00
Adriano f238984828 longevity: il motore legge curve e pesi dal registro, e congela i punteggi
Task 5: applicaCurva smista sulla curva dichiarata in registro_test (non
scritta nel codice); calcolaSessione aggrega le misure attive per
sotto-dominio (media quando piu' test contribuiscono allo stesso
sotto-dominio) e usa cascata.ts coi pesi letti da pesi (pesiDi) per i
sette assi, i tre macro-score e la Fitness Age; salvaScore congela il
risultato in score, con valore a null quando lo stato e' insufficiente.

Corretto anche un refuso nel test del brief ('un test disattivato non
entra nel calcolo'): disattivava q_ore_sonno, ma quella voce sta nel
sotto-dominio questionario_sonno (peso 0.20 su Recupero & Sistema
Nervoso, sempre sotto COPERTURA_MINIMA=0.4 finche' hrv/pressione/hrr non
hanno una voce nel registro), quindi l'asse restava insufficiente prima
e dopo e il JSON risultava identico (verificato: il test falliva con le
due stringhe uguali). Corretto usando q_attivita, l'unica voce del
sotto-dominio questionario_lifestyle che pesa 1.00 su Stile di Vita &
Sonno: disattivarla fa cadere la copertura da 1.00 a 0 e lo stato passa
da ok a insufficiente, una differenza vera.
2026-08-22 09:53:17 +02:00
Adriano 93ecc3d367 longevity: la copertura si confronta grezza, non arrotondata
Bug in aggrega(): la soglia veniva confrontata con la copertura gia'
arrotondata a due decimali, quindi 0.396 (< 0.40) passava per 'ok' perche'
arrotondava a 0.40. Confronto ora sulla copertura grezza; l'arrotondamento
resta solo sul valore esposto. Generalizzata arrotonda1 in curve.ts a un
numero qualsiasi di decimali (bit-exact, banker's rounding) e riusata qui a
due decimali, invece di tenere una seconda logica di arrotondamento nel
motore.
2026-08-22 09:39:58 +02:00
Adriano 98a633b3e1 longevity: la cascata a quattro livelli, con il tipo che chiude la trappola dell insufficiente 2026-08-22 09:28:40 +02:00
Adriano f029820a14 longevity: il test delle funzioni escluse copre anche score_generic_range_local
Il test controllava solo l'assenza dell'agilita (scoreAgilita/scoreAgilityMs)
e non della seconda funzione esclusa dal brief, score_generic_range_local:
se qualcuno la reintroducesse un domani nessun test se ne accorgerebbe.
Aggiunta l'assertion con lo stesso criterio, rinominato il test perche' ora
copre entrambe le esclusioni.
2026-08-22 09:24:42 +02:00
Adriano 4d76030839 longevity: unico arrotondamento al pari nel motore, esatto sul valore binario
Sposta arrotonda1 (round-half-to-even come Python) da test-fisici.ts a
curve.ts, esportata, cosi' entrambi i moduli condividono la stessa funzione
invece di due copie che sui pareggi potrebbero divergere.

Nel farlo, la versione precedente (moltiplica per 10, tolleranza 1e-9 sul
pareggio) si e' rivelata sbagliata su un caso reale: 86.35 non e' un
pareggio nel double che lo rappresenta (vale 86.34999999999999431...,
Python arrotonda a 86.3 senza ambiguita'), ma 86.35*10 arrotonda ESATTAMENTE
a 863.5 in virgola mobile, un pareggio fasullo creato dalla moltiplicazione.
La versione naive dava 86.4. Sostituita con un confronto esatto: scompone il
double in mantissa/esponente (bit IEEE 754) e confronta con aritmetica
razionale su BigInt, senza mai passare per una moltiplicazione che possa
introdurre o cancellare un pareggio.

Aggiunti in motore-curve.test.ts i casi che dimostrano il pareggio vero
(86.25 -> 86.2, 86.75 -> 86.8) e quello fasullo (86.35 -> 86.3, non 86.4:
blocca la regressione appena descritta).
2026-08-22 09:16:31 +02:00
Adriano 6f926e3197 longevity: le curve dei test fisici, verificate contro l oracolo 2026-08-22 09:09:23 +02:00
Adriano c63bb1fe80 longevity: chiude i tre buchi nei test delle curve — ramo crescente di curvaGradini mai esercitato, test zeroVal non discriminante, cardinalita mancante su due confronti con l oracolo 2026-08-22 09:02:20 +02:00
Adriano 36a2cf3fb6 longevity: le cinque curve del questionario, verificate contro l oracolo 2026-08-22 08:51:27 +02:00
Adriano 11ecc8305b piano: il codice sta nel repo, non anche nel piano 2026-08-22 08:48:26 +02:00
Adriano 236cdce076 longevity: rigenera riferimento.json con la griglia corretta (plateau offset, 2km walk multi-punto, confini plank) 2026-08-22 08:42:29 +02:00
Adriano c5249255f0 piano: rimette i commenti della griglia al posto giusto 2026-08-22 08:39:06 +02:00
Adriano 998487ed62 piano: infittita la griglia dove non aveva capacita diagnostica 2026-08-22 08:38:54 +02:00
Adriano 7cb49c6a49 longevity: l'oracolo del motore e la griglia di riferimento 2026-08-22 08:29:53 +02:00
Adriano bc9d31e193 docs: piano del motore di calcolo, verificato contro l'oracolo del cliente 2026-08-22 08:27:51 +02:00
Adriano 59b453c67e longevity: annotate le conferme di Nicola del 22/08 (plank su due assi, soglia unica al 40%) 2026-08-22 08:02:20 +02:00
Adriano 52ff28b69d spec: i sei punti emersi costruendo lo strato dati, piu' la curva x10_inv che mancava 2026-08-21 20:34:49 +02:00
Adriano 763e5611e6 longevity: revisione finale - codice cliente non riusabile, ruoli allineati, middleware verificato
- creaCliente: il codice si deduce dal massimo fra identity E longevity (non piu' solo
  identity), cosi' un client_code cancellato dall'anagrafica non torna mai disponibile
  e non si attribuiscono le misure di un vecchio cliente a uno nuovo. Compensazione se
  la scrittura su longevity fallisce dopo quella su identity.
- ROLES unica fonte in auth.ts (Role e isRole derivati); users.astro e users/new.astro
  usano quella lista invece di array scritti a mano che dimenticavano cliente/trainer.
- Commento falso su cosa protegge il middleware, riscritto: rimanda a isProtectedPath.
- vitest.config.ts: alias per astro:middleware (stesso bersaglio della pipeline vite di
  Astro), cosi' src/middleware.ts e' finalmente importabile e testabile in isolamento -
  prima nessun test lo caricava davvero.
- export.test.ts: asserzione posizionale per colonna, non piu' solo intestazioni scritte
  a mano; intercetta un riordino della SELECT che disallinea etichette e valori.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 20:30:23 +02:00
Adriano e6c2ac9148 longevity: test atomicità corretto - verifica rollback anche dei campi liberi 2026-08-21 20:08:12 +02:00
Adriano 2024c32965 longevity: export pseudonimizzato per costruzione 2026-08-21 20:05:19 +02:00
Adriano 91e6487da3 longevity: atomicità compilazione questionario con transazione unica 2026-08-21 20:02:23 +02:00
Adriano f21b64fcc5 longevity: salvataggio compilazione questionario, versionata 2026-08-21 19:57:35 +02:00
Adriano d3abbf9bf7 task-5 fix: isRole, login redirect, create-user script per cliente e trainer 2026-08-21 19:53:12 +02:00
Adriano 473c0d8c36 fix: isProtectedPath nel middleware, landingFor per cliente e trainer 2026-08-21 19:44:08 +02:00
Adriano 502ccb1585 longevity: ruoli cliente e trainer con le rispettive rotte 2026-08-21 19:41:45 +02:00
Adriano b7ceece4ed test(anagrafica): allarga la scansione a tutto src/ per rilevare giunzioni non autorizzate
Rafforza il test che verifica l'isolamento tra identity e longevity.
Invece di scandire solo src/lib/longevity/, ora scandisce ricorsivamente
tutto src/ inclusi .astro e .ts, saltando node_modules e dist.

Preserva le due sole esclusioni autorizzate: anagrafica.ts (il modulo
autorizzato) e db.ts (dove le factory sono definite).

In questo modo il test rileva violazioni anche da route API e pagine
Astro, non solo da moduli della cartella longevity.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 19:38:56 +02:00
Adriano 9943312071 longevity: anagrafica, unico punto di giunzione fra identita e clinica
Implementa il modulo che unisce i due database (identity e longevity)
rispettando la separazione richiesta dal cliente: pseudonimizzazione
dei dati clinici. Il modulo esporta tre funzioni:

- creaCliente: registra un soggetto con il suo codice progressivo ISL-NNNN
- codicePerUtente: risale dal user_id del sito al client_code
- etaAllaData: calcola l'eta' al momento della misura (non la data di nascita)

Un test verifica che nessun altro modulo apra entrambe le connessioni,
preservando il firewall fra dati clinici e identita'.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 19:33:23 +02:00
Adriano 7779c6b830 test: verifica che fuoriRange non trunca il valore numerico 2026-08-21 19:31:15 +02:00
Adriano a18bb7c02f longevity: registraMisure, unico varco con validazione sul registro 2026-08-21 19:27:41 +02:00
Adriano 370dcbda56 longevity: registro dei test e pesi versionati come dato 2026-08-21 19:22:32 +02:00
Adriano 1821a0a8d4 spec: score.nome diventa score.elemento, coerente con la tabella pesi 2026-08-21 19:17:42 +02:00
Adriano ad2fc0d510 fix: rinomina score.nome in score.elemento per coerenza con tabella pesi 2026-08-21 19:17:03 +02:00
Adriano f16012c8bd longevity: schema dei due database, identity separato da longevity 2026-08-21 19:15:17 +02:00
Adriano bdc44dd3c5 chore: ignora gli artefatti di lavoro dei subagenti in .superpowers/ 2026-08-21 19:11:44 +02:00
Adriano 534833c767 docs: piano di implementazione dello strato dati longevity
Sette task in TDD: schema dei due database, registro test e pesi versionati,
registraMisure come unico varco, anagrafica che tiene separata identita' e
clinica, ruoli cliente/trainer, salvataggio del questionario, export.

Alla fine esiste una libreria testata e nessuna pagina: il motore di calcolo
e le schermate sono i due piani successivi.
2026-08-21 19:05:07 +02:00
Adriano 02d0224e5a docs: design della piattaforma IN-SANITY LONGEVITY PROJECT
Fascicolo unico del cliente dentro il sito esistente, ramo separato e nessun
deploy. Tre database: sito, identity (codice <-> nome) e longevity (misure,
mai un nome) - cosi' l'export per le statistiche e' gia' pseudonimizzato per
costruzione, come chiesto da Nicola il 21/08.

Le misure sono un registro (una riga per valore) e i test sono dato, non
codice: togliere l'agilita' o passare l'HRR da 2' a 1' non e' una migrazione.
I pesi stanno in tabella versionata, cosi' il congelamento degli score
storici ha davvero qualcosa dietro.

Punteggi congelati con due versioni distinte (domande e modello di calcolo).
Il tipo di ritorno del motore rende impossibile mostrare un numero pieno dove
la regola vuole il tratteggio.
2026-08-21 18:41:06 +02:00
Adriano ecab67805b Data dell'inaugurazione piu' grande 2026-08-19 10:34:38 +02:00
Adriano cdfbf194c2 Eventi: la data dell'inaugurazione si legge da lontano
Chiesto da Adriano il 19/08: era 0,82rem sotto un titolo che arriva a 2,6rem, e
sembrava una didascalia. Ora cresce con la finestra come il titolo, restando un
gradino sotto.
2026-08-19 10:34:12 +02:00
Adriano 76adef3fc7 Titolo e data dell'inaugurazione su due righe 2026-08-19 10:31:49 +02:00
Adriano d079019dcf Eventi: titolo e data dell'inaugurazione su due righe
Chiesto da Adriano il 19/08. Sono due tag e non un testo con un a capo dentro:
la data si cambia da sola se l'evento si sposta, e un ritorno a capo dentro un
valore di testo non sopravvive all'editor del pannello.

Il titolo vive anche nel database: in produzione va aggiornato
eventi.inaugurazione.title a "Inaugurazione", altrimenti resta la riga unica.
Il tag nuovo (.date) non e' in tabella e viene dal seed.
2026-08-19 10:30:07 +02:00
Adriano 48909e8c8b Foto del team senza logo, e la foto di Eva torna nel repo 2026-08-19 10:25:18 +02:00
Adriano cc6945c3d1 Eva: la foto della pagina About torna nel repo, senza logo
La foto di Eva in pagina non veniva dal repo ma da un file caricato dal
pannello il 12/07 (tag team.eva.image-about): lo scatto giusto, ritagliato piu'
largo, ma con il logo ancora nell'angolo. Il file del repo era l'altro ritaglio,
piu' stretto, e non lo vedeva nessuno.

Ora team-eva-dark.jpg E' quella foto, col logo rimosso: maschera dei pixel del
logo allargata di 4 px per prendere i bordi sfumati, ricostruzione biharmonic
(scikit-image) e grana del tessuto ripresa dalla stoffa 80 px piu' in alto —
una toppa liscia si sarebbe notata.

Cosi' tutte le foto stanno in un posto solo e passano dall'ottimizzazione della
build (54 KB webp invece di 177 KB jpeg servito com'e'). In produzione va
svuotato il tag team.eva.image-about, altrimenti continua a vincere il file
caricato. Originale col logo su
wasabi://.../foto-team/team-eva-dark-ORIGINALE-col-logo.jpeg; il file su
uploads/ resta dov'e'.

Il ritaglio e' 864x1240 (0.697) invece di 1547x1920 (0.806): le card ritagliano
a 3:4, quindi qui si perde qualcosa sopra e sotto invece che ai lati. Verificato
sulla card renderizzata.
2026-08-19 10:24:13 +02:00
Adriano f7a9ae47d1 Foto del team senza il logo nell'angolo
Adriano ha tolto un piccolo logo dall'angolo in basso a destra delle sette foto
scure della pagina About: alessandro, alice, donata, eleonora, eva, giulia,
nicola. Originali in wasabi://.../Sito_Web/foto-team-originali-20260819.zip.

Stesso formato di prima (JPEG 1547x1920, RGB): l'unica differenza e' un riquadro
di ~80x80 px nell'angolo, verificato confrontando le immagini. I file pesano il
doppio perche' risalvati a qualita' piu' alta, ma in pagina non cambia nulla: la
build li ricomprime in webp attorno agli 85 KB.

Nota: la foto di Eva in pagina NON e' questa, e' un ritaglio piu' largo dello
stesso scatto caricato dal pannello il 12/07 (tag team.eva.image-about), che il
logo ce l'ha ancora.
2026-08-19 10:16:29 +02:00
Adriano 9772b3a3f6 Il titolo della pagina About us 2026-08-19 09:43:49 +02:00
592 changed files with 56753 additions and 316 deletions
+11 -4
View File
@@ -3,7 +3,14 @@ dist/
.astro/
.env
.env.bak*
data/*.db*
uploads/*
!uploads/.gitkeep
assets-src/
**/data/*.db*
**/uploads/*
!**/uploads/.gitkeep
**/assets-src/
# Artefatti di lavoro dei subagenti (ledger, brief, review package)
.superpowers/
# Cache di Python generata rieseguendo l'oracolo
__pycache__/
*.pyc
+7
View File
@@ -0,0 +1,7 @@
node_modules
dist
data
.env
.git
tests
*.md
+30
View File
@@ -0,0 +1,30 @@
# Stage 1: build
FROM node:22-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: runtime
FROM node:22-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=build /app/dist ./dist
COPY scripts ./scripts
# Contenuti del Biohacking Campus (generati con sync-campus e committati nel repo)
COPY campus-content ./campus-content
# Tre database distinti, tutti nello stesso volume:
# - piattaforme.db utenti e sessioni di quest'app (NON quelli del sito)
# - longevity.db misure e punteggi, senza nomi
# - identity.db la mappatura codice ↔ persona, l'unico file con dei nomi dentro
ENV HOST=0.0.0.0 \
PORT=4321 \
DB_PATH=/app/data/piattaforme.db \
LONGEVITY_DB_PATH=/app/data/longevity.db \
IDENTITY_DB_PATH=/app/data/identity.db
EXPOSE 4321
CMD ["node", "./dist/server/entry.mjs"]
+69
View File
@@ -0,0 +1,69 @@
# InsanityLab — aree riservate (`apps.insanitylab.it`)
Applicazione Astro autosufficiente con le tre aree riservate: **Biohacking Campus**,
**Stress Index** e **Longevity**. Nasce il 03/09/2026 separando dal sito
(`apps/sito`, `insanitylab.it`) tutto ciò che sta dietro un login.
## Cosa la rende separata
| | Sito | Quest'app |
|---|---|---|
| Host | `insanitylab.it` | `apps.insanitylab.it` |
| Utenti | 3 redazionali (`admin`, `superuser`, `user`) | uno per ogni cliente del centro (`piattaforme`, `cliente`, `trainer`, `admin`) |
| Database | `insanitylab.db` (contenuti, articoli, utenti del sito) | `piattaforme.db` + `longevity.db` + `identity.db` |
| Login | `/admin/login`, solo redazione | `/login`, ed è l'unica porta |
Non c'è SSO e non c'è cookie condiviso: **una sessione del sito qui non vale**, ed è la
separazione, non un difetto. Le due `lib/auth.ts` sono parenti ma non sono la stessa cosa —
~170 righe duplicate una volta sola, libere di divergere come già divergono i ruoli.
## Sviluppo
```bash
npm ci
npm run dev # http://localhost:4321
npm test # 26 file, 197 test
npm run create-user -- <nome> <password> <ruolo> # ruoli: piattaforme, cliente, trainer, admin
```
I dati stanno in `data/` (gitignorata): `piattaforme.db`, `longevity.db`, `identity.db`.
## Rilascio
⚠️⚠️ **Il DNS viene prima del container.** `apps.insanitylab.it` deve già risolvere verso
l'IP della VPS quando si alza lo stack: avviarlo prima significa una catena di sfide TLS
fallite, e Let's Encrypt limita proprio quelle. La zona è su **Aruba**, serve un record
**A** `apps` → l'IP della macchina.
```bash
mkdir -p /opt/docker/insanitylab-platforms/data # i dati stanno FUORI dal clone git
docker compose up -d --build
node scripts/migra-utenti.mjs # prova a vuoto
node scripts/migra-utenti.mjs --applica # porta gli utenti dal DB del sito
```
## Trappole già pagate
⚠️ **Le versioni si fissano esatte.** `^7.0.5` sulla nuova app aveva risolto ad **astro
7.3.0**, che con `@astrojs/node 11.1.5` non si parla (`"./_internal/logger" is not
exported`) e il build muore. Astro, l'adapter node e l'integrazione react sono pinnati
alle stesse versioni che girano in produzione sul sito.
⚠️ **`security.allowedDomains` in `astro.config.mjs` non è decorativo.** Dietro Traefik il
TLS termina al proxy: senza quell'elenco Astro ignora `X-Forwarded-Proto`/`Host` e il
controllo CSRF respinge **ogni** POST con 403 — cioè il login smette di funzionare e
l'errore sembra «credenziali sbagliate».
⚠️ **`name: insanitylab-platforms` nel compose.** Senza, il progetto si chiamerebbe
`platforms` (dal nome della cartella) e un `down --remove-orphans` dato altrove potrebbe
toccare container che non sono suoi.
⚠️ **Il default di `canAccessPath` è chiuso**, al contrario del sito da cui viene: là le
rotte non elencate (blog, upload) erano di chiunque fosse loggato, qui ogni sezione è di
qualcuno. Una rotta nuova senza regola risulta **vietata**, e lo si scopre provandola.
⚠️ **Le API rispondono con uno stato, non con un redirect** (401/403 su `/api/*`): mandare
un 302 verso il login a chi ha chiesto del JSON produce un 200 con dentro dell'HTML.
⚠️ **Stress Index non ha un backend**: `src/lib/stress-index/data.ts` sono dieci clienti
inventati scritti a mano. In linea c'è la sua interfaccia, non i suoi dati.
+27
View File
@@ -0,0 +1,27 @@
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
import react from '@astrojs/react';
export default defineConfig({
site: 'https://apps.insanitylab.it',
output: 'server',
// ⚠️ Dietro Traefik il TLS termina al proxy: senza allowedDomains Astro ignora
// X-Forwarded-Proto/Host e il checkOrigin CSRF respinge OGNI POST con 403 — cioè
// il login smette di funzionare, e l'errore sembra "credenziali sbagliate".
// Stessa configurazione del sito, con l'host di quest'app.
security: {
allowedDomains: [
// L'host definitivo, quando il record A su Aruba ci sara'.
{ hostname: 'apps.insanitylab.it', protocol: 'https' },
// ⚠️ L'host in uso oggi (03/09/2026): il dominio del cliente resta su Aruba e le aree
// riservate stanno su un sottodominio nostro. Finche' e' in uso deve stare qui:
// senza, dietro Traefik il controllo CSRF di Astro respinge ogni POST con 403 e il
// login sembra rifiutare le credenziali.
{ hostname: 'piattaforme.tielogic.xyz', protocol: 'https' },
],
},
adapter: node({ mode: 'standalone' }),
integrations: [react()],
});

Before

Width:  |  Height:  |  Size: 370 KiB

After

Width:  |  Height:  |  Size: 370 KiB

Before

Width:  |  Height:  |  Size: 230 KiB

After

Width:  |  Height:  |  Size: 230 KiB

Before

Width:  |  Height:  |  Size: 229 KiB

After

Width:  |  Height:  |  Size: 229 KiB

Before

Width:  |  Height:  |  Size: 276 KiB

After

Width:  |  Height:  |  Size: 276 KiB

Before

Width:  |  Height:  |  Size: 255 KiB

After

Width:  |  Height:  |  Size: 255 KiB

Before

Width:  |  Height:  |  Size: 222 KiB

After

Width:  |  Height:  |  Size: 222 KiB

Before

Width:  |  Height:  |  Size: 390 KiB

After

Width:  |  Height:  |  Size: 390 KiB

Before

Width:  |  Height:  |  Size: 283 KiB

After

Width:  |  Height:  |  Size: 283 KiB

Before

Width:  |  Height:  |  Size: 408 KiB

After

Width:  |  Height:  |  Size: 408 KiB

Before

Width:  |  Height:  |  Size: 308 KiB

After

Width:  |  Height:  |  Size: 308 KiB

Before

Width:  |  Height:  |  Size: 196 KiB

After

Width:  |  Height:  |  Size: 196 KiB

Before

Width:  |  Height:  |  Size: 257 KiB

After

Width:  |  Height:  |  Size: 257 KiB

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 172 KiB

Before

Width:  |  Height:  |  Size: 377 KiB

After

Width:  |  Height:  |  Size: 377 KiB

Before

Width:  |  Height:  |  Size: 247 KiB

After

Width:  |  Height:  |  Size: 247 KiB

Some files were not shown because too many files have changed in this diff Show More