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>
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>
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>
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.
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>
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>
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
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.
- 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`.
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.
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>
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>
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>