Commit Graph

74 Commits

Author SHA1 Message Date
Adriano 34c66ca438 longevity: il guscio della piattaforma, senza semaforo sui punteggi 2026-08-22 13:33:21 +02:00
Adriano 82a4cd2000 test: chiude l'errore di tipi pre-esistente in middleware.test.ts
Cast di FakeContext a onRequest via 'unknown' invece che diretto: i
due tipi non si sovrappongono abbastanza per tsc (FakeContext è un
doppio minimale, non un APIContext completo). Il typecheck è il
meccanismo di sicurezza di questo motore: uno rosso in permanenza è un
cancello che nessuno guarda più.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 19:33:23 +02:00
Adriano 7779c6b830 test: verifica che fuoriRange non trunca il valore numerico 2026-08-21 19:31:15 +02:00
Adriano a18bb7c02f longevity: registraMisure, unico varco con validazione sul registro 2026-08-21 19:27:41 +02:00
Adriano 370dcbda56 longevity: registro dei test e pesi versionati come dato 2026-08-21 19:22:32 +02:00
Adriano ad2fc0d510 fix: rinomina score.nome in score.elemento per coerenza con tabella pesi 2026-08-21 19:17:03 +02:00
Adriano f16012c8bd longevity: schema dei due database, identity separato da longevity 2026-08-21 19:15:17 +02:00
Adriano e315887964 Check-up: via la domanda "Che cosa comprende?" in fondo alla scheda
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.
2026-08-19 08:45:35 +02:00
Adriano 3741ce34ac Modifiche chieste dal cliente il 17 agosto
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.
2026-08-18 17:06:05 +02:00
Adriano 69bd17a296 Calendario: si vedono tutti gli orari validi, e il PDF ne fa una pagina ciascuno
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>
2026-08-17 17:22:22 +02:00
Adriano d03c62641e Calendario: il 19 e il 20 settembre non sono un buco, sono un evento e una chiusura
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>
2026-08-17 17:07:24 +02:00
Adriano 5be0fa787c Calendario: il PDF e' un file vero, generato dal server
"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>
2026-08-17 16:14:02 +02:00
Adriano 01f49ab6d2 Calendario delle class: popup a pieno schermo dalle pagine Performance e Holistic
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>
2026-08-17 15:58:47 +02:00
Adriano 0b5d9f4106 Promo: le edizioni si scambiano da sole alla data stabilita
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>
2026-08-17 12:27:31 +02:00
Adriano d8862ba8ec Blog: numero di articoli accanto a ogni tematica
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>
2026-07-31 17:16:39 +02:00
Adriano 6a97a98325 Articoli: cestino con ripristino, e azioni a icona come in Utenti
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>
2026-07-31 16:47:27 +02:00
Adriano 3cda76b4c4 Stress Index: Analytics e Sport rifatti sulle viste reali
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>
2026-07-31 16:20:40 +02:00
Adriano c4eb598a3a Piattaforme riservate: ruolo dedicato e port della dashboard Stress Index
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>
2026-07-31 15:49:47 +02:00
Adriano c4107b84a5 Sezione Campus: layout dedicato, route protette e documentazione
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 15:40:07 +02:00
Adriano 7baebebc09 Script sync-campus: importa le guide del corso in campus-content/
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 15:30:59 +02:00
Adriano a7d918d241 Ruolo campus: accesso limitato a /campus, landing dedicata
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 15:26:35 +02:00
Adriano b17b5c831a Fix contatti + rinomina servizi + orario sabato
- 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>
2026-07-21 17:58:26 +02:00
Adriano 22c9cadb43 Migrazione: rimuove i tag orfani training.performance-class.*
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>
2026-07-21 00:16:52 +02:00