Piattaforma full-stack per la raccolta, la governance, la consultazione e l'analisi dei dati della ginnastica artistica.
Milestone corrente (v0.4.0-public-frontend): completato il frontend pubblico USER nelle sezioni Home, Athletes, Events, Rankings e Analytics, incluse Scheda Atleta e Scheda Evento. La fase successiva riguarda autenticazione reale, area personale e strumenti Admin/Super Admin.
- Python 3.10+
- FastAPI
- SQLAlchemy
- SQLite (MVP)
- JWT authentication
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtConfigura le variabili locali copiando .env.example in .env.
alembic upgrade head
uvicorn app.main:app --reloadL'interfaccia pubblica minimal si trova in frontend/. E dependency-free e realizzata in HTML, CSS e JavaScript vanilla. Il perimetro pubblico USER comprende Home, Athletes, Events, Rankings, Analytics, Scheda Atleta e Scheda Evento.
La scheda atleta include una prima sezione analytics interattiva: profilo attrezzi MAG a esagono, profilo WAG a rombo, metrica selezionabile (Final Score, D Score, E Score, Penalty, Bonus), filtro multi-selezione apparatus (FX, PH, SR, VT, PB, HB, UB, BB, AA, VT AVG in base alla disciplina), cursore temporale a intervallo, statistiche riepilogative, trend e medie per anno in SVG/CSS senza librerie esterne. La vista iniziale mostra il profilo completo senza un pulsante All/Tutto; nelle statistiche totali non include AA, per evitare di sommare una seconda volta un punteggio gia derivato dagli attrezzi. Quando l'utente seleziona uno o piu attrezzi il poligono evidenzia i vertici pertinenti e i grafici si aggiornano live. AA entra nell'analisi solo quando selezionato esplicitamente, mantiene evidenziata l'intera area e mostra le curve componenti sullo sfondo, mentre VT AVG usa la curva principale del vault average e curve secondarie per VT 1/VT 2 quando disponibili. Le analytics indicano anche i cicli olimpici/scoring cycles coinvolti nella selezione, mostrano un warning quando il periodo attraversa codici di punteggio diversi e il trend visualizza assi temporali/punteggio con linee verticali di separazione tra i cicli. Per gli eventi multigiorno senza data sessione nelle fonti Gymternet/Calendar, i punti analytics sono ordinati usando l'inizio evento ma vengono presentati come appartenenti al Periodo evento, evitando una falsa precisione temporale.
La sezione pubblica Analytics consente inoltre di cercare e confrontare due atleti della stessa disciplina. Un unico set di controlli sincronizza attrezzi, metrica, periodo/istante e timeline per entrambi; radar e trend possono essere visualizzati affiancati oppure sovrapposti. Scale numeriche e dominio temporale sono condivisi, evitando confronti visivi alterati da autoscaling differente. Il vincolo MAG/MAG o WAG/WAG e applicato anche dal backend.
cd frontend
python3 -m http.server 5173Poi aprire http://localhost:5173. Se la porta 5173 e gia occupata, si puo usare il fallback locale:
python3 -m http.server 5174e aprire http://localhost:5174. Il frontend usa di default l'API su http://localhost:8000.
POST /auth/registerRegistra un account con email e password; l'account resta non verificato finche l'utente non conferma il link emailPOST /auth/verify-emailConferma l'account tramite token inviato via emailPOST /auth/resend-verificationReinvia il link di verifica con rate limitPOST /auth/loginLogin con email/password; per admin e super admin richiede MFA TOTP o recovery codePOST /auth/password/forgotPOST /auth/password/resetPOST /auth/password/changePOST /auth/mfa/setupPOST /auth/mfa/confirmGET /auth/meRestituisce l'utente loggato, ruolo, lingua preferita e stato MFAGET /admin/usersEndpoint admin-only per cercare utenti registrati per email, ruolo e statoPUT /admin/users/{user_id}/roleEndpoint admin-only per cambiare ruolo a un utente tramite idPUT /admin/users/role-by-emailEndpoint admin-only per promuovere o modificare ruolo a un utente tramite email registrataGET /analytics/filter-optionsRestituisce i valori realmente presenti nel database per costruire filtri globali della dashboard: anni, discipline, categorie, format, round, apparatus, country, livelli evento e metriche disponibiliGET /analytics/rankingsRanking globale pubblico con filtri per evento, periodo, disciplina, category, apparatus, format, round, country, livello, qualita dato e metrica (score,D_score,execution_estimate,E_score,Penalty,Bonus)GET /analytics/athletes/compareRestituisce serie sovrapponibili per confrontare atleti su una metrica, con aggregazioniraw,best_by_event,average_by_year,average_by_apparatusGET /analytics/athletes/{athlete_id}/apparatus-profileRestituisce il profilo attrezzi per scheda atleta:hexagonMAG orhombusWAG, vertici ordinati, metrica selezionata, criterio (average,best,latest) e valori normalizzati per disegnare il poligonoGET /analytics/athletes/{athlete_id}/profile-viewRestituisce una vista composita per scheda atleta: dati atleta, dashboard trend/statistiche, profilo attrezzi, metriche e qualità dato disponibiliGET /analytics/athletes/{athlete_id}/dashboardRestituisce una scheda dashboard pronta per grafici: trend, riepilogo, breakdown per anno, breakdown per apparatus e risultati recentiGET /analytics/age-by-countryRestituisce punti eta atleta-gara e media eta per country, usandobirth_yeardell'atleta eyeardell'eventoPOST /site-analytics/eventsEndpoint pubblico leggero per registrare eventi d'uso privacy-friendly: page view, search, athlete view, event view, dashboard view e session endGET /site-analytics/admin/summaryEndpoint admin-only per visualizzare statistiche aggregate del sito webGET /athletesPOST /athletesPUT /athletes/{athlete_id}Se modificacountrye includecountry_change_year, registra anche una riga incountry_changesDELETE /athletes/{athlete_id}POST /athletes/{athlete_id}/imagePOST /athletes/{athlete_id}/country-changesEndpoint admin-only per registrare un cambio country conto_countryechange_yearGET /athletes/{athlete_id}/admin-viewEndpoint admin-only: restituisce la scheda Athlete ufficiale e i suggerimenti pendenti visibili solo agli adminPOST /athletes/{source_athlete_id}/merge-previewEndpoint admin-only per verificare se una scheda atleta duplicata puo essere unita a un atleta canonico indicato tramitetarget_athlete_idPOST /athletes/{source_athlete_id}/mergeEndpoint admin-only per fondere una scheda atleta duplicata nell'atleta canonico, conconfirm=true, audit log e blocco se esistono conflitti ResultGET /athletes/suggestionsGET /athletes/{athlete_id}/resultsGET /athletes/{athlete_id}/events/{event_id}/resultsGET /athletes/compare?ids=1,2,3GET /athletes/{athlete_id}/scores-over-timeGET /athletes/compare/scoresGET /athletes/{athlete_id}/statsGET /eventsPOST /eventsPUT /events/{event_id}DELETE /events/{event_id}POST /events/{event_id}/imageGET /events/{event_id}/admin-viewEndpoint admin-only: restituisce la scheda Event ufficiale e i suggerimenti pendenti visibili solo agli adminGET /events/{event_id}/resultsSupporta filtri performat,round,discipline,category,apparatus,athlete,data_qualityathletepuo essere id, nome, cognome,nome cognomeoppurecognome nomeSupporta anchesort_byconscore,D_score,execution_estimate,E_score,Penalty,BonusLa risposta costituisce la classifica dei result di quell'evento per i filtri selezionati Supportalimiteoffsetper non scaricare classifiche troppo grandi in una sola richiestaGET /events/{event_id}/ranking-viewRestituisce in una sola rispostaevent, opzioni filtro, suggerimenti atleta, filtri applicati e classifica arricchitaGET /events/{event_id}/profile-viewRestituisce una vista composita per scheda evento: dati calendario, stato evento, filtri reali, gruppi result e classifica principaleGET /events/{event_id}/athlete-suggestionsRestituisce suggerimenti di completamento per il filtroathlete, limitati agli atleti che hanno result in quell'eventoGET /events/calendarRestituisce eventi in formato calendario pubblico, anche futuri e anche senza result, conresult_count,has_resultsecalendar_statusSupportalimiteoffsetGET /events/{event_id}/result-filter-optionsRestituisce solo i valori realmente presenti nei result dell'evento performat,round,discipline,category,apparatusRestituisce ancheranking_metricsedata_qualitiesdavvero disponibili, condefault_ranking_metric=scoreedefault_data_quality=allGET /events/{event_id}/result-groupsPOST /events/{event_id}/result-contextGET /events/{event_id}/result-contextDELETE /events/{event_id}/result-contextGET /events/{event_id}/manual-entry-optionsEndpoint admin-only per costruire una UI di inserimento manuale: restituisce discipline, categorie, attrezzi, format, round, campi richiesti, campi opzionali, contesto corrente e suggerimenti atleta ammessi dall'eventoGET /events/{event_id}/result-athlete-suggestionsEndpoint admin-only per suggerire atleti gia presenti nel database durante la digitazione di un result, anche connome cognomeocognome nomeGET /events/manual-entry-optionsEndpoint admin-only per costruire la UI di creazione evento: restituisce discipline, categorie, livelli e campi richiesti/opzionaliPOST /events/{event_id}/athletes/resolveEndpoint admin-only per trovare un atleta esistente oppure creare una nuova entitaAthletecompatibile con l'eventoPOST /events/{event_id}/results/bulkGET /admin/calendarEndpoint admin-only per alimentare una sezione calendario gestionale: restituisce eventi, summary per stato, conteggi result e reminder degli eventi conclusi senza risultatiGET /admin/entities-to-completeEndpoint admin-only per alimentare una futura sezione di controllo: restituisceAthleteedEventcon campi opzionali ancora da completare dopo data entry manuale o importGET /admin/event-result-remindersEndpoint admin-only per vedere eventi conclusi che non hanno ancora result inseritiPOST /admin/event-result-reminders/notifyEndpoint admin-only per creare notificheevent_results_reminderper gli admin, senza duplicarle per lo stesso eventoGET /resultsSupportalimiteoffsetGET /results/analytics/rankingsSupportasort_bycon la stessa semantica della classifica eventoGET /results/analytics/trendsPOST /resultsPUT /results/{result_id}DELETE /results/{result_id}GET /admin/audit-logsEndpoint super-admin-only per consultare lo storico delle modifiche critiche suAthlete,Event,Resulte ruoliPUT /admin/athletes/{athlete_id}/restorePUT /admin/events/{event_id}/restorePUT /admin/results/{result_id}/restoreEndpoint super-admin-only per ripristinare entita cancellate con soft deletePOST /preferences/athletes/followGET /preferences/athletes/followedGET /preferences/athletes/followed/detailsEndpoint user-only per mostrare in dashboard personale gli atleti seguiti con scheda atleta, conteggio result e ultimo resultDELETE /preferences/athletes/follow/{athlete_id}POST /preferences/events/saveGET /preferences/events/savedGET /preferences/events/saved/detailsEndpoint user-only per mostrare in dashboard personale gli eventi salvati con dati calendario,calendar_status,result_countehas_resultsDELETE /preferences/events/save/{event_id}GET /preferences/language-optionsEndpoint pubblico per esporre le lingue supportate dalla UI:en,it,es,fr; la lingua principale/default eenGET /preferences/languageEndpoint pubblico per risolvere la lingua corrente: per utenti loggati restituiscepreferred_language; per visitatori anonimi usa?language=it|es|fr|en, poiAccept-Language, poi defaultenPUT /preferences/languageEndpoint user-only per aggiornare la lingua preferita salvata sull'accountPOST /preferences/dashboard-viewsGET /preferences/dashboard-viewsGET /preferences/dashboard-views/defaultGET /preferences/dashboard-views/{view_id}PUT /preferences/dashboard-views/{view_id}DELETE /preferences/dashboard-views/{view_id}Endpoint user-only per salvare ricerche, filtri e configurazioni dashboard personaliGET /notificationsPUT /notifications/{notification_id}/readPUT /notifications/read-allLe notifichenew_resultsono cumulative per atleta, gara, round e format: piu punteggi dello stesso atleta nella stessa gara/round/format aggiornano una sola notifica. Le notifichedata_entry_summaryricordano all'admin quando il data entry manuale ha creato nuovi atleti da completare.GET /data-suggestionsPOST /data-suggestionsPOST /data-suggestions/generateEndpoint admin-only per chiedere al provider AI/web configurato di proporre suggerimenti pendenti per campi mancantiPOST /data-suggestions/{suggestion_id}/acceptPOST /data-suggestions/{suggestion_id}/rejectEndpoint admin-only per creare, consultare, approvare, modificare o rifiutare suggerimenti sui campi mancanti diAthleteeEventGET /world-gymnastics/athletes/{athlete_id}/candidatesEndpoint admin-only per cercare profili atleta ufficiali World Gymnastics compatibili con la scheda AthletePOST /world-gymnastics/athletes/{athlete_id}/suggestionsEndpoint admin-only per creare suggerimenti pending dalla scheda profilo atleta World Gymnastics scelta dall'adminGET /world-gymnastics/events/{event_id}/candidatesEndpoint admin-only per cercare eventi ufficiali World Gymnastics compatibili con la scheda EventPOST /world-gymnastics/events/{event_id}/suggestionsEndpoint admin-only per creare suggerimenti pending dalla scheda evento World Gymnastics scelta dall'admin
AthleteCampi principali:first_name,last_name,birth_year,country,discipline,image_url,is_profile_verified,country_changes,world_gymnastics_athlete_id,world_gymnastics_profile_url,world_gymnastics_statusbirth_year: opzionale; anno di nascita ufficiale/confermato, validato lato API come anno non futurocountryrappresenta la nazionalita corrente;country_changesregistra eventuali cambi confrom_country,to_country,change_yeardiscipline:MAGoppureWAGworld_gymnastics_athlete_id,world_gymnastics_profile_urleworld_gymnastics_status: opzionali; vengono pensati come metadati ufficiali World Gymnastics e restanoNULLfinche un admin non verifica/approva il profilo tramite il motore World Gymnasticsworld_gymnastics_verified_ateworld_gymnastics_verified_by_admin_id: valorizzati automaticamente quando un admin approva un suggerimento World Gymnastics legato al profilo ufficialeis_profile_verified: booleano amministrativo indipendente dal collegamento World Gymnastics; governa il badge pubblico di verifica della Scheda Atleta ed e modificabile soltanto tramite endpoint protetti Admin/Super AdminEventCampi principali:name,location,venue,start_date,end_date,year,discipline,category,level,image_url,world_gymnastics_event_id,world_gymnastics_event_url,world_gymnastics_statusdiscipline:MAG,WAG,MAG and WAGcategory:junior,senior,junior and seniorNei filtri API,MAG and WAGcorrisponde alla selezione contemporanea diMAGeWAGNei filtri API,junior and seniorcorrisponde alla selezione contemporanea dijunioreseniorworld_gymnastics_event_id,world_gymnastics_event_urleworld_gymnastics_status: opzionali; restanoNULLfinche un admin non verifica/approva l'evento tramite il motore World Gymnasticsworld_gymnastics_verified_ateworld_gymnastics_verified_by_admin_id: valorizzati automaticamente quando un admin approva un suggerimento World Gymnastics legato all'evento ufficialeResultCampi principali:athlete_id,event_id,represented_country,discipline,category,apparatus,vt_attempt,day,format,round,D_score,E_score,Penalty,Bonus,score,rankrepresented_country: country rappresentata dall'atleta in quella specifica gara; resta separata daAthlete.country, che indica la nazionalita correntediscipline: deve coincidere con la disciplina dell'atleta e deve essere ammessa dall'eventocategory:junioroppuresenior, mai combinata, e deve essere ammessa dall'eventoday: opzionale;NULLper result senza distinzione giornaliera, numero positivo per gare su piu giornatescore,D_score,E_score,Penalty,Bonus: opzionali quando il dominio lo consentee_score_statusepenalty_status: campi API calcolati per spiegare il significato diE_scoreePenalty:availableoppurenot_availableper i dati Gymternet legacy,E_scoreePenaltyrestano normalmenteNULLperche non conosciuti; la UI deve mostrarli comenon disponibiledal 2026 in poi, nei flussi manuali o standard moderni,E_scoree obbligatorio;PenaltyeBonusvuoti/nulli vengono salvati come0.0ed esposti con statusavailableBonus: resta numerico oppureNULL; non va popolato con stringhe testualibonus_status: campo API calcolato per spiegare il significato diBonus:available,not_available,not_applicableper il periodo 2018-2024 il Bonus non esisteva nel codice dei punteggi:Bonus=NULLviene esposto comebonus_status=not_applicabledal 2025 il Bonus esiste nel regolamento, ma nei dati Gymternet non e registrato: se il Result e in un caso in cui il Bonus potrebbe esistere eBonus=NULL, viene esposto comebonus_status=not_availableregole Bonus note dal 2025:WAGsolo suVT AVGcon valore0.2se conosciuto;MAGsolo suFX,SR,VT,PB,HBcon valore0.1se conosciuto La UI deve mostrare i campi score opzionaliNULLcomenon disponibilesolo quando il relativo status enot_availableLe risposte API aggiungonoexecution_estimate = score - D_scoresolo quandoscoreeD_scoresono disponibili; non e un E-score ufficiale e non viene salvato nel database Quandoexecution_estimatee presente, le risposte API aggiungono indata_warningsun avviso calcolato in base agli status disponibili Sepenalty_status=not_available, l'avviso cita le Penalties non disponibili; sebonus_status=not_available, cita anche il possibile Bonus non registrato Le risposte API aggiungono ancheis_completeemissing_fields; un Result senzascoreo senzaD_scoree valido solo nei casi previsti e viene marcato come incompletovault_attempt_order_uncertain:truequando unVT attempt 1/2deriva dalla logica Gymternet pre-2025 e l'ordine effettivo dei vault potrebbe essere invertito; le risposte API aggiungonodata_warningsper permettere alla UI di mostrare un piccolo!apparatus: perMAG:FX,PH,SR,VT,PB,HB,AA,VT AVGperWAG:VT,UB,BB,FX,AA,VT AVGDataSuggestionCampi principali:entity_type,entity_id,field_name,suggested_value,reviewed_value,confidence,source_url,source_title,evidence,status,created_by_admin_id,reviewed_by_admin_identity_type:athleteoppureeventstatus:pending,accepted,edited,rejectedsuggested_valueconserva il valore proposto dall'assistente;reviewed_valueconserva il valore finale approvato o modificato dall'admin I suggerimenti sono visibili solo agli admin e non modificano mai i dati ufficiali finche un admin non li approva o li modifica esplicitamente
Aggiungi le variabili d'ambiente per inviare link di verifica email e reset password via SMTP:
DATABASE_URL(opzionale, defaultsqlite:///./leverage.db)APP_ENV(defaultdevelopment)SECRET_KEY(opzionale in locale, consigliata in produzione)FRONTEND_BASE_URL(obbligatoria in staging/production e deve usare HTTPS)SMTP_HOSTSMTP_PORTSMTP_USERSMTP_PASSWORDEMAIL_FROMAI_SUGGESTIONS_PROVIDER(disabledoppureopenai)OPENAI_API_KEYOPENAI_BASE_URL(defaulthttps://api.openai.com/v1)OPENAI_MODEL(defaultgpt-5)AI_SUGGESTIONS_TIMEOUT_SECONDS(default30)WORLD_GYMNASTICS_TIMEOUT_SECONDS(default10)
Se le variabili SMTP non sono configurate in locale, il backend continua a funzionare in modalità development/test stampando il contenuto email in console.
In production e staging, SECRET_KEY, SMTP e FRONTEND_BASE_URL HTTPS devono essere configurati esplicitamente.
Il primo super_admin va creato da terminale, non tramite auto-promozione pubblica:
python -m app.bootstrap_admin --email owner@example.comIl comando crea o aggiorna l'account come super_admin, imposta password e verifica email. Al primo login web l'account dovra completare il setup MFA.
Gli admin successivi vengono nominati da un super_admin. Il flusso consigliato per la UI admin e:
GET /admin/users?search=email@example.comCerca l'utente registrato tramite email.PUT /admin/users/role-by-emailPromuove l'utente aadmincon payload{"email": "...", "role": "admin"}.
LEVERAGE impedisce di rimuovere l'ultimo super_admin e impedisce a un super_admin di degradare se stesso. Quando un utente viene promosso ad admin/super admin, riceve una notifica personale di tipo admin_promotion e al login successivo deve usare/configurare MFA. Quando un admin viene declassato a user, riceve una notifica personale di tipo admin_demotion.
Le operazioni ordinarie di data entry restano disponibili agli admin, ma le operazioni distruttive e di governo sono riservate ai super_admin.
Athlete,EventeResultusano soft delete:DELETEnon rimuove fisicamente il record, ma valorizzais_deleted,deleted_atedeleted_by_admin_id.- Le API pubbliche e analytics escludono i record soft-deleted.
- Il
super_adminpuo ripristinare entita cancellate tramite endpoint/admin/.../restore. - Le modifiche critiche producono record in
audit_logs, consultabili conGET /admin/audit-logs. - Le operazioni distruttive o di cambio ruolo possono generare notifiche
security_alertper gli altri super admin.
Il soft delete non sostituisce i backup: in produzione sara comunque necessario configurare backup automatici del database a livello infrastrutturale, idealmente giornalieri o piu frequenti durante import massivi.
Gli Event possono essere creati anche prima che la gara si svolga e possono quindi non avere ancora Result associati.
GET /events/calendar espone una vista calendario pubblica con stato calcolato automaticamente:
upcoming: evento futuroongoing: evento in corsocompleted_no_results: evento concluso senza resultcompleted_with_results: evento concluso con result
Lo stato non viene salvato manualmente nel database: deriva da start_date, end_date, data corrente e numero di Result associati.
Per gli admin, GET /admin/calendar restituisce una vista gestionale aggregata con eventi, summary per stato e reminder. GET /admin/event-result-reminders mostra solo gli eventi conclusi senza result; POST /admin/event-result-reminders/notify crea una notifica event_results_reminder per ricordare l'inserimento dei risultati.
LEVERAGE e pensato come sito pubblico consultabile senza login: atleti, eventi, risultati, classifiche e analytics sono leggibili da visitatori anonimi.
Il login serve per entrare nell'area personale o nell'area admin:
- visitatore anonimo: puo leggere e filtrare dati ufficiali
user: puo leggere dati ufficiali e salvare preferenze personaliadmin: puo fare data entry, import, correzioni, upload immagini e approvazione suggerimenti
Per la UI il flusso consigliato e: POST /auth/register, verifica email tramite POST /auth/verify-email, poi POST /auth/login. Dopo il login, la UI puo chiamare GET /auth/me per sapere ruolo, stato account e MFA. Se un admin non ha ancora MFA attivo, POST /auth/login restituisce mfa_setup_required e un token temporaneo per completare POST /auth/mfa/setup e POST /auth/mfa/confirm.
La lingua principale del backend e l'inglese. I dati sportivi e i codici API (MAG, WAG, FX, score, enum tecniche) non vengono tradotti per non rompere il contratto dati.
I visitatori anonimi possono cambiare lingua nella UI senza account: il frontend deve conservarla localmente, per esempio in local storage o cookie, e puo validarla/risolverla con GET /preferences/language?language=it. Se non c'e scelta esplicita, il backend puo usare l'header Accept-Language; se non e supportato, torna en.
Gli utenti registrati hanno preferred_language, con default en e valori ammessi en, it, es, fr. La UI puo leggere le opzioni da GET /preferences/language-options, salvare la scelta persistente con PUT /preferences/language e recuperarla anche da GET /auth/me.
Le notifiche generate dal backend usano sempre la lingua preferita del destinatario loggato. Per visitatori anonimi, testi di interfaccia e label visuali restano responsabilita del frontend.
La creazione, modifica, cancellazione e upload immagini di Athlete, Event e Result sono operazioni riservate agli admin.
Per una interfaccia semplice di data entry, il frontend puo usare questo flusso:
GET /events/manual-entry-optionsMostra i valori ammessi per creare una nuova gara.POST /eventsCrea la gara.PUT /events/{event_id}Completa o corregge i dati della gara.GET /events/{event_id}/manual-entry-optionsMostra le opzioni coerenti con quella gara.POST /events/{event_id}/result-contextImposta il contesto di inserimento, soprattuttoformateround, cosi non vanno ripetuti su ogni result. Per gare su piu giornate puo includere ancheday.GET /events/{event_id}/result-athlete-suggestionsDurante la digitazione del result suggerisce atleti gia presenti nel database e compatibili con la gara.POST /events/{event_id}/athletes/resolveMentre l'admin digita il nome atleta, il sistema puo riusare un atleta gia presente oppure crearne uno nuovo se manca.POST /events/{event_id}/results/bulkInserisce i result usando il contesto corrente.
Gli endpoint manual-entry-options evitano valori hardcoded nella UI e restituiscono solo opzioni coerenti con l'evento selezionato.
Nel bulk results ogni riga puo usare athlete_id se l'atleta esiste gia, oppure athlete con first_name, last_name, country, birth_year, image_url se deve essere creato automaticamente. represented_country e facoltativo e, se omesso, viene inizializzato con la country corrente dell'atleta.
Dal 2026, nel data entry manuale E_score e obbligatorio. Penalty e Bonus lasciati vuoti vengono salvati come 0.0. Prima di creare i result, il bulk verifica per ogni riga Final Score = D_score + E_score - Penalty + Bonus: se almeno una riga non torna, l'intero inserimento viene bloccato e l'admin riceve una notifica cumulativa data_entry_summary con i result da correggere.
GET /events/{event_id}/manual-entry-options segnala questa regola alla UI: per eventi dal 2026 in poi E_score viene restituito tra i required_result_fields, mentre per eventi storici resta tra i campi opzionali.
Il bulk manuale e il POST /results bloccano anche duplicati sullo stesso contesto sportivo: atleta, evento, discipline, category, apparatus, vault attempt, day, format e round.
Quando POST /events/{event_id}/athletes/resolve o il bulk results creano nuovi atleti, LEVERAGE genera una notifica admin data_entry_summary con il riepilogo degli atleti creati e il link logico all'evento.
La futura UI admin puo usare GET /admin/entities-to-complete per mostrare gli atleti e gli eventi con campi ancora vuoti, cosi l'admin puo completare le schede dopo aver finito la classifica in corso.
La futura UI admin puo usare anche GET /admin/result-duplicate-groups per controllare eventuali duplicati gia presenti nel database prima o dopo import storici: l'endpoint raggruppa i Result con stessa identita sportiva e mostra gli ID da verificare.
Se dopo un import o un controllo manuale emerge che lo stesso atleta e stato salvato come due entita diverse per un errore di battitura, LEVERAGE espone un flusso admin-only per unirle.
Flusso consigliato per la UI admin:
- L'admin apre la scheda duplicata e inserisce l'ID dell'atleta corretto/canonico.
POST /athletes/{source_athlete_id}/merge-previewcon{"target_athlete_id": ...}.- La UI mostra dati dei due atleti, result da spostare, preferenze utente coinvolte, country history, suggerimenti/notification da riallacciare e conflitti bloccanti.
- Se
can_merge=true, l'admin conferma conPOST /athletes/{source_athlete_id}/mergee payload{"target_athlete_id": ..., "confirm": true, "reason": "..."}.
Il merge sposta i Result verso l'atleta canonico, mantiene represented_country sui result, riallaccia i follower, sposta country history non duplicate, sposta suggerimenti e notifiche collegate, copia nel target solo metadati mancanti, soft-delete della scheda duplicata e registra audit log. Se il merge creerebbe due result nello stesso contesto sportivo sullo stesso atleta target, l'operazione viene bloccata con 409 e la preview restituisce i conflitti da risolvere.
Prima della popolazione storica 2018-2025, LEVERAGE include una migrazione dedicata agli indici (0027_add_scalability_indexes) per rendere piu efficienti classifiche evento, schede atleta, filtri calendario, ranking, ricerca duplicati e query sui country rappresentati.
Gli endpoint pubblici principali che possono crescere molto supportano limit e offset: /athletes, /events, /events/calendar, /results, /events/{event_id}/results, /events/{event_id}/ranking-view, /athletes/{athlete_id}/results e /athletes/{athlete_id}/events/{event_id}/results.
Gli endpoint /analytics sono pubblici in lettura e costituiscono il contratto della UI interattiva delle Schede Atleta, della Sezione Analytics di confronto e dei Rankings.
Filtri supportati:
- periodo:
start_year,end_year,start_date,end_date - gara:
event_id,level - result:
discipline,category,format,round,apparatus,day - atleta/country:
idsper confronti,countryper filtri aggregati - metrica:
score,D_score,execution_estimate,E_score,Penalty,Bonus - qualita dato:
data_quality=all,data_quality=complete,data_quality=missing_d_score,data_quality=missing_score
Le serie per confronto atleti possono essere restituite come punti grezzi (raw) oppure aggregate per evento, anno o apparatus. Questo permette al frontend di costruire grafici sovrapponibili senza dover ricostruire la semantica dei dati.
I punti dei grafici includono execution_estimate, is_complete, missing_fields, complete_result_count e partial_result_count, cosi la UI puo evidenziare quando una media o un trend contiene result con dati non disponibili.
Per i grafici sull'eta, LEVERAGE usa birth_year e Event.year, quindi l'eta e una stima annuale coerente con i dati ufficiali disponibili. La media per country usa Result.represented_country e viene calcolata su coppie uniche atleta-gara, cosi un atleta con piu result nella stessa gara non pesa piu volte sull'eta media.
Gli utenti loggati possono salvare viste dashboard personali tramite /preferences/dashboard-views. Una vista contiene name, view_type, chart_type, metric, filters, is_default e position; questo permette alla UI di riproporre ricerche frequenti, filtri preferiti e grafici principali senza rendere definitivo un layout specifico.
LEVERAGE include una prima versione leggera di analytics del sito, separata dalle analytics sportive.
Il frontend puo inviare eventi a POST /site-analytics/events:
page_viewsearchathlete_viewevent_viewdashboard_viewsession_end
Il payload puo includere visitor_id, session_id, path, search_query, entity_type, entity_id e duration_seconds. Se la richiesta contiene un token valido, LEVERAGE collega l'evento allo user loggato; altrimenti resta anonimo.
Per scelta privacy-friendly questa prima versione non salva IP, user-agent completo, fingerprint del browser o dati sensibili. La dashboard admin usa solo dati aggregati.
GET /site-analytics/admin/summary restituisce:
- visitatori unici
- sessioni
- page views
- ricerche
- atleti piu visualizzati
- gare piu visualizzate
- tempo medio stimato dalle sessioni chiuse
- numero iscritti, verificati/non verificati, attivi/inattivi
LEVERAGE espone un motore leggero admin-only per proporre dati mancanti di Athlete usando solo pagine pubbliche ufficiali World Gymnastics, senza AI e senza API a pagamento.
Flusso consigliato nella scheda admin Athlete:
GET /athletes/{athlete_id}/admin-viewMostra la scheda ufficiale LEVERAGE e i suggerimenti pendenti gia presenti.GET /world-gymnastics/athletes/{athlete_id}/candidatesCerca candidati World Gymnastics usando cognome, disciplina e country quando disponibile.- L'admin seleziona il profilo corretto tra i candidati.
POST /world-gymnastics/athletes/{athlete_id}/suggestionsConfig_athlete_idoppurefig_profile_url, legge la pagina profilo ufficiale e crea suggerimentipending.- L'admin approva, modifica o rifiuta i suggerimenti tramite gli endpoint
data-suggestions.
Campi suggeribili attuali:
birth_year, daYear of birthcountry, da codice country del profiloimage_url, solo se il profilo espone una immagine chiara e riusabileworld_gymnastics_athlete_id, dal profilo selezionatoworld_gymnastics_profile_url, link diretto al profilo ufficiale selezionatoworld_gymnastics_status, dallo status World Gymnastics quando disponibile
first_name, last_name e discipline non vengono modificati automaticamente: se differiscono dal profilo World Gymnastics, il backend restituisce warning per revisione admin.
Quando un admin approva world_gymnastics_athlete_id, world_gymnastics_profile_url o world_gymnastics_status, LEVERAGE registra anche world_gymnastics_verified_at e world_gymnastics_verified_by_admin_id.
LEVERAGE espone anche un motore leggero admin-only per collegare una scheda Event a una pagina evento ufficiale World Gymnastics, usando l'endpoint pubblico sportevents del sito ufficiale.
Flusso consigliato nella scheda admin Event:
GET /events/{event_id}/admin-viewMostra la scheda ufficiale LEVERAGE e i suggerimenti pendenti gia presenti.GET /world-gymnastics/events/{event_id}/candidatesCerca candidati World Gymnastics usando nome evento, anno/date, location e disciplina.- L'admin seleziona l'evento ufficiale corretto tra i candidati.
POST /world-gymnastics/events/{event_id}/suggestionsConfig_event_idoppurefig_event_url, legge il dettaglio ufficiale e crea suggerimentipending.- L'admin approva, modifica o rifiuta i suggerimenti tramite gli endpoint
data-suggestions.
Campi suggeribili attuali:
location, da city/country ufficialivenue, dalla venue ufficiale quando disponibilestart_date, da event datesend_date, da event datesdiscipline, mappata suMAG,WAG,MAG and WAGcategory, mappata sujunior,senior,junior and seniorlevel, mappato sui livelli LEVERAGEworld_gymnastics_event_id, dal dettaglio selezionatoworld_gymnastics_event_url, link diretto al dettaglio ufficiale selezionatoworld_gymnastics_status, dallo status World Gymnastics quando disponibile
image_url per Event resta volutamente fuori dal motore World Gymnastics Event: in seguito si puo valutare se usare immagini prefissate per tipologia evento, per esempio cerchi olimpici per Olympic Games, oppure eliminare il campo se non serve davvero.
Quando un admin approva world_gymnastics_event_id, world_gymnastics_event_url o world_gymnastics_status, LEVERAGE registra anche world_gymnastics_verified_at e world_gymnastics_verified_by_admin_id.
LEVERAGE supporta una coda admin-only di suggerimenti per completare campi mancanti di Athlete e Event.
Il motore AI/web deve essere trattato solo come assistente: puo proporre valori, fonte ed evidenza, ma il dato diventa ufficiale solo dopo una decisione esplicita di un admin. Gli utenti non-admin non vedono i suggerimenti e gli endpoint pubblici continuano a mostrare solo dati ufficiali gia approvati.
Il provider e disabilitato di default. Per usare OpenAI con web search lato server:
AI_SUGGESTIONS_PROVIDER=openai
OPENAI_API_KEY=...
OPENAI_MODEL=gpt-5Questa integrazione usa la Responses API con strumento web_search e output strutturato JSON. ChatGPT Plus e l'API sono prodotti distinti: per questa funzione serve una chiave API server-side configurata nell'ambiente del backend.
Flusso consigliato per la UI admin:
- Aprire
GET /athletes/{athlete_id}/admin-viewoppureGET /events/{event_id}/admin-view. - Se mancano dati, usare
POST /data-suggestions/generateconentity_type,entity_ide, opzionalmente,fields. - Mostrare i
pending_suggestionsdirettamente nella scheda admin, accanto al campo interessato. - Usare
POST /data-suggestions/{suggestion_id}/acceptper approvare il valore suggerito. - Usare lo stesso endpoint con
{"value": "..."}per approvare una versione corretta dall'admin. In questo casosuggested_valueresta invariato ereviewed_valuesalva il valore finale. - Usare
POST /data-suggestions/{suggestion_id}/rejectper rifiutare il suggerimento.
Campi suggeribili attuali:
Athlete:birth_year,country,image_urlEvent:location,venue,start_date,end_date,level,image_url
Accanto al data entry manuale, LEVERAGE espone uno strumento admin-only per caricare file Gymternet standardizzati in formato .xlsx o .csv.
Il contratto comune che un import parallelo futuro dovra rispettare e documentato in docs/import_contract.md. In sintesi: parser diversi sono ammessi, ma tutti gli importer devono convergere sulla stessa preview admin, sugli stessi controlli di atleta/evento/result, sulla stessa logica anti-duplicato, sulle stesse verifiche di country storica e sulle notifiche cumulative.
Il popolamento storico 2018-2025 viene documentato passo passo in docs/LEVERAGE_popolamento_massivo_diario.md, con preview, statistiche, scelte admin, commit e controlli post-import per ogni anno.
Una sintesi metodologica in forma di capitolo da tesi e disponibile in docs/LEVERAGE_capitolo_metodologia_popolamento_db.md, con copia Word in docs/LEVERAGE_capitolo_metodologia_popolamento_db.docx.
Flusso consigliato per la UI admin:
POST /imports/gymternet/previewCarica il file e mostra quantiAthlete,EventeResultverrebbero creati, duplicati identici da saltare, conflitti da risolvere, warning, un campione di righe importabili e la review dei D-score non agganciati.POST /imports/gymternet/review-target-suggestionsUsa lo stesso file della preview e restituisce targetResultsuggeriti mentre l'admin risolve manualmente un D-score non agganciato. Accettaquery,review_id,year_hint,csv_discipline,csv_score_kindelimit; la UI puo usarlo come autocomplete per scegliere iltarget_idcorretto.POST /imports/gymternet/commitRipete il parsing e scrive nel database solo se non ci sono errori strutturali. Di default blocca il commit se ci sono conflitti; conallow_partial=trueimporta le righe pulite e lascia i conflitti non importati nel report. I duplicati identici vengono saltati. Puo ricevere decisioni admin per applicare D-score non agganciati. Al termine del commit genera una singola notifica adminimport_summarycon il report generale dell'import: nuoviAthlete, nuoviEvent, nuoviResult, atleti ed eventi con nuovi risultati, aggiornamenti applicati e duplicati saltati. Se un nuovo atleta importato assomiglia a unAthletegia presente nel database, il commit viene bloccato finche l'admin non decide se usare l'atleta esistente, creare un nuovo atleta o indicare manualmente l'atleta corretto.
Parametri opzionali:
year_hint: anno da usare quando il file o il nome gara non contengono l'anno.csv_discipline:MAGoWAG, utile per CSV pivot senza disciplina nel file.csv_score_kind:finalodscore, utile per CSV pivot.allow_partial: solo sul commit; setrue, importa i result senza conflitti e salta quelli conflittuali.orphan_review_limit: limita quanti problemi D-score restituire nella preview.orphan_dscore_decisions: solo sul commit, come campo form JSON. Ogni decisione usareview_iddalla preview e unaaction:accept_suggestion,discard, oppuremanual_target.athlete_review_limit: limita quanti possibili match atleta restituire nella preview.athlete_match_decisions: solo sul commit, come campo form JSON. Ogni decisione usareview_iddalla preview. Per i match ordinari sono disponibiliaccept_suggestion,create_newemanual_target; per un possibile cambio country sono disponibili ancheupdate_countryekeep_existing_country. Per un match con atleta gia presente, l'admin puo aggiungeretarget_name_updatequando verifica che il nome salvato nella scheda atleta esistente contiene un errore di data entry. Il commit aggiorna la scheda atleta target senza creare un duplicato.- se lo stesso nome e la stessa disciplina compaiono con country diverse nello stesso file, la preview crea una verifica bloccante
possible_athlete_identity_collision. L'admin puo sceglierekeep_separate,merge_as_same_athleteindicandocanonical_country,accept_suggestionverso un atleta esistente oppuremanual_target. Permerge_as_same_athlete, il default ecountry_strategy="preserve_represented_country": una sola scheda Athlete, ma ogni Result conserva la country sorgente come storico di rappresentanza. Se invece una country e un errore di data entry, l'admin puo aggiungerecountry_corrections, per esempio{"RUS": "ISR"}, e il commit salvera i Result interessati conrepresented_countrycorretto. Quando l'admin sceglieupdate_country, il sistema aggiornaAthlete.countrye registra una riga incountry_changescon l'anno del file importato. In assenza di correzioni esplicite, ogni Result importato salva la country sorgente inrepresented_country, preservando la nazionalita storica usata da classifiche, filtri e analytics. - se lo stesso file contiene lo stesso atleta con nome/cognome invertiti o formato equivalente, il tool applica automaticamente
merge name order: crea una sola chiave atleta e usa come ordine canonico il nome gia presente nel database, quando disponibile, oppure la variante piu ricorrente nel file importato. Se dopo questo merge emergono country diverse, la preview crea comunque una verifica bloccantepossible_athlete_identity_collision: l'admin decide solo la parte country (country_history,country_correction,keep_separateo target manuale), non l'inversione nome/cognome. represented_countrynon fa parte della chiave anti-duplicato delResult: se il sistema trova lo stesso contesto sportivo con paese rappresentato diverso, il record viene trattato come conflitto da review admin e non come duplicato innocuo.
LEVERAGE espone anche un import admin-only per file calendario Gymternet con fogli annuali e colonne DATE / EVENT.
Endpoint:
POST /imports/calendar/previewLegge il file.xlsx,.xlsmo.csv, interpreta date comeJan 11,Jan 11-15,Jan 11-Feb 3, confronta gli eventi con il database e restituisce cosa verrebbe aggiornato o creato.POST /imports/calendar/commitRipete il parsing, aggiornastart_date/end_datedegli eventi gia presenti e crea automaticamente solo gli eventi mancanti dacreate_missing_from_yearin poi. Le righe storiche non matchate restano in review e non vengono create automaticamente. Se la preview rileva duplicati sorgente, cioe stesso nome evento nello stesso foglio/anno con righe diverse, il commit viene bloccato finche il file non viene corretto o disambiguato. Il matching considera anche sinonimi semantici essenziali:MAGpuo corrispondere a nomi evento DB conMen's/Mens, mentreWAGpuo corrispondere aWomen's/Womens. Se due righe calendario diverse vengono agganciate allo stesso evento DB con date diverse, il commit viene bloccato e la preview restituiscematched_event_source_conflictsper review admin.
Default per nuovi eventi calendario:
discipline:MAG and WAG, salvo suffissi espliciti come(MAG)o(WAG);category:junior and senior, salvo indicazioni esplicite nel nome;level: inferito da parole chiave essenziali (Olympic,World Cup,World Championships, ecc.), altrimentiInternational Event.
Il commit genera una notifica admin import_summary con riepilogo degli eventi aggiornati, futuri creati e righe storiche rimaste in review.
Esempio decisione admin:
[
{
"review_id": "orphan_...",
"action": "accept_suggestion",
"suggestion_id": "suggestion_..."
},
{
"review_id": "orphan_...",
"action": "discard"
},
{
"review_id": "orphan_...",
"action": "manual_target",
"target": {
"event_name": "All-Japan Team Championships",
"year": 2018,
"athlete_name": "Daiki Hasimoto",
"discipline": "MAG",
"category": "senior",
"apparatus": "HB",
"vt_attempt": null,
"format": "individual",
"round": "final",
"day": null
}
}
]Esempio decisione admin per possibile atleta gia esistente:
[
{
"review_id": "athlete_match_...",
"action": "accept_suggestion",
"suggestion_id": "suggestion_..."
},
{
"review_id": "athlete_match_...",
"action": "create_new"
},
{
"review_id": "athlete_match_...",
"action": "manual_target",
"target": {
"athlete_id": 123
}
},
{
"review_id": "athlete_country_...",
"action": "update_country"
},
{
"review_id": "athlete_match_...",
"action": "accept_suggestion",
"suggestion_id": "suggestion_...",
"country_action": "keep_existing_country"
},
{
"review_id": "athlete_identity_collision_...",
"action": "merge_as_same_athlete",
"canonical_country": "ITA",
"country_strategy": "preserve_represented_country"
},
{
"review_id": "athlete_identity_collision_...",
"action": "merge_as_same_athlete",
"canonical_country": "ISR",
"country_strategy": "country_correction",
"country_corrections": {
"RUS": "ISR"
}
}
]Regole Gymternet attualmente applicate:
- file
.xlsxcon fogliMAG,MAG D,WAG,WAG D; - file
.csvflat con colonne comediscipline,athlete,country,event,apparatus,score,d_score; - atleta con asterisco (
*) =junior; l'asterisco viene rimosso dal nome salvato; - nazione convertita in codice ufficiale quando riconosciuta;
daye opzionale suResult:NULLper result senza distinzione giornaliera, valore positivo per gare su piu giornate;score,D_score,E_score,PenaltyeBonusrestano opzionali nei casi ammessi: per Gymternet legacy i valori non presenti vengono salvati comeNULLe mostrati comenon disponibilequando il dato e rilevante ma mancante;- per Gymternet legacy anche i dati successivi al 2025 seguono la policy del 2025; la normalizzazione a
0.0dei campi vuoti vale per data entry manuale e import standard moderni, non per questo importer legacy; - la UI deve usare
bonus_statusper distinguerenot_availabledanot_applicable; - i record con final score ma senza
D_scorevengono importati come result parziali validi; - i record con solo
D_scoree senza final score non creano Result: vengono tenuti nel report/review per eventuale aggancio admin a un result con final score; - il report
import_summarydistingue nuovi punteggi completi, nuovi punteggi con datinot availablee D-score orfani rimasti in review; - se il file contiene una colonna
Day, il valore viene usato direttamente; - se il file non contiene
Day, risultati ripetuti con stessa chiave ma score/D-score diverso ricevono automaticamenteday=1,day=2, ecc.; - event senza suffix =
format=individual,round=final; - suffix
QF=individual/qualification,TF=team/final,AA=individual/final,EF=apparatus/final; AAviene salvato come apparatusAA;VT AVGviene salvato come apparatusVT AVG;- per gli anni 2018-2024, nei fogli
MAGeWAG,VTviene salvato comevt_attempt=1convault_attempt_order_uncertain=true; - per gli anni 2018-2024,
VT attempt 2viene ricavato daVT AVGcome final score e daVT SUMcome D-score, sempre convault_attempt_order_uncertain=true; - dal 2025 in poi, Gymternet legacy applica sempre la policy 2025;
- per MAG dal 2025 in poi,
VTviene salvato comevt_attempt=1convault_attempt_order_uncertain=trueeVT attempt 2viene ricavato daVT AVGcome final score e daVT SUMcome D-score; - per WAG dal 2025 in poi,
VTviene salvato comevt_attempt=1convault_attempt_order_uncertain=true;VT attempt 2riceve solo il D-score ricavato daVT SUM, mentre il final score viene salvato comeNULLe mostrato in UI comenot available; - se il tool Gymternet legacy viene usato per dati successivi al 2025, genera un warning per ricordare che sta applicando le regole 2025; il percorso consigliato per dati futuri completi resta un import standard dedicato;
VT SUMviene riconosciuto come colonna sorgente ma non viene salvato nel database comeResult;- duplicate key:
athlete_id,event_id,discipline,category,apparatus,format,round,vt_attempt,day; - duplicato identico = skip; stessa key con score/D-score diversi = conflitto in preview, blocco del commit standard oppure skip esplicito con
allow_partial=true; - stessa key con score/D-score uguali ma
represented_countrydiverso = conflittocountry_conflict_*, per evitare fusioni errate di atleti o perdita della nazionalita storica.
pytestI test usano test_leverage.db tramite DATABASE_URL, separato dal database locale di sviluppo.
Ultimo audit pre-Git locale:
- 28 migrazioni Alembic;
- 99 test automatici;
- 106 endpoint router;
alembic upgrade headverificato su database SQLite temporaneo;.gitignoreconfigurato per escludere.venv, cache, database locali, log,uploads/e file sorgente locali inimport_files/.
Alembic è configurato in alembic.ini e nella cartella migrations/.
LEVERAGE non crea più le tabelle automaticamente all'avvio: lo schema va gestito tramite migrazioni.
alembic upgrade head
alembic revision --autogenerate -m "descrizione modifica"