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>
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>
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>
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>
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>
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>
Calendario: il mese si cambiava solo con due frecce da 36 px. Ora si scorre,
e il punto d'arrivo si sceglie proiettando la velocità di rilascio invece che
dalla posizione del dito, così un colpo secco «lancia» il mese. La griglia
entra ed esce dallo stesso lato del gesto, `dragElastic` dà resistenza
progressiva al bordo al posto di uno stop netto.
Le celle con più tipi di evento generavano un gradiente a fette e ci mettevano
sopra un'ombra bianca sul numero per tenerlo leggibile: un cerotto su un
problema di contrasto. Ora fondo neutro e un puntino per tipo, con il numero
sempre su superficie piena. L'etichetta accessibile dice quanti eventi ci
sono, non solo che ce ne sono.
Profilo: le quattro switch «Notifiche convocazioni», «Promemoria allenamenti»,
«Cambi orario» e «Bacheca squadra» erano `defaultChecked` e non facevano
niente — promettevano una funzione che non esiste. Via anche la conferma
nativa prima di *cambiare* la foto: non è un'azione distruttiva, e chiedere
conferma per tutto insegna a rispondere sì senza leggere. Resta su quella che
la rimuove, che è irreversibile.
Home: la sezione «Da confermare» mostrava titolo e contenitore vuoto quando
non c'era altro da confermare, mentre tutte le altre sezioni hanno un empty
state.
Squadra: i badge in riga avevano solo `title=`, che su touch non appare mai —
aggiunto il testo per gli screen reader. I filtri per criterio erano alti
~26 px.
Più, in tutte le schermate: la primitiva `Card` al posto delle classi
ripetute, i bersagli sotto i 44 px portati in misura e il floor tipografico a
12 px.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`viewport-fit=cover` mancava, quindi `env(safe-area-inset-bottom)` valeva
sempre 0: il codice safe-area della BottomNav c'era già ma era inerte, e su
iPhone in standalone la barra finiva sotto la home bar.
`theme-color` e `background_color` erano `#111111` su un'app con fondo quasi
bianco: barra di stato nera e splash nero prima di una UI chiara.
`lang="en"` su un'app interamente in italiano: gli screen reader leggevano
tutto con fonetica inglese. Con esso 404 e schermata d'errore, che erano
rimaste in inglese.
L'anteprima social puntava a un'immagine di preview Lovable su R2 ormai
scaduta e dichiarava `twitter:site @Lovable`: ora usa l'icona della squadra,
con `summary` al posto di `summary_large_image` visto che l'unica immagine
disponibile è quadrata.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
I template GitHub guidano chi apre una issue a fornire titolo,
descrizione strutturata ed eventuali screenshot invece di un campo
libero. Dal profilo, prima di "Esci", due link aprono direttamente
il template giusto su GitHub.
Rinomina "Cacche/partita" in "Cacche" e passa i pill da flex-wrap a
flex-nowrap con flex-1/truncate, così entrano tutti su una riga sola
senza andare a capo né richiedere swipe orizzontale.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
I pill (Presenze, Media voto, MVP, Palloni, Cacche/partita) andavano su
una riga scrollabile orizzontalmente; ora vanno a capo con flex-wrap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
convocatiEvento() e compleanniEventi() prendono ora la rosa come parametro
invece di leggere la lista statica; csvScoutMatch() idem. Le schermate di
gestione eventi, dettaglio allenamento/partita, calendario e scout live,
più i widget di presenze/pagelle/voto MVP/voto social/sondaggio
cacche/turno palloni, passano tutte la rosa letta da useRosa() o
useGiocatoriSquadra(): un giocatore aggiunto o disattivato dalla dashboard
admin ora si riflette ovunque.
obiettivi.ts, palloni-core.ts, presenze-mese.ts e user-store.ts non
dipendono più dalla lista statica di crapp-data.ts ma ricevono/leggono la
rosa reale (percentuali obiettivo, rotazione turno palloni, riepilogo
presenze mensile, identità del dispositivo). Le route API per le notifiche
push (push-messaggio, promemoria-palloni, sollecita-presenze) leggono la
squadra lato server con leggiGiocatoriSquadra(), filtrando attivo.
useGiocatoreBase() ora interroga giocatori_squadra con useQuery, ma
RootComponent la chiamava prima di montare QueryClientProvider: ogni pagina
falliva in SSR con "No QueryClient set". Il provider avvolge ora tutto fin
dall'inizio; la logica di redirect/loading si sposta in un componente
AppShell interno, comportamento invariato.
Numero e data della tessera arrivano dal comitato dopo l'iscrizione, quindi
li scrive solo un admin (come numero/ruolo): estende il trigger di M1/M5,
aggiunge il pannello dedicato in /admin con badge e conteggio in dashboard.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Due bug architetturali, entrambi con la stessa causa: dati che vivevano
solo in localStorage, quindi visibili a un solo dispositivo.
- Il blocco "chi sta scoutando" (scout-live.ts) non funzionava mai tra
telefoni diversi: due persone potevano prendere il controllo insieme
da dispositivi diversi, sovrascrivendosi a vicenda le azioni nel
salvataggio condiviso. Ora usa scout_sessioni, tabella già presente
nello schema ma mai collegata al codice.
- La partita scoutata finita (scout-store.ts) veniva salvata solo in
localStorage: "Punti/Ace/Muri squadra" e le presenze derivate dallo
scout esistevano solo sul telefono di chi aveva chiuso la partita.
Nuova tabella scout_partite archivia la partita completa (azioni
incluse) per tutta la squadra.
useScoutMatches() mantiene la stessa firma di prima (ScoutMatch[]), ora
alimentata da una query invece che da localStorage: nessuna modifica
necessaria nei punti che la leggono (squadra, classifica, home,
dettaglio partita, rosa). Rigenerati i tipi Supabase.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Le foto caricate in Profilo vivevano solo in localStorage: ogni giocatore
vedeva la propria foto solo sul proprio dispositivo, mai quella dei
compagni nella rosa (Squadra). Ora vengono caricate in un bucket
pubblico dedicato (avatar-giocatori, migration m6) e il componente
Avatar carica l'URL pubblico con fallback su numero/iniziali se assente
o non ancora caricata.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
"Ultima partita" in home e le card di "Storico match" in squadra ora
sono link a /partita/$id quando esiste un evento di calendario con la
stessa data del risultato (CSI o scout), con un'indicazione "Vota MVP"
quando non è ancora stato eletto. Prima erano card statiche: per votare
bisognava trovare la partita a mano dal calendario.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Solo whitespace: tabelle allineate, enfasi normalizzata (* -> _), a capo
coerenti. Nessuna modifica di contenuto.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
La classifica e lo storico partite mostravano dati inventati (squadre e
risultati finti) come base, poi sovrascritti dai dati CSI quando
disponibili. Ora classifica, ultimi risultati, storico match e obiettivi
di squadra legati alle vittorie usano solo dati reali (CSI o scout live),
con stati vuoti quando i dati CSI non sono ancora disponibili.
Include anche la correzione di tutti gli errori di formattazione
prettier segnalati da `npm run lint` sul resto del codice sorgente.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Aggiungi giocatore: nuovo form in /admin (nome, cognome, numero,
ruolo, email opzionale), id g<N> calcolato in automatico.
- Email modificabile anche per i giocatori già in rosa dal pannello
Dati squadra, non solo alla creazione — completa quanto rimandato
da DD-018.
- Disattiva/Riattiva: un giocatore che lascia la squadra sparisce
dalla rosa attiva senza che la riga venga eliminata, così presenze,
voti, pagelle e badge della stagione restano agganciati al suo id.
Nuova sezione "Giocatori disattivati" per riattivarli.
- Messaggio d'errore leggibile per l'unico vincolo unique della
tabella (email duplicata) invece del codice Postgres grezzo.
Nessuna migration: sia l'inserimento sia la modifica passano dalla
policy admin FOR ALL già esistente su giocatori_squadra.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
useMeseNav aveva un default hardcoded (anno 2026, mese 7) invece di
calcolare oggi. Chiunque apra /calendario finiva sempre su agosto
2026, indipendentemente dalla data reale.
Co-Authored-By: Claude Mythos <noreply@anthropic.com>
Al primo accesso l'app collega da sola l'account Google al giocatore
la cui email registrata coincide (case-insensitive), invece di far
scegliere il nome da un elenco. Senza corrispondenza compare solo un
messaggio d'errore con un pulsante per uscire e riprovare con un altro
account: nessuna scelta manuale di ripiego.
- Migration m5: colonna `email` su giocatori_squadra, seed per Ivan
Cacciari e Davide Grilli, trigger esteso per richiedere anche il
match email oltre allo slot libero.
- slotPerEmail() sostituisce slotLiberi() (rimossa, senza più
chiamanti in produzione).
Co-Authored-By: Claude Mythos <noreply@anthropic.com>
Estrae in palloni-core.ts la logica pura di push-messaggio e
promemoria-palloni (finora mista alle chiamate Supabase), così da
poterla testare senza scrivere sul database. Aggiunge test per le
funzioni pure di giocatori-squadra.ts, finora senza copertura.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Remove the free player selection from /benvenuto: without a Supabase session
no screen renders, and the VITE_AUTH_OBBLIGATORIA bridge flag is gone.
Admin rights now come only from user_roles, so the hardcoded name list in
crapp-data.ts is deleted along with its tests.
Add migration m4_solo_autenticati, which revokes anon access to the v1.0
tables. Apply it only once the whole team has linked an account.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Attua DD-017. La scheda della dashboard era di sola lettura: ora l'admin apre il
giocatore e modifica.
- Dati squadra (nome, cognome, numero, ruolo): le docs li assegnavano già agli
amministratori, ma non esisteva nessuna schermata per cambiarli.
- Dati personali e del documento: compilabili al posto del giocatore, perché un
export CSI incompleto rimanda il lavoro in chat.
- Scollega account: libera uno slot assegnato per errore, come previsto da
DD-016 regola 2.
I file restano fuori: l'admin li scarica ma non li carica al posto di altri.
Nessuna migration: le policy di M1 e M2 riconoscevano già l'admin. I campi del
profilo diventano un componente condiviso (CampiProfilo) tra la schermata del
giocatore e la dashboard, con gli upload passati come slot: da admin quelle
righe non compaiono.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Implementa la voce "Dashboard amministratore" della roadmap v1.1, come descritta
in docs/modules/profilo-giocatore.md, insieme alla parte di profilo che la
alimenta.
- /admin: stato dei profili della squadra, download di documento, certificato e
foto tessera, export CSV con i 12 campi del tesseramento CSI. Un certificato
scaduto non conta come valido.
- Profilo giocatore: dati personali, documento (fronte e retro), certificato e
foto tessera, con widget di completamento in Home che sparisce al 100%.
- I permessi di amministrazione arrivano da user_roles (DD-011) e non più dalla
lista di nomi in crapp-data.ts, che resta come ponte finché
VITE_AUTH_OBBLIGATORIA non viene acceso in produzione.
- Login Google via Supabase Auth: al primo accesso l'account si collega a uno
slot libero di giocatori_squadra, e il vincolo lo fa rispettare il trigger di
M1 (DD-016 regola 2).
Migration additive: M2 crea profili_giocatore, M3 il bucket privato
profili-giocatore. Nessuna tabella v1.0 viene toccata, quindi si possono
applicare senza cambiare il comportamento attuale dell'app.
supabase/config.toml e seed.sql configurano lo stack locale: serve perché il
progetto Supabase è uno solo, condiviso tra sviluppo e produzione.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Campionato page showed hardcoded demo data. It now reads the real
2025/26 season (Campionato Open Misto Eccellenza, project 767, team
3359, Girone B) from the Livescore CSI Bologna portal.
The portal has no documented API: we call the same endpoints its own
pages call over ajax, so parsing must degrade gracefully. A single
server route fetches them, caches for 6 hours and serves the last good
payload on failure; the page falls back to the previous data when
nothing is available. No browser ever contacts the portal, keeping the
request count independent of how many players open the app.
Also fixes the header, which claimed "Girone C - CSI Milano".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>