fix: correzioni critiche e checklist test manuali

CORREZIONI:
- Badge confrontato ESATTAMENTE come stringa (rimosso .lstrip("0"))
- Success modal si chiude quando arriva nuovo badge (fix dipendenze useCallback)
- Polling ogni 30s per invalidare sessione se server riparte
- Area carosello allargata per testi lunghi (es. russo)

DOCUMENTAZIONE:
- API_SPECIFICATION.md aggiornata: badge come stringa esatta
- Creata TEST_CHECKLIST.md con 22 test manuali
- Aggiornati piani backend e frontend

Badge sono STRINGHE, non numeri:
- "0008988288" != "8988288" (zeri iniziali significativi)
This commit is contained in:
2026-01-17 23:32:33 +01:00
parent e68f299feb
commit b467d4753d
11 changed files with 681 additions and 84 deletions

158
TEST_CHECKLIST.md Normal file
View File

@@ -0,0 +1,158 @@
# ✅ Checklist Test Manuali - Focolari Voting System
## Pre-requisiti
- [ ] Backend avviato con `./dev.sh server`
- [ ] Browser aperto su `http://localhost:8000`
- [ ] Lettore RFID collegato (o usa tastiera per simulare)
---
## 🔐 Test Login Validatore
### T1: Login con password corretta
1. Passa un badge qualsiasi (es. `ò0008988288_`)
2. Inserisci password: `focolari`
3. **Atteso:** Accesso al varco attivo ✅
### T2: Login con password errata
1. Passa un badge qualsiasi
2. Inserisci password sbagliata
3. **Atteso:** Messaggio errore rosso, rimane su schermata password ❌
### T3: Annulla login
1. Passa un badge
2. Clicca "Annulla"
3. **Atteso:** Torna a "Passa badge validatore" ✅
---
## 👤 Test Anagrafica Utenti
### T4: Badge ammesso trovato
1. Login come validatore
2. Passa badge `ò0008988288_` (Marco Bianchi)
3. **Atteso:** Card verde con "Utente ammesso all'ingresso" ✅
### T5: Badge NON ammesso trovato
1. Passa badge `ò0000514162_` (Giuseppe Verdi)
2. **Atteso:** Card rossa lampeggiante "ACCESSO NON CONSENTITO" ⚠️
### T6: Badge non esistente nel DB
1. Passa badge `ò0006478281_`
2. **Atteso:** Schermata errore con badge in grassetto, countdown 30s ❌
### T7: Stesso badge passato due volte
1. Visualizza un utente (es. Marco Bianchi)
2. Passa lo STESSO badge di nuovo
3. **Atteso:** Nessun ricaricamento, utente rimane visualizzato ✅
### T8: Badge diverso sostituisce utente corrente
1. Visualizza Marco Bianchi (`0008988288`)
2. Passa Laura Rossi (`ò0007399575_`)
3. **Atteso:** Card cambia mostrando Laura Rossi ✅
---
## ✓ Test Conferma Ingresso
### T9: Conferma utente ammesso
1. Visualizza Marco Bianchi (ammesso)
2. Passa badge VALIDATORE (quello usato per login)
3. **Atteso:** Modal verde con carosello "Benvenuto/Welcome/..." per 8 secondi ✅
### T10: Badge validatore su utente NON ammesso
1. Visualizza Giuseppe Verdi (non ammesso)
2. Passa badge validatore
3. **Atteso:** Banner arancione "Badge validatore rilevato, se cambiato fare logout" ⚠️
### T11: Badge validatore senza utente visualizzato
1. Torna alla schermata "In attesa partecipante"
2. Passa badge validatore
3. **Atteso:** Banner arancione come sopra ⚠️
---
## 🎠 Test Success Modal
### T12: Carosello scorre tutte le lingue
1. Conferma ingresso di un utente ammesso
2. Osserva il carosello
3. **Atteso:** Scorre IT→EN→FR→DE→ES→PT→ZH→JA→AR→RU con animazione smooth ✅
### T13: Badge durante carosello chiude modal
1. Durante il carosello, passa un NUOVO badge
2. **Atteso:** Modal si chiude immediatamente, carica nuovo utente ✅
### T14: Carosello non troppo stretto
1. Osserva le scritte durante il carosello
2. **Atteso:** "Добро пожаловать!" (russo) deve essere visibile per intero ✅
---
## ⏱️ Test Timeout e Sessione
### T15: Timeout utente (60s)
1. Visualizza un utente
2. Aspetta 60 secondi
3. **Atteso:** Torna automaticamente a "In attesa partecipante" ✅
### T16: Timeout badge non trovato (30s)
1. Passa badge inesistente (`0006478281`)
2. Aspetta 30 secondi
3. **Atteso:** Torna a "In attesa partecipante", barra countdown visiva ✅
### T17: Logout manuale
1. Clicca "Esci" nell'header
2. **Atteso:** Torna a "Passa badge validatore" ✅
### T18: Sessione invalidata al riavvio server
1. Login come validatore, vai al varco attivo
2. **Ferma il server** (Ctrl+C)
3. **Riavvia il server** (`./dev.sh server`)
4. Aspetta ~30 secondi (polling)
5. **Atteso:** Torna automaticamente a "Passa badge validatore" ✅
---
## 🔧 Test Debug
### T19: Pagina debug accessibile
1. Vai a `http://localhost:8000/debug`
2. **Atteso:** Pagina con log tasti, stato scanner, buffer corrente ✅
### T20: Log tasti funzionante
1. Nella pagina debug, premi tasti sulla tastiera
2. **Atteso:** Tasti appaiono nella lista eventi ✅
---
## 📱 Test UI/UX
### T21: NumLock banner su desktop
1. Su browser desktop, verifica schermata "In attesa partecipante"
2. **Atteso:** Banner giallo con stato NumLock visibile ✅
### T22: Occhio toggle password
1. Nella schermata password validatore
2. Clicca l'icona occhio
3. **Atteso:** Password visibile/nascosta ✅
---
## Badge di Test
| Formato RFID | Badge | Nome | Risultato Atteso |
|----------------------|------------|----------------|----------------------|
| `ò0008988288_` | 0008988288 | Marco Bianchi | ✅ Ammesso |
| `ò0007399575_` | 0007399575 | Laura Rossi | ✅ Ammessa |
| `ò0000514162_` | 0000514162 | Giuseppe Verdi | ❌ Non ammesso |
| `ò0006478281_` | 0006478281 | - | ⚠️ Non trovato (404) |
**Password validatore:** `focolari`
---
## Note
- I badge sono stringhe, gli zeri iniziali sono significativi
- Il pattern RFID italiano usa `ò` come start e `_` come end (+ Enter)
- Il pattern US usa `;` come start e `?` come end (+ Enter)

