Commit Graph
97 Commits
Author SHA1 Message Date
Ivan CacciariandCursor ca1eee5425 Ripulisce profilo e tab, e mette Apri evento sull'ultima riga delle presenze.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-07 08:48:21 +02:00
Ivan CacciariandCursor 6bc5e8f006 Ingrandisce i caratteri delle tab sottosezioni e dei controlli calendario.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-07 08:24:59 +02:00
davideandClaude Opus 5 f149d02e9a Apre il voto MVP due ore dopo l'inizio e lo riserva ai presenti.
La votazione non dipendeva dal tempo ma dal risultato: compariva solo con un
referto CSI o uno scout salvato, e chiunque avesse un profilo poteva votare.
Ora `votoMvpAperto()` la apre due ore dopo `data`+`ora` dell'evento, il pannello
sta in una sezione sua e votano — e sono votabili — solo i presenti (o in
ritardo) di quell'evento.

I voti passano dall'id scout/CSI a quello dell'evento CrAPP: home e classifica
leggono il vincitore tramite la mappa data → evento che avevano già. Le regole
sono applicative, non RLS: chi scrive su PostgREST le aggira, come già annotato
tra i limiti noti del modulo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 22:11:26 +02:00
davideandClaude Opus 5 7c1f08d943 Copre la cache delle presenze, le tabelle aperte e le letture server.
Chiude i tre buchi di copertura rimasti dalla rilettura della documentazione:
comportamenti che la doc descrive come regole ma che nessun test verificava.

- conRisposta() esce da useSalvaPresenza e diventa una funzione pura in presenze.ts.
  La mutation non rilegge dopo la scrittura, quindi la cache deve imitare il database:
  l'istante si scrive solo se manca (??=) e il ritiro della risposta lo cancella. Erano
  due dettagli che serie-presenze.md chiede di preservare e che un refactor poteva
  perdere in silenzio, falsando la serie "Conferme 24h" fino al refresh successivo.
- permessi.test.ts copre ora anche il terzo gruppo di DD-023, le tabelle lasciate
  aperte di proposito: scout_sessioni, scout_live, scout_partite e push_subscriptions.
  Non dicono che sono sicure, dicono che sono aperte per scelta: se una prende un gate
  nell'interfaccia, le policy devono seguirlo e questi casi vanno cambiati con loro.
- lettori-server.test.ts esegue leggiEventi() e leggiGiocatoriSquadra() sul database
  locale, mettendo le credenziali in process.env prima della prima chiamata perché
  supabaseAdmin nasce pigramente. Nessuna delle due controlla l'errore di PostgREST:
  una colonna rinominata darebbe zero righe, zero destinatari e nessuna push, senza
  che niente segnali il problema. Scrivendolo è emerso che eventi_app.note è NOT NULL,
  mentre RigaEvento lo dichiara nullable.

PROJECT_STATE: m12_niente_autovoto è applicata in produzione dal 06/09/2026,
verificata con `npx supabase migration list` (23 migration, local = remote).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 19:46:52 +02:00
davideandClaude Opus 5 f687322c3f Vieta l'autovoto, allinea la doc al codice e toglie tre riletture.
Rilettura completa della documentazione confrontata con il codice. Dove la doc
diceva il falso l'ho corretta; dove aveva ragione lei ho corretto il codice.

Autovoto (la doc aveva ragione)

- migration m12_niente_autovoto: vincoli mvp_no_autovoto e badge_social_no_autovoto,
  gli stessi che pagelle_voti ha dalla v1.0. Le righe che li violano vengono
  cancellate prima dell'ALTER, altrimenti fallisce; in locale non ce n'erano.
  M11 garantisce solo che il voto sia firmato con il proprio votante_id, non che il
  votato sia un altro: eleggersi MVP restava a un POST di distanza.
- VotazioneMvp non mostra più il votante nell'elenco, come già faceva VotoSocial.

Test che guardavano la colonna sbagliata

- scritture.test.ts verificava che aggiornato_il si muovesse, chiamandolo "quello che
  alimenta la serie di conferme". È l'opposto: la serie usa risposto_il, che il trigger
  di M9 deve tenere fermo. Ora il test prova a riscriverlo e controlla che il database
  abbia tenuto la prima risposta; prima passava anche senza trigger.
- destinatariSollecito() esce dalla route sollecita-presenze e diventa una funzione pura
  in presenze.ts, con i suoi test — stesso trattamento di avvisiPalloniEvento.

Tre riletture in meno

- giocatori-squadra, scout-store e avatar-store usavano invalidateQueries dove il dato
  scritto era già noto: ora setQueryData, come il resto dell'app. Resta scout-live, dove
  il lock può averlo vinto un altro dispositivo.

Documentazione riallineata

- presenze.md, badge.md, mvp.md: i limiti su RLS aperta e route non autenticata erano
  superati da M11 e DD-024;
- serie-presenze.md: il filtro è e.data < oggi, non <=, e l'evento di oggi non conta
  (conterebbe come assenza per tutti); aggiunta la tabella risposto_il/aggiornato_il;
- ARCHITECTURE.md ed EFFICIENZA_CLOUD.md: una sola eccezione a setQueryData;
- DATABASE.md: i vincoli delle tre tabelle di voto;
- PROJECT_STATE.md: fermo a M9, ora arriva a M12.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 19:30:49 +02:00
davideandClaude Opus 5 752b300474 Copre presenze, calendario e guardia admin con nuovi test.
Confrontando le funzioni esportate di src/lib/ con i riferimenti in test/,
le uniche funzioni pure ancora scoperte erano i conteggi presenze, tre
funzioni del calendario, i titoli MVP e i rifiuti della guardia notifiche.

- unit: contaPresenzeGiocatore e totaliEventiGiocatore, compleanniEventi,
  convocatiEvento, eventoVuoto, mvpVintiPerGiocatore, e il nuovo
  auth-route.test.ts sui 401 di richiediAdmin (DD-024);
- integration/permessi: badge_social_voti, che mancava tra le tabelle di M11,
  e le deroghe "Gli admin gestiscono tutti/e ...", mai verificate finora.

Due modifiche al codice servivano per poter scrivere i test:

- presenze.ts: i conteggi leggevano la data da dataOggi() e non erano
  verificabili; ora accettano oggi come parametro opzionale, come già
  facevano serieConsecutiva e serieConferme. Toglie anche la dipendenza
  dall'orologio che avrebbe cambiato i risultati dei test delle serie dal
  10 settembre in poi;
- auth-route.server.ts: "Bearer .." passava il pre-check con tre segmenti
  vuoti e arrivava fino alla chiamata di rete; ora i segmenti devono essere
  non vuoti.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 19:03:18 +02:00
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 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 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 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 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
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