- JavaScript 75.2%
- CSS 19%
- HTML 5.8%
| api | ||
| app | ||
| docs | ||
| .gitignore | ||
| README.md | ||
Accessi
PWA di controllo accessi con tessera a QR per La Corte dei Miracoli (Spazio Livi APS). Sottoprogetto del sito: vive qui dentro e ne eredita l'estetica, ma è un'applicazione a sé.
Il backend non esiste ancora. Le risposte arrivano da un finto server in
app/src/api/mock.js. Serve a validare i flussi e l'interfaccia prima di decidere dove girerà il server vero.
Farla girare
Serve solo un server statico. Da docker-compose.yaml non passa niente: è
un'app di file, non ha bisogno di PHP.
python3 -m http.server 5180 --directory accessi/app
Poi apri http://localhost:5180.
⚠️ Deve essere
localhost, non l'indirizzo IP della macchina. La fotocamera (getUserMedia) e il service worker funzionano solo in un "contesto sicuro": HTTPS, oppure localhost. Suhttp://192.168.1.x:5180il browser non chiede nemmeno il permesso, si limita a non far funzionare niente. È la causa numero uno di "non va".Per provare da un telefono vero sulla rete di casa serve un tunnel HTTPS (
cloudflared tunnel --url http://localhost:5180, o simili).
Tessere di prova: dalle Impostazioni, in fondo → Tessere di prova da
inquadrare, oppure direttamente http://localhost:5180/dev/qr-demo.html.
Sono QR veri: si inquadrano da un altro schermo, o si stampano.
Che cosa c'è dentro
accessi/
├── docs/note-di-progetto.md decisioni, cose aperte, idee parcheggiate
├── api/ il backend, quando ci sarà
│ └── db/migrazioni/001_init.sql lo schema, già concordato
└── app/ la PWA
├── index.html · manifest.webmanifest · service-worker.js
├── icone/
├── vendor/ librerie incluse nel pacchetto, non da CDN
├── dev/qr-demo.html QR di prova (solo sviluppo)
└── src/
├── main.js rotte
├── registrazione.js ⭐ il cuore: cosa succede a codice letto
├── stato.js preferenze del dispositivo
├── api/ client.js (unico aggancio) + mock.js (finto)
├── scanner/ fotocamera · decoder · ciclo di lettura
├── offline/ IndexedDB · coda · sincronizzazione
├── schermate/ una per rotta
├── componenti/esito.js l'esito a tutto schermo
└── stili/ tokens.css (copiati dal tema) + app.css
Niente build, niente npm install, niente framework. Sono moduli ES nativi
serviti così come sono. Quattro schermate non giustificano una catena di
compilazione, e ogni dipendenza in più è una cosa che fra due anni va aggiornata.
I tre flussi
| Chi scansiona | Chi viene scansionato | Come è classificato | |
|---|---|---|---|
| Verifica ingresso | operatore | la tessera del socio | origine=operatore |
| Accesso evento (ven/sab) | operatore | la tessera del socio | tipo=evento |
| Automisurazione A | il socio, dal proprio telefono | il cartello al muro | origine=self |
| Automisurazione B | chi sta al bar | la tessera del socio | origine=bar |
Il "tipo" lo decide il server, guardando il calendario: venerdì e sabato l'accesso conta come evento, gli altri giorni come automisurazione. Il telefono non lo sa e non deve saperlo — un orologio sbagliato non deve poter falsare le statistiche.
Le due modalità di automisurazione non sono un dettaglio tecnico: chi ha lo
smartphone in mano si registra da solo, chi non ce l'ha (o non ha voglia) chiede
al banco. Vedi docs/note-di-progetto.md.
Il QR
Nel QR della tessera non c'è il numero di tessera: c'è un token opaco di 128 bit casuali.
CDM1:CCJX5WT5QMETG9HR386E1CN7XS
└┬─┘ └────────────┬────────────┘
│ └ 128 bit casuali, base32 senza I/L/O/U
└ prefisso + versione: fa scartare all'istante un QR estraneo, offline
Il numero (SL-2026-0142) resta stampato in chiaro sul cartoncino, per gli
umani. Tessera persa → si revoca il token e se ne emette uno nuovo: stesso
socio, stesso numero, storico intatto.
Il QR di luogo invece è un URL (…/#/qui/LUOGO-XV958HEJYEEQ), perché va
inquadrato con la fotocamera di sistema e il telefono deve saper aprire qualcosa.
Lettura del QR: due strade
- BarcodeDetector, nativa del browser — veloce, zero download. C'è su Chrome/Edge Android e desktop.
- zxing-wasm, la riserva per Safari e iOS, dove la prima non esiste.
Il .wasm è incluso nel pacchetto (app/vendor/), non preso da una CDN:
altrimenti al primo avvio senza rete il lettore non partirebbe. Misurato: ~60 ms
per fotogramma, più che sufficiente a 10 letture al secondo.
Dalle Impostazioni si può forzare la strada 2 anche su Chrome, per provare il ramo che serve a iOS senza procurarsi un iPhone.
Offline
Senza rete l'app non si blocca:
- la scansione entra in coda (IndexedDB) prima di provare a partire — se l'app viene chiusa a metà invio, non si perde;
- all'operatore compare In sospeso (arancione), con quello che risulta dai dati locali: mai un verde che non possiamo garantire;
- quando la rete torna, la coda si svuota da sola, riusando la stessa chiave di idempotenza: anche se il primo invio era arrivato, l'accesso non viene contato due volte.
L'istantanea per il giudizio offline contiene solo impronte SHA-256 dei token e lo stato: niente nomi, niente token in chiaro. Se il telefono dell'operatore viene perso, da lì non si ricava né una tessera funzionante né l'elenco dei soci.
Quando ci sarà il backend
- Lo schema è già scritto:
api/db/migrazioni/001_init.sql. - Si implementa un client REST in
app/src/api/rest.jscon le stesse firme dimock.js(config,scan,miaTessera,istantanea,stats). - Si cambia una riga in
app/src/api/client.js.
Il resto dell'app non se ne accorge, e gli interruttori di prova nelle Impostazioni spariscono da soli.
Endpoint previsti:
POST /api/scan |
riceve token + origine + orario + chiave di idempotenza, decide il tipo, registra, risponde con l'esito |
GET /api/config |
giorni evento, cooldown, luoghi |
GET /api/stats |
accessi per intervallo di date, filtrabili per tipo |
GET /api/me/tessera |
la tessera di chi è collegato |
GET /api/istantanea |
impronte per il giudizio offline |
| CRUD tessere | creazione, stato attiva/scaduta/bloccata, revoca token |
In produzione serve HTTPS, non è negoziabile: senza, niente fotocamera e niente installazione.
Coerenza col sito
app/src/stili/tokens.css è una copia di
wp/wp-content/themes/spazio-livi/assets/css/tokens.css. Se cambi i colori nel
tema, ricopia il file: è l'unico punto di contatto fra il sito e la PWA.
Gli esiti usano i colori ambito invece di un verde/rosso generico:
valida → --formazione, non valida → --sociale, in sospeso →
--altre-attivita, già dentro → --eventi.