View File

@@ -39,6 +39,7 @@ VotoFocolari/
├── backend-mock/
│ ├── main.py # Entry point con argparse
│ ├── Pipfile # Dipendenze Python
│ ├── API_SPECIFICATION.md # Specifiche per backend reale
│ ├── api/routes.py # Endpoint API
│ ├── schemas/models.py # Modelli Pydantic
│ └── data/
@@ -65,14 +66,15 @@ VotoFocolari/
1. **Login Validatore**: Passa badge + inserisci password → Sessione 30 min
2. **Attesa Partecipante**: Schermata grande "Passa il badge"
3. **Visualizzazione Utente**: Card con foto, nome, ruolo, stato ammissione
4. **Conferma Ingresso**: Validatore ripassa il badge → Carosello benvenuto multilingua
4. **Conferma Ingresso**: Validatore ripassa il badge → Carosello benvenuto multilingua (8s)
### Gestione RFID
- **Multi-pattern**: Supporta layout tastiera US (`;?`) e IT (`ò_`)
- **Multi-pattern**: Supporta layout tastiera US (`;?`) e IT (`ò_`), con `\n` dopo fine sequenza
- **Timeout 2.5s**: Per scansioni accidentali
- **ESC annulla**: Scansione in corso
- **Enter handling**: Gestito automaticamente
- **Stesso badge ignorato**: Se passato più volte di seguito
### Sicurezza Sessioni
@@ -125,6 +127,7 @@ VotoFocolari/
| Chiamate API | `frontend/src/services/api.ts` |
| Endpoint backend | `backend-mock/api/routes.py` |
| Dati mock utenti | `backend-mock/data/users_default.json` |
| Specifiche API produzione | `backend-mock/API_SPECIFICATION.md` |
| Configurazione Vite | `frontend/vite.config.ts` |
---
@@ -142,6 +145,10 @@ VotoFocolari/
4. **NumLock**: Su desktop, viene mostrato un banner per ricordare di attivare NumLock.
5. **Badge Duplicati**: Se lo stesso badge viene passato più volte di seguito, viene ignorato (no ricaricamento).
6. **Success Modal Interrompibile**: Se durante il carosello di benvenuto si passa un nuovo badge, la modal si chiude e viene caricato subito il nuovo utente.
---
## TODO (da concordare con committenti)

View File

