Commit Graph
134 Commits
Author SHA1 Message Date
Ivan CacciariandCursor 3d8cb22235 Compatta la classifica interna Squadra e allinea le tab a sottolineatura.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-06 18:50:11 +02:00
davideandClaude Opus 5 fb6bde279e Toglie la notifica di prova dalle Opzioni.
Era impalcatura per diagnosticare "le push non arrivano": ha fatto il suo
lavoro, la causa e' documentata nei limiti noti. Lasciare il pulsante avrebbe
tenuto in piedi anche la route push-prova e notificaDiProva, senza piu' nessuno
che le usa.

Via il pulsante, la funzione client, la route e i test che la coprivano. Resta
in git se dovesse servire di nuovo.

inviaPush continua a tornare { stato, corpo } e a loggare i rifiuti: serve alle
tre route che mandano notifiche, non solo alla prova.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 15:16:41 +02:00
davideandClaude Opus 5 5c709c1a12 Annota la causa vera del "non arriva" su Android: Chrome non svegliabile.
Su Android non riceve la webapp installata: riceve Chrome, tramite Google Play
Services. Il WebAPK e' solo l'identita' con cui la notifica viene mostrata. Se
il sistema non puo' avviare Chrome, il messaggio resta in coda e compare tutto
insieme al lancio successivo — "arriva solo quando riapro l'app".

Verificato sul campo su Motorola: con Chrome vivo in secondo piano la push
arriva ad app chiusa e schermo bloccato, WebAPK compreso. Quindi server,
cifratura, service worker, permesso notifiche e canale erano gia' corretti e
l'unica condizione che fallisce e' Chrome non in esecuzione. Mettere "Senza
restrizioni" solo su CrAPP non basta e depista.

Documentato anche come misurarlo con chrome://gcm-internals, incluso il
tranello: tenere quella scheda aperta tiene Chrome vivo e falsa la prova.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 14:57:05 +02:00
davideandClaude Opus 5 78d41daf4f Ritarda di 10 secondi la notifica di prova, per poter chiudere l'app.
Il pulsante si preme con l'app aperta, quindi la push arrivava sempre in primo
piano: l'unico caso che non serve provare. Ora la route aspetta `ritardoMs`
(10s di default, 25 al massimo) prima di inviare, e il toast dice di chiudere
l'app subito.

L'attesa tiene aperta la funzione, quindi il tetto e' la durata massima
concessa dall'hosting: annotato come ponytail nel codice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 14:44:29 +02:00
davideandClaude Opus 5 8047e58ae0 Mostra l'ora italiana nella notifica di prova.
Il server gira in UTC: `toLocaleTimeString("it-IT")` senza `timeZone` formattava
nel fuso del server, quindi la prova diceva 12:40 con un telefono che segnava le
14:40 e sembrava un orologio sfasato. E' solo il messaggio di prova: i testi
delle notifiche vere prendono data e ora dai campi dell'evento, non
dall'orologio del server.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 14:42:05 +02:00
davideandClaude Opus 5 d8f334007b Fa arrivare le push ad app chiusa e rende diagnosticabile quando non arrivano.
Il testo della notifica viaggia ora cifrato dentro la push (aes128gcm, RFC
8188/8291) invece di essere recuperato dal service worker con una fetch al
risveglio. Era quella fetch a non chiudersi in tempo: ad app chiusa il browser
tiene vivo il worker pochi secondi, showNotification non veniva mai chiamata e
non compariva niente, mentre ad app aperta con la rete calda sembrava tutto a
posto. La POST porta anche Urgency: high, che chiede la consegna immediata
invece di far accumulare i messaggi fino al risveglio del dispositivo.

Cadono i pezzi che esistevano solo per rimediare al payload vuoto: la route
push-messaggio, la coda promemoria_push con la sua scadenza a 12 ore,
messaggioPalloniOggi() e il timeout nel worker. Tutti e tre i mittenti avevano
gia il testo pronto prima di inviare.

Il worker si aggiorna da solo all'avvio e a ogni ritorno in primo piano
(mantieniWorkerPushAggiornato): nella webapp installata quello vecchio puo
sopravvivere a lungo, e senza questo un dispositivo resterebbe fermo alla
versione che va a cercare il testo in rete.

Profilo -> Opzioni ha "Mandami una notifica di prova", visibile solo a notifiche
attive: manda una push a questo dispositivo e riporta stato HTTP, corpo della
risposta e se l'endpoint risulta davvero in push_subscriptions. Senza, "non
arriva" era cieco: ogni prova richiedeva un admin, un evento nello stato giusto
e una seconda persona, e la risposta del servizio push veniva buttata via.
inviaPush torna { stato, corpo } e logga il corpo sui rifiuti.

