Compare commits

..

36 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
585 changed files with 11602 additions and 439 deletions
+5 -4
View File
@@ -3,13 +3,14 @@ dist/
.astro/ .astro/
.env .env
.env.bak* .env.bak*
data/*.db* **/data/*.db*
uploads/* **/uploads/*
!uploads/.gitkeep !**/uploads/.gitkeep
assets-src/ **/assets-src/
# Artefatti di lavoro dei subagenti (ledger, brief, review package) # Artefatti di lavoro dei subagenti (ledger, brief, review package)
.superpowers/ .superpowers/
# Cache di Python generata rieseguendo l'oracolo # Cache di Python generata rieseguendo l'oracolo
__pycache__/ __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