@@ -70,7 +70,7 @@ backend-mock/
- [x] `POST /login-validate` - verifica solo password validatore
- [x] `GET /anagrafica/{badge_code}` - ricerca utente
- [x] Pulizia caratteri sentinel dal badge
- [x] Confronto con e senza zeri iniziali
- [x] **Confronto ESATTO come stringa** (zeri iniziali significativi)
- [x] Warning automatico se non ammesso
- [x] `POST /entry-request` - registrazione ingresso
- [x] Verifica password validatore
@@ -173,3 +173,17 @@ curl -X POST http://localhost:8000/entry-request \
## ✅ BACKEND COMPLETATO
Tutti i task sono stati implementati e testati.
### 📄 Documentazione API
Per le specifiche complete da implementare nel backend reale, vedere:
**`backend-mock/API_SPECIFICATION.md`**
Questo documento contiene:
- Descrizione completa di tutti gli endpoint
- Schema request/response JSON
- Codici di errore e gestione
- Meccanismo invalidazione sessioni (server_start_time)
- Considerazioni di sicurezza
- Struttura database suggerita
- Casi di test minimi

View File

@@ -71,7 +71,7 @@ Ottimizzata per tablet in orizzontale.
- [x] `RFIDStatus.tsx` - indicatore stato scanner
- [x] `UserCard.tsx` - card utente con foto e ruolo
- [x] `CountdownTimer.tsx` - timer con progress bar
- [x] `WelcomeCarousel.tsx` - carosello messaggi multilingua
- [x] `WelcomeCarousel.tsx` - carosello messaggi multilingua con **animazione smooth sliding**
- [x] `NumLockBanner.tsx` - avviso NumLock per desktop
### 7. Schermate (`screens/`)
@@ -80,10 +80,10 @@ Ottimizzata per tablet in orizzontale.
- [x] `ValidatorLoginScreen.tsx` - attesa badge + password + NumLockBanner
- [x] `ActiveGateScreen.tsx` - varco attivo:
- [x] Card utente (layout largo per tablet)
- [x] **Schermata "badge non trovato"** con countdown 30s
- [x] **Schermata "badge non trovato"** con countdown barra visiva (30s)
- [x] **Notifica badge validatore ignorato**
- [x] NumLockBanner
- [x] `SuccessModal.tsx` - conferma ingresso con carosello
- [x] `SuccessModal.tsx` - conferma ingresso con carosello (**durata aumentata 8s**)
- [x] `ErrorModal.tsx` - errore fullscreen
- [x] `DebugScreen.tsx` - pagina diagnostica RFID
@@ -95,10 +95,13 @@ Ottimizzata per tablet in orizzontale.
- [x] **Qualsiasi badge può essere validatore** (verificato con password)
- [x] Password salvata in sessione per conferme ingresso
- [x] **Invalidazione sessione se server riparte** (serverStartTime)
- [x] **Polling periodico (30s) per verificare server restart**
- [x] Timeout sessione 30 minuti
- [x] Timeout utente 60 secondi
- [x] **Timeout badge non trovato 30 secondi**
- [x] **Ignora stesso badge passato più volte** (no ricaricamento)
- [x] Cambio rapido badge partecipante
- [x] **Badge durante success modal chiude modal e carica nuovo utente** (fix dipendenze useCallback)
- [x] Conferma con badge validatore (quello della sessione)
- [x] **Notifica se badge validatore rippassato senza utente**
- [x] Logging transizioni con prefisso `[FLOW]`
@@ -107,9 +110,10 @@ Ottimizzata per tablet in orizzontale.
- [x] Componente `WelcomeCarousel.tsx`
- [x] 10 lingue supportate
- [x] Scorrimento automatico ogni 800ms
- [x] **Animazione smooth sliding** (slide up/down)
- [x] Scorrimento automatico (intervallo calcolato dinamicamente)
- [x] Modale fullscreen verde
- [x] Durata totale: 5 secondi
- [x] **Durata totale: 8 secondi** (più rilassato)
### 10. Debug & Diagnostica
@@ -135,4 +139,46 @@ Ottimizzata per tablet in orizzontale.
---
## 🧪 TODO: Test Automatici
### Test da Implementare
- [ ] **Test RFID Scanner:**
- [ ] Rilevamento pattern US (`;``?`)
- [ ] Rilevamento pattern IT (`ò``_`)
- [ ] Timeout scansione incompleta
- [ ] ESC annulla scansione
- [ ] Enter post-completamento ignorato
- [ ] **Test Flow Validatore:**
- [ ] Login con password corretta
- [ ] Login con password errata
- [ ] Sessione persistente in localStorage
- [ ] Invalidazione sessione al riavvio server
- [ ] Logout manuale
- [ ] **Test Flow Partecipante:**
- [ ] Badge trovato ammesso → mostra card verde
- [ ] Badge trovato non ammesso → mostra card rossa + warning
- [ ] Badge non trovato → mostra schermata errore con countdown
- [ ] Stesso badge passato più volte → ignorato
- [ ] Badge diverso passato → cambia utente visualizzato
- [ ] Timeout 60s → torna in attesa
- [ ] **Test Conferma Ingresso:**
- [ ] Badge validatore su utente ammesso → success modal
- [ ] Badge validatore su utente NON ammesso → notifica ignorato
- [ ] Badge validatore senza utente → notifica ignorato
- [ ] **⚠️ IMPORTANTE:** Simulare bug che bypassa frontend → backend DEVE bloccare
- [ ] **Test Success Modal:**
- [ ] Carosello scorre tutte le lingue
- [ ] Durata corretta (8s)
- [ ] Badge durante modal → chiude modal e carica nuovo utente
---
## ✅ FRONTEND COMPLETATO
Tutte le funzionalità principali sono state implementate. Rimangono da sviluppare i test automatici.