Dalle prove sul campo: a parita di server, iPhone installato da Home riceve ad
app chiusa. Su Android installato come webapp resta da verificare: il WebAPK e
un'app Android a se, con permesso notifiche (Android 13+) e voce batteria
distinti da quelli di Chrome. Annotato nei limiti noti.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 14:24:07 +02:00
davideandClaude Opus 5 2b8ba340a6 Fa scadere i promemoria in coda e smette di dare la colpa alle iscrizioni.
Provando il promemoria palloni in produzione la notifica risultava inviata ma
non arrivava. La causa non era il codice: l'iscrizione di destinazione era
scaduta, FCM l'ha accettata con 2xx e ha buttato via il messaggio, e un minuto
dopo il dispositivo si è re-iscritto con un endpoint nuovo. Un 2xx dal server
push non significa consegnato, e non manda il 404/410 che farebbe pulire
push_subscriptions: è annotato fra i limiti noti, perché dal server non è
distinguibile.

Il difetto vero l'ha fatto emergere quella caccia. promemoria_push si svuota
solo quando il dispositivo legge il messaggio, quindi se la push non arriva mai
la riga resta per sempre — e push-messaggio serve la coda con priorità sul testo
calcolato. In produzione ce n'erano dieci, la più vecchia del 2 settembre: alla
notifica successiva, di qualunque tipo, quel telefono avrebbe mostrato un
sollecito presenze per un evento già passato.

Ora un promemoria vale 12 ore. La riga si cancella comunque alla prima lettura,
scaduta o no: cancellare solo le fresche lascerebbe le vecchie in coda a
dirottare ogni notifica futura, cioè il bug. Così la coda si smaltisce da sola e
le righe orfane già in produzione non vanno ripulite a mano.

Il messaggio del pulsante distingue infine i due casi che prima confondeva:
nessuna iscrizione fra gli incaricati, oppure iscrizioni presenti e invio non
riuscito. Il primo è informativo, il secondo è un errore — dirlo sbagliato
manda a cercare il problema dalla parte opposta.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 12:25:21 +02:00
davideandClaude Opus 5 ee9f3f1b8c Il promemoria palloni lo manda un admin dalla pagina evento (DD-025).
La route era disegnata per un cron quotidiano: calcolava chi è di turno oggi e
gli mandava una push. Ma nel repository nessun cron esiste, e palloni.md lo
annotava già come "da verificare lato hosting": nei fatti quel promemoria non è
mai partito. Il segreto condiviso introdotto ieri proteggeva una porta che
nessuno apriva, al prezzo di una variabile d'ambiente da configurare ovunque.

Ora la fa partire un amministratore dal pulsante "Avvisa chi è di turno", dentro
il riquadro palloni dell'evento. La route accetta un eventoId e avvisa i
destinatari di quell'evento invece della giornata corrente: chi deve prendere i
palloni e chi deve riportarli, con un testo diverso per ciascuno. Stesso
precedente di apri-sondaggio, manuale fin dalla v1.0.6.

Il testo va in coda su promemoria_push prima dell'invio. Serve: la push parte
vuota e il service worker chiede a push-messaggio cosa mostrare, ma quella route
sa raccontare solo la giornata corrente, quindi un avviso mandato il martedì per
il sabato arriverebbe con il testo generico.

avvisiPalloniEvento() è la nuova funzione pura, con i suoi test: due destinatari
con testi diversi, un avviso solo quando sono la stessa persona, niente avvisi
per un evento inesistente o senza turni. Un'asserzione verifica che il testo non
dica mai "oggi", perché può arrivare giorni prima.

richiediSegreto e CRON_SEGRETO spariscono: tutte e tre le route di notifica
usano ora richiediAdmin, e non resta nessuna variabile da configurare.
destinatariPromemoriaPalloni() resta in palloni-core.ts con i suoi test perché
push-messaggio continua a usarla per il testo calcolato al volo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 12:15:39 +02:00
davideandClaude Opus 5 847972b582 Chiede le credenziali alle route che avvisano tutta la squadra (DD-024).
Le route in src/routes/api/public/ girano con la service role e saltano la RLS,
quindi DD-023 non le copre. Nessuna faceva un controllo di accesso: cercando
"authorization" in quella cartella l'unico header era lo User-Agent con cui
csi.ts chiama il portale CSI. Chiunque conoscesse l'URL poteva far suonare i
telefoni della squadra, e promemoria-palloni accetta perfino una POST con il
corpo vuoto.

