Piattaforma multi-tenant per l'intelligence della performance nel rugby
SaaS per club: anagrafica, gestione medica e tesseramenti, eventi e presenze, dati di sessione, intelligence (carico, readiness, rischio infortunio, genome, body insight), pianificazione dell'allenamento fino alla periodizzazione stagionale, AI Coach generativo e notifiche.
Backend FastAPI + SQLAlchemy 2 + Alembic + PostgreSQL. Frontend React 18 + Vite + TypeScript + Tailwind. Codice, commenti e commit in italiano.
Indice
Stack & struttura
Backend — FastAPI 0.115, SQLAlchemy 2.0.36, Alembic 1.14, Pydantic-settings 2.7, PostgreSQL. OpenAPI disabilitato (openapi_url=None). Prefisso API /api/v1. Config solo da env (nessun segreto hardcoded). X-Request-Id per-richiesta.
Frontend — React 18, Vite, TypeScript, Tailwind, zustand (store auth), axios. PWA (workbox, service worker, scrittura offline coalescente, Web Push). Comandi: npm run dev (:5174), npm run type-check, npm run build, npm test.
Layout repository
app/
api/v1/ router per dominio (atleti, eventi, wellness, intelligence, …)
api/deps.py require_permission, get_current_active_user
api/scoped_crud.py scoping helpers + audit_safe
core/ rbac, scoping, storage, audit, benchmark, notifiche, security
intelligence/ motori PURI + orchestrator + feature_store + config
models/ modelli SQLAlchemy (52 tabelle)
schemas/ schemi Pydantic
migrations/versions/ 0001 … 0040 (Alembic)
frontend/src/ pages/ components/ api/ stores/ design-system/ lib/
docs/ DOCUMENTAZIONE.md · STAGING.md · LLM_SETUP.md
CLAUDE.md
Comandi essenziali
python -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt
pytest # suite (SQLite in-memory, no DB)
alembic upgrade head # migrazioni (Postgres)
python -m app.cli create-superuser --email admin@x.it # primo platform_admin
python -m app.cli seed-demo # società demo (solo dev/demo)
Architettura del backend
Flusso canonico di un endpoint di dominio (template: app/api/v1/atleti.py):
router → require_permission("codice") → risolve contesto società → scoped_select/get_scoped_or_404 → muta → commit → audit_safe
require_permission(codice)— dependency factory che verifica il permesso RBAC nella società di lavoro.platform_adminshort-circuita ogni check.- Contesto società —
context_societa(current)/resolve_societa_context(user, requested): l'utente di dominio è confinato alla propria società (403 altrimenti);platform_adminè cross-società. - Config versionata — gli algoritmi leggono i parametri da
AlgorithmConfig(statusactive) viaget_active_config; i default vivono nel codice e sono materializzati come config v1 (seed idempotente).
Multi-tenancy & scoping
Isolamento logico per societa_id (app/core/scoping.py). Ogni modello con colonna societa_id è auto-rilevato come scoped via introspezione del registry → un nuovo modello di dominio è scoped per costruzione.
scoped_select(model, societa_id)— ogni lettura di dominio passa di qui; fail-closed (rifiuta modelli non-scoped esocieta_id=None).ensure_societa_on_create— timbra/validasocieta_idin scrittura (rifiuta mismatch cross-società).get_scoped_or_404— 404 (non 403) per righe di altri tenant → ne nasconde l'esistenza.scoped_atleta_ids→{atleta}|{figli}|None;require_atleta_accessper ogni lettura per-atleta.
⚠️ SQL: col.in_([id, None]) non matcha NULL → usare or_(col==id, col.is_(None)). La dep che setta il ContextVar "società di lavoro" (header X-Societa-Id) deve essere async, o il set non arriva al corpo sync.
RBAC — ruoli e permessi
Default-deny (app/core/rbac.py). 53 permessi, 10 ruoli, matrice ROLE_PERMISSIONS. seed_rbac riconcilia: la matrice è source-of-truth e rimuove le mappature obsolete.
| Ruolo | Permessi | Note |
|---|---|---|
| platform_admin | ALL (53) | super-admin piattaforma, cross-società |
| admin_societa | ALL (53) | pieno nella propria società |
| presidente | ALL (53) | vertice società |
| direttore_tecnico | 36 | tecnico + sanitario (con gate) + microciclo |
| allenatore | 27 | tecnico, microciclo, inserimento sessione |
| preparatore | 20 | tecnico, test, inserimento sessione |
| segreteria | 16 | gestionale (tesseramenti/documenti/pagamenti) |
| medico | 8 | sanitario (richiede sanitary_access) |
| giocatore | 7 | self-report /me/* |
| genitore | 6 | vista read-only dei figli |
- Permessi verificati nella società home (nessuna escalation cross-società).
- Gate sanitario:
sanitario.read/writerichiedonosanitary_access=Truesull'assegnazione, oltre al permesso./me/capabilitiesespone solo i permessi effettivi.
Ownership dati: antropometria, contatto e test = staff-only (qualità dato + tutela minori). Wellness/RPE/recupero = self-report atleta via /me/*, con deroga staff tramite i permessi *.write_team (allenatore/preparatore/DT) — l'autore resta tracciato in audit.
Storage cifrato, audit, notifiche
Storage file — envelope encryption per-tenant
Standard di piattaforma (app/core/storage.py): ogni oggetto è AES-256-GCM cifrato con la DEK della società; la DEK è salvata wrapped (cifrata con la master KEK, env-only FILE_ENCRYPTION_KEK) in societa_storage_key. I file non toccano il DB e non sono mai in chiaro su S3 (bucket condiviso → isolamento a livello dato). Upload/download proxati dall'API (no presigned). Il read non crea mai la DEK.
Audit
audit_safe(...) registra l'azione dopo il commit, best-effort (non rompe mai l'operazione), legata alla richiesta via request_id.
Notifiche
Inbox (Notifica, dedup per chiave_dedup) + Web Push (VAPID). Cron tick ogni 15' con 6 blocchi: promemoria eventi, scadenze (tesseramenti/certificati), insoluti, diario mancante, carico alto + readiness bassa (staff), dolore ricorrente (medico) + trigger su piano individuale approvato. Web Push iOS solo su PWA installata (Safari 16.4+).
Modello dati
52 tabelle, raggruppate per dominio. Ogni tabella di dominio ha societa_id.
Anagrafica & organizzazione
societa, societa_storage_key, sede, stagione, categoria, squadra, gruppo, atleta, assegnazione_atleta (atleta↔squadra, data_da/data_a), tutore, atleta_tutore.
Utenti & RBAC
utente (con atleta_id/tutore_id per collegare account self/genitore), permesso, ruolo, ruolo_permesso, assegnazione_ruolo (scope + sanitary_access).
Medico · consensi · gestionale
certificato_medico, consenso, tesseramento (categoria + deroga, stato FIR, quota), quota_categoria, pagamento, documento_gestionale (file cifrato, condiviso).
Eventi & presenze
evento (allenamento/partita/test/riunione), convocazione, presenza, nota (private staff), indisponibilita (periodo infortunio/malattia, data_fine NULL = in corso).
Dati di sessione & performance
wellness_entry, rpe_entry (session_load, evento/seduta), dato_recupero (HRV/sonno/FC), carico_contatto; test_definizione, sessione_test, tentativo, risultato_test, protocollo_versione, riferimento_test (benchmark), categoria_baseline (population genome), rilevazione_antropometrica (pliche/BF%).
Pianificazione
profilo_metodologico, macrociclo, mesociclo, microciclo, seduta, diario_seduta.
Intelligence & AI
algorithm_config, algorithm_input_snapshot, algorithm_result; ai_conversazione, ai_messaggio, kb_documento, kb_chunk (RAG); notifica, push_subscription, audit_event.
Intelligence — motori e algoritmi
I motori vivono in app/intelligence/ come funzioni pure (no DB, no tempo); l'orchestratore legge il DB, chiama i motori e materializza i risultati in AlgorithmResult; il feature_store espone gli ultimi risultati per il serving. Config versionata per 5 algoritmi: load_intelligence, readiness_score, injury_risk, athlete_genome, body_insight.
⭐ Paletto trasversale: readiness / injury / carico sono indicatori di supporto alla decisione, non predittori. Nessun modello predittivo; INSUFFICIENT_DATA finché mancano i dati; i pesi si rinormalizzano sui componenti presenti; gate su confidence.
Load intelligence
Serie giornaliera del session_load (RPE×durata). I giorni di riposo dentro la finestra di monitoraggio (dal 1° RPE ad as_of) sono KNOWN_ZERO, i pre-monitoraggio UNKNOWN. EWMA acuto λ=0.25 (N=7), cronico λ≈0.069 (N=28). ACWR / Load Balance Ratio = acuto/cronico, pubblicabile solo con coverage_28 ≥ 70% e history ≥ 21 e cronico ≥ minimo. robust_z dell'acuto vs baseline (median/MAD) → load_state. Monotony (Foster) = media/SD settimanale (riposi inclusi come 0); Strain = weekly × monotony.
Readiness
Punteggio 0–100 di prontezza "oggi". Componenti pesati (rinormalizzati): wellness (z-composito di sonno/fatica/doms/stress/umore/recupero), load_state, opzionali HRV / sonno oggettivo / recupero post-gara. Bande LOW/MODERATE/GOOD; gate su confidence. Override UNAVAILABLE se un periodo di indisponibilità copre as_of.
Injury risk
Indicatore 0–100 + livello. Componenti: acwr (~0.25), monotony_risk, readiness_risk, soreness_risk (DOMS), biomeccanica/stress_articolare opzionale (peso 0.15). Nessuna pretesa predittiva.
Genome
Profilo per aree (velocità/potenza/forza/resistenza): domain_score 0–100 dai test confrontati con la baseline della categoria (CategoriaBaseline, percentile per categoria). Confidenza graduata (min 5 / pieno 20 atleti); sotto 5 → fallback sviluppo-personale. Radar nel FE.
Body insight
Da antropometria: WHtR, BMI, somma pliche, BF% (metodo + errore atteso), masse derivate (solo con BF% valido). Qualità L1/L2/L3. Hodgdon-Beckett = legacy, non pubblicato.
Metriche derivate & beep test
- Peak power (Sayers):
60.7·CMJ + 45.3·peso − 2055W. - Efficienza aerobica:
livello_beep / peso^0.75. - Beep test / Léger 20m: inserito come livello + navetta, salvato come navette totali (valore monotòno). VAM =
8.0 + 0.5·Lkm/h; VO₂max (Léger) =31.025 + 3.238·v − 3.248·età + 0.1536·v·età.
Correlazioni, simulazione, benchmark
flag_carico_readiness = flag descrittivi (overlay, niente coefficiente). simulate.project_microcycle valida i piani (weekly load, monotonia, ACWR vs cronico). RiferimentoTest per categoria (U14/U16/U18/Seniores; Yo-Yo in metri, Beep in navette); risolvi_benchmark con preferenza società+categoria > società > globale+categoria > globale.
Pianificazione & periodizzazione
AI Coach = generatore di piani (non Q&A). Pipeline: ProfiloMetodologico → scaffold deterministico → Microciclo/Seduta bozza → genera-contenuti (LLM) → approva → applica al calendario (crea Eventi allenamento, idempotente). Scaffold/carico/editor/applica funzionano anche senza LLM.
Piano individuale: tailoring su injury/readiness/aree deboli; non crea Eventi squadra (esposto via /me/sedute); l'atleta compila il diario → upsert RpeEntry per seduta → alimenta Load/Readiness.
Periodizzazione stagionale (Macrociclo→Mesociclo→Microciclo→Seduta): la stagione è esplosa alla generazione. Rugby = partita ~ogni domenica → macro stagionale per fasi; date incontri manuali con flag chiave che guida mini-taper (×0.85) e scarico (×0.6). Aggancio calendario bidirezionale (Evento tipo=partita = sorgente). Analytics pianificato-vs-reale (aderenza per settimana e mesociclo). FE: ridgeline StagioneTimeline.
API — superficie completa
~130 endpoint sotto /api/v1. Notazione: METODO path — [permesso].
Auth & utenti
POST /auth/login · GET /me/capabilities · utenti/ruoli [utente.manage] · assegnazioni-ruolo [rbac.manage]
Organizzazione & anagrafica
squadre · sedi · stagioni · categorie · gruppi · atleti [atleta.*] · tutori · legami-tutore · assegnazioni atleta↔squadra + roster [squadra.*]
Medico · gestionale
certificati+file [sanitario.*] · consensi · tesseramenti(+rinnova) · quote-categoria · pagamenti · documenti+file cifrato · GET /ricerca · /gestione/riepilogo
Eventi · presenze · indisponibilità
eventi(+ricorrenti,piano) · convocazioni · GET/PUT /eventi/{id}/presenze · note · GET/POST /atleti/{id}/indisponibilita, PATCH/DELETE /indisponibilita/{id} [presenza.*]
Dati di sessione — self + staff
Self: POST /me/wellness · /me/rpe · /me/recupero. Staff: PUT /eventi/{id}/rpe [rpe.write_team] · POST /atleti/{id}/wellness [wellness.write_team] · POST /atleti/{id}/recupero [recupero.write_team] · /eventi/{id}/contatto [contatto.*] · antropometria [antropometria.*].
Test & benchmark
test-definizioni · POST /atleti/{id}/test · PUT /eventi/{id}/test · sessioni-test/import · Batterie: GET /squadre/{id}/test/confronto|serie|batterie|griglia · PUT …/batteria · GET /test/confronto-squadre · riferimenti-test · /atleti/{id}/test/valutazione|serie · baseline-categoria.
Intelligence
GET /intelligence/{load|readiness|insights|injury-risk|genome|body-insight}/atleti/{id} · GET /intelligence/features/squadre/{id} (cruscotto prontezza: readiness+carico+injury+disponibilità) · POST /intelligence/recompute/… · simulate/{session-load|microcycle} · /atleti/{id}/carico-readiness.
Pianificazione · calendario · AI · notifiche
pianificazione/profilo · microcicli/genera|applica|approva · individuali/genera · macro/genera|incontri|analytics · PUT /me/sedute/{id}/diario · GET /calendario (feed unificato) · aicoach/* · notifiche/* · POST /jobs/notifiche/tick.
Frontend
SPA React. Auth in zustand: JWT in localStorage, /me + /me/capabilities al login; can(permission) / <Can> per il gating UI (codici allineati alla matrice RBAC). Axios fa force-logout su 401. Design-system con componente ComeLeggere (spiegazioni "come leggere" sotto ogni grafico). Regole dataviz: niente dual-axis (small-multiples), palette di stato con icona+etichetta, SVG xMidYMid meet.
| Pagina | Rotta | Contenuto |
|---|---|---|
| Dashboard | / | home |
| Atleti / dettaglio | /atleti | anagrafica + tab: readiness (+indisponibilità/insights/injury/load), genome, test fisici (trend/VO₂max/beep), body insight, benessere (+form staff), diario, medico, consensi, documenti, note |
| Prontezza squadra | /intelligence | cruscotto readiness+injury+carico+disponibilità, ordinamento concern-first |
| Batterie test | /batterie | Confronto · Andamento (trend p25–p75) · Squadre · Inserimento (griglia, cella beep L+N) · Baseline |
| Campo / evento | /campo | agenda Lista/Mese/Giorno (drag&drop, undo); dettaglio evento con griglia sessione (RPE/contatto/test), convocazioni, presenze |
| Pianificazione | /pianificazione | microciclo (editor calendario) / individuale / Stagione (timeline + analytics) |
| AI Coach · Gestione · Tesseramenti | /ai-coach … | conversazioni, gestionale + cruscotto |
| Il mio benessere / I miei figli | /me · /figli | self atleta e vista genitore |
| Notifiche · Tutori · Ruoli staff | /notifiche … | inbox + Web Push, tutori, ruoli |
Flussi funzionali principali
- Onboarding società: create-superuser → login → società/stagioni/categorie/squadre → utenti+ruoli (con gate sanitario).
- Anagrafica & tesseramento: atleta → tesseramento (categoria auto o deroga) → certificato + consensi → quote/pagamenti.
- Agenda & presenze: eventi (anche ricorrenti) → convocazioni → presenze; le partite alimentano periodizzazione e readiness.
- Dati di sessione → algoritmi: RPE (self o sRPE di squadra staff) + wellness (self o staff) + recupero + contatto + test/antropometria → materializzazione Load/Readiness/Injury/Genome/BodyInsight → serving per-atleta e cruscotto squadra.
- Test batterie: catalogo → griglia roster×test (cella beep L+N) o import CSV → confronto/trend/baseline → Genome/VO₂max.
- Pianificazione: profilo → microciclo/individuale (scaffold→LLM→approva→applica) → periodizzazione stagionale → analytics; il diario chiude il loop col carico.
- Disponibilità:
Indisponibilita(periodo) → override readinessUNAVAILABLE→ visibile nel cruscotto. - Notifiche: cron tick → promemoria/scadenze/insoluti + diario mancante + carico↔readiness + dolore ricorrente + seduta assegnata.
Deploy & operations
Docker: cp .env.example .env (set PG_PASSWORD, SECRET_KEY, FILE_ENCRYPTION_KEK, VAPID) → docker compose up -d (postgres + api su 127.0.0.1:5010) → curl /health.
Staging (stack separato, progetto rugbypi-stg, porta 5011, dietro Cloudflare):
docker compose -p rugbypi-stg --env-file .env.staging \
-f docker-compose.staging.yml up -d --build --force-recreate api
docker exec rugbypi_api_stg alembic upgrade head # se ci sono migrazioni
cp -r frontend/dist/* /var/www/rugby-pi-stg/ # FE statico
⚠️ up -d --build può NON ricreare il container col codice nuovo (cache) → verificare con docker exec … grep -c "<stringa>" <file>; usare --force-recreate. Env nuove nel compose staging vanno nel mapping environment: esplicito. VAPID = raw base64url. Cache Cloudflare/PWA: se il FE nuovo non appare → incognito o purge CF.
Test: pytest su SQLite in-memory (StaticPool, no Postgres); fixture in tests/conftest.py. Suite corrente: 482 test.
Convenzioni & gotchas
- Modelli: importarli in
app/models/__init__.py(+__all__) per la registrazione. - Direzione test HIGHER/LOWER coerente ovunque; l'unità nel benchmark deve combaciare col catalogo (Yo-Yo = metri, Beep = navette).
- Load: distinguere riposo monitorato (
KNOWN_ZERO) da non-monitorato (UNKNOWN), altrimenti ACWR non pubblicabile e monotonia sovrastimata. compute_load(persist=False)per i chiamanti interni (readiness/injury);materialize_atletapersiste il load una volta.- Nessun admin hardcoded: primo
platform_admindaapp.cli create-superuser. OpenAPI disabilitato.
⚠️ Config/dati persistiti NON fanno backfill (bug ricorrente): aggiungere una chiave a un DEFAULT non aggiorna i record già salvati → leggere sempre con .get(chiave, default) o {**DEFAULT, **stored}. Vale per ProfiloMetodologico.config, AlgorithmConfig.params e i campi Pydantic non dichiarati (droppati).
main · 52 tabelle · 40 migrazioni · ~130 endpoint · 12 motori intelligence.