View File

@@ -0,0 +1,325 @@
# 📄 Specifiche API Backend - Focolari Voting System
Questo documento descrive le specifiche che il backend reale deve implementare per funzionare correttamente con il frontend del sistema di controllo accessi.
**Versione:** 1.0
**Data:** Gennaio 2026
---
## 🌐 Configurazione Server
### CORS
Il server deve abilitare CORS con le seguenti impostazioni:
- **Origins:** `*` (o lista specifica di domini autorizzati)
- **Methods:** `GET, POST, OPTIONS`
- **Headers:** `Content-Type, Authorization`
### Serving Frontend
Il backend dovrebbe servire il frontend buildato (file statici) dalla root `/`.
Questo permette di avere un unico endpoint per frontend e API.
---
## 🔑 Meccanismo di Invalidazione Sessioni
### Server Start Time
All'avvio, il server deve generare un **timestamp univoco** (es. Unix timestamp in secondi).
Questo valore viene restituito nell'endpoint `/info-room` e serve al frontend per invalidare sessioni obsolete.
**Comportamento:**
1. Il frontend salva `server_start_time` nella sessione locale
2. Al caricamento successivo, confronta il valore salvato con quello attuale
3. Se differiscono, la sessione viene invalidata (il server è stato riavviato)
Questo garantisce che:
- Un riavvio del server forza il re-login di tutti i validatori
- Sessioni zombie non rimangono attive dopo manutenzione
---
## 📡 Endpoint API
### 1. `GET /info-room`
Restituisce informazioni sulla sala/meeting corrente e lo stato del server.
#### Response `200 OK`
```json
{
"room_name": "Sala Assemblea",
"meeting_id": "VOT-2024",
"server_start_time": 1737100800
}
```
| Campo | Tipo | Descrizione |
|-------|------|-------------|
| `room_name` | string | Nome della sala visualizzato nell'header |
| `meeting_id` | string | Identificativo del meeting/votazione |
| `server_start_time` | integer | Unix timestamp dell'avvio server (per invalidazione sessioni) |
---
### 2. `POST /login-validate`
Verifica le credenziali del validatore. **IMPORTANTE:** Qualsiasi badge può diventare un badge validatore se la password è corretta.
#### Request Body
```json
{
"badge": "0007399575",
"password": "focolari"
}
```
| Campo | Tipo | Descrizione |
|-------|------|-------------|
| `badge` | string | Codice badge scansionato (numerico, senza sentinel) |
| `password` | string | Password inserita dall'utente |
#### Response `200 OK` (Successo)
```json
{
"success": true,
"message": "Login effettuato con successo",
"token": "optional-jwt-token"
}
```
#### Response `401 Unauthorized` (Errore)
```json
{
"detail": "Password non valida"
}
```
**Note:**
- La password è l'unico fattore di autenticazione
- Il badge viene memorizzato dal frontend come "badge validatore" per conferme successive
- Il token è opzionale (per future implementazioni di autenticazione JWT)
---
### 3. `GET /anagrafica/{badge_code}`
Recupera i dati anagrafici di un utente dato il suo codice badge.
#### Path Parameters
- `badge_code`: Codice badge (stringa, es. "0008988288")
**IMPORTANTE - Confronto Badge:**
Il badge è una **stringa**, non un numero. Va confrontato **esattamente** carattere per carattere.
Gli zeri iniziali sono significativi: `"0008988288"` e `"8988288"` sono badge **diversi**.
#### Response `200 OK` (Utente trovato e ammesso)
```json
{
"badge_code": "0008988288",
"nome": "Marco",
"cognome": "Bianchi",
"url_foto": "https://example.com/foto.jpg",
"ruolo": "Votante",
"ammesso": true # <-- utente ammesso all'ingresso
}
```
#### Response `200 OK` (Utente trovato ma NON ammesso)
```json
{
"badge_code": "0000514162",
"nome": "Giuseppe",
"cognome": "Verdi",
"url_foto": "https://example.com/foto.jpg",
"ruolo": "Tecnico",
"ammesso": false, #<-- utente NON ammesso all'ingresso
"warning": "Utente non ammesso all'ingresso"
}
```
#### Response `404 Not Found` (Utente non trovato)
```json
{
"detail": "Badge non trovato nel sistema"
}
```
| Campo Response | Tipo | Descrizione |
|----------------|------|-------------|
| `badge_code` | string | Codice badge |
| `nome` | string | Nome dell'utente |
| `cognome` | string | Cognome dell'utente |
| `url_foto` | string | URL immagine profilo (può essere placeholder) |
| `ruolo` | string | Ruolo dell'utente (es. "Votante", "Tecnico", "Ospite") |
| `ammesso` | boolean | `true` se autorizzato all'ingresso |
| `warning` | string? | Opzionale, presente se `ammesso: false` |
---
### 4. `POST /entry-request`
Registra l'ingresso di un utente. Richiede conferma del validatore.
#### Request Body
```json
{
"user_badge": "0008988288",
"validator_password": "focolari"
}
```
| Campo | Tipo | Descrizione |
|-------|------|-------------|
| `user_badge` | string | Badge dell'utente che sta entrando |
| `validator_password` | string | Password del validatore (ri-verifica) |
#### Response `200 OK` (Successo)
```json
{
"success": true,
"message": "Ingresso registrato con successo"
}
```
#### Response `401 Unauthorized` (Password errata)
```json
{
"detail": "Password validatore non valida"
}
```
#### Response `403 Forbidden` (Utente non ammesso)
```json
{
"detail": "Utente non autorizzato all'ingresso"
}
```
#### Response `404 Not Found` (Badge non trovato)
```json
{
"detail": "Badge utente non trovato"
}
```
**IMPORTANTE - Sicurezza:**
Anche se il frontend non dovrebbe permettere di inviare entry-request per utenti non ammessi, il backend **DEVE** sempre verificare:
1. Che la password validatore sia corretta
2. Che l'utente esista
3. Che l'utente sia ammesso (`ammesso: true`)
Non fidarsi mai del frontend per la validazione!
---
## 🔒 Considerazioni di Sicurezza
### Validazione Lato Backend
Il backend deve **sempre** eseguire tutte le validazioni, indipendentemente da cosa fa il frontend:
1. **Login:** Verificare che la password sia corretta
2. **Entry:**
- Verificare password validatore
- Verificare che utente esista
- Verificare che utente sia ammesso
- Loggare l'operazione per audit
### Logging e Audit
Si raccomanda di loggare:
- Ogni tentativo di login (successo/fallimento)
- Ogni registrazione di ingresso
- Badge non trovati (potenziale tentativo di accesso non autorizzato)
### Protezione Rate Limiting
Implementare rate limiting su:
- `/login-validate`: max 5 tentativi/minuto per IP
- `/entry-request`: max 30 richieste/minuto per IP
---
## 📊 Struttura Dati Suggerita
### Database Utenti
```sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
badge_code VARCHAR(20) UNIQUE NOT NULL,
nome VARCHAR(100) NOT NULL,
cognome VARCHAR(100) NOT NULL,
url_foto VARCHAR(500),
ruolo VARCHAR(50) NOT NULL,
ammesso BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_badge_code ON users(badge_code);
```
### Tabella Accessi (Audit Log)
```sql
CREATE TABLE access_log (
id SERIAL PRIMARY KEY,
user_badge VARCHAR(20) NOT NULL,
validator_badge VARCHAR(20) NOT NULL,
room_id VARCHAR(50),
action VARCHAR(20) NOT NULL, -- 'entry', 'denied', 'not_found'
timestamp TIMESTAMP DEFAULT NOW(),
ip_address VARCHAR(45)
);
```
---
## 🧪 Test e Validazione
### Casi di Test Minimi
1. **Login con password corretta** → 200 OK
2. **Login con password errata** → 401 Unauthorized
3. **Anagrafica badge esistente ammesso** → 200 OK con `ammesso: true`
4. **Anagrafica badge esistente non ammesso** → 200 OK con `ammesso: false` + warning
5. **Anagrafica badge inesistente** → 404 Not Found
6. **Entry utente ammesso con password corretta** → 200 OK
7. **Entry utente ammesso con password errata** → 401 Unauthorized
8. **Entry utente NON ammesso** → 403 Forbidden (anche se password corretta!)
9. **Entry badge inesistente** → 404 Not Found
### Badge di Test (Mock)
```
0008988288 - Marco Bianchi (Votante, ammesso)
0007399575 - Laura Rossi (Votante, ammessa)
0000514162 - Giuseppe Verdi (Tecnico, NON ammesso)
0006478281 - NON nel database (per test 404)
```
---
## 📝 Note Implementative
### Content-Type
Tutte le richieste e risposte usano `application/json`.
### Codici Errore HTTP
- `200`: Successo
- `401`: Non autorizzato (password errata)
- `403`: Vietato (utente non ammesso)
- `404`: Risorsa non trovata
- `500`: Errore interno server
### Formato Errori
Tutti gli errori devono restituire un JSON con campo `detail`:
```json
{
"detail": "Messaggio di errore descrittivo"
}
```
---
## 🔄 Changelog
### v1.0 (Gennaio 2026)
- Specifica iniziale
- Endpoint: `/info-room`, `/login-validate`, `/anagrafica/{badge}`, `/entry-request`
- Meccanismo invalidazione sessioni con `server_start_time`

