Architettura - mattiaboero/lascuolaamica GitHub Wiki
Il sito è un'applicazione statica: nessun server applicativo, nessun database, nessuna build framework. Il deploy produce file HTML, CSS, JS e JSON serviti direttamente dalla piattaforma di hosting statico.
Questa scelta non è per semplicità: è per controllabilità. Ogni file che arriva al browser è ispezionabile, ogni comportamento è deterministico, ogni dipendenza è esplicita.
HTML statico per pagina. Ogni materia ha la propria pagina HTML. Nessun routing lato client, nessun SPA.
CSS condiviso con tema accessibile e font self-hosted. I font .woff2 sono in assets/fonts/ e referenziati da fonts.css — nessuna richiesta esterna a Google Fonts o CDN.
JavaScript vanilla organizzato per responsabilità. La logica è suddivisa in file condivisi e file pagina dedicati, così il comportamento resta leggibile e controllabile.
| File | Ruolo |
|---|---|
shared.js |
Footer, modali, log aggiornamenti, palette colori |
subject-quiz-core.js |
Motore quiz runtime condiviso per tutte le 8 materie |
js/<subject>-page.js |
Config materia dichiarativa (window.SA.subjectConfig) |
questions-loader.js |
Caricamento e parsing dataset JSON |
sw.js |
Service Worker: cache PWA, fallback offline, clean URLs |
js/rewards.js |
Bacheca premi: badge/coppe/trofei, salvataggio locale |
js/rewards.js è incluso con defer in tutte le pagine HTML (non solo premi.html), non solo dove serve. Nessun import/chiamata diretta tra i due file: sono disaccoppiati a compile-time e comunicano solo a runtime tramite window.SA + eventi DOM. Scelta deliberata, non un gap — un grafo di dipendenze statico (AST) non troverà mai un edge tra questi due file.
Flusso:
-
rewards.jssi registra suwindow.SA.rewards = { recordGame, ... }al caricamento. - A fine partita,
subject-quiz-core.jschiama con guard difensivo:Seif (!window.SA || !window.SA.rewards || typeof window.SA.rewards.recordGame !== 'function') return; window.SA.rewards.recordGame({ ... });
rewards.jsnon è caricato per qualche motivo, la chiamata viene saltata senza crash. - Quando
recordGame()sblocca un badge,rewards.jsdispatchadocument.dispatchEvent(new CustomEvent('sa:rewards-updated', { detail: { unlocked, state } })). -
rewards.jsstesso riascoltasa:rewards-updatedper ridisegnare la bacheca (renderBoard()), utile sepremi.htmlè aperta in un'altra tab o dopo un aggiornamento asincrono.
Perché non un import diretto: ogni pagina materia carica rewards.js indipendentemente da subject-quiz-core.js; il contratto è sul namespace globale window.SA, non sul grafo di file. Aggiungere un nuovo consumer di eventi rewards (es. una nuova bacheca) richiede solo ascoltare sa:rewards-updated, zero modifiche a subject-quiz-core.js.
- Un solo motore runtime:
subject-quiz-core.js - Un file config per materia:
js/<subject>-page.js - Dataset domande:
json/<subject>.json+json/index.json - Materie supportate: matematica, geografia, italiano, scienze, storia, civica, problemi, inglese
- Creare
json/<subject>.jsoncon shape compatibile conquestions-loader.js - Aggiornare
json/index.jsoncon path e cardinalita` - Creare
js/<subject>-page.jsconwindow.SA.subjectConfig = { ... }Campi obbligatori:subjecttotalQlbKeycursorKeyhistoryKeymetricsKeyclassPrefKeydefaultAreaareas-
questionsSourceCampi opzionali: classProfileslevelsrenderModeanswerModeclassMetamixedRepeatLimitleaderboardAreaFallback
- Creare la pagina HTML seguendo lo scaffold di
civica.htmloinglese.html - Aggiornare
sitemap.xmle gli eventuali metadati pagina necessari - Eseguire
bash prepublish-check.sh
- Hook funzione: 3 slot riservati (
onBuildSession,onPickBonus,onScore) - Stato attuale:
0/3consumati - Config field passivi: illimitati
- Vietato nel core:
if (cfg.subject === ...) - Riferimento: docs/archive/refactor-quiz-engine-2026/extension-contract.md
Il sistema usa un planner stocastico a slot per ridurre i pattern ripetitivi tra sessioni. Per ogni partita:
- Le domande vengono raggruppate per
areaedifficoltà - I candidati vengono selezionati con selezione
softmax(non casuale pura) - Un sistema di cooldown su ID e firma domanda riduce le ripetizioni multi-sessione
- Le metriche di qualità sessione (
repeat rate,coverage,entropy,novelty) vengono salvate nella memoria locale del browser con media rolling
Tutte le 8 materie usano oggi la stessa pipeline di selezione nel core condiviso, con variazioni espresse solo tramite config field e metadata dei dataset.
Aggiunte in subject-quiz-core.js, condivise da tutte le 8 materie senza branch per subject:
| Funzione | Comportamento | Chiave storage / opt-out |
|---|---|---|
| A1 — Spiegazione risposta | Dopo ogni risposta appare #qExplanation con il campo explanation della domanda JSON, colorata in base a corretto/sbagliato |
nessuna |
| A2 — Difficoltà adattiva | A fine partita calcola una media mobile esponenziale (EMA) per classe e la usa per pesare la selezione delle domande successive |
${CURSOR_KEY}_adapt_v1; opt-out con cfg.adaptiveDifficulty: false
|
| A3 — Ripassa i tuoi errori | Le domande sbagliate (max 30) restano disponibili per una sessione dedicata dalla schermata iniziale | ${CURSOR_KEY}_wrong_q_v1 |
| A4 — Filtro sotto-ambito | Dopo la selezione dell'area, una griglia di sotto-ambiti derivata dinamicamente dal dataset (nessuna config per materia necessaria). Conta sullo stesso pool della partita (getClassAwarePool, modo strict) e mostra solo i sotto-ambiti con almeno mezza partita di domande (MIN_SUBAREA_POOL) |
nessuna |
| B1 — Streak feedback | Risposte corrette consecutive: alle soglie configurate (default 3/5/8) messaggio dedicato + mascotte celebrate invece di happy
|
cfg.streakMilestones per personalizzare le soglie |
| C1 — Overlay progressi | Pulsante "Progressi" nella schermata risultati apre statistiche per classe/area e le ultime partite | legge loadStats()/loadLB() esistenti |
Riferimento storico: CHANGELOG.md release 4.11.0 e 4.12.0.
/breakout (breakout.html + breakout.css + js/breakout.js) è un rompi-mattoni ispirato a Breakout/Arkanoid, motore Canvas 2D vanilla indipendente da subject-quiz-core.js: non fa parte del quiz engine unificato, non usa window.SA.subjectConfig, non consuma onBuildSession/onPickBonus/onScore.
Punti di contatto con il resto del sito:
-
Domande: usa
questions-loader.js(SA.questionsLoader.getSubjectRows) per pescare dagli stessijson/<materia>.jsondelle 8 materie, filtrati per classe a inizio partita. Nessun dataset dedicato al gioco. -
Premi:
js/rewards.jsesponerecordBreakout()accanto arecordGame()— stessoSTORAGE_KEY/stessa bacheca (premi.html), ma sotto-stato (state.breakout) e contatori separati da quelli quiz, così giocare non altera "partite totali" o "materie giocate". Registrazione progressiva durante la partita (muro abbattuto, salvataggio, nuovo bonus scoperto), non solo a fine partita, per non perdere il progresso se si esce con "Ricomincia"/"Cambia classe"/timer scaduto/cambio scheda — vediflushBreakoutProgress()injs/breakout.js, invio a delta e idempotente sul conteggio partite. -
Service Worker:
breakout.cssejs/breakout.jssono inOPTIONAL_PRECACHE_URLSdisw.js; la pagina/breakoutva offline dopo la prima apertura, come le pagine materia. -
Grafica: mattoni/pallina/barra disegnati su canvas con gradienti pre-renderizzati (sprite offscreen riusati via
drawImage, non ridisegnati ogni frame). Palette mattoni più vivace dei token testo del sito (lecito: sono grafica di gioco, soglia WCAG 1.4.11 non-text 3:1, non 4.5:1), con token separati (--breakout-feedback-ok/-ko) per gli usi testuali (overlay risposta, punteggio volante, simbolo capsula) che restano a 4.5:1. Effetti decorativi (particelle, squash/stretch, wobble capsula) rispettano il toggle "riduci animazioni" eprefers-reduced-motion; la fisica di gioco non ne è mai influenzata. Modalità Okabe-Ito: stessi 4 colori mattone certificati, nessuna differenza. -
Accessibilità: overlay domanda con focus trap dedicato (Tab/Shift+Tab intrappolati nel dialog, focus iniziale sulla prima opzione, ripristino del focus precedente alla chiusura) —
aria-modalda solo non impedisce a Tab di uscire dal dialog nei browser.
/bosco (bosco.html + bosco.css + js/bosco.js) è un gioco di ortografia per la 2ª e la 3ª, motore Canvas 2D vanilla indipendente da subject-quiz-core.js, come il gioco arcade.
Differenze rispetto a Spacca-Muri:
-
Dati propri. Non usa
questions-loader.js: il banco di 129 parole vive dentrojs/bosco.js(PAROLE), conskill, classe, difficoltà, indizio e distrattori per parola. È l'unico contenuto del sito che non sta injson/, perché non è una domanda a risposta multipla ma una parola con un buco. -
Modello verificabile senza canvas.
init()esce subito se non trova#boscoCanvase il modello è esposto suSA.bosco:scripts/check_bosco.jslo carica conwindow/documentfinti e verifica banco parole, confini della radura e simulazione dell'acqua. È innpm run verify. -
Distrattori confondibili per costruzione.
CONFUSIONIelenca, per ciascuna delle 10 famiglie ortografiche, gli errori che i bambini fanno davvero (GNIperGN,CUperQU, la vocale nuda al posto di quella accentata). Il controllo verifica che i distrattori del banco restino dentro quelle famiglie invece di scivolare verso lettere a caso quando si aggiungono parole. -
Due modalità sullo stesso banco.
parolava dal suono al segno (parola bucata sul tabellone, cartelli nella radura),suonofa il contrario (gruppo sul tabellone, tre parole intere nella radura). La modalità scelta è inlocalStorage(lascuolaamica_bosco_modalita_v1), insieme a classe, audio e memoria delle abilità. -
Memoria per abilità e recupero spaziato.
lascuolaamica_bosco_abilita_v1tiene precisione e ultimo incontro per famiglia;pescaPesata()ripropone più spesso le famiglie deboli,daRipassare()le riporta dopo un intervallo.
In comune con il resto del sito: palette letta dai token CSS via readPalette() (quindi la modalità accessibile Okabe-Ito ridipinge il bosco senza toccare il JS), premi nella bacheca condivisa (js/rewards.js, sei premi dedicati), precache opzionale nel Service Worker, storageGet/storageSet con fallback in memoria quando localStorage non è disponibile.
Timer di gioco facoltativo. Stesso controllo dei quiz e di Spacca-Muri. Il timer è spento di default e si accende dal pannello Info (scuolaAmica_play_timer_v1): acceso, SA.playWindow.ensureActive() fa partire da solo la sessione di 30 minuti alla prima partita e durante la pausa di 60 risponde false con un avviso; spento, risponde sempre true. Il bosco si apre in pausa sotto l'overlay «Entra nel bosco» (state.inGioco falso): il clic chiama entra() → SA.playWindow.ensureActive(), ed è anche il gesto utente che sblocca l'AudioContext. Finché non si è entrati, setPaused(false) passa dal cancello invece di riprendere, Esc non fa niente e scegliere classe o modalità ricompone la partita restando sotto l'overlay. Alla scadenza (evento SA.playWindow.eventName con detail.expired vero, solo a timer acceso) onPlayWindowExpired() chiude la partita, mostra SA.ui.alert «Tempo di gioco terminato» e torna al cancello con una partita nuova. Non si perde niente: SA.rewards.recordBosco registra ogni risposta e ogni parola mentre si gioca, non a fine partita. scripts/check_bosco.js verifica rifiuto, ingresso e scadenza con un SA.playWindow finto.
/sistema-solare (sistema-solare.html + sistema-solare.css + js/sistema-solare*.js) è un modello 3D del Sistema Solare per la 4ª e la 5ª, indipendente da subject-quiz-core.js e senza punteggi né timer di gioco: è uno strumento da esplorare, non un gioco a quiz.
-
Motore WebGL 1 vanilla (
js/sistema-solare-motore.js, nessuna libreria): posizioni dagli elementi orbitali JPL (validi 1800-2050), lune da JPL Horizons, poli IAU, ISS da TLE, sonde Voyager da traiettorie JPL Horizons. Due modi di tempo: «movimento rallentato» (di partenza, velocità compresse per guardarle) e «tempo vero». Le scale di distanze e raggi sono compresse e la pagina lo dichiara. -
Interfaccia (
js/sistema-solare.js): vista immersiva a tutto schermo, schede per bambini, missioni, passaporto, viaggio guidato con voce, film delle Voyager. Se WebGL manca la pagina resta nella vista normale. -
Dati con fonte, generati.
docs/sistema-solare/*.json(ogni valore con URL e note, rivisti da revisori indipendenti) →scripts/build_sistema_solare.py→js/sistema-solare-dati.jsoneassets/sistema-solare/voyager-traiettorie-v1.json. Lo stesso script scrive insistema-solare.html, fra i marcatoriSCHEDEeFAQ-JSONLD, le schede di tutti i corpi, le FAQ (testo eFAQPageidentici, dafaq.json) e i crediti delle immagini: la pagina si legge anche senza JavaScript e dai crawler. Non si modificano a mano. -
Asset pesanti fuori dal precache. Mappe (
assets/sistema-solare/*.webp2048×1024 ebasse/512×256), modelli (*.bin) e audio si scaricano quando servono; mappe e modelli finiscono nella cache stabile del Service Worker (binè fra le estensioni stabili)./assets/*è immutabile: un file corretto cambia nome. -
Controlli:
npm run check:sistema-solare(build--checkescripts/check_sistema_solare.js: posizioni note, leggi di Keplero, coerenza fisica delle schede). È inverifye inprepublish-check.sh. -
Revisione e SEO:
docs/sistema-solare/REVISIONE.md(guida per fisico e bambini) edocs/sistema-solare/AUDIT-SEO.md.
json/
├── index.json # Indice con cardinalità per materia e timestamp
├── matematica.json
├── italiano.json
├── inglese.json
├── problemi.json
├── civica.json
├── geografia.json
├── storia.json
└── scienze.json
Il riferimento legacy a questions.json (file aggregato) non è usato nel runtime pubblico. I controlli pre-pubblicazione bloccano riferimenti diretti non desiderati.
Dalla 4.26.0 le 8 pagine materia non hanno CSS bloccanti. scripts/build_css_critico.js (npm run css:critico, serve Playwright) apre ogni pagina come la lascia il motore, in ogni classe, e scrive fra i marcatori CSS-CRITICO un <style> con le regole che toccano quegli elementi (@media e @font-face comprese). I fogli completi restano linkati con media="print" e data-css-completo; js/lazy-css.js li accende. Arrivano dopo il <style>, che ne è un sottoinsieme: la cascata finale è quella di prima. Dopo ogni modifica a tokens.css, subject-quiz-theme.css, inglese.css, utilities.css o fonts.css: npm run css:critico, poi npm run freshness (hash CSP). npm run verify lo ricorda (check:css-critico, impronta dei fogli nel marcatore); npm run test:e2e:css-critico confronta pixel per pixel la pagina col solo CSS critico e coi fogli completi.
La source of truth runtime e il caricamento JSON tramite questions-loader.js.
-
subject-quiz-core.jslegge i dataset materia dajson/index.json -
questions-loader.jscostruisce bank e bonus questions a partire dai JSON materia - le righe bonus sono marcate con
bonus: truee bucketbonusRaw -
Domande per classe (dalla 4.26.0).
json/<materia>.jsonresta la fonte, ma il sito non lo scarica più.scripts/build_json_classi.py(dentronpm run freshness,--checkinnpm run verify) scrivejson/<materia>/c1.json…c5.json(domande attive di una classe, senza bonus, solo i campi letti dal sito) ejson/<materia>/bonus.json(tutte le bonus).subject-quiz-core.jsscarica solo le classi che una partita della classe scelta può usare (gradesForClass: distanzamaxGradeDistance, omaxLevelDistancecon i livelli, e la regola della 1ª: per la 3ª sono 2ª, 3ª e 4ª), le altre al cambio classe (data-domande="in-arrivo"sull'htmlfinché non arrivano)./breakoutscarica solo la classe scelta. Test:npm run test:e2e:json-classi
Tutto nella memoria locale del browser, tutto locale, niente server:
- Punteggi e progressi per materia/classe
- Preferenze UI (palette, riduzione animazioni)
- Metriche qualità sessione quiz
sw.js gestisce:
- Precache degli asset statici principali all'install
-
CSS e JS con la versione nell'URL — in
export/le pagine caricano CSS e JS locali con?v=<APP_VERSION>(scripts/versiona_asset.js, chiamato dall'export; i sorgenti non cambiano), eprecacheUrlmette in precache gli stessi URL. Dopo un rilascio il worker vecchio non trova gli URL nuovi e va in rete: una pagina non esegue mai CSS o JS di una versione diversa dal suo HTML.rewards.cssepalette-okabe.css, caricati da JS, prendono la?v=dello script che li carica. Anche i JSON scaricati confetch:SA.urlVersioneinshared.js(indice e changelog), la?v=del proprio script inquestions-loader.jsejs/sistema-solare.js, che girano prima diapp-version.js; in precache e inrefetchOpenedJsoncon la stessa?v=,manifest.jsonescluso. Senza rete, un CSS o un JS con un'altra?v=ripiega sul file con lo stesso percorso (cacheFirst,ignoreSearchsolo lì). Font e immagini restano senza versione.npm run test:e2e:versioniprova il passaggio da N a N+1 -
Precache lazy per i dataset materia —
json/index.jsonè precached all'install (serve alla home per il conteggio totale); i file delle domande (json/<materia>/c<N>.jsonebonus.json) NON sono precached: vengono salvati offline al primo fetch reale, quindi offline c'è ogni classe già aperta (con le vicine che la partita usa), non le altre -
Materie già aperte al cambio di versione —
refetchOpenedJsonlegge dalle cache vecchie quali filejson/c'erano (anche nelle cartelle per classe) e li riscarica nella cache nuova; chi aveva il file intero di prima (json/<materia>.json, fino alla 4.25.0) riceve tutte le classi e il bonus di quella materia, all'install e di nuovo all'activate (tetto di 5 s). Riscarica, non copia; se un file non arriva resta fuori dall'offline fino alla prossima apertura con la rete. Controllo:scripts/check_sw_opened_json.js -
Solo le cache della versione corrente —
cacheFirste i ripieghi offline cercano conmatchCurrent, cioè solo inKEEP_CACHE_NAMES.caches.match(request)senza nome guarda tutte le cache dalla più vecchia: se ne resta una di una versione precedente risponde lei. Non usarlo insw.js -
Pulizia anche fuori da activate — su Safari, se l'app viene chiusa con un aggiornamento in attesa, alla riapertura il worker nuovo è attivo ma la pulizia di
activatenon c'è stata. All'installrememberOlderCachesannota le cache che esistevano già; alla prima richiesta servitadropOlderCachesOnceriporta le materie aperte e cancella solo quelle. Non si cancella «tutto ciò che non è mio» fuori daactivate: un worker attivo toglierebbe la cache di una versione più nuova ancora in attesa -
Pagine HTML fuori dal precache — alla prima installazione l'unica pagina scaricata è
/; ai cambi di versione nessuna, se una cache vecchia ha già/(altrimenti/e basta).ensureHomegarantisce/nella cache nuova anche se la copia manca. Precachate erano 25 richieste di pagina a ogni install e a ogni versione, che Cloudflare contava come visualizzazioni (circa 13 per visita). Una pagina entra in cache quando viene aperta (networkFirstWithFallback); al cambio di versionecarryVisitedHtmlcopia le pagine HTML dalle cache vecchie alla nuova concache.matchecache.put, senza rete, inactivatee indropOlderCachesOnce. CSS e JS delle pagine restano in precache.check_sw_precache.jsblocca una pagina della sitemap rimessa in precache - Fallback offline: una pagina già visitata si apre dalla cache; una mai visitata ripiega sulla home
- Strategie differenziate per contenuti statici e dati quiz
La PWA e progettata per un deploy in root del dominio. Questo vincolo e intenzionale oggi:
-
shared.jsregistra il Service Worker su/sw.js -
sw.jsimporta/app-version.js - il manifest usa
start_url: "/"escope: "/" - le clean URL dipendono da rewrite lato hosting compatibili con
_redirects
Di conseguenza, un deploy in sottocartella o su GitHub Pages non e considerato supportato senza adattamenti infrastrutturali o refactor dedicato.
Per le navigazioni HTML la strategia e Network First con fallback alla home. Questo significa che offline, se una rotta non e disponibile in cache, il fallback finale torna a / invece di mostrare una pagina offline dedicata.
I file in /json/* (dataset domande per materia + index.json) richiedono
un header specifico in _headers:
/json/*
Cache-Control: public, max-age=0, must-revalidate
X-Robots-Tag: noindex, nofollow
Senza max-age=0, must-revalidate il browser potrebbe servire una copia
stale dei JSON anche dopo un upgrade della PWA. Sintomi tipici:
- conteggio domande non aggiornato dopo release dati
- bonus mancanti se un nuovo bucket e stato aggiunto al JSON
- materie con cardinalita incoerente tra index.json e UI
Il flusso atteso ad ogni release dati:
- APP_VERSION viene bumpato in
app-version.js -
sw.jsricostruisce la cache con nuovoCACHE_NAME - al primo accesso post-upgrade il browser rivalida i JSON contro
l'origine grazie a
must-revalidate - la nuova cache contiene i dataset aggiornati
Il check check_pwa_cache_headers in prepublish-check.sh verifica
la presenza dell'header /sw.js e /app-version.js ma non gli header
/json/*. La presenza degli header /json/* e invece controllata da
prepublish-check.sh nella sezione _headers: split json noindex/cache rules.
Regola operativa: non rimuovere o ammorbidire la riga
Cache-Control: public, max-age=0, must-revalidate su /json/* senza
una revisione architetturale. Una cache piu aggressiva su questi file
causa drift dati invisibile lato utente.
Riferimenti:
-
_headers sezione
/json/* -
prepublish-check.sh check
_headers: split json noindex/cache rules - sw.js per la strategia di cache runtime
- Policy di sicurezza restrittive per ridurre l’esecuzione di contenuti non previsti
- Header di protezione applicati a livello infrastrutturale
- Dati tecnici esclusi dall’indicizzazione diretta
- Dettagli completi: Sicurezza, privacy e minori