La difesa apparente delle altre due — serve un id evento valido — non è una
difesa: l'id è "e" più il timestamp in base 36, compare negli URL che la squadra
si scambia ed è elencabile da qualsiasi utente loggato.

auth-route.server.ts porta i due controlli, diversi perché i chiamanti sono
diversi. apri-sondaggio e sollecita-presenze usano richiediAdmin: token della
sessione verificato con auth.getUser, poi ruolo admin da user_roles, la stessa
fonte di ruoli.ts. Il controllo precede la validazione dell'input, così la
risposta non rivela nemmeno se un evento esiste. promemoria-palloni usa
richiediSegreto, perché la chiama un cron che una sessione non ce l'ha: se
CRON_SEGRETO non è configurata la route resta chiusa con 503, perché una porta
che si riapre da sola quando manca una variabile non se ne accorge nessuno.

csi, push-config, push-subscribe e push-messaggio restano aperte: le chiamano il
browser prima del login e il service worker, dove qualsiasi segreto finirebbe
nel bundle.

Lato client i due pulsanti admin mandano il token con intestazioniAutenticate(),
letto al momento della chiamata e non da uno stato React.

permessi-route.test.ts copre il giro intero — nessun token, giocatore, admin —
avviando il server di sviluppo puntato al database locale, perché servono utenti
veri. Il controllo positivo è il 404: l'admin supera l'accesso e arriva alla
validazione. In api.test.ts restano i rifiuti che non richiedono un utente e
sparisce la verifica della validazione di sollecita-presenze, che ora sta dietro
all'accesso.

I limiti noti di palloni.md sono aggiornati: il secret che il piano originale
prevedeva ora c'è. Resta vero che nessun cron chiama la route, quindi il
promemoria quotidiano non parte da solo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 12:05:39 +02:00
davideandClaude Opus 5 93b61d422a Limita le scritture del database a chi le fa (M11, DD-023).
Le tabelle della v1.0 sono nate con policy USING (true) per anon e
authenticated. M4 ha tolto il GRANT ad anon e la cosa è passata per "ora è
chiuso", ma per gli autenticati non era rimasto nessun limite. Verificato sul
database locale con un utente appena creato, senza ruolo e senza slot nella
rosa: POST su eventi_app risponde 201, DELETE risponde 200. Qualsiasi giocatore
loggato poteva svuotare il calendario o riscrivere il voto di un altro parlando
direttamente con PostgREST, saltando l'interfaccia che quei pulsanti glieli
nasconde. Il permesso viveva solo nei componenti, cioè nel posto che un
attaccante non usa.

M11 fa dire alle policy quello che l'interfaccia già fa: eventi_app agli admin,
risposte_presenze e cacche_partita alla propria riga, i tre voti al proprio
votante_id. L'admin resta incluso ovunque, perché DD-017 gli riconosce già il
diritto di agire al posto del giocatore.

turni_palloni e le tabelle scout restano aperte di proposito: nell'interfaccia
non hanno nessun gate, quindi stringerle sarebbe una funzionalità nuova e non
una messa in sicurezza. Un test lo fissa, così se il gate arriva qualcuno se ne
accorge.

L'identità è lo slot di giocatori_squadra collegato all'account, con lo stesso
EXISTS delle policy dei profili: mio_giocatore_id() di M2 era già stata rimossa
dalla migration di correzione e non va reintrodotta.

I cinque test nuovi in permessi.test.ts hanno ognuno il proprio controllo
positivo — l'admin crea l'evento, il giocatore salva la propria presenza —
perché un database che rifiuta tutto passerebbe qualsiasi test di sola
negazione. Provata con db reset da zero; non applicata in produzione.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 11:51:27 +02:00
davideandClaude Opus 5 fe0c938358 Verifica che gli upsert dell'app scrivano sulla riga giusta.
Ogni salvataggio di CrAPP è un upsert con un onConflict scritto a mano nei hook
di src/lib/. Se quella chiave non corrisponde al vincolo UNIQUE della tabella
non arriva nessun errore: il database sovrascrive la riga sbagliata, e il difetto
si vede settimane dopo in una media che non torna. Nessuna delle sedici
scritture era mai stata eseguita da un test.

test/integration/scritture.test.ts ripete le stesse chiamate dei hook contro il
database locale e conta cosa resta nella tabella. Le due regole opposte che
nessuno verificava: le pagelle tengono un voto per ogni votato — se il conflitto
fosse su (match, votante) ogni voto cancellerebbe il precedente — mentre l'MVP
ne tiene uno solo per votante e partita. Più badge social per categoria, cacche,
turni palloni, risposte presenze (con l'istante che alimenta la serie di
conferme) e lo stato jsonb dello scout, che viene sostituito e non fuso. In più
i due CHECK su cui l'app conta: niente autovoto, voto fra 1 e 10.