View File

@@ -46,10 +46,11 @@ def clean_badge(badge: str) -> str:
def find_user(badge_code: str) -> dict | None:
"""Cerca un utente per badge code"""
"""Cerca un utente per badge code (confronto esatto come stringa)"""
clean = clean_badge(badge_code)
for user in _data["users"]:
if user["badge_code"] == clean or user["badge_code"].lstrip("0") == clean.lstrip("0"):
# Confronto esatto: il badge è una stringa, non un numero
if user["badge_code"] == clean:
return user
return None

View File

@@ -5,14 +5,6 @@
"meeting_id": "VOT-2024"
},
"users": [
{
"badge_code": "0008988288",
"nome": "Marco",
"cognome": "Bianchi",
"url_foto": "https://randomuser.me/api/portraits/men/1.jpg",
"ruolo": "Votante",
"ammesso": true
},
{
"badge_code": "0007399575",
"nome": "Laura",

View File

@@ -12,6 +12,7 @@ import type {AppState, RoomInfo, User, ValidatorSession} from './types';
// Costanti
const SESSION_DURATION_MS = 30 * 60 * 1000; // 30 minuti
const USER_TIMEOUT_SECONDS = 60;
const SUCCESS_MODAL_DURATION_MS = 8000; // Aumentato per carosello più rilassato
const STORAGE_KEY = 'focolari_validator_session';
function App() {
@@ -38,6 +39,9 @@ function App() {
// Badge non trovato (con timeout per tornare all'attesa)
const [notFoundBadge, setNotFoundBadge] = useState<string | null>(null);
// Badge corrente visualizzato (per evitare ricaricamenti multipli dello stesso)
const [currentBadgeCode, setCurrentBadgeCode] = useState<string | null>(null);
// ============================================
// Session Management
// ============================================
@@ -120,10 +124,22 @@ function App() {
setTimeout(() => setShowValidatorBadgeNotice(false), 4000);
}
} else {
// Badge partecipante - verifica se è lo stesso già visualizzato
if (cleanCode === currentBadgeCode) {
console.log('[FLOW] Same badge scanned again - ignoring');
break;
}
console.log('[FLOW] Loading participant:', cleanCode);
// Badge partecipante - carica utente
// Se c'era un badge non trovato, cancellalo e carica il nuovo
setNotFoundBadge(null);
// Se era aperta la success modal, chiudila subito
if (showSuccessModal) {
console.log('[FLOW] Closing success modal for new badge');
setShowSuccessModal(false);
setSuccessUserName(undefined);
}
await handleLoadUser(cleanCode);
}
break;
@@ -131,7 +147,7 @@ function App() {
default:
break;
}
}, [appState, currentUser, validatorSession]);
}, [appState, currentUser, validatorSession, showSuccessModal, currentBadgeCode]);
// ============================================
// Initialize RFID Scanner
@@ -153,6 +169,7 @@ function App() {
setLoading(true);
setError(undefined);
setNotFoundBadge(null);
setCurrentBadgeCode(badgeCode); // Traccia il badge corrente
setAppState('showing-user');
try {
@@ -195,6 +212,7 @@ function App() {
setSuccessUserName(userName);
setShowSuccessModal(true);
setCurrentUser(null);
setCurrentBadgeCode(null); // Reset badge corrente dopo successo
setAppState('gate-active');
}
} catch (err) {
@@ -255,6 +273,7 @@ function App() {
const handleCancelUser = useCallback(() => {
setCurrentUser(null);
setNotFoundBadge(null);
setCurrentBadgeCode(null); // Reset badge corrente
setError(undefined);
setAppState('gate-active');
}, []);
@@ -263,6 +282,7 @@ function App() {
console.log('[App] User timeout - tornando in attesa');
setCurrentUser(null);
setNotFoundBadge(null);
setCurrentBadgeCode(null); // Reset badge corrente
setError(undefined);
setAppState('gate-active');
}, []);
@@ -323,6 +343,29 @@ function App() {
return () => clearInterval(interval);
}, [validatorSession, clearSession]);
// Polling per verificare se il server è stato riavviato
useEffect(() => {
if (!validatorSession || !roomInfo) return;
const checkServerRestart = async () => {
try {
const info = await getRoomInfo();
if (info.server_start_time !== validatorSession.serverStartTime) {
console.log('[FLOW] Server restarted - invalidating session');
clearSession();
}
} catch {
// Ignora errori di connessione durante il polling
console.warn('[FLOW] Server unreachable during polling');
}
};
// Polling ogni 30 secondi
const interval = setInterval(checkServerRestart, 30000);
return () => clearInterval(interval);
}, [validatorSession, roomInfo, clearSession]);
// ============================================
// Render
// ============================================
@@ -387,6 +430,7 @@ function App() {
isOpen={showSuccessModal}
onClose={handleSuccessModalClose}
userName={successUserName}
durationMs={SUCCESS_MODAL_DURATION_MS}
/>
{/* Error Modal */}

View File

@@ -2,7 +2,7 @@
* Welcome Carousel Component - Focolari Voting System
*
* Carosello automatico di messaggi di benvenuto multilingua.
* Scorre automaticamente ogni 800ms durante la visualizzazione.
* Scorrimento fluido con animazione smooth.
*/
import {useEffect, useState} from 'react';
@@ -25,55 +25,51 @@ const WELCOME_MESSAGES: WelcomeMessage[] = [
{lang: 'ru', text: 'Добро пожаловать!'},
];
const CAROUSEL_INTERVAL_MS = 800;
interface WelcomeCarouselProps {
/** Se true, il carosello è in pausa */
paused?: boolean;
/** Nome dell'utente da visualizzare (opzionale) */
userName?: string;
/** Intervallo tra i messaggi in ms (default 800) */
intervalMs?: number;
}
export function WelcomeCarousel({paused = false, userName}: WelcomeCarouselProps) {
export function WelcomeCarousel({paused = false, userName, intervalMs = 800}: WelcomeCarouselProps) {
const [currentIndex, setCurrentIndex] = useState(0);
const [isTransitioning, setIsTransitioning] = useState(false);
useEffect(() => {
if (paused) return;
const interval = setInterval(() => {
setIsTransitioning(true);
setTimeout(() => {
setCurrentIndex((prev) => (prev + 1) % WELCOME_MESSAGES.length);
setIsTransitioning(false);
}, 150); // Durata transizione fade
}, CAROUSEL_INTERVAL_MS);
setCurrentIndex((prev) => (prev + 1) % WELCOME_MESSAGES.length);
}, intervalMs);
return () => clearInterval(interval);
}, [paused]);
}, [paused, intervalMs]);
const currentMessage = WELCOME_MESSAGES[currentIndex];
return (
<div className="flex flex-col items-center justify-center text-center">
{/* Messaggio di benvenuto */}
<div
className={`transition-opacity duration-150 ${
isTransitioning ? 'opacity-0' : 'opacity-100'
}`}
>
<h1 className="text-6xl md:text-8xl font-bold text-white mb-4 drop-shadow-lg">
{currentMessage.text}
</h1>
<p className="text-xl text-white/70 uppercase tracking-wider">
{currentMessage.lang.toUpperCase()}
</p>
<div className="flex flex-col items-center justify-center text-center w-full max-w-4xl mx-auto px-4">
{/* Container con overflow hidden per animazione slide - altezza maggiore per testi lunghi */}
<div className="relative h-40 md:h-52 w-full overflow-hidden">
{/* Messaggio corrente */}
<div
key={currentIndex}
className="absolute inset-0 flex flex-col items-center justify-center animate-slide-in-up"
>
<h1 className="text-5xl md:text-7xl lg:text-8xl font-bold text-white mb-2 drop-shadow-lg whitespace-nowrap">
{currentMessage.text}
</h1>
<p className="text-lg text-white/60 uppercase tracking-wider">
{currentMessage.lang.toUpperCase()}
</p>
</div>
</div>
{/* Nome utente */}
{userName && (
<div className="mt-8 pt-8 border-t border-white/30">
<div className="mt-6 pt-6 border-t border-white/30">
<p className="text-3xl md:text-4xl text-white font-medium">
{userName}
</p>
@@ -81,18 +77,43 @@ export function WelcomeCarousel({paused = false, userName}: WelcomeCarouselProps
)}
{/* Indicatori */}
<div className="flex gap-2 mt-8">
<div className="flex gap-2 mt-6">
{WELCOME_MESSAGES.map((_, index) => (
<div
key={index}
className={`w-2 h-2 rounded-full transition-all duration-300 ${
className={`h-2 rounded-full transition-all duration-500 ease-out ${
index === currentIndex
? 'bg-white w-6'
: 'bg-white/40'
? 'bg-white w-8'
: 'bg-white/30 w-2'
}`}
/>
))}
</div>
{/* Stili per animazione smooth */}
<style>{`
@keyframes slideInUp {
0% {
opacity: 0;
transform: translateY(40px);
}
20% {
opacity: 1;
transform: translateY(0);
}
80% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(-40px);
}
}
.animate-slide-in-up {
animation: slideInUp ${intervalMs}ms ease-in-out;
}
`}</style>
</div>
);
}

View File

@@ -3,7 +3,6 @@
* Schermata principale del varco attivo
*/
import {useEffect, useState} from 'react';
import {Button, CountdownTimer, Logo, NumLockBanner, RFIDStatus, UserCard} from '../components';
import type {RFIDScannerState, RoomInfo, User} from '../types';
@@ -39,27 +38,6 @@ export function ActiveGateScreen({
onUserTimeout,
showValidatorBadgeNotice = false,
}: ActiveGateScreenProps) {
// Timer per countdown badge non trovato
const [notFoundCountdown, setNotFoundCountdown] = useState(NOT_FOUND_TIMEOUT_SECONDS);
// Reset countdown quando cambia notFoundBadge
useEffect(() => {
if (notFoundBadge) {
setNotFoundCountdown(NOT_FOUND_TIMEOUT_SECONDS);
const interval = setInterval(() => {
setNotFoundCountdown(prev => {
if (prev <= 1) {
clearInterval(interval);
onUserTimeout();
return 0;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(interval);
}
}, [notFoundBadge, onUserTimeout]);
return (
<div className="min-h-screen flex flex-col bg-gradient-to-br from-slate-50 to-slate-100">
{/* Header */}
@@ -123,6 +101,17 @@ export function ActiveGateScreen({
) : notFoundBadge ? (
// Badge non trovato
<div className="glass rounded-3xl p-12 md:p-16 shadow-xl text-center max-w-2xl w-full">
{/* Timer bar in alto come per l'utente trovato */}
<div className="mb-8">
<CountdownTimer
key={`not-found-${notFoundBadge}`}
seconds={NOT_FOUND_TIMEOUT_SECONDS}
onExpire={onUserTimeout}
warningThreshold={15}
dangerThreshold={8}
/>
</div>
<div
className="inline-flex items-center justify-center w-32 h-32 rounded-full bg-error/10 mb-8">
<svg
@@ -142,14 +131,6 @@ export function ActiveGateScreen({
<p className="text-2xl text-gray-700 mb-2">Utente con badge:</p>
<p className="text-3xl font-bold text-error font-mono mb-4">{notFoundBadge}</p>
<p className="text-2xl text-gray-700">non trovato nel sistema</p>
{/* Footer con countdown */}
<div className="mt-10 pt-6 border-t border-gray-200">
<p className="text-gray-500">
Ritorno all'attesa in <span
className="font-bold text-focolare-blue">{notFoundCountdown}</span> secondi
</p>
</div>
</div>
) : error ? (
// Error state

View File

@@ -9,19 +9,27 @@ interface SuccessModalProps {
isOpen: boolean;
onClose: () => void;
userName?: string;
durationMs?: number;
}
// Numero messaggi nel carosello
const CAROUSEL_MESSAGE_COUNT = 10;
export function SuccessModal({
isOpen,
onClose,
userName
userName,
durationMs = 8000
}: SuccessModalProps) {
// Calcola intervallo carosello per mostrare tutti i messaggi durante la durata
const carouselIntervalMs = Math.floor(durationMs / (CAROUSEL_MESSAGE_COUNT * 1.2));
return (
<Modal
isOpen={isOpen}
onClose={onClose}
variant="success"
autoCloseMs={5000}
autoCloseMs={durationMs}
fullscreen
>
<div className="flex flex-col items-center justify-center min-h-screen p-8">
@@ -46,7 +54,7 @@ export function SuccessModal({
</div>
{/* Carosello Messaggi Benvenuto */}
<WelcomeCarousel userName={userName}/>
<WelcomeCarousel userName={userName} intervalMs={carouselIntervalMs} />
{/* Sub text */}
<p className="text-2xl md:text-3xl text-white/80 mt-8 animate-fade-in">
@@ -59,7 +67,7 @@ export function SuccessModal({
<div
className="h-full bg-white rounded-full"
style={{
animation: 'shrink 5s linear forwards',
animation: `shrink ${durationMs}ms linear forwards`,
}}
/>
</div>