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
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.
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.
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'.
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.
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.
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
- 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
- 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
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
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`.
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.
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.
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.
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.
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).
- 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>
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>
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>
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.
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.
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.
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.
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.
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.
Segnalato da Adriano il 19/08. Il valore vive anche nel database dei contenuti,
dove viene letto per primo: il seed da solo non avrebbe cambiato la pagina, e
l'aggiornamento del tag about.hero.title va fatto in produzione.
Segnalato da Adriano il 19/08: "Check-up Avanzato" era appiccicato alla foto
sopra e si leggeva come una didascalia invece che come l'inizio di una parte
nuova.
Lo stacco e' un padding e non un margine: sopra c'e' gia' il margine
dell'immagine, e due margini verticali adiacenti collassano fra loro — con
margin-top lo spazio sarebbe rimasto quello di prima.
Chiesta da Adriano il 19/08. Restava in coda alle FAQ e ripeteva, in versione
piu' povera, cio' che la sezione Check-up Avanzato racconta per esteso.
In pagina resta una sola domanda. I due tag del pannello
(service.profilazione.faq.2.q e .a) non sono piu' letti da nessuno.
Il documento MODIFICHE_PER_SITO.pdf porta cinque cose.
1. "Small Group" si chiama "Personalized Training Group". Il nome vive anche
nel database dei contenuti, dove `resolveContent` legge prima della copia
nel codice: `scripts/testi-17-agosto.mjs` aggiorna le 19 righe rimaste,
una volta sola, correggendo anche gli articoli ("Lo Small Group" -> "Il
Personalized Training Group"). Lo slug della scheda resta `small-group`:
cambiarlo romperebbe i link gia' girati su WhatsApp.
2. Dalla pagina Servizi le card instradano dove serve. "One to One" apre la
vista Training con One to One, Personalized Training Group e Rehab;
"Class" quella con Holistic e Reformer. Le viste sono `/training/<vista>`;
una vista inventata e' 404, non l'elenco intero, altrimenti un indirizzo
rotto continuerebbe a sembrare buono.
3. La Performance Class non si eroga piu': la scheda che la spiegava
risponde 404, la card "Class" resta in griglia. Con la scheda e' caduta la
tabella prezzi, che era solo sua.
4. Il Check-up racconta Base e Avanzato: trafiletto nuovo nella card e
sezione "Check-up Avanzato" nella scheda, con la Wellness Tower e i cinque
punti. Il calendario delle class si apre ora anche dalla card Class in
griglia e dalla scheda Reformer, oltre che da Holistic.
5. "Promo ed eventi" diventa una pagina di scelta: le promozioni restano su
/promo-eventi/promo, l'inaugurazione del 19 settembre sta su
/promo-eventi/eventi. Il testo dice che il programma della giornata manca
ancora, ed e' editabile dal pannello.
Corretti due link del footer che portavano a schede inesistenti
(`/services/personal-training` non e' mai stato uno slug).
181 test verdi (157 + 24 nuovi), build ok, `astro check` invariato.
Il popup mostrava solo l'orario del momento: chi guarda ad agosto vuole sapere
com'e' settembre per intero, e una griglia sola gli fa credere che valga per
sempre. Ora ci sono tutti quelli non ancora scaduti, ognuno con la sua etichetta
- In vigore, oppure la data da cui parte - e quelli passati spariscono da soli.
Il bottone scarica /calendario/tutti.pdf: un orario per pagina, con le giornate
fuori griglia ripetute su ognuna, perche' un foglio stampato da solo deve bastare
a se stesso. Gli id singoli restano validi per mandarne uno solo.
calendarioDaMostrare (uno) diventa calendariDaMostrare (tutti): una funzione
sola, non due che dicono cose simili - e' lo stesso difetto dei due link della
pagina servizi, che erano due espressioni per lo stesso indirizzo e sono
divergute.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Quei due giorni non stanno in nessuna delle due griglie perche' non hanno class:
il 19 c'e' la presentazione della nuova apertura, il 20 si sta chiusi. Scritto
come avviso con la sua finestra, il vuoto smette di sembrare una trascrizione
incompleta e diventa un'informazione.
Gli avvisi compaiono in cima al popup e in fondo al PDF - e' il file che gira su
WhatsApp, e chi lo riceve non ha la pagina davanti. Spariscono da soli quando
sono passati: un avviso scaduto dice che il centro e' chiuso in un giorno gia'
trascorso, e chi legge non sa piu' se valga per quest'anno o per il prossimo.
Lo spazio riservato in fondo alla pagina cresce con gli avvisi, altrimenti la
griglia si allunga fino a spingerli su una seconda pagina.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Le due celle coperte dal riquadro di Excel nel foglio dal 21 settembre: Adriano
conferma che non c'e' nulla. Il dato non cambia, cambia il commento - quel vuoto
adesso e' una risposta, non una lettura mancata.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Scarica in PDF" non apre piu' la stampa del browser: e' un link a
/calendario/<id>.pdf che restituisce un documento disegnato dal dato con PDFKit,
con Content-Disposition attachment. Serve perche' il calendario si gira su
WhatsApp, ed e' cosi' che questo centro parla coi clienti: chi lo riceve non ha
il sito aperto davanti.
Niente browser headless: Chromium in un container da 2 vCPU costa centinaia di
megabyte e un processo per richiesta, per una griglia di sette colonne.
Le linee della griglia si tracciano in una passata a parte, dopo i riempimenti:
disegnandole riga per riga, il riempimento della riga seguente copriva quella
appena fatta e due lezioni uguali in fasce consecutive si leggevano come un
blocco unico lungo il doppio.
Quattro test: intestazione %PDF- e coda %%EOF (un documento non chiuso arriva
monco e il browser mostra una pagina bianca invece di un errore), una pagina
sola, nomi di file distinti. Un id sconosciuto e' 404, non un PDF vuoto.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Le due griglie fornite dal cliente (1-18 settembre, dal 21 in poi) sono
trascritte come dato e ridisegnate nello stile del sito: si leggono da telefono,
si stampano in PDF e per correggere una cella non serve rifare uno screenshot.
Quale griglia compare lo decide la data, come per le edizioni della promo: quella
in vigore, oppure - se nessuna lo e' ancora - la prossima, dichiarando da quando
entra. Serve perche' il link esiste da subito e ad agosto un "nessun calendario"
non direbbe niente a chi vuole gli orari di settembre.
Il popup e' un <dialog> nativo: fondale, Esc e trappola del focus senza
librerie. "Scarica in PDF" stampa la sola griglia, gia' impaginata in orizzontale
dal CSS di stampa. Le discipline hanno una barra colorata a sinistra oltre al
fondo, cosi' restano distinguibili anche quando il browser stampa senza sfondi.
Quattordici test sul dato: confini al secondo in ora italiana, fasce orarie
uniche e in ordine, nessuna lezione in un giorno di chiusura, e il buco vero fra
il 19 e il 20 settembre resta dichiarato invece che nascosto.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Erano due espressioni diverse per lo stesso link e sono divergute: su One to One e
Performance Class la foto apriva la scheda del servizio mentre il testo mandava a
/training, quindi la destinazione dipendeva da dove capitava il dito. Adesso
l'indirizzo lo calcola una funzione sola, usata da entrambi. Verificato sulla
pagina renderizzata: sei card, zero link discordanti.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Senza login (o con una data illeggibile) il parametro veniva ignorato e la pagina
rispondeva con l'edizione corrente: due date diverse davano lo stesso risultato,
e si legge come una funzione rotta invece che come un accesso mancante. Ora una
barra rossa dichiara il motivo e come rimediare.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
La pagina non mostra piu' una promozione sola scritta nel codice: sceglie
l'edizione la cui finestra contiene l'istante della richiesta. Il 20 agosto alle
00:00 la promo agosto lascia il posto agli Opening Bundles senza deploy, e dopo
il 4 settembre la pagina dichiara che non c'e' nessuna promozione invece di
lasciare online prezzi scaduti.
Le edizioni sono dato (data/promo-edizioni.ts): finestra, quante card, quali
campi ha ciascuna. I testi restano tag modificabili dal pannello, col prefisso
dell'edizione. I bundle aggiungono domanda, descrizione e riquadro del periodo
di utilizzo, presi dal volantino del cliente.
Le date portano l'offset esplicito: il server gira in UTC e senza offset lo
scambio avverrebbe due ore prima di quanto scritto sul sito. La pagina esce con
Cache-Control: no-store, perche' una copia in cache la congelerebbe all'edizione
vecchia.
Dieci test: le finestre non si sovrappongono e non lasciano buchi, il countdown
scade quando finisce l'edizione, e ogni tag richiesto esiste nel seed - un tag
mancante non romperebbe la pagina, lascerebbe una card senza prezzo.
Anteprima con ?anteprima=<data>, solo per chi e' loggato.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MethodSteps passa a una sola colonna con step in orizzontale, PartnersStrip
riduce loghi e spaziature, TrainingCards ridimensiona testi e padding.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nuovo tag promo.faq.0 (q/a) numerato 0 per evitare di rinumerare le FAQ
esistenti: il seed è INSERT OR IGNORE e non riscriverebbe i tag già
presenti nei DB, causando disallineamento tra domanda e risposta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Documenta il ruolo piattaforme al posto di campus, la piattaforma Stress
Index con le scelte fatte sullo stile e sulle isole React, il cestino
degli articoli e il caricamento degli articoli via script, oltre alle
trappole incontrate: il percorso nuovo del progetto Campus, la cache
dell'indice in sviluppo e la lettura del database in WAL dopo il deploy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Porta in main il lavoro di questa sessione: il ruolo piattaforme al posto
di campus con il menu dedicato, la dashboard Stress Index tradotta nello
stile del sito, l'area riservata ridisegnata con cestino degli articoli e
gestione utenti rivista, il Campus riallineato alle 167 guide del corso,
venti articoli divulgativi nel blog e la promo di agosto con countdown.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
I filtri non dicevano dove ci fosse materiale: si sceglieva una categoria
per scoprirla vuota. Ora ognuna porta il proprio conteggio, cestino e
bozze esclusi, e il numero si aggiorna insieme ai filtri quando si cambia
tematica senza ricaricare.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dieci articoli tecnici a firma Nicola Antonelli e dieci contenuti
educativi a firma Donata Dal Pastro, ricavati dalle lezioni del corso
base: sonno, movimento, proteine, freddo, respirazione, luce, recupero,
dati, alimentazione e salute del cervello. Taglio breve e concreto, per
chi frequenta la palestra e tiene alla salute senza essere del mestiere.
Gli articoli arrivano da uno script versionato invece che dal pannello,
perché il database in produzione è un volume del VPS e non viene toccato
dal deploy: creati a mano in locale sarebbero rimasti in locale. Lo script
salta gli slug già presenti, quindi si può rieseguire senza danni.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Il progetto Biohacking Campus è stato spostato in Wasabi-Adp-Work, mentre
sync-campus puntava ancora al vecchio percorso, ormai vuoto: da lì in poi
nessun aggiornamento è più arrivato. Il capitolo Q&A si era fermato alla
numero 35, mentre nel progetto ne esistono 84.
Ora la sezione conta 167 guide invece di 118. L'elenco dei Q&A è
rigenerato per intero dal filesystem, in ordine di numero, così il prossimo
allineamento è una sola riga di script.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ogni clic su una categoria ricaricava l'intera pagina, header e hero
compresi, con lo sfarfallio che ne consegue. Ora si scarica la stessa
pagina e si sostituiscono soltanto la griglia e i filtri, con una breve
dissolvenza; l'indirizzo resta corretto e avanti/indietro funzionano.
I link restano link veri: senza JavaScript, o se la risposta non ha la
forma attesa, la navigazione avviene come prima.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Eliminare un articolo non lasciava nulla di recuperabile: ora la riga
finisce nel cestino (colonna deleted_at, aggiunta anche ai database già
esistenti) e sparisce da sito e pannello, ma resta ripristinabile dalla
nuova pagina /admin/cestino, dove sta anche la cancellazione definitiva.
L'API la accetta solo su articoli già cestinati, così il contenuto non si
perde per un clic distratto.
Ogni lettura pubblica — elenco, recenti, archivio per mese, pagina del
singolo articolo — filtra il cestino, e chi non gestisce vede nel cestino
soltanto i propri articoli.
Le azioni di riga diventano icone (matita e cestino) come nell'elenco
utenti, con etichetta accessibile che nomina l'articolo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Le due azioni di riga diventano icone (chiave e cestino) con etichetta
accessibile: pesavano quanto il contenuto della tabella. Il ruolo smette
di essere il menu a tendina di sistema e prende la forma di una pillola
colorata per livello, restando un <select> che salva al cambio.
La chiave apre un pannello dove la password si può scrivere a mano oppure
far generare: l'endpoint accetta ora una password scelta, con lo stesso
minimo di otto caratteri della creazione, e continua a restituirla in
chiaro una volta sola perché nel database ne resta solo l'hash. Il valore
si copia con un pulsante invece che dal prompt del browser.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
L'elenco aveva il modulo di creazione appiccicato sopra la tabella: ora
c'è il pulsante "+ Nuovo utente" che porta a /admin/users/new, dove il
modulo ha spazio per etichette vere e per spiegare cosa comporta ogni
ruolo. A creazione avvenuta si torna all'elenco.
Nel menu la voce "Sito" diventa "Vai a sito", più esplicita sul fatto che
porta fuori dal pannello.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Il menu in alto si riduce alle voci che servono davvero — Articoli,
Contenuti, Utenti, Sito, Esci — con la voce corrente evidenziata; ognuna
resta visibile solo a chi ha il ruolo per aprirla, e in pagina di login la
barra mostra il solo marchio. "Nuovo" esce dal menu perché il pulsante per
creare un articolo è già in cima all'elenco Articoli.
Il pannello smette di essere un foglio di stile a sé: adotta i font e la
palette del sito, come Campus e Stress Index, con tabelle, pulsanti, campi
e pillole di stato riportati allo stesso linguaggio.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Le due sezioni erano ferme al prototipo locale, più povero della
piattaforma vera. Analytics ha ora l'intervallo di date, i KPI con la
nota sotto il valore, la distribuzione dello score a quattro serie per
fascia, le classifiche per recupero medio col numero di misurazioni e il
confronto segmenti con selettore di dimensione. Sport diventa Dashboard,
Sessioni con filtri atleta/periodo/sport e schede atleta con carico e
tendenza.
I numeri non sono più costanti scritte a mano: si calcolano dai dati nel
periodo scelto. La logica sta in lib/stress-index/analytics.ts e sport.ts,
coperta da test — che hanno subito trovato uno slittamento delle settimane
dovuto alle date costruite in ora locale invece che in UTC.
Le sessioni hanno ora ora di inizio, HR max, RPE e durata al secondo, che
il prototipo non prevedeva.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Il ruolo `campus` diventa `piattaforme` e non apre più solo la sezione
Biohacking: copre tutte le aree riservate, con migrazione guardata sugli
utenti esistenti. Nell'header la vecchia voce Biohacking lascia il posto a
un menu Piattaforme con i due link, visibile solo a chi ha il ruolo, e
/piattaforme diventa la pagina di atterraggio.
Stress Index è portata dentro il sito: le nove viste dell'area
professionisti diventano pagine Astro sotto /piattaforme/stress-index,
con lo stile del sito al posto del design system del prototipo. React
resta solo dove serve davvero — grafici recharts e scheda cliente — il
resto è Astro con un filo di JS. I dati sono ancora quelli dimostrativi,
raccolti in lib/stress-index/data.ts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Prezzi aggiornati (One to One 200, Small Group 125, Reformer 115,
Holistic 75) con migrazione guardata sul vecchio valore, così le righe
già ritoccate dal pannello non vengono sovrascritte. I barrati restano
invariati. Nota di validità con la finestra 1-19 agosto e countdown alla
scadenza sopra le card: la deadline è un tag editabile, il blocco si
nasconde da solo quando la promo è finita.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Campo opzionale Service.heroTitle: l'hero (H1) di /services/one-to-one
mostra "PERSONAL TRAINING" mentre H2/card restano "One to One" (tag
condiviso service.one-to-one.title invariato). Nessuna migrazione DB.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Contatti: rate-limit per IP reale (X-Forwarded-For) invece dell'IP del
proxy Traefik, condiviso da tutti; controllo spostato dopo la validazione
così bot/honeypot non consumano la quota. Honeypot rinominato da "website"
a "hp_field" per non farlo riempire dall'autofill del browser (falsi 400).
- Servizi: "Performance Class" -> "Class" (titolo/card/H1/footer); migrazione
DB invertita e guardata.
- One to One: migrazione titolo prod "Personal training" -> "One to One".
- Footer: orario sabato 8-17 (non 8-19), seed + migrazione guardata.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Il riquadro training "performance-class" è stato rimosso dal seed nel commit
9966ec3, ma i suoi 5 tag restavano nei DB esistenti come orfani non
renderizzati (uno con un edit errato dell'editor, "Holistic Class"). Aggiunge
una DELETE idempotente per l'intero gruppo; il seed non li reinserisce
(trainings.ts non ha più questo id). Verificata su copia del DB prod: 5 -> 0
orfani, riquadri validi e 39 edit reali del pannello intatti.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Nei DB esistenti (prod + dev) service.performance-class.title era rimasto
"Class", mostrando "CLASS" come H1 della pagina /services/performance-class
e nelle card di /services e home. Il seed è INSERT OR IGNORE e non lo
aggiornava; una nuova installazione era invece corretta.
Aggiunge una migrazione guardata dal vecchio valore (idempotente, non tocca
le modifiche del pannello) che allinea a "Performance Class". Verificata su
copia del DB di produzione: titolo corretto, 40 edit del pannello intatti.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Il commit 1522d97 ha spostato la freccia "→" dei bottoni promo nel CSS
(::after) e il maiuscolo in text-transform, cambiando il seed in minuscolo
senza freccia. Ma il seed è INSERT OR IGNORE: sui DB esistenti (produzione)
le righe restavano a "LE NOSTRE PROPOSTE →" / "ACQUISTA →", e il ::after
avrebbe prodotto una doppia freccia.
Aggiunge una migrazione guardata dal vecchio valore (idempotente, non tocca
le righe modificate dal pannello) che le allinea al seed corrente. Verificata
su copia del DB di produzione: frecce corrette e 40 edit del pannello intatti.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Rimossa la colonna "invia messaggio" (nome, cognome, telefono, mail,
messaggio) e l'import ContactForm inutilizzato.
- Griglia a 3 colonne con contenuto centrato, logo incluso.
- Training e Servizi allineati alla riga "Promuoviamo il movimento...".
- Altezza del footer ridotta (padding compattato).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
I test erano rimasti indietro rispetto a due modifiche intenzionali:
- size 'xxl' è ora un valore valido (whitelist + CSS): i test lo usavano
come esempio di valore da rifiutare → sostituito con 'xxxl'.
- il menu è stato riordinato (commit 5ff4771): la voce "Servizi" è passata
da nav.3 a nav.2. Allineata la voce di migrazione OLD_SECTION_DEFAULTS a
global.nav.2.label e il relativo test. Impatto nullo sui DB reali (nessuna
riga ha ancora value 'Programmi'), solo coerenza semantica.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Contatti: nome, cognome, telefono e mail resi obbligatori (validazione
client + server); il form del footer ora include cognome e telefono.
- Home hero: rimossa la foto (file conservato su disco), fascia ridotta
da 100vh a padding contenuto, testo allargato.
- Menu: la voce "Promo 2 (test)" è visibile solo agli admin loggati.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Riporta nel codice le modifiche fatte da Adriano nel canvas di Claude Design: la
freccia dei bottoni diventa decorazione CSS (::after) invece di far parte del testo
del tag. I tag promo.hero.cta e promo.buy-label perdono la freccia e il maiuscolo
forzato (gestiti dal CSS .btn). promo-eventi2 usa la ::after globale di .btn;
promo-eventi (.lp) aggiunge ::after ai suoi bottoni custom per non perdere la freccia.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
promo.hero.badge e promo.launch.badge tornano al set di stili di default
(size/color/weight/style/align): erano stati seedati con styleOptions vuoto,
quindi il pannello mostrava solo il testo senza opzioni di stile.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claim su due righe pulite (niente più spezzatura del corsivo), titolo FAQ reso
visibile e uppercase come lo stile del sito, card con prezzi più prominenti,
separatore feature, ombra/hover e featured evidenziata, spaziature bilanciate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
La prima versione unificava solo font/colori ma teneva il box centrato su sfondo crema.
Ora la pagina di test usa il banner PageHero a tutta larghezza, sezioni .section/.container
su sfondo bianco e bottoni .btn globali, come Servizi e Training. Aggiunto il tag
promo.pagehero.title per il titolo del banner.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Variante di /promo-eventi con font Montserrat/Open Sans e palette --c-* al posto di
Inter e dei colori hardcoded; struttura e contenuti identici (stessi tag promo.*).
Root class `.lp2` per isolare gli stili is:global dalla pagina originale. Aggiunta la
voce di menu "Promo 2 (test)" → /promo-eventi2 per il confronto affiancato.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Riprende il tagging della landing (tag promo.* + global.whatsapp centralizzato) e
risolve la regressione visiva: gli elementi editabili sono renderizzati dal componente
<T>, fuori dallo scope della pagina, quindi il CSS scoped non li raggiungeva (font e
allineamenti persi). Lo <style> diventa is:global; ogni regola è già prefissata `.lp`,
quindi resta confinata alla landing senza toccare il resto del sito.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tutti i testi della landing (hero, launch, card con prezzi, nota validità e FAQ)
passano ora dal sistema di content-tag `promo.*`, editabili inline e da pannello.
Il numero WhatsApp è centralizzato nel tag globale `global.whatsapp`: le risposte
FAQ usano il segnaposto {{whatsapp}}, sostituito a runtime, così si aggiorna in un
punto solo. Struttura delle 4 card e link di acquisto bsport restano nel codice.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GTM viene caricato nel layout Base solo se la variabile GTM_ID è valorizzata.
L'ID è letto a runtime da process.env (non inlinato a build-time), così si
attiva/disattiva senza ricompilare. Con GTM_ID vuoto il sito non carica alcuno
script Google.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 10:59:34 +02:00
422 changed files with 94874 additions and 535 deletions
Sito ufficiale di InsanityLab: vetrina aziendale, blog integrato, form di contatti e sistema di contenuti modificabili senza toccare il codice. In produzione su **https://insanitylab.it**.
Sito ufficiale di InsanityLab: vetrina aziendale, blog integrato, form di contatti, piattaforme riservate ai clienti abilitati e sistema di contenuti modificabili senza toccare il codice. In produzione su **https://insanitylab.it**.
## Descrizione
Il sito è costruito su **Astro 7** in modalità server (SSR) con adapter **Node.js standalone**, servito in un container Docker dietro **Traefik** (TLS Let's Encrypt automatico). I contenuti testuali e le immagini della parte vetrina sono "taggati" e salvati su **SQLite**: si modificano da un pannello di amministrazione o direttamente in pagina, senza deploy. Il blog è gestito internamente con editor rich‑text, e il form di contatti invia email tramite SMTP.
Accanto alla vetrina vivono due sezioni riservate, raccolte sotto `/piattaforme` e visibili solo agli utenti abilitati: il **Biohacking Campus** (guide di studio generate da un progetto esterno) e **Stress Index** (piattaforma di monitoraggio HRV). Quest'ultima è l'unica parte del sito che usa React, sotto forma di isole limitate ai grafici e alle viste interattive.
@@ -25,8 +29,10 @@ Il sito è costruito su **Astro 7** in modalità server (SSR) con adapter **Node
- **Vetrina**: home, chi siamo, training, servizi (con schede di dettaglio), blog, contatti, landing "Promo ed eventi".
- **Contenuti taggati**: ogni testo/immagine ha un tag (es. `home.hero.title-line-1`) modificabile dal pannello o in pagina.
- **Modifica inline**: chi ha i permessi può attivare l'overlay dei tag e modificare i contenuti direttamente mentre naviga il sito.
- **Blog** con editor TipTap, immagini caricate, firma autore e proprietà degli articoli per ruolo.
- **Gestione utenti** con tre ruoli (`admin`, `superuser`, `user`).
- **Blog** con editor TipTap, immagini caricate, firma autore, proprietà degli articoli per ruolo, filtro per tematica senza ricaricare la pagina e conteggio degli articoli per categoria.
- **Cestino degli articoli**: l'eliminazione sposta in `/admin/cestino`, da dove si ripristina o si cancella in via definitiva.
- **Gestione utenti** con quattro ruoli (`admin`, `superuser`, `user`, `piattaforme`), creazione su pagina dedicata e password impostabile a mano o generata.
- **Piattaforme riservate** (`/piattaforme`): indice delle sezioni accessibili ai soli utenti abilitati — Biohacking Campus e Stress Index.
- **Form contatti** con validazione, honeypot anti‑spam, rate‑limit e invio email via SMTP.
## Installazione (sviluppo)
@@ -53,7 +59,9 @@ Il database `data/insanitylab.db` viene creato al primo avvio ed è popolato aut
| `npm run preview` | Anteprima locale della build |
| `npm run start` | Server di produzione (`node --env-file=.env`) |
| `npm run sync-campus` | Rigenera i contenuti del Campus: `-- <path-progetto-biohacking-campus>` |
| `npm run seed-blog` | Carica i venti articoli divulgativi tratti dal corso base del Campus (salta quelli già presenti) |
## Gestione contenuti
@@ -67,33 +75,65 @@ Il seed dei contenuti è in `src/data/content-seed.ts`; le variazioni sui conten
> Nota tecnica: gli elementi resi dai componenti `<T>`/`<TImg>` **non** ereditano il `data-astro-cid` del componente che li usa. Uno stile scoped basato solo sulla classe (es. `.hero__line`) non li raggiunge: va ancorato a un contenitore che ha il cid usando `:global()` (es. `.hero__text :global(.hero__line)`).
## Blog
Gli articoli stanno nella tabella `posts` e si gestiscono da `/admin`. La pagina pubblica `/blog` filtra per tematica e pagina senza ricaricare — si scarica la stessa pagina e si sostituiscono soltanto griglia e filtri — mantenendo indirizzi veri e cronologia del browser: senza JavaScript la navigazione funziona come prima.
L'eliminazione di un articolo non cancella nulla: valorizza `deleted_at` e lo sposta nel **cestino** (`/admin/cestino`), da dove si ripristina o si elimina davvero. Ogni lettura pubblica — elenco, recenti, archivio per mese, pagina del singolo articolo — esclude il cestino, e chi non ha ruolo di gestione vede nel cestino soltanto i propri articoli.
Lo script `npm run seed-blog` carica venti articoli divulgativi ricavati dal corso base del Campus, dieci tecnici e dieci educativi. È versionato apposta: il database di produzione è un volume del VPS e il deploy non lo tocca, quindi articoli creati a mano in locale resterebbero in locale. Lo script salta gli slug già presenti e crea, se mancano, i due utenti che firmano gli articoli — la firma pubblica è lo `username` dell'autore.
## Ruoli e accessi
Tre ruoli, con autorizzazione a matrice di prefissi in `src/lib/auth.ts`:
Quattro ruoli, con autorizzazione a matrice di prefissi in `src/lib/auth.ts`:
| `piattaforme` | Solo le sezioni riservate (`/piattaforme`, `/campus`); nessun accesso al pannello |
Login pubblico su `/login` (`?next` sanitizzato contro open redirect). La gestione utenti (`/admin/users`, solo admin) consente creazione, reset password una‑tantum, cambio ruolo ed eliminazione, con guardie contro l'auto‑eliminazione e la rimozione dell'ultimo admin.
Login pubblico su `/login` (`?next` sanitizzato contro open redirect). Dopo l'autenticazione ogni ruolo viene indirizzato alla propria pagina iniziale: il pannello per admin e user, i contenuti per superuser, l'indice delle piattaforme per il ruolo `piattaforme`. La gestione utenti (`/admin/users`, solo admin) consente creazione su pagina dedicata (`/admin/users/new`), cambio password — scelta a mano o generata, mostrata una sola volta perché nel database resta il solo hash — cambio ruolo ed eliminazione, con guardie contro l'auto‑eliminazione e la rimozione dell'ultimo admin.
> Il ruolo `campus` è stato rinominato in `piattaforme` quando le sezioni riservate sono diventate due. Una migrazione in `src/lib/db.ts` aggiorna da sé gli utenti esistenti.
## Piattaforme riservate
Le sezioni riservate sono raccolte sotto `/piattaforme`, che è anche la pagina di atterraggio del ruolo omonimo. L'elenco vive in `src/data/piattaforme.ts`: aggiungerne una è una riga, e il menu dell'header la mostra da sé a chi ha i permessi.
### Biohacking Campus (`/campus`)
La sezione `/campus` raccoglie le guide di studio del corso Biohacking Campus. È protetta dal middleware come il pannello: un visitatore anonimo viene mandato al login, un utente con ruolo non abilitato torna alla propria pagina iniziale. Le pagine non montano l'header e il footer del sito — la navigazione avviene attraverso la barra laterale delle aree, con un unico richiamo per tornare all'indice delle piattaforme — ma usano i font, i colori e il CSS globale di InsanityLab.
I contenuti non vengono scritti a mano: lo script `npm run sync-campus` legge le guide sorgente (`*.studio.md`) dal progetto Biohacking Campus, le converte in frammenti HTML e produce la cartella `campus-content/` (indice `nav.json`, pagine e immagini delle slide), che va committata perché il deploy la copia nell'immagine Docker. Lo script è idempotente: rigenerarlo dopo l'aggiunta di nuove lezioni aggiorna indice e pagine e rimuove quelle non più presenti. Le nuove guide vanno prima dichiarate nella struttura dei capitoli in `scripts/campus/structure.mjs`.
Le immagini delle slide sono servite dalla route `/campus/assets/...`, anch'essa protetta: non sono raggiungibili senza una sessione valida.
> Il progetto sorgente sta in `/home/adriano/Wasabi-Adp-Work/AI-OS/projects/personale/biohacking-campus` (percorso predefinito dello script). In sviluppo, dopo un sync, il server va riavviato: `src/lib/campus.ts` tiene l'indice in cache in memoria e le guide nuove darebbero 404.
### Stress Index (`/piattaforme/stress-index`)
Piattaforma di monitoraggio HRV portata dentro il sito da un prototipo Next.js esterno: nove viste — cruscotto giornaliero, elenco clienti, scheda cliente, analytics di studio, modulo sport con sessioni e atleti, team live, impostazioni, organizzazione. Il design system del prototipo (Tailwind, palette teal) è stato sostituito con quello del sito: lo stile sta in `src/styles/stress-index.css`, un foglio globale prefissato `.si` perché le stesse classi servono sia ai componenti Astro sia alle isole React, che lo scope di Astro non raggiungerebbe.
React è limitato a ciò che ha davvero bisogno di stato: la scheda cliente, la vista Analytics, il modulo Sport e i grafici Recharts. Il resto è Astro con qualche riga di JavaScript. I dati sono **dimostrativi** e stanno in `src/lib/stress-index/data.ts`; le aggregazioni con una logica propria (finestre temporali, distribuzioni, riepiloghi atleta) vivono in `analytics.ts` e `sport.ts` accanto, coperte da test.
@@ -115,6 +155,7 @@ Il file `.env` (non versionato) richiede:
| `CONTACT_TO` | Destinatario dei messaggi del form |
| `DB_PATH` | Percorso del database SQLite |
| `UPLOADS_DIR` | Cartella degli upload |
| `CAMPUS_DIR` | Cartella dei contenuti del Campus (default `./campus-content`) |
La posta del dominio `@insanitylab.it` è ospitata su **Aruba**; l'invio del form usa l'SMTP autenticato Aruba (`smtps.aruba.it:465`). Aruba impone che il mittente sia la casella autenticata, quindi `CONTACT_FROM` deve essere uguale a `SMTP_USER`.
@@ -124,11 +165,20 @@ Il sito gira in Docker su un VPS, dietro Traefik. Lo stack è in `/opt/docker/in
```bash
cd /opt/docker/insanitylab/src
git pull
docker compose up -d --build
sudo git -c safe.directory="$PWD" pull origin main # il repo è di root: serve sudo, con password
docker compose up -d --build# docker gira senza sudo
```
Le migrazioni del database (in `src/lib/db.ts`) sono idempotenti e girano all'avvio del container: aggiornano lo schema e i contenuti senza perdere le modifiche fatte dal pannello.
Le migrazioni del database (in `src/lib/db.ts`) sono idempotenti e girano all'avvio del container: aggiornano lo schema e i contenuti senza perdere le modifiche fatte dal pannello. Le migrazioni che cambiano un valore sono sempre guardate dal valore precedente, quindi non sovrascrivono ciò che è stato modificato dal pannello — con la conseguenza, da tenere presente, che un contenuto già ritoccato a mano **non** riceve il nuovo default: va corretto dal pannello.
Prima di ogni deploy conviene fare il backup del database, che è in modalità WAL: un `cp` del solo `.db` perderebbe le modifiche non ancora consolidate.
sqlite3 /dest/insanitylab.db "PRAGMA integrity_check;"# deve rispondere 'ok'
```
Gli articoli del blog non viaggiano con il codice: per caricarli in produzione, una volta sola, `docker exec insanitylab node scripts/seed-blog-biohacking.mjs`.
### Dominio e DNS
@@ -149,6 +199,10 @@ Le migrazioni del database (in `src/lib/db.ts`) sono idempotenti e girano all'av
**Database "locked" durante i test** — assicurarsi che nessun processo stia accedendo a `data/insanitylab.db`.
**Le guide nuove del Campus danno 404 in sviluppo** — `src/lib/campus.ts` tiene l'indice in cache in memoria: dopo `npm run sync-campus` va riavviato il server.
**Il database di produzione sembra non aggiornato dopo il deploy** — `sqlite3 -readonly` non legge il file `-wal`, dove stanno le modifiche non ancora consolidate, e mostra valori vecchi. Per verificare, interrogare il database in lettura‑scrittura oppure — meglio — guardare direttamente le pagine del sito.
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.