Le righe usano il prefisso test-scritture e spariscono in un finally. La lettura
delle credenziali locali passa da test/helpers/locale.ts, ora che la usano due
file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 11:41:26 +02:00
davideandClaude Opus 5 0addafa76f Separa il calcolo delle presenze del mese dagli hook e lo copre con i test.
La percentuale di presenze degli ultimi 30 giorni decide le convocazioni, ma
stava dentro due useMemo — scritta due volte, una per giocatore e una per la
rosa — e nessun test la toccava. Le tre regole che la determinano non sono
ovvie: la finestra è di 30 giorni, il ritardo conta come presenza, e convocati
vuoto significa tutta la rosa attiva.

presenze-mese-core.ts contiene ora il calcolo puro, presenze-mese.ts solo il
collegamento agli hook. Comportamento invariato, inclusa la differenza fra i due
percorsi: per giocatore si filtrano i convocati, per la rosa l'elenco vuoto si
espande agli attivi.

Il test fissa le tre regole e i casi che le rompono: nessun evento nella
finestra dà 0 e non NaN, chi non è convocato resta fuori dal denominatore, chi
non ha mai risposto ci resta dentro.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 11:41:14 +02:00
davideandClaude Opus 5 94673090f1 Verifica i permessi per ruolo contro il database Supabase locale.
Le policy RLS scritte su auth.uid() e il trigger di DD-016 non erano coperti da
nessun test: schema-profili prova solo l'utente anonimo, e sul database di .env
non si può scrivere perché è quello di produzione.

Il nuovo test/integration/permessi.test.ts crea utenti veri sullo stack locale
(npx supabase start) e interroga il database come loro: un giocatore vede e
modifica solo il proprio profilo, non ne cancella, non cambia numero e ruolo
mentre reclama uno slot, non prende lo slot di un altro, non si assegna il ruolo
admin e non vede i ruoli altrui. Un controllo positivo sull'admin evita il falso
verde di un database completamente chiuso.

Prende URL e chiavi da `supabase status` invece che da .env e si ferma se l'URL
non è locale: un .env puntato alla produzione non deve poter trasformare un test
in una scrittura sul database vero. Senza stack locale si salta con il motivo,
quindi la suite resta verde su una macchina senza Docker. Ogni test ripristina
in un finally lo stato che tocca, così si rilancia senza db reset.

test/README.md documenta il flusso Docker e corregge la convenzione: non è più
«nessun test scrive sul database» ma «sul database di .env non scrive nessuno».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 11:22:45 +02:00
davideandClaude Sonnet 5 50a712c276 Allinea l'altezza di input, select e date nei form a un'unica misura.
input[type="date"] resta più alto di un input di testo anche con
appearance-none: h-10 esplicito su classiInput allinea tutti i campi.
La textarea note usa h-auto per non perdere le due righe.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 11:07:15 +02:00
davideandClaude Sonnet 5 77f587b53b Uniforma l'altezza dei <select> agli altri campi dei form su Android.
Su Android il controllo nativo di <select> ignora parte del padding di
classiInput e risultava più alto o più basso degli input accanto.
appearance-none lo riporta a una scatola CSS normale; la freccia va
ridisegnata a mano perché appearance-none la fa sparire.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 10:57:05 +02:00
davideandClaude Sonnet 5 ee21278e82 Rimuove mem/, superata dalla documentazione in docs/.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 10:43:16 +02:00
davideandClaude Sonnet 5 d5035cbf55 Mostra la versione dell'app nella tab Opzioni del profilo.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 10:40:34 +02:00
davideandClaude Sonnet 5 a8bde94153 Il link "Storico" in Ultima partita apre lo storico partite in classifica, non la rosa.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 10:34:03 +02:00
davideandClaude Sonnet 5 41ed3a1f0f Conta le presenze solo sugli eventi già passati, non su quelli odierni o futuri.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 10:29:34 +02:00
davideandClaude Sonnet 5 208dd5f2f6 Blocca la modifica delle presenze e il sollecito per eventi già passati.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 10:27:12 +02:00
davideandClaude Sonnet 5 d4a02fb606 Rimuove il link duplicato per tornare al calendario, basta la navbar in basso.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 10:21:14 +02:00
davide 90f16403fb Merge branch 'Frontend': Scout Live nella pagina partita e sondaggio pre-partita alle 8:00. 2026-09-06 00:25:38 +02:00
davideandClaude Opus 5 0c136f8f53 Apre lo Scout Live dalla pagina partita e a tutta la squadra.
Tolto dalla home e poi da Squadra, `ScoutEntry` era rimasto orfano:
`/scout` si raggiungeva solo scrivendo l'URL a mano. Ora la card sta
nella sezione «Scout live» di `/partita/$id`. La prop `eventoId` la
accende solo se la partita aperta è quella di oggi: senza, da una
partita futura o passata si sarebbe finiti sullo scout di un'altra.

Cade anche la riserva agli admin, in `ScoutEntry` e nella route: può
scoutare chiunque sia autenticato, uno per volta grazie al lock di
sessione. È anche l'unico controllo che c'era, visto che le policy RLS
sono sempre state aperte a tutti gli autenticati.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 00:24:39 +02:00
davideandClaude Opus 5 73902cc0a4 Apre il sondaggio pre-partita alle 8:00 con avviso push manuale.
Il sondaggio cacche era votabile in qualsiasi momento, anche settimane
prima della partita. Ora `sondaggioAperto()` lo sblocca alle 8:00 del
giorno della partita e prima la card mostra solo l'avviso di apertura.

Aggiunge la route `POST /api/public/apri-sondaggio` e, per gli
amministratori, il pulsante «Avvisa tutti del sondaggio» nella card:
manda la push a tutti i dispositivi iscritti, con lo stesso meccanismo
del sollecito presenze. Nessun invio automatico.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 00:08:58 +02:00
davideandClaude Opus 5 a7d0f5af5b Mostra le note dell'evento nella pagina di dettaglio.
Il campo Note del form eventi si poteva scrivere ma non lo leggeva
nessuno: ora compare nella scheda di /allenamento/$id e /partita/$id,
sotto orario e luogo, con gli a capo mantenuti. Se è vuoto non compare
niente.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 23:50:51 +02:00
davideandClaude Opus 5 2a1a0839c0 Estende alla card degli eventi il fix delle date su iOS.
Il `min-w-0` di 366928b era finito solo in `ProfiloAmministrativo` perché
`Campo` e le classi degli input erano ricopiati identici in tre file: il
form «Nuovo evento» e la dashboard admin («Data tessera» in `grid-cols-2`)
sfondavano ancora la card. Ora `Campo` e `classiInput` stanno una volta
sola in `ui-bits` e le tre copie spariscono.

La regola CSS che rende ridimensionabili i controlli nativi copre anche
`input[type="time"]`, che nel form eventi sta affiancato alla data.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 23:41:20 +02:00
davideandClaude Opus 5 107407b854 Rinomina le tab del profilo in Documenti e Opzioni.
«Tesseramento» nominava lo scopo invece del contenuto (dati personali,
documento, certificato medico, foto tessera); cambia anche la rotta in
/profilo?tab=documenti. «Impostazioni» diventa «Opzioni» così le quattro
etichette stanno in uno schermo da telefono: le voci della barra si
dividono la riga in parti uguali (grow basis-0) e tornano a scorrere solo
se non ci stanno, quindi vale anche per Squadra e Classifica.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 23:37:41 +02:00
davideandClaude Opus 5 366928b61f Impedisce ai campi data di sfondare la card del tesseramento su iOS.
In `grid-cols-2` l'elemento di griglia è la label di `Campo`, che ha
`min-width: auto` e quindi non scende sotto la larghezza del contenuto. Su iOS
`input[type="date"]` è un controllo nativo con una larghezza intrinseca che
`width: 100%` non riduce: le due date affiancate sfondavano la card e
l'`overflow-hidden` del pannello a tab tagliava via quello che usciva.

`min-w-0` sulla label e sulla classe condivisa degli input, più `min-width: 0`
e `appearance: none` sulle sole date — è `appearance` a rendere il controllo
davvero ridimensionabile, e restando sulle date il select tiene la sua freccia.

Il fix sta in `CampiProfilo`, quindi copre anche la dashboard amministratore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 23:22:49 +02:00
davideandClaude Opus 5 84c4c0fc25 Revert "Corregge il rendering sfasato del tesseramento su iOS."
Annulla a5e627a: la diagnosi era sbagliata. I bordi sfalsati nella foto erano
distorsione prospettica dello scatto, non un artefatto di composizione, e il
sintomo vero era un altro (le card escono dallo spazio). Né il will-change del
pannello né il backdrop-blur della barra tab c'entravano.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 23:22:39 +02:00
davideandClaude Opus 5 a5e627a57b Corregge il rendering sfasato del tesseramento su iOS.
Il pannello delle sottosezioni è trascinabile: Motion gli lasciava
`will-change: transform` in permanenza e su iOS il sottoalbero finiva in un
layer composito. Sul tab Tesseramento, il più alto, il layer supera il tile di
WebKit e il contenuto veniva ridipinto a fasce sfalsate.

Disattiva la gestione automatica del will-change e toglie il backdrop-blur
dalla barra tab: non è sticky, quindi non sfocava nulla, ma il backdrop root
forzava il rendering a layer degli antenati.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 23:12:20 +02:00
Ivan CacciariandCursor 46f5c28237 Rinomina il bilancio home in Bilancio W-L.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 22:57:17 +02:00
Ivan CacciariandCursor 1e8168d413 Rinomina il bilancio home in Bilancio vittorie.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 22:52:52 +02:00
Ivan CacciariandCursor 61c6aaaaed Toglie la proposta automatica dei palloni sugli allenamenti.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 22:44:25 +02:00
Ivan CacciariandCursor 033c45df4d Apre Tesseramento dal widget Completa profilo in home.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 22:38:48 +02:00
Ivan CacciariandCursor 2345a45f14 Allinea le notifiche del profilo al canale intero e stacca Esci in rosso chiaro.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 22:31:56 +02:00
Ivan CacciariandCursor ce1c7cfd4b Alleggerisce il cambio tab delle sottosezioni togliendo exit e molle.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 22:24:57 +02:00
Ivan CacciariandCursor e9755d364c Aggiunge sottosezioni a swipe nel profilo, con stagione e serie unite.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 22:07:58 +02:00
Ivan CacciariandCursor f4398fce25 Aggiunge lo stemma in home e ravviva blu allenamenti e verde compleanni.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 21:31:41 +02:00
Ivan CacciariandCursor ede02b7575 Aggiunge sottosezioni a swipe su Squadra e Classifica.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 21:18:19 +02:00
davideandClaude Opus 5 c089d9e8ba Dà profondità alle superfici e trasforma la barra in vetro
Fondo pagina, carte e `secondary` stavano dentro tre punti di luminosità:
carta e fondo avevano un rapporto di contrasto di 1.04, cioè erano lo stesso
bianco. Le carte non si leggevano come carte, e l'unica cosa che le staccava
era `shadow-card` — 6% di nero, che su un telefono al sole non esiste. Da qui
l'impressione che fondo, barra ed elementi fossero «tre bianchi».

La rampa dei neutri è ridistanziata: fondo 0.93, carta 1.0, `secondary` 0.90.
Carta e fondo passano a 1.23, un elemento dentro una carta sta a 1.35 dalla
carta ed è più scuro di essa, come nelle liste raggruppate di iOS. 0.93 è il
fondo più scuro possibile senza toccare altro: il limite lo impone
`text-accent` appoggiato sul fondo, che a 0.92 scenderebbe sotto 4.5.

Due colori vanno corretti di conseguenza, altrimenti la modifica peggiora la
leggibilità: l'accento scende a 0.54 (a 0.58 come testo faceva 4.07 sul nuovo
fondo; ora fa 4.62, e migliora anche il bianco che ci sta sopra, da 4.65 a
5.51) e `muted-foreground` a 0.48. Verificati convertendo le oklch in sRGB e
calcolando i rapporti, non a occhio.

`--destructive` e `--ring` erano *lo stesso identico colore* dell'accento:
«elimina», «selezionato» e «ho il focus qui» davano il medesimo segnale. Il
distruttivo resta rosso ma più scuro e cupo, il focus viene dal neutro.

Le ombre passano da una a tre quote: `shadow-card` per il contenuto,
`shadow-chrome` per barra, drawer e toast (più profonda, più il filo chiaro sul
bordo alto), `shadow-alza` neutra per l'hover — `premi` metteva un alone rosso
sotto ogni riga di classifica.

La barra ora usa `vetro`: fondo al 62% invece dell'85%, così ci si vede
attraverso, più tre ombre interne che sono il bordo illuminato. L'85% di
`materiale` esiste per tenere allineati WebKit e Blink (608f2ed) su una fascia
larga quanto lo schermo; su una pillola di 60 px lo scarto non si legge.

Dietro `@supports`, un `feDisplacementMap` distorce davvero il fondale: solo
Blink accetta un filtro SVG in `backdrop-filter`, e WebKit ignorando l'url
scarterebbe anche il blur. Miglioria progressiva, non l'effetto principale.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 20:02:13 +02:00
davideandClaude Opus 5 bb4fb624f5 Mette lo stemma della squadra nell'intestazione del profilo
In alto a destra sul profilo c'era `/icon-192.png`, cioè l'icona della PWA: ha
dentro il nome dell'app, e come marchio in una schermata dell'app stessa non
dice niente. Lì serve lo stemma della squadra.

`TeamLogo` prende una prop `src` con l'icona come default, così cambia solo la
chiamata del profilo. L'icona della PWA resta quella dappertutto altrove
(ripiego di `LinkProfilo`, schermata di caricamento, manifest, apple-touch-icon):
sulla home dello schermo è giusto che l'app si presenti come «CrAPP».

Il file arrivava come `logo_nerorosso.jpg` ma è un SVG: rinominato con
l'estensione giusta, altrimenti verrebbe servito come JPEG e non si vedrebbe.
Sta in `public/`, che è la cartella servita così com'è.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 19:35:44 +02:00
davideandClaude Opus 5 37a04e5103 Rimette «Home» come etichetta della prima voce
`5c49294` aveva rinominato la voce in «Oggi» per dire cosa contiene la
schermata, ma il nome che la squadra usa è «Home».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 19:24:54 +02:00
davideandClaude Opus 5 383643392a Porta il profilo in alto a destra e lascia quattro voci nella barra
La barra aveva cinque voci e la quinta era il profilo, che non è una
destinazione di pari livello con Oggi, Calendario, Squadra e Classifica: è la
propria scheda, e su iOS si cerca in alto a destra. Toglierla dalla barra
restituisce anche un quinto di larghezza alle altre quattro.

`LinkProfilo` mostra l'avatar del giocatore e porta a `/profilo`. Sta in
`PageHeader`, quindi compare su calendario, squadra, classifica e profilo, più
l'hero della home che ha un'intestazione propria. Se l'avatar non è stato
caricato `Avatar` ricade sulle iniziali; se il giocatore non c'è ancora (primo
render) resta il logo, così il posto non resta vuoto.

Sulla pagina del profilo l'avatar sarebbe un link a sé stessa: `PageHeader`
prende una prop `azione` e lì si passa il logo della squadra.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 19:22:08 +02:00
davideandClaude Opus 5 fabee4df1d Stacca la barra di navigazione dal fondo
La barra era una striscia a filo schermo con la sfumatura al bordo superiore
(scroll edge effect). Ora è una pillola staccata: il `<nav>` esterno tiene solo
posizione, safe area e margine laterale, mentre il materiale, il raggio, il bordo
e l'ombra passano al contenitore interno.

`pointer-events-none` sul wrapper e `pointer-events-auto` sulla pillola: ai lati
della barra i tocchi devono raggiungere il contenuto sotto, non una striscia
trasparente larga quanto lo schermo.

Via `bordo-sfumato`: la sfumatura serve a mascherare il taglio netto di una barra
attaccata al bordo, e su una che galleggia non ha più niente da mascherare.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 19:17:36 +02:00
davideandClaude Opus 5 c8cd8c9720 Rinomina il titolo della home in CRAP Volley
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 15:20:59 +02:00
davideandClaude Opus 5 6e05f5134f Non taglia più l'anello del giorno corrente
Regressione mia, introdotta con lo swipe in 9b28a86: la griglia è avvolta in
un `overflow-hidden` che serve a nascondere il mese che entra ed esce di lato.
L'anello del giorno corrente però è `ring-2 ring-offset-1`, quindi disegna 3 px
FUORI dai bordi della cella: sulla prima riga quei 3 px cadono oltre il bordo
del riquadro di ritaglio e vengono tagliati. Sarebbe successo lo stesso
sull'ultima riga e sulle colonne di bordo, se oggi fosse caduto di lunedì o di
domenica.

`p-1` sul contenitore che ritaglia dà all'anello i suoi 4 px di aria dentro il
riquadro; `-mx-1` e `mt-1` al posto di `mt-2` rimettono la griglia esattamente
dov'era, quindi le celle non si spostano di un pixel: cambia solo dove passa il
bordo di ritaglio.

Non si può usare `overflow-x: hidden` da solo: in CSS, se un asse è `hidden` e
l'altro `visible`, quello visibile diventa `auto` e ritaglia comunque, per
giunta creando uno scroll container.

Scartato anche `ring-inset`, che non può essere tagliato per costruzione: nei
giorni in cui oggi coincide con un evento l'anello scuro finirebbe appiccicato
al colore pieno, senza lo stacco bianco che ha adesso.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 15:15:36 +02:00
davideandClaude Opus 5 1685d0b57a Ripristina il fondo diviso sui giorni con più tipi di evento
Avevo sostituito la cella divisa con fondo neutro e puntini colorati. Si
perdeva l'informazione a colpo d'occhio: il colore dice subito che tipo di
impegno c'è, i puntini vanno cercati.

Torna la resa di main: `linear-gradient(135deg, …)` con gli stop duplicati,
che non è una sfumatura ma bande a taglio netto in diagonale, una per tipo. Il
conteggio resta per **tipo** e non per numero di eventi: due partite nello
stesso giorno restano una cella rossa piena, si divide solo se i tipi sono
diversi.

`coloreTipo` sale a livello di modulo: era ricostruito per ogni cella, cioè
una trentina di volte per mese a ogni render.

Nota sul contrasto, per chi tornerà qui. Il numero sta direttamente sulle
bande con un alone bianco in `drop-shadow`, ed è una scelta di resa esplicita:
il colore deve restare pieno fino al bordo. Ha un costo, però, perché nel
frattempo success e training sono stati scuriti da 0.62 a 0.53 per far passare
il testo bianco dei chip: il numero scuro sulle bande scende da 5.6:1 a 3.9:1
sul blu e da 5.7:1 a 4.0:1 sul verde. Se un giorno la leggibilità dà fastidio,
la leva è una parola — `text-foreground` -> testo bianco sulle sole celle
divise, che risale a ~4.9:1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 15:15:11 +02:00
davideandClaude Opus 5 608f2eda2f Rende il materiale della barra coerente fra WebKit e Blink
A parità di CSS la barra sembrava piena su Safari e trasparente su Chrome
Android. Il colore di fondo era lo stesso sui due: la differenza veniva tutta
dal blur. WebKit rende `backdrop-filter: blur()` molto più denso e "latteo"
di Blink, e `saturate(180%)` amplificava lo scarto — con un fondo al 72%
l'aspetto dipendeva quasi solo dal filtro, cioè dalla parte su cui i due
motori non sono d'accordo.

Il materiale ora si regge sul colore (85%) e usa il blur come rifinitura: la
profondità resta, la differenza fra i due motori quasi sparisce perché pesa
molto meno. `saturate` scende a 150% per la stessa ragione.

`color-mix` passa da `in oklab` a `in srgb`: mescolare con `transparent`, che
è nero con alpha 0, dipende da come il motore premoltiplica, e in srgb è
implementato in modo molto più uniforme.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 14:52:51 +02:00
davideandClaude Opus 5 78ae3fbbd7 Allinea la barra di navigazione fra iOS e Android
L'altezza della barra era dedotta dai padding delle voci e poi ricopiata a
occhio altrove: `pb-24` nel contenitore delle pagine, `pb-28` nella
celebrazione badge. Su iPhone i conti tornavano per 2 px — barra ~60 px più
34 px di safe area contro 96 px riservati — quindi bastava un inset diverso
(orizzontale, o un modello con home indicator più alta) perché l'ultimo
elemento della pagina finisse sotto la barra. Solo su iOS: su Android
avanzavano 36 px.

Ora la geometria sta in due token, `--altezza-nav` e `--pad-sicura-fondo`, e
le utility `spazio-nav` e `pad-sicura-fondo` la leggono da lì. La striscia
toccabile ha altezza fissa, così misura uguale sui due sistemi e sotto varia
solo l'inset: `max(env(safe-area-inset-bottom), 0.5rem)` dà ad Android un
respiro minimo dove iOS mette la home indicator. Identiche al pixel non
possono essere — quell'inset esiste per un motivo fisico che Android non ha —
ma la differenza è ora un margine in più, non un allineamento diverso.

`100vh` diventa `100dvh` ovunque: su iOS Safari `vh` misura il viewport con la
toolbar collassata, cioè più alto di quello visibile, e le schermate a tutta
altezza risultavano più lunghe dello schermo. `dvh` segue la toolbar.

Aggiunto il ripiego per Chrome su Android, che disattiva `backdrop-filter`
senza accelerazione hardware: senza, su quei device la barra restava
semitrasparente e il contenuto si leggeva attraverso.

Resta fuori portata una differenza sola: in Safari non installata la toolbar
di sistema sta in basso e collassa allo scroll, quindi la barra si muove con
essa. Su Chrome la toolbar è in alto. Si risolve installando la PWA.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 14:50:32 +02:00
davideandClaude Opus 5 5a534547ae Riformatta i file che prettier segnalava da prima
`npm run format` ha toccato anche tre file estranei a questo lavoro:
`presenze.ts` aveva un errore prettier che `npm run lint` già segnalava, gli
altri due erano solo a capo nelle tabelle markdown. Stanno qui da soli per non
sporcare i commit di merito.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 12:43:02 +02:00