feat: Controllo accessi RFID completo con gestione sessioni

- Aggiunto supporto multi-pattern RFID (US/IT layout)
- Implementata invalidazione sessioni al restart del server
- Schermata "badge non trovato" con countdown 30s
- Notifica quando badge validatore passato senza utente
- Database aggiornato con badge reali di test
- Layout ottimizzato per tablet orizzontale
- Banner NumLock per desktop
- Toggle visibilità password
- Carosello benvenuto multilingua (10 lingue)
- Pagina debug RFID (/debug)
This commit is contained in:
2026-01-17 20:06:50 +01:00
parent 21b509c6ba
commit e68f299feb
48 changed files with 3625 additions and 2445 deletions

View File

@@ -4,39 +4,58 @@ Sistema di controllo accessi per le assemblee di voto del **Movimento dei Focola
## 📖 Descrizione ## 📖 Descrizione
Applicazione web ottimizzata per tablet che gestisce i varchi d'ingresso alle sale votazione. Il sistema utilizza lettori RFID USB (che emulano tastiera) per identificare validatori e partecipanti. Applicazione web ottimizzata per tablet che gestisce i varchi d'ingresso alle sale votazione. Il sistema utilizza
lettori RFID USB (che emulano tastiera) per identificare validatori e partecipanti.
## 🏗️ Struttura Progetto ## 🏗️ Struttura Progetto
``` ```
VotoFocolari/ VotoFocolari/
├── dev.sh # Script sviluppo (install, dev, server, build, ...)
├── ai-prompts/ # Documentazione sviluppo e prompt ├── ai-prompts/ # Documentazione sviluppo e prompt
├── backend-mock/ # API mock in Python FastAPI ├── backend-mock/ # API mock in Python FastAPI
│ └── static/ # Frontend buildato (generato)
└── frontend/ # App React + TypeScript + Tailwind └── frontend/ # App React + TypeScript + Tailwind
``` ```
## 🚀 Quick Start ## 🚀 Quick Start
### Backend ### Setup Iniziale
```bash ```bash
cd backend-mock ./dev.sh install
pipenv install
pipenv run start
# Server: http://localhost:8000
# Docs API: http://localhost:8000/docs
``` ```
### Frontend ### Sviluppo (hot reload)
```bash ```bash
cd frontend ./dev.sh dev
npm install # Backend API: http://localhost:8000
npm run dev # Frontend Dev: http://localhost:5173
# App: http://localhost:5173 ```
### Produzione Locale
```bash
./dev.sh server
# App completa: http://localhost:8000
```
### Altri Comandi
```bash
./dev.sh build # Solo build frontend
./dev.sh backend # Solo backend (API)
./dev.sh frontend # Solo frontend dev
./dev.sh shell # Shell pipenv backend
./dev.sh clean # Pulisce build e cache
./dev.sh help # Mostra tutti i comandi
``` ```
## 📚 Documentazione ## 📚 Documentazione
Per dettagli tecnici, consulta la cartella `ai-prompts/`: Per dettagli tecnici, consulta la cartella `ai-prompts/`:
- `00-welcome-agent.md` - Panoramica progetto - `00-welcome-agent.md` - Panoramica progetto
- `01-backend-plan.md` - Piano sviluppo backend - `01-backend-plan.md` - Piano sviluppo backend
- `02-frontend-plan.md` - Piano sviluppo frontend - `02-frontend-plan.md` - Piano sviluppo frontend
@@ -46,6 +65,10 @@ Per dettagli tecnici, consulta la cartella `ai-prompts/`:
- **Badge Validatore:** `999999` - **Badge Validatore:** `999999`
- **Password:** `focolari` - **Password:** `focolari`
## 🔍 Debug
Accedi a `/debug` per diagnostica RFID in tempo reale.
## 📄 Licenza ## 📄 Licenza
Progetto privato - Movimento dei Focolari Progetto privato - Movimento dei Focolari

View File

@@ -1,206 +1,150 @@
# 🎯 Focolari Voting System - Guida Agente # 🗳️ Focolari Voting System - Welcome Agent
## Panoramica Progetto ## Panoramica Progetto
**Nome:** Sistema Controllo Accessi "Focolari Voting System" Sistema di controllo accessi per le assemblee di voto del **Movimento dei Focolari**.
**Committente:** Movimento dei Focolari Applicazione web ottimizzata per **tablet in orizzontale** che gestisce i varchi d'ingresso alle sale votazione tramite
**Scopo:** Gestione dei varchi di accesso per le assemblee di voto del Movimento. **lettori RFID USB** (emulano tastiera).
### Contesto d'Uso
Il sistema funziona su **tablet Android** (via browser Chrome) posizionati ai varchi d'ingresso delle sale votazione. Ogni tablet è collegato a un **lettore RFID USB** che emula tastiera.
--- ---
## 🏗️ Architettura ## Stack Tecnologico
### Backend Mock
- **Python 3.11+** con **FastAPI**
- **Pydantic** per validazione dati
- **Uvicorn** come server ASGI
- **Pipenv** per gestione dipendenze
### Frontend
- **React 18** + **TypeScript**
- **Vite** come build tool
- **Tailwind CSS 4** per styling
- **React Router** per navigazione
---
## Struttura Progetto
``` ```
VotoFocolari/ VotoFocolari/
├── ai-prompts/ # Documentazione e piani di sviluppo ├── dev.sh # Script di sviluppo (install, dev, server, ...)
├── backend-mock/ # Python FastAPI (server mock) ├── README.md
├── ai-prompts/ # Documentazione sviluppo
│ ├── 00-welcome-agent.md # Questo file
│ ├── 01-backend-plan.md # Piano backend
│ └── 02-frontend-plan.md # Piano frontend
├── backend-mock/
│ ├── main.py # Entry point con argparse │ ├── main.py # Entry point con argparse
│ ├── Pipfile # Dipendenze pipenv │ ├── Pipfile # Dipendenze Python
│ ├── api/ # Routes FastAPI │ ├── api/routes.py # Endpoint API
│ ├── schemas/ # Modelli Pydantic │ ├── schemas/models.py # Modelli Pydantic
│ └── data/ # Dataset JSON (default, test) │ └── data/
└── frontend/ # React + TypeScript + Vite + Tailwind │ ├── users_default.json
│ └── users_test.json
└── frontend/
├── package.json
├── vite.config.ts
└── src/ └── src/
├── App.tsx # State machine principale ├── App.tsx # State machine principale
├── hooks/ # Custom hooks (RFID scanner) ├── hooks/ # useRFIDScanner
├── components/ # UI components ├── components/ # UI components
├── screens/ # Schermate complete ├── screens/ # Schermate
├── services/ # API layer ├── services/ # API client
── types/ # TypeScript definitions ── types/ # TypeScript types
└── tests/ # Test automatici use case
``` ```
--- ---
## 🔧 Stack Tecnologico ## Funzionalità Principali
### Backend Mock ### Flusso Utente
- **Python 3.10+**
- **FastAPI** - Framework web asincrono
- **Uvicorn** - ASGI server
- **Pydantic** - Validazione dati
- **pipenv** - Gestione ambiente virtuale
- **argparse** - Parametri CLI (porta, dataset)
### Frontend 1. **Login Validatore**: Passa badge + inserisci password → Sessione 30 min
- **React 19** - UI Library 2. **Attesa Partecipante**: Schermata grande "Passa il badge"
- **TypeScript 5.9** - Type safety 3. **Visualizzazione Utente**: Card con foto, nome, ruolo, stato ammissione
- **Vite 7** - Build tool 4. **Conferma Ingresso**: Validatore ripassa il badge → Carosello benvenuto multilingua
- **Tailwind CSS 4** - Styling utility-first
- **React Router** - Routing (/, /debug) ### Gestione RFID
- **Vitest** - Testing framework
- **Design:** Ottimizzato per touch tablet - **Multi-pattern**: Supporta layout tastiera US (`;?`) e IT (`ò_`)
- **Timeout 2.5s**: Per scansioni accidentali
- **ESC annulla**: Scansione in corso
- **Enter handling**: Gestito automaticamente
### Sicurezza Sessioni
- Sessione salvata in localStorage
- **Invalidazione automatica** se il server riparte
- Timeout 30 minuti
--- ---
## 🎨 Design System ## Comandi Rapidi
| Colore | Hex | Uso |
|--------|-----|-----|
| Blu Istituzionale | `#0072CE` | Colore primario, brand |
| Arancio Accento | `#F5A623` | Azioni secondarie |
| Giallo | `#FFD700` | Evidenze |
| Verde Success | `#22C55E` | Conferme, ammesso |
| Rosso Error | `#EF4444` | Errori, non ammesso |
---
## 📟 Logica RFID (CRITICA)
Il lettore RFID simula una tastiera. **Non possiamo distinguerlo dalla digitazione umana** in base alla velocità.
### Protocollo
- **Formato:** `<start_sentinel><codice><end_sentinel>`
- **Esempio US:** `;00012345?`
- **Esempio IT:** `ò00012345_`
### Pattern Supportati
```typescript
const VALID_PATTERNS = [
{ start: ';', end: '?' }, // Layout US
{ start: 'ò', end: '_' }, // Layout IT
];
```
### Strategia
1. Ascolto globale `keydown`
2. Se ricevo un carattere `start` → avvio buffer + timeout (2.5s)
3. Accumulo caratteri nel buffer
4. Se ricevo il corretto `end` → emetto codice pulito
5. Se timeout scade → scarto buffer
---
## 🔄 State Machine Applicativa
```
LOADING → WAITING_VALIDATOR → [badge validatore]
→ VALIDATOR_PASSWORD → [password OK]
→ GATE_ACTIVE → [badge partecipante]
→ SHOWING_USER → [badge validatore]
→ SUCCESS_MODAL (5s, carosello multilingua) → GATE_ACTIVE
```
---
## 🌍 Messaggi Benvenuto (Frontend)
Il backend risponde in modo **asettico** (solo `success: true`).
Il frontend mostra un **carosello automatico** di messaggi multilingua:
- Italiano, English, Français, Deutsch, Español, Português, 中文, 日本語
Scorrimento ogni ~2 secondi, durata modale 5 secondi.
---
## 📁 File Chiave
| File | Descrizione |
|------|-------------|
| `backend-mock/main.py` | Entry point con argparse |
| `backend-mock/api/routes.py` | Definizione endpoint |
| `backend-mock/schemas/models.py` | Modelli Pydantic |
| `backend-mock/data/*.json` | Dataset utenti |
| `frontend/src/hooks/useRFIDScanner.ts` | Cuore del sistema - gestione lettore |
| `frontend/src/App.tsx` | State machine e orchestrazione |
| `frontend/src/components/WelcomeCarousel.tsx` | Carosello multilingua |
| `frontend/src/screens/DebugScreen.tsx` | Diagnostica RFID |
| `frontend/src/tests/` | Test automatici use case |
---
## 🚀 Quick Start
### Backend
```bash ```bash
cd backend-mock # Setup iniziale
pipenv install ./dev.sh install
pipenv run python main.py # Default
pipenv run python main.py -p 9000 # Porta custom
pipenv run python main.py -d data/test.json # Dataset test
```
### Frontend # Sviluppo (hot reload)
```bash ./dev.sh dev
cd frontend # Frontend: http://localhost:5173
npm install # Backend: http://localhost:8000
npm run dev # Sviluppo
npm run test # Test suite # Produzione locale
npm run test:ui # Test con UI ./dev.sh server
# App completa: http://localhost:8000
# Debug RFID
# Vai a http://localhost:8000/debug
``` ```
--- ---
## 🧪 Test Automatici ## Badge di Test
Suite di test per validazione use case: | Badge | Nome | Ruolo | Ammesso |
|--------------|----------------|---------|---------------|
| `0008988288` | Marco Bianchi | Votante | ✅ |
| `0007399575` | Laura Rossi | Votante | ✅ |
| `0000514162` | Giuseppe Verdi | Tecnico | ❌ |
| `0006478281` | - | - | ⚠️ Non nel DB |
| Test | Descrizione | **Password validatore:** `focolari`
|------|-------------|
| UC01 | Login validatore |
| UC02 | Accesso partecipante ammesso |
| UC03 | Accesso negato |
| UC04 | Timeout sessione |
| UC05 | Cambio rapido badge |
| UC06 | Pattern RFID multipli |
--- ---
## 🔍 Debug ## Dove Trovare Cosa
### Console Browser | Cosa cerchi | Dove guardare |
Tutti i log sono prefissati: |----------------------------|----------------------------------------|
- `[RFID]` - Eventi lettore badge | Logica flusso applicazione | `frontend/src/App.tsx` |
- `[FLOW]` - Transizioni stato | Hook lettura RFID | `frontend/src/hooks/useRFIDScanner.ts` |
- `[API]` - Chiamate HTTP | Chiamate API | `frontend/src/services/api.ts` |
| Endpoint backend | `backend-mock/api/routes.py` |
### Pagina Debug (`/debug`) | Dati mock utenti | `backend-mock/data/users_default.json` |
Accesso: logo cliccabile 5 volte. | Configurazione Vite | `frontend/vite.config.ts` |
Mostra in tempo reale:
- Ultimi 20 tasti premuti
- Stato scanner (idle/scanning)
- Buffer corrente
- Pattern attivo
--- ---
## ⚠️ Note Importanti ## Note Importanti
1. **Badge Validatore:** `999999` con password `focolari` 1. **Layout Tastiera**: Il lettore RFID potrebbe inviare caratteri diversi in base al layout OS. La pagina `/debug`
2. **Sessione:** 30 minuti di timeout, salvata in localStorage aiuta a diagnosticare.
3. **Timeout utente:** 60 secondi sulla schermata decisione
4. **Multi-layout:** Il sistema supporta RFID su tastiera US e IT 2. **Server Restart**: Quando il server riparte, tutte le sessioni frontend vengono invalidate (controllato via
5. **Backend asettico:** Nessun messaggio multilingua dal server `server_start_time`).
6. **Git:** Il progetto sfrutta un Repository per versionare e sviluppare, ma solo l'utente può eseguire comandi git, eccetto se richiesto diversamente, l'agente può solo chiedere di eseguire o suggerire cosa fare, ma mai prendere iniziative
3. **Badge Validatore**: Qualsiasi badge può diventare validatore se la password è corretta. Il badge viene memorizzato
nella sessione.
4. **NumLock**: Su desktop, viene mostrato un banner per ricordare di attivare NumLock.
--- ---
## 📚 Documentazione Correlata ## TODO (da concordare con committenti)
- `01-backend-plan.md` - Piano sviluppo backend - [ ] Verificare se il badge validatore debba essere validato anche lato server
- `02-frontend-plan.md` - Piano sviluppo frontend - [ ] Test automatici E2E per regression detection

View File

@@ -13,7 +13,6 @@ Struttura modulare con separazione tra API, modelli e dati.
backend-mock/ backend-mock/
├── main.py # Entry point con argparse ├── main.py # Entry point con argparse
├── Pipfile # Dipendenze pipenv ├── Pipfile # Dipendenze pipenv
├── requirements.txt # Backup dipendenze
├── .gitignore ├── .gitignore
├── api/ ├── api/
│ ├── __init__.py │ ├── __init__.py
@@ -22,7 +21,7 @@ backend-mock/
│ ├── __init__.py │ ├── __init__.py
│ └── models.py # Modelli Pydantic │ └── models.py # Modelli Pydantic
└── data/ └── data/
├── users_default.json # Dataset utenti default ├── users_default.json # Dataset utenti default (badge reali)
└── users_test.json # Dataset per test └── users_test.json # Dataset per test
``` ```
@@ -35,36 +34,40 @@ backend-mock/
- [x] Creare cartella `backend-mock/` - [x] Creare cartella `backend-mock/`
- [x] Creare `Pipfile` per pipenv - [x] Creare `Pipfile` per pipenv
- [x] Configurare `.gitignore` per Python - [x] Configurare `.gitignore` per Python
- [ ] Creare struttura cartelle (`api/`, `schemas/`, `data/`) - [x] Creare struttura cartelle (`api/`, `schemas/`, `data/`)
### 2. Modelli Pydantic (`schemas/models.py`) ### 2. Modelli Pydantic (`schemas/models.py`)
- [x] `LoginRequest` - badge + password - [x] `LoginRequest` - badge + password
- [x] `EntryRequest` - user_badge + validator_password - [x] `EntryRequest` - user_badge + validator_password
- [x] `UserResponse` - dati utente + warning opzionale - [x] `UserResponse` - dati utente + warning opzionale
- [x] `RoomInfoResponse` - nome sala + meeting_id - [x] `RoomInfoResponse` - nome sala + meeting_id + **server_start_time**
- [x] `LoginResponse` - success + message + token - [x] `LoginResponse` - success + message + token
- [x] `EntryResponse` - success + message (SENZA welcome_message) - [x] `EntryResponse` - success + message (SENZA welcome_message)
- [ ] **DA FARE:** Spostare modelli in file dedicato - [x] Spostare modelli in file dedicato
### 3. Dati Mock (`data/*.json`) ### 3. Dati Mock (`data/*.json`)
- [x] Costanti validatore (badge `999999`, password `focolari`) - [x] Password validatore (solo password, badge gestito dal frontend)
- [x] Lista utenti mock (7 utenti con dati realistici) - [x] Lista utenti mock con **badge reali**:
- [x] Mix di ruoli: Votante, Tecnico, Ospite - `0008988288` - Marco Bianchi (Votante, ammesso)
- [x] Alcuni con `ammesso: false` per test - `0007399575` - Laura Rossi (Votante, ammessa)
- [x] URL foto placeholder (randomuser.me) - `0000514162` - Giuseppe Verdi (Tecnico, NON ammesso)
- [ ] **DA FARE:** Estrarre dati in file JSON separato - `0006478281` - **NON nel DB** (per test "non trovato")
- [ ] **DA FARE:** Creare dataset alternativo per test - [x] Estrarre dati in file JSON separato
- [ ] **DA FARE:** Caricare dati dinamicamente all'avvio - [x] Creare dataset alternativo per test (`users_test.json`)
- [x] Caricare dati dinamicamente all'avvio
**Nota:** I messaggi di benvenuto multilingua sono stati **RIMOSSI** dal backend. **Nota:** I messaggi di benvenuto multilingua sono gestiti dal frontend con carosello.
Il frontend gestirà autonomamente la visualizzazione internazionale con carosello.
**TODO (da concordare con committenti):**
- Valutare se `login-validate` debba ricevere e verificare anche il badge del validatore
### 4. Routes API (`api/routes.py`) ### 4. Routes API (`api/routes.py`)
- [x] `GET /info-room` - info sala - [x] `GET /info-room` - info sala + **server_start_time** per invalidare sessioni
- [x] `POST /login-validate` - autenticazione validatore - [x] `POST /login-validate` - verifica solo password validatore
- [x] `GET /anagrafica/{badge_code}` - ricerca utente - [x] `GET /anagrafica/{badge_code}` - ricerca utente
- [x] Pulizia caratteri sentinel dal badge - [x] Pulizia caratteri sentinel dal badge
- [x] Confronto con e senza zeri iniziali - [x] Confronto con e senza zeri iniziali
@@ -73,27 +76,25 @@ Il frontend gestirà autonomamente la visualizzazione internazionale con carosel
- [x] Verifica password validatore - [x] Verifica password validatore
- [x] Verifica utente ammesso - [x] Verifica utente ammesso
- [x] Risposta asettica (solo success + message) - [x] Risposta asettica (solo success + message)
- [ ] **DA FARE:** Spostare routes in file dedicato - [x] Spostare routes in file dedicato
### 5. Entry Point (`main.py`) ### 5. Entry Point (`main.py`)
- [x] Blocco `if __name__ == "__main__"` - [x] Blocco `if __name__ == "__main__"`
- [x] Configurazione uvicorn (host, port) - [x] Configurazione uvicorn (host, port)
- [x] Messaggi console all'avvio - [x] Messaggi console all'avvio
- [ ] **DA FARE:** Implementare argparse con opzioni: - [x] Implementare argparse con opzioni:
- `--port` / `-p` : porta server (default: 8000) - `--port` / `-p` : porta server (default: 8000)
- `--data` / `-d` : path file JSON dati (default: `data/users_default.json`) - `--data` / `-d` : path file JSON dati (default: `data/users_default.json`)
- `--host` : host binding (default: `0.0.0.0`) - `--host` : host binding (default: `0.0.0.0`)
- [ ] **DA FARE:** Caricamento dinamico dati da JSON - [x] Caricamento dinamico dati da JSON
- [ ] **DA FARE:** Import routes da modulo `api` - [x] Import routes da modulo `api`
### 6. Struttura Base FastAPI ### 6. Invalidazione Sessioni
- [x] Import FastAPI e dipendenze - [x] `SERVER_START_TIME` generato all'avvio del server
- [x] Configurazione app con titolo e descrizione - [x] Restituito in `/info-room` per permettere al frontend di invalidare sessioni vecchie
- [x] Middleware CORS (allow all origins) - [x] Se il server riparte, tutte le sessioni frontend vengono invalidate
- [x] Endpoint root `/` per health check
- [ ] **DA FARE:** Refactor in struttura modulare
--- ---
@@ -101,19 +102,16 @@ Il frontend gestirà autonomamente la visualizzazione internazionale con carosel
```json ```json
{ {
"validator": { "validator_password": "focolari",
"badge": "999999",
"password": "focolari"
},
"room": { "room": {
"room_name": "Sala Assemblea", "room_name": "Sala Assemblea",
"meeting_id": "VOT-2024" "meeting_id": "VOT-2024"
}, },
"users": [ "users": [
{ {
"badge_code": "000001", "badge_code": "0008988288",
"nome": "Maria", "nome": "Marco",
"cognome": "Rossi", "cognome": "Bianchi",
"url_foto": "https://...", "url_foto": "https://...",
"ruolo": "Votante", "ruolo": "Votante",
"ammesso": true "ammesso": true
@@ -126,25 +124,22 @@ Il frontend gestirà autonomamente la visualizzazione internazionale con carosel
## Comandi Esecuzione ## Comandi Esecuzione
### Avvio Standard ### Via Script (consigliato)
```bash
./dev.sh server # Avvia server con frontend
./dev.sh server -p 9000 # Porta custom
./dev.sh backend # Solo API, no frontend
```
### Manuale
```bash ```bash
cd backend-mock cd backend-mock
pipenv install pipenv install
pipenv run python main.py pipenv run python main.py
``` ```
### Avvio con Parametri Custom
```bash
# Porta diversa
pipenv run python main.py --port 9000
# Dataset test
pipenv run python main.py --data data/users_test.json
# Combinato
pipenv run python main.py -p 9000 -d data/users_test.json
```
--- ---
## Test Rapidi ## Test Rapidi
@@ -153,29 +148,28 @@ pipenv run python main.py -p 9000 -d data/users_test.json
# Health check # Health check
curl http://localhost:8000/ curl http://localhost:8000/
# Info sala # Info sala (include server_start_time)
curl http://localhost:8000/info-room curl http://localhost:8000/info-room
# Ricerca utente # Ricerca utente reale
curl http://localhost:8000/anagrafica/000001 curl http://localhost:8000/anagrafica/0008988288
# Badge non trovato
curl http://localhost:8000/anagrafica/0006478281
# Login validatore # Login validatore
curl -X POST http://localhost:8000/login-validate \ curl -X POST http://localhost:8000/login-validate \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"badge": "999999", "password": "focolari"}' -d '{"badge": "qualsiasi", "password": "focolari"}'
# Richiesta ingresso (risposta asettica) # Richiesta ingresso
curl -X POST http://localhost:8000/entry-request \ curl -X POST http://localhost:8000/entry-request \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"user_badge": "000001", "validator_password": "focolari"}' -d '{"user_badge": "0008988288", "validator_password": "focolari"}'
``` ```
--- ---
## Note Implementative ## ✅ BACKEND COMPLETATO
- Gli endpoint puliscono automaticamente i caratteri `;`, `?`, `ò`, `_` dai badge Tutti i task sono stati implementati e testati.
- Il confronto badge avviene sia con che senza zeri iniziali
- Le risposte seguono lo standard HTTP (200 OK, 401 Unauthorized, 404 Not Found, 403 Forbidden)
- La documentazione OpenAPI è auto-generata su `/docs`
- **Risposta `/entry-request`:** JSON asettico `{ success: true, message: "..." }` senza messaggi multilingua

View File

@@ -3,7 +3,7 @@
## Obiettivo ## Obiettivo
Applicazione React per tablet che gestisce il flusso di accesso ai varchi votazione, con lettura badge RFID. Applicazione React per tablet che gestisce il flusso di accesso ai varchi votazione, con lettura badge RFID.
Include sistema di test automatici per validazione e regression detection. Ottimizzata per tablet in orizzontale.
--- ---
@@ -14,8 +14,11 @@ Include sistema di test automatici per validazione e regression detection.
- [x] Inizializzare Vite + React + TypeScript - [x] Inizializzare Vite + React + TypeScript
- [x] Installare Tailwind CSS 4 - [x] Installare Tailwind CSS 4
- [x] Configurare `tsconfig.json` - [x] Configurare `tsconfig.json`
- [x] Configurare `vite.config.ts` - [x] Configurare `vite.config.ts` con proxy API
- [x] Configurare `.gitignore` - [x] Configurare `.gitignore`
- [x] Path relativi per build (base: './')
- [x] Output build in `frontend/dist/`
- [x] Favicon con logo Focolari
### 2. Design System ### 2. Design System
@@ -29,15 +32,14 @@ Include sistema di test automatici per validazione e regression detection.
### 3. Tipi TypeScript (`types/index.ts`) ### 3. Tipi TypeScript (`types/index.ts`)
- [x] `RoomInfo` - info sala - [x] `RoomInfo` - info sala + **server_start_time**
- [x] `User` - dati utente - [x] `User` - dati utente
- [x] `LoginRequest/Response` - [x] `LoginRequest/Response`
- [x] `EntryRequest/Response` (SENZA welcome_message) - [x] `EntryRequest/Response` (SENZA welcome_message)
- [x] `AppState` - stati applicazione - [x] `AppState` - stati applicazione
- [x] `ValidatorSession` - sessione validatore - [x] `ValidatorSession` - sessione validatore + **serverStartTime**
- [x] `RFIDScannerState` - stato scanner - [x] `RFIDScannerState` - stato scanner
- [x] `RFIDScanResult` - risultato scan - [x] `RFIDScanResult` - risultato scan
- [ ] **DA FARE:** Aggiornare `EntryResponse` rimuovendo `welcome_message`
### 4. API Service (`services/api.ts`) ### 4. API Service (`services/api.ts`)
@@ -47,259 +49,90 @@ Include sistema di test automatici per validazione e regression detection.
- [x] `loginValidator()` - POST /login-validate - [x] `loginValidator()` - POST /login-validate
- [x] `getUserByBadge()` - GET /anagrafica/{badge} - [x] `getUserByBadge()` - GET /anagrafica/{badge}
- [x] `requestEntry()` - POST /entry-request - [x] `requestEntry()` - POST /entry-request
- [ ] **DA FARE:** Logging con prefisso `[API]` - [x] Logging con prefisso `[API]`
- [x] Path relativi (proxy Vite in dev, stesso server in prod)
### 5. Hook RFID Scanner (`hooks/useRFIDScanner.ts`) ### 5. Hook RFID Scanner (`hooks/useRFIDScanner.ts`)
#### Implementazione Base (v1) - [x] Supporto pattern multipli (US: `;?`, IT: `ò_`)
- [x] Rilevamento automatico pattern in uso
- [x] Listener `keydown` globale - [x] Gestione Enter post-completamento
- [x] Stati: idle → scanning → idle - [x] Timeout 2.5s per scansioni accidentali
- [x] Singolo pattern (`;` / `?`) - [x] ESC annulla scansione in corso
- [x] Timeout sicurezza (3s) - [x] Logging avanzato con prefisso `[RFID]`
- [x] `preventDefault` durante scan - [x] Export `keyLog` per debug
- [x] Callback `onScan`, `onTimeout`, `onScanStart`
#### ⚠️ AGGIORNAMENTO RICHIESTO (v2) - Multi-Pattern
- [ ] Supporto pattern multipli (US, IT, altri)
```typescript
const VALID_PATTERNS = [
{ start: ';', end: '?' }, // Layout US
{ start: 'ò', end: '_' }, // Layout IT
];
```
- [ ] Rilevamento automatico pattern in uso
- [ ] Memorizzazione pattern attivo durante scan
- [ ] Validazione `end` solo per pattern corretto
- [ ] Logging avanzato con prefisso `[RFID]`
- [ ] Esportare info pattern attivo per debug page
### 6. Componenti UI (`components/`) ### 6. Componenti UI (`components/`)
- [x] `Logo.tsx` - logo Focolari - [x] `Logo.tsx` - logo Focolari
- [x] `Button.tsx` - varianti primary/secondary/danger - [x] `Button.tsx` - varianti primary/secondary/danger
- [x] `Input.tsx` - campo input styled - [x] `Input.tsx` - campo input styled + **toggle password visibility**
- [x] `Modal.tsx` - modale base - [x] `Modal.tsx` - modale base
- [x] `RFIDStatus.tsx` - indicatore stato scanner - [x] `RFIDStatus.tsx` - indicatore stato scanner
- [x] `UserCard.tsx` - card utente con foto e ruolo - [x] `UserCard.tsx` - card utente con foto e ruolo
- [x] `CountdownTimer.tsx` - timer con progress bar - [x] `CountdownTimer.tsx` - timer con progress bar
- [x] `index.ts` - barrel export - [x] `WelcomeCarousel.tsx` - carosello messaggi multilingua
- [ ] **DA FARE:** `WelcomeCarousel.tsx` - carosello messaggi multilingua - [x] `NumLockBanner.tsx` - avviso NumLock per desktop
### 7. Schermate (`screens/`) ### 7. Schermate (`screens/`)
- [x] `LoadingScreen.tsx` - caricamento iniziale - [x] `LoadingScreen.tsx` - caricamento iniziale + ping automatico
- [x] `ValidatorLoginScreen.tsx` - attesa badge + password - [x] `ValidatorLoginScreen.tsx` - attesa badge + password + NumLockBanner
- [x] `ActiveGateScreen.tsx` - varco attivo + scheda utente - [x] `ActiveGateScreen.tsx` - varco attivo:
- [x] `SuccessModal.tsx` - conferma ingresso fullscreen - [x] Card utente (layout largo per tablet)
- [x] **Schermata "badge non trovato"** con countdown 30s
- [x] **Notifica badge validatore ignorato**
- [x] NumLockBanner
- [x] `SuccessModal.tsx` - conferma ingresso con carosello
- [x] `ErrorModal.tsx` - errore fullscreen - [x] `ErrorModal.tsx` - errore fullscreen
- [x] `index.ts` - barrel export - [x] `DebugScreen.tsx` - pagina diagnostica RFID
- [ ] **DA FARE:** `DebugScreen.tsx` - pagina diagnostica RFID
### 8. State Machine (`App.tsx`) ### 8. State Machine (`App.tsx`)
- [x] Stati applicazione gestiti - [x] Stati applicazione gestiti
- [x] Integrazione `useRFIDScanner` - [x] Integrazione `useRFIDScanner`
- [x] Gestione sessione validatore (localStorage) - [x] Gestione sessione validatore (localStorage)
- [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] Timeout sessione 30 minuti - [x] Timeout sessione 30 minuti
- [x] Timeout utente 60 secondi - [x] Timeout utente 60 secondi
- [x] **Timeout badge non trovato 30 secondi**
- [x] Cambio rapido badge partecipante - [x] Cambio rapido badge partecipante
- [x] Conferma con badge validatore - [x] Conferma con badge validatore (quello della sessione)
- [ ] **DA FARE:** Logging transizioni con prefisso `[FLOW]` - [x] **Notifica se badge validatore rippassato senza utente**
- [x] Logging transizioni con prefisso `[FLOW]`
### 9. Modale Successo - Carosello Internazionale ### 9. Modale Successo - Carosello Internazionale
#### ⚠️ MODIFICA RICHIESTA - [x] Componente `WelcomeCarousel.tsx`
- [x] 10 lingue supportate
Il backend **NON** restituisce più messaggi multilingua. - [x] Scorrimento automatico ogni 800ms
Il frontend gestisce autonomamente la visualizzazione con un **carosello automatico**. - [x] Modale fullscreen verde
- [x] Durata totale: 5 secondi
**Specifiche:**
- [ ] Creare componente `WelcomeCarousel.tsx`
- [ ] Lista messaggi di benvenuto in diverse lingue:
```typescript
const WELCOME_MESSAGES = [
{ lang: 'it', text: 'Benvenuto!' },
{ lang: 'en', text: 'Welcome!' },
{ lang: 'fr', text: 'Bienvenue!' },
{ lang: 'de', text: 'Willkommen!' },
{ lang: 'es', text: 'Bienvenido!' },
{ lang: 'pt', text: 'Bem-vindo!' },
{ lang: 'zh', text: '欢迎!' },
{ lang: 'ja', text: 'ようこそ!' },
];
```
- [ ] Scorrimento automatico (es. ogni 2 secondi)
- [ ] Animazione transizione fluida (fade o slide)
- [ ] Modale fullscreen verde con carosello al centro
- [ ] Durata totale modale: 5 secondi
### 10. Debug & Diagnostica ### 10. Debug & Diagnostica
#### ⚠️ DA IMPLEMENTARE - [x] Pagina `/debug` dedicata
- [x] Logging console strutturato `[RFID]`, `[FLOW]`, `[API]`
- [ ] Pagina `/debug` dedicata
- [ ] Log ultimi 20 tasti premuti (key + code)
- [ ] Stato scanner real-time (idle/scanning)
- [ ] Buffer corrente
- [ ] Pattern attivo (US/IT/...)
- [ ] Ultimo codice rilevato
- [ ] Timestamp eventi
- [ ] Link/pulsante nascosto per accesso debug (es. logo cliccabile 5 volte)
- [ ] Logging console strutturato:
- [ ] `[RFID]` - eventi scanner
- [ ] `[FLOW]` - transizioni stato
- [ ] `[API]` - chiamate HTTP
### 11. Routing ### 11. Routing
- [ ] Installare React Router - [x] React Router
- [ ] Route principale `/` - [x] Route principale `/`
- [ ] Route debug `/debug` - [x] Route debug `/debug`
### 12. Test Automatici (E2E / Use Case Validation)
#### ⚠️ NUOVA SEZIONE
Sistema di test per validazione formale dei flussi e regression detection.
**Struttura:**
```
frontend/
├── src/
│ └── tests/
│ ├── usecase/
│ │ ├── UC01_ValidatorLogin.test.ts
│ │ ├── UC02_ParticipantAccess.test.ts
│ │ ├── UC03_DeniedAccess.test.ts
│ │ ├── UC04_SessionTimeout.test.ts
│ │ ├── UC05_QuickBadgeSwitch.test.ts
│ │ └── UC06_RFIDMultiPattern.test.ts
│ └── helpers/
│ ├── mockRFID.ts # Simulatore eventi tastiera RFID
│ └── testUtils.ts # Utility comuni
```
**Use Case da Testare:**
- [ ] **UC01 - Login Validatore**
- Simula badge validatore (`;999999?`)
- Inserisce password corretta
- Verifica transizione a stato `gate-active`
- Verifica sessione salvata in localStorage
- [ ] **UC02 - Accesso Partecipante Ammesso**
- Da stato `gate-active`
- Simula badge partecipante ammesso
- Verifica caricamento dati utente
- Simula badge validatore per conferma
- Verifica modale successo con carosello
- Verifica ritorno a `gate-active`
- [ ] **UC03 - Accesso Negato**
- Simula badge partecipante NON ammesso
- Verifica visualizzazione warning rosso
- Verifica che conferma validatore sia bloccata
- Verifica pulsante annulla funzionante
- [ ] **UC04 - Timeout Sessione**
- Verifica scadenza sessione 30 minuti
- Verifica redirect a login validatore
- Verifica pulizia localStorage
- [ ] **UC05 - Cambio Rapido Badge**
- Con utente a schermo
- Simula nuovo badge partecipante
- Verifica sostituzione immediata dati
- [ ] **UC06 - Pattern RFID Multipli**
- Testa pattern US (`;` / `?`)
- Testa pattern IT (`ò` / `_`)
- Verifica stesso risultato finale
**Dipendenze Test:**
```bash
npm install -D vitest @testing-library/react @testing-library/user-event jsdom
```
**Script npm:**
```json
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage"
}
}
```
--- ---
## Correzioni Necessarie ## Badge di Test
### Hook RFID - Da Aggiornare | Badge | Nome | Ruolo | Ammesso |
|--------------|----------------|---------|---------------|
Il file `useRFIDScanner.ts` attualmente supporta **solo** il pattern US (`;` / `?`). | `0008988288` | Marco Bianchi | Votante | ✅ Sì |
| `0007399575` | Laura Rossi | Votante | ✅ Sì |
**Modifiche richieste:** | `0000514162` | Giuseppe Verdi | Tecnico | ❌ No |
| `0006478281` | - | - | ⚠️ Non nel DB |
1. Aggiungere costante `VALID_PATTERNS` con pattern multipli
2. Modificare logica `handleKeyDown` per:
- Riconoscere qualsiasi `start` sentinel
- Memorizzare quale pattern è in uso
- Validare solo con l'`end` corrispondente
3. Aggiungere stato `activePattern` per debug
4. Migliorare logging
### Success Modal - Da Aggiornare
Attualmente usa `welcome_message` dal backend.
**Modifiche richieste:**
1. Rimuovere dipendenza da `welcome_message` API
2. Implementare `WelcomeCarousel` con messaggi hardcoded
3. Carosello auto-scroll ogni 2 secondi
4. Animazioni fluide tra messaggi
### Logging Console
Attualmente i log usano messaggi generici. Aggiornare tutti i `console.log` con prefissi standardizzati.
--- ---
## Dipendenze da Aggiungere ## ✅ FRONTEND COMPLETATO
```bash
# Routing
npm install react-router-dom
# Testing
npm install -D vitest @testing-library/react @testing-library/user-event jsdom @vitest/ui @vitest/coverage-v8
```
---
## Comandi Esecuzione
```bash
cd frontend
npm install
npm run dev # Sviluppo
npm run build # Build produzione
npm run test # Test suite
npm run test:ui # Test con UI interattiva
npm run test:coverage # Coverage report
```
---
## Note UI/UX
- Font grandi per leggibilità tablet
- Pulsanti touch-friendly (min 48px)
- Feedback visivo immediato su azioni
- Animazioni fluide ma non invasive
- Supporto landscape e portrait
- Carosello benvenuto: transizioni smooth, leggibilità massima

View File

@@ -28,3 +28,5 @@ Thumbs.db
# Local environment # Local environment
.env .env
.env.local .env.local

View File

@@ -4,14 +4,14 @@ verify_ssl = true
name = "pypi" name = "pypi"
[packages] [packages]
fastapi = ">=0.109.0" fastapi = "*"
uvicorn = {extras = ["standard"], version = ">=0.27.0"} uvicorn = "*"
pydantic = ">=2.5.0" pydantic = "*"
[dev-packages] [dev-packages]
[requires] [requires]
python_version = "3.10" python_version = "3.14"
[scripts] [scripts]
start = "python main.py" start = "python main.py"

View File

@@ -0,0 +1,7 @@
"""
Focolari Voting System - API Package
"""
from .routes import router, init_data
__all__ = ["router", "init_data"]

153
backend-mock/api/routes.py Normal file
View File

@@ -0,0 +1,153 @@
"""
Focolari Voting System - API Routes
"""
import time
from fastapi import APIRouter, HTTPException
from schemas import (
LoginRequest,
EntryRequest,
UserResponse,
RoomInfoResponse,
LoginResponse,
EntryResponse,
)
router = APIRouter()
# Timestamp di avvio del server (per invalidare sessioni frontend)
SERVER_START_TIME = int(time.time() * 1000)
# Dati caricati dinamicamente dal main
_data = {
"validator_password": "",
"room": {"room_name": "", "meeting_id": ""},
"users": []
}
# Caratteri sentinel da pulire (tutti i layout supportati)
SENTINEL_CHARS = [";", "?", "ò", "_", "ç", "+"]
def init_data(data: dict):
"""Inizializza i dati caricati dal file JSON"""
global _data
_data = data
def clean_badge(badge: str) -> str:
"""Rimuove i caratteri sentinel dal badge"""
clean = badge.strip()
for char in SENTINEL_CHARS:
clean = clean.replace(char, "")
return clean
def find_user(badge_code: str) -> dict | None:
"""Cerca un utente per badge code"""
clean = clean_badge(badge_code)
for user in _data["users"]:
if user["badge_code"] == clean or user["badge_code"].lstrip("0") == clean.lstrip("0"):
return user
return None
@router.get("/info-room", response_model=RoomInfoResponse)
async def get_room_info():
"""Restituisce le informazioni sulla sala e la riunione corrente."""
return RoomInfoResponse(
room_name=_data["room"]["room_name"],
meeting_id=_data["room"]["meeting_id"],
server_start_time=SERVER_START_TIME
)
@router.post("/login-validate", response_model=LoginResponse)
async def login_validate(request: LoginRequest):
"""
Valida la password del validatore.
Il badge viene passato dal frontend ma attualmente non viene verificato
lato server - serve solo per essere memorizzato nella sessione frontend.
TODO: Concordare con committenti se il badge debba essere verificato
anche lato server (es. lista badge validatori autorizzati).
"""
if request.password != _data["validator_password"]:
raise HTTPException(
status_code=401,
detail="Password non corretta"
)
# Il badge viene restituito per conferma, ma non è validato lato server
clean = clean_badge(request.badge) if request.badge else "unknown"
return LoginResponse(
success=True,
message="Login validatore effettuato con successo",
token=f"focolare-token-{clean}"
)
@router.get("/anagrafica/{badge_code}", response_model=UserResponse)
async def get_user_anagrafica(badge_code: str):
"""
Cerca un utente tramite il suo badge code.
"""
user = find_user(badge_code)
if not user:
clean = clean_badge(badge_code)
raise HTTPException(
status_code=404,
detail=f"Utente con badge {clean} non trovato nel sistema"
)
response = UserResponse(
badge_code=user["badge_code"],
nome=user["nome"],
cognome=user["cognome"],
url_foto=user["url_foto"],
ruolo=user["ruolo"],
ammesso=user["ammesso"]
)
if not user["ammesso"]:
response.warning = "ATTENZIONE: Questo utente NON è autorizzato all'ingresso!"
return response
@router.post("/entry-request", response_model=EntryResponse)
async def process_entry_request(request: EntryRequest):
"""
Processa una richiesta di ingresso.
Risposta asettica senza messaggi multilingua.
"""
if request.validator_password != _data["validator_password"]:
raise HTTPException(
status_code=401,
detail="Password validatore non corretta"
)
user = find_user(request.user_badge)
if not user:
clean = clean_badge(request.user_badge)
raise HTTPException(
status_code=404,
detail=f"Utente con badge {clean} non trovato"
)
if not user["ammesso"]:
raise HTTPException(
status_code=403,
detail=f"L'utente {user['nome']} {user['cognome']} NON è autorizzato all'ingresso"
)
return EntryResponse(
success=True,
message=f"Ingresso registrato per {user['nome']} {user['cognome']}"
)

View File

@@ -0,0 +1,33 @@
{
"validator_password": "focolari",
"room": {
"room_name": "Sala Assemblea",
"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",
"cognome": "Rossi",
"url_foto": "https://randomuser.me/api/portraits/women/2.jpg",
"ruolo": "Votante",
"ammesso": true
},
{
"badge_code": "0000514162",
"nome": "Giuseppe",
"cognome": "Verdi",
"url_foto": "https://randomuser.me/api/portraits/men/3.jpg",
"ruolo": "Tecnico",
"ammesso": false
}
]
}

View File

@@ -0,0 +1,33 @@
{
"validator_password": "test123",
"room": {
"room_name": "Sala Test",
"meeting_id": "TEST-001"
},
"users": [
{
"badge_code": "111111",
"nome": "Test",
"cognome": "Ammesso",
"url_foto": "https://randomuser.me/api/portraits/lego/1.jpg",
"ruolo": "Votante",
"ammesso": true
},
{
"badge_code": "222222",
"nome": "Test",
"cognome": "NonAmmesso",
"url_foto": "https://randomuser.me/api/portraits/lego/2.jpg",
"ruolo": "Ospite",
"ammesso": false
},
{
"badge_code": "333333",
"nome": "Test",
"cognome": "Tecnico",
"url_foto": "https://randomuser.me/api/portraits/lego/3.jpg",
"ruolo": "Tecnico",
"ammesso": true
}
]
}

View File

@@ -1,21 +1,72 @@
""" """
Focolari Voting System - Backend Mock Focolari Voting System - Backend Mock
Sistema di controllo accessi per votazioni del Movimento dei Focolari Sistema di controllo accessi per votazioni del Movimento dei Focolari
Utilizzo:
python main.py # Default: porta 8000, dati default
python main.py -p 9000 # Porta custom
python main.py -d data/users_test.json # Dataset custom
python main.py -p 9000 -d data/users_test.json
""" """
import random import argparse
from fastapi import FastAPI, HTTPException import json
from fastapi.middleware.cors import CORSMiddleware import sys
from pydantic import BaseModel from pathlib import Path
from typing import Optional
import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from api.routes import router, init_data
# Configurazione default
DEFAULT_PORT = 8000
DEFAULT_HOST = "0.0.0.0"
DEFAULT_DATA = "data/users_default.json"
STATIC_DIR = Path(__file__).parent.parent / "frontend" / "dist"
def load_data(data_path: str) -> dict:
"""Carica i dati dal file JSON"""
path = Path(data_path)
if not path.is_absolute():
# Path relativo alla directory del main.py
base_dir = Path(__file__).parent
path = base_dir / path
if not path.exists():
print(f"❌ Errore: File dati non trovato: {path}")
sys.exit(1)
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
print(f"📂 Dati caricati da: {path}")
print(f" - Password validatore: {'*' * len(data['validator_password'])}")
print(f" - Sala: {data['room']['room_name']}")
print(f" - Utenti: {len(data['users'])}")
return data
except json.JSONDecodeError as e:
print(f"❌ Errore parsing JSON: {e}")
sys.exit(1)
except KeyError as e:
print(f"❌ Errore struttura JSON: chiave mancante {e}")
sys.exit(1)
def create_app(data: dict, serve_frontend: bool = True) -> FastAPI:
"""Crea e configura l'applicazione FastAPI"""
app = FastAPI( app = FastAPI(
title="Focolari Voting System API", title="Focolari Voting System API",
description="Backend mock per il sistema di controllo accessi", description="Backend mock per il sistema di controllo accessi",
version="1.0.0" version="1.0.0"
) )
# CORS abilitato per tutti # CORS abilitato per tutti (utile in sviluppo)
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], allow_origins=["*"],
@@ -24,254 +75,117 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
# ============================================ # Inizializza i dati nelle routes
# MODELLI PYDANTIC init_data(data)
# ============================================
class LoginRequest(BaseModel): # Registra le routes API
badge: str app.include_router(router)
password: str
class EntryRequest(BaseModel): # Serve frontend statico se la cartella esiste
user_badge: str if serve_frontend and STATIC_DIR.exists():
validator_password: str print(f"🌐 Frontend statico servito da: {STATIC_DIR}")
class UserResponse(BaseModel): # Serve index.html per la root e tutte le route SPA
badge_code: str @app.get("/")
nome: str async def serve_index():
cognome: str return FileResponse(STATIC_DIR / "index.html")
url_foto: str
ruolo: str
ammesso: bool
warning: Optional[str] = None
class RoomInfoResponse(BaseModel): @app.get("/debug")
room_name: str async def serve_debug():
meeting_id: str return FileResponse(STATIC_DIR / "index.html")
class LoginResponse(BaseModel): # Monta i file statici (JS, CSS, assets) - DEVE essere dopo le route
success: bool app.mount("/assets", StaticFiles(directory=STATIC_DIR / "assets"), name="assets")
message: str
token: Optional[str] = None
class EntryResponse(BaseModel):
success: bool
message: str
welcome_message: Optional[str] = None
# ============================================
# DATI MOCK
# ============================================
# Credenziali Validatore
VALIDATOR_BADGE = "999999"
VALIDATOR_PASSWORD = "focolari"
MOCK_TOKEN = "focolare-validator-token-2024"
# Lista utenti mock
USERS_DB = [
{
"badge_code": "000001",
"nome": "Maria",
"cognome": "Rossi",
"url_foto": "https://randomuser.me/api/portraits/women/1.jpg",
"ruolo": "Votante",
"ammesso": True
},
{
"badge_code": "000002",
"nome": "Giuseppe",
"cognome": "Bianchi",
"url_foto": "https://randomuser.me/api/portraits/men/2.jpg",
"ruolo": "Votante",
"ammesso": True
},
{
"badge_code": "000003",
"nome": "Anna",
"cognome": "Verdi",
"url_foto": "https://randomuser.me/api/portraits/women/3.jpg",
"ruolo": "Tecnico",
"ammesso": True
},
{
"badge_code": "000004",
"nome": "Francesco",
"cognome": "Neri",
"url_foto": "https://randomuser.me/api/portraits/men/4.jpg",
"ruolo": "Ospite",
"ammesso": False # Non ammesso!
},
{
"badge_code": "000005",
"nome": "Lucia",
"cognome": "Gialli",
"url_foto": "https://randomuser.me/api/portraits/women/5.jpg",
"ruolo": "Votante",
"ammesso": True
},
{
"badge_code": "000006",
"nome": "Paolo",
"cognome": "Blu",
"url_foto": "https://randomuser.me/api/portraits/men/6.jpg",
"ruolo": "Votante",
"ammesso": False # Non ammesso!
},
{
"badge_code": "123456",
"nome": "Teresa",
"cognome": "Martini",
"url_foto": "https://randomuser.me/api/portraits/women/7.jpg",
"ruolo": "Votante",
"ammesso": True
},
]
# Messaggi di benvenuto multilingua
WELCOME_MESSAGES = [
"Benvenuto! / Welcome!",
"Bienvenue! / Willkommen!",
"Bienvenido! / Bem-vindo!",
"欢迎! / 歓迎!",
"Добро пожаловать! / مرحبا!"
]
# ============================================
# ENDPOINTS
# ============================================
# Fallback per altri file statici nella root (favicon, ecc.)
@app.get("/{filename:path}")
async def serve_static(filename: str):
file_path = STATIC_DIR / filename
if file_path.exists() and file_path.is_file():
return FileResponse(file_path)
# Per route SPA sconosciute, serve index.html
return FileResponse(STATIC_DIR / "index.html")
else:
# API-only mode
@app.get("/") @app.get("/")
async def root(): async def root():
"""Endpoint di test per verificare che il server sia attivo""" """Endpoint di test per verificare che il server sia attivo"""
return {"status": "ok", "message": "Focolari Voting System API is running"} return {
"status": "ok",
"message": "Focolari Voting System API is running",
"room": data["room"]["room_name"],
"frontend": "not built - run 'npm run build' in frontend/"
}
return app
@app.get("/info-room", response_model=RoomInfoResponse) def parse_args():
async def get_room_info(): """Parse degli argomenti da linea di comando"""
parser = argparse.ArgumentParser(
description="Focolari Voting System - Backend Mock Server",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Esempi:
python main.py # Avvio standard
python main.py -p 9000 # Porta 9000
python main.py -d data/users_test.json # Dataset test
python main.py --host 127.0.0.1 -p 8080 # Solo localhost
python main.py --api-only # Solo API, no frontend
""" """
Restituisce le informazioni sulla sala e la riunione corrente.
"""
return RoomInfoResponse(
room_name="Sala Assemblea",
meeting_id="VOT-2024"
) )
parser.add_argument(
@app.post("/login-validate", response_model=LoginResponse) "-p", "--port",
async def login_validate(request: LoginRequest): type=int,
""" default=DEFAULT_PORT,
Valida le credenziali del validatore. help=f"Porta del server (default: {DEFAULT_PORT})"
Il badge deve essere quello del validatore (999999) e la password corretta.
"""
# Pulisci il badge da eventuali caratteri sentinel
clean_badge = request.badge.strip().replace(";", "").replace("?", "")
if clean_badge != VALIDATOR_BADGE:
raise HTTPException(
status_code=401,
detail="Badge validatore non riconosciuto"
) )
if request.password != VALIDATOR_PASSWORD: parser.add_argument(
raise HTTPException( "-d", "--data",
status_code=401, type=str,
detail="Password non corretta" default=DEFAULT_DATA,
help=f"Path al file JSON con i dati (default: {DEFAULT_DATA})"
) )
return LoginResponse( parser.add_argument(
success=True, "--host",
message="Login validatore effettuato con successo", type=str,
token=MOCK_TOKEN default=DEFAULT_HOST,
help=f"Host di binding (default: {DEFAULT_HOST})"
) )
parser.add_argument(
@app.get("/anagrafica/{badge_code}", response_model=UserResponse) "--api-only",
async def get_user_anagrafica(badge_code: str): action="store_true",
""" help="Avvia solo le API senza servire il frontend"
Cerca un utente tramite il suo badge code.
Restituisce i dati anagrafici e un warning se non è ammesso.
"""
# Pulisci il badge da eventuali caratteri sentinel
clean_badge = badge_code.strip().replace(";", "").replace("?", "")
# Normalizza: rimuovi zeri iniziali per il confronto, ma cerca anche con zeri
for user in USERS_DB:
if user["badge_code"] == clean_badge or user["badge_code"].lstrip("0") == clean_badge.lstrip("0"):
response = UserResponse(
badge_code=user["badge_code"],
nome=user["nome"],
cognome=user["cognome"],
url_foto=user["url_foto"],
ruolo=user["ruolo"],
ammesso=user["ammesso"]
) )
# Aggiungi warning se non ammesso return parser.parse_args()
if not user["ammesso"]:
response.warning = "ATTENZIONE: Questo utente NON è autorizzato all'ingresso!"
return response
# Utente non trovato
raise HTTPException(
status_code=404,
detail=f"Utente con badge {clean_badge} non trovato nel sistema"
)
@app.post("/entry-request", response_model=EntryResponse) def main():
async def process_entry_request(request: EntryRequest): """Entry point principale"""
""" args = parse_args()
Processa una richiesta di ingresso.
Richiede il badge dell'utente e la password del validatore.
"""
# Pulisci i dati
clean_user_badge = request.user_badge.strip().replace(";", "").replace("?", "")
# Verifica password validatore print("🚀 Avvio Focolari Voting System Backend...")
if request.validator_password != VALIDATOR_PASSWORD: print("=" * 50)
raise HTTPException(
status_code=401,
detail="Password validatore non corretta"
)
# Cerca l'utente # Carica i dati
user_found = None data = load_data(args.data)
for user in USERS_DB:
if user["badge_code"] == clean_user_badge or user["badge_code"].lstrip("0") == clean_user_badge.lstrip("0"):
user_found = user
break
if not user_found: print("=" * 50)
raise HTTPException( print(f"📍 Server in ascolto su http://{args.host}:{args.port}")
status_code=404, print(f"📚 Documentazione API su http://{args.host}:{args.port}/docs")
detail=f"Utente con badge {clean_user_badge} non trovato" if not args.api_only and STATIC_DIR.exists():
) print(f"🌐 Frontend disponibile su http://{args.host}:{args.port}/")
print("=" * 50)
if not user_found["ammesso"]: # Crea e avvia l'app
raise HTTPException( app = create_app(data, serve_frontend=not args.api_only)
status_code=403, uvicorn.run(app, host=args.host, port=args.port)
detail=f"L'utente {user_found['nome']} {user_found['cognome']} NON è autorizzato all'ingresso"
)
# Successo! Genera messaggio di benvenuto casuale
welcome = random.choice(WELCOME_MESSAGES)
return EntryResponse(
success=True,
message=f"Ingresso registrato per {user_found['nome']} {user_found['cognome']}",
welcome_message=welcome
)
# ============================================
# AVVIO SERVER
# ============================================
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn main()
print("🚀 Avvio Focolari Voting System Backend...")
print("📍 Server in ascolto su http://localhost:8000")
print("📚 Documentazione API su http://localhost:8000/docs")
uvicorn.run(app, host="0.0.0.0", port=8000)

View File

@@ -0,0 +1,21 @@
"""
Focolari Voting System - Schemas Package
"""
from .models import (
LoginRequest,
EntryRequest,
UserResponse,
RoomInfoResponse,
LoginResponse,
EntryResponse,
)
__all__ = [
"LoginRequest",
"EntryRequest",
"UserResponse",
"RoomInfoResponse",
"LoginResponse",
"EntryResponse",
]

View File

@@ -0,0 +1,50 @@
"""
Focolari Voting System - Modelli Pydantic
"""
from typing import Optional, Literal
from pydantic import BaseModel
class LoginRequest(BaseModel):
"""Richiesta login validatore"""
badge: str
password: str
class EntryRequest(BaseModel):
"""Richiesta ingresso partecipante"""
user_badge: str
validator_password: str
class UserResponse(BaseModel):
"""Risposta dati utente"""
badge_code: str
nome: str
cognome: str
url_foto: str
ruolo: Literal["Tecnico", "Votante", "Ospite"]
ammesso: bool
warning: Optional[str] = None
class RoomInfoResponse(BaseModel):
"""Risposta info sala"""
room_name: str
meeting_id: str
server_start_time: int # Timestamp avvio server per invalidare sessioni
class LoginResponse(BaseModel):
"""Risposta login"""
success: bool
message: str
token: Optional[str] = None
class EntryResponse(BaseModel):
"""Risposta richiesta ingresso (asettica, senza welcome_message)"""
success: bool
message: str

258
dev.sh Executable file
View File

@@ -0,0 +1,258 @@
#!/bin/bash
# ============================================
# Focolari Voting System - Script di Sviluppo
# ============================================
#
# Utilizzo:
# ./dev.sh install - Installa dipendenze frontend e backend
# ./dev.sh dev - Avvia frontend (dev) + backend (api-only)
# ./dev.sh build - Builda il frontend
# ./dev.sh server - Builda frontend (se necessario) e avvia server completo
# ./dev.sh backend - Avvia solo il backend (api-only)
# ./dev.sh frontend - Avvia solo il frontend in dev mode
# ./dev.sh shell - Apre shell pipenv del backend
# ./dev.sh clean - Pulisce build e cache
# ./dev.sh help - Mostra questo messaggio
#
set -e
# Directory del progetto
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BACKEND_DIR="$SCRIPT_DIR/backend-mock"
FRONTEND_DIR="$SCRIPT_DIR/frontend"
FRONTEND_DIST="$FRONTEND_DIR/dist"
# Colori per output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Funzioni di utilità
info() {
echo -e "${BLUE} $1${NC}"
}
success() {
echo -e "${GREEN}$1${NC}"
}
warn() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
error() {
echo -e "${RED}$1${NC}"
exit 1
}
# Verifica prerequisiti
check_prereqs() {
command -v node >/dev/null 2>&1 || error "Node.js non trovato. Installalo prima."
command -v npm >/dev/null 2>&1 || error "npm non trovato. Installalo prima."
command -v python3 >/dev/null 2>&1 || error "Python3 non trovato. Installalo prima."
command -v pipenv >/dev/null 2>&1 || error "pipenv non trovato. Installa con: pip install pipenv"
}
# Installa dipendenze
cmd_install() {
info "Installazione dipendenze..."
check_prereqs
# Backend
info "Installazione dipendenze backend (pipenv)..."
cd "$BACKEND_DIR"
pipenv install
success "Backend installato"
# Frontend
info "Installazione dipendenze frontend (npm)..."
cd "$FRONTEND_DIR"
npm install
success "Frontend installato"
success "Tutte le dipendenze installate!"
}
# Build frontend
cmd_build() {
info "Build frontend..."
cd "$FRONTEND_DIR"
npm run build
success "Frontend buildato in $FRONTEND_DIST"
}
# Verifica se serve rebuild
needs_rebuild() {
# Se dist non esiste, serve build
if [ ! -d "$FRONTEND_DIST" ]; then
return 0
fi
# Se dist/index.html non esiste, serve build
if [ ! -f "$FRONTEND_DIST/index.html" ]; then
return 0
fi
# Controlla se ci sono file frontend più recenti della build
local latest_src=$(find "$FRONTEND_DIR/src" -type f -newer "$FRONTEND_DIST/index.html" 2>/dev/null | head -1)
if [ -n "$latest_src" ]; then
return 0
fi
# Controlla anche index.html e config
if [ "$FRONTEND_DIR/index.html" -nt "$FRONTEND_DIST/index.html" ]; then
return 0
fi
if [ "$FRONTEND_DIR/vite.config.ts" -nt "$FRONTEND_DIST/index.html" ]; then
return 0
fi
return 1
}
# Server completo (build + backend)
cmd_server() {
check_prereqs
# Rebuild se necessario
if needs_rebuild; then
warn "Rilevati cambiamenti nel frontend, rebuild in corso..."
cmd_build
else
info "Frontend già aggiornato, skip build"
fi
# Avvia backend con frontend
info "Avvio server completo..."
cd "$BACKEND_DIR"
pipenv run python main.py "$@"
}
# Solo backend
cmd_backend() {
check_prereqs
info "Avvio backend (API only)..."
cd "$BACKEND_DIR"
pipenv run python main.py --api-only "$@"
}
# Solo frontend dev
cmd_frontend() {
check_prereqs
info "Avvio frontend in dev mode..."
cd "$FRONTEND_DIR"
npm run dev
}
# Dev mode: backend api-only + frontend dev (in parallelo)
cmd_dev() {
check_prereqs
info "Avvio ambiente di sviluppo..."
info "Backend API: http://localhost:8000"
info "Frontend Dev: http://localhost:5173"
echo ""
# Avvia backend in background
cd "$BACKEND_DIR"
pipenv run python main.py --api-only &
BACKEND_PID=$!
# Trap per cleanup
trap "kill $BACKEND_PID 2>/dev/null" EXIT
# Avvia frontend
cd "$FRONTEND_DIR"
npm run dev
}
# Shell pipenv
cmd_shell() {
info "Apertura shell pipenv backend..."
cd "$BACKEND_DIR"
pipenv shell
}
# Pulizia
cmd_clean() {
info "Pulizia build e cache..."
# Frontend
rm -rf "$FRONTEND_DIR/node_modules/.vite"
rm -rf "$FRONTEND_DIST"
# Backend
find "$BACKEND_DIR" -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
find "$BACKEND_DIR" -type f -name "*.pyc" -delete 2>/dev/null || true
success "Pulizia completata"
}
# Help
cmd_help() {
echo "============================================"
echo "Focolari Voting System - Script di Sviluppo"
echo "============================================"
echo ""
echo "Utilizzo: ./dev.sh <comando> [opzioni]"
echo ""
echo "Comandi disponibili:"
echo " install Installa dipendenze frontend e backend"
echo " dev Avvia frontend (dev) + backend (api-only) in parallelo"
echo " build Builda il frontend per produzione"
echo " server Builda frontend (se cambiato) e avvia server completo"
echo " backend Avvia solo il backend (api-only)"
echo " frontend Avvia solo il frontend in dev mode"
echo " shell Apre shell pipenv del backend"
echo " clean Pulisce build e cache"
echo " help Mostra questo messaggio"
echo ""
echo "Esempi:"
echo " ./dev.sh install # Setup iniziale"
echo " ./dev.sh dev # Sviluppo (hot reload)"
echo " ./dev.sh server # Produzione locale"
echo " ./dev.sh server -p 9000 # Server su porta 9000"
echo " ./dev.sh server -d data/users_test.json # Con dataset test"
echo ""
}
# Main
case "${1:-help}" in
install)
cmd_install
;;
build)
cmd_build
;;
server)
shift
cmd_server "$@"
;;
backend)
shift
cmd_backend "$@"
;;
frontend)
cmd_frontend
;;
dev)
cmd_dev
;;
shell)
cmd_shell
;;
clean)
cmd_clean
;;
help|--help|-h)
cmd_help
;;
*)
error "Comando sconosciuto: $1. Usa './dev.sh help' per la lista comandi."
;;
esac

View File

@@ -4,12 +4,16 @@ This template provides a minimal setup to get React working in Vite with HMR and
Currently, two official plugins are available: Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used
in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc)
uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler ## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it,
see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration ## Expanding the ESLint configuration
@@ -43,7 +47,10 @@ export default defineConfig([
]) ])
``` ```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: You can also
install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x)
and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom)
for React-specific lint rules:
```js ```js
// eslint.config.js // eslint.config.js

View File

@@ -1,13 +1,13 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="it">
<head> <head>
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link href="./favicon.jpg" rel="icon" type="image/jpeg"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>frontend</title> <title>Focolari Voting System</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script src="/src/main.tsx" type="module"></script>
</body> </body>
</html> </html>

View File

@@ -9,7 +9,8 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0" "react-dom": "^19.2.0",
"react-router-dom": "^7.12.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",
@@ -2265,6 +2266,19 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/cookie": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3496,6 +3510,44 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-router": {
"version": "7.12.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz",
"integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.12.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.12.0.tgz",
"integrity": "sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==",
"license": "MIT",
"dependencies": {
"react-router": "7.12.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/resolve-from": { "node_modules/resolve-from": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -3567,6 +3619,12 @@
"semver": "bin/semver.js" "semver": "bin/semver.js"
} }
}, },
"node_modules/set-cookie-parser": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",

View File

@@ -11,7 +11,8 @@
}, },
"dependencies": { "dependencies": {
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0" "react-dom": "^19.2.0",
"react-router-dom": "^7.12.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",

BIN
frontend/public/favicon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -3,27 +3,13 @@
* State Machine per il controllo accessi * State Machine per il controllo accessi
*/ */
import { useState, useEffect, useCallback } from 'react'; import {useCallback, useEffect, useState} from 'react';
import {useRFIDScanner} from './hooks/useRFIDScanner'; import {useRFIDScanner} from './hooks/useRFIDScanner';
import { import {ActiveGateScreen, ErrorModal, LoadingScreen, SuccessModal, ValidatorLoginScreen,} from './screens';
LoadingScreen, import {ApiError, getRoomInfo, getUserByBadge, loginValidator, requestEntry,} from './services/api';
ValidatorLoginScreen,
ActiveGateScreen,
SuccessModal,
ErrorModal,
} from './screens';
import {
getRoomInfo,
loginValidator,
getUserByBadge,
requestEntry,
ApiError,
} from './services/api';
import type {AppState, RoomInfo, User, ValidatorSession} from './types'; import type {AppState, RoomInfo, User, ValidatorSession} from './types';
// Costanti // Costanti
const VALIDATOR_BADGE = '999999';
const VALIDATOR_PASSWORD = 'focolari';
const SESSION_DURATION_MS = 30 * 60 * 1000; // 30 minuti const SESSION_DURATION_MS = 30 * 60 * 1000; // 30 minuti
const USER_TIMEOUT_SECONDS = 60; const USER_TIMEOUT_SECONDS = 60;
const STORAGE_KEY = 'focolari_validator_session'; const STORAGE_KEY = 'focolari_validator_session';
@@ -42,10 +28,16 @@ function App() {
// Modal states // Modal states
const [showSuccessModal, setShowSuccessModal] = useState(false); const [showSuccessModal, setShowSuccessModal] = useState(false);
const [successMessage, setSuccessMessage] = useState(''); const [successUserName, setSuccessUserName] = useState<string | undefined>(undefined);
const [showErrorModal, setShowErrorModal] = useState(false); const [showErrorModal, setShowErrorModal] = useState(false);
const [errorModalMessage, setErrorModalMessage] = useState(''); const [errorModalMessage, setErrorModalMessage] = useState('');
// Notifica badge validatore ignorato
const [showValidatorBadgeNotice, setShowValidatorBadgeNotice] = useState(false);
// Badge non trovato (con timeout per tornare all'attesa)
const [notFoundBadge, setNotFoundBadge] = useState<string | null>(null);
// ============================================ // ============================================
// Session Management // Session Management
// ============================================ // ============================================
@@ -63,7 +55,7 @@ function App() {
setAppState('waiting-validator'); setAppState('waiting-validator');
}, []); }, []);
const loadSession = useCallback((): ValidatorSession | null => { const loadSession = useCallback((serverStartTime?: number): ValidatorSession | null => {
try { try {
const stored = localStorage.getItem(STORAGE_KEY); const stored = localStorage.getItem(STORAGE_KEY);
if (!stored) return null; if (!stored) return null;
@@ -72,6 +64,14 @@ function App() {
// Check if session is expired // Check if session is expired
if (Date.now() > session.expiresAt) { if (Date.now() > session.expiresAt) {
console.log('[FLOW] Session expired by time');
localStorage.removeItem(STORAGE_KEY);
return null;
}
// Check if server restarted (invalidate old sessions)
if (serverStartTime && session.serverStartTime !== serverStartTime) {
console.log('[FLOW] Session invalidated - server restarted');
localStorage.removeItem(STORAGE_KEY); localStorage.removeItem(STORAGE_KEY);
return null; return null;
} }
@@ -87,35 +87,43 @@ function App() {
// ============================================ // ============================================
const handleRFIDScan = useCallback(async (code: string) => { const handleRFIDScan = useCallback(async (code: string) => {
console.log('[App] Badge scansionato:', code); console.log('[RFID] Badge scansionato:', code);
// Pulisci il codice // Pulisci il codice
const cleanCode = code.trim(); const cleanCode = code.trim();
switch (appState) { switch (appState) {
case 'waiting-validator': case 'waiting-validator':
// Verifica se è un badge validatore // Qualsiasi badge può essere un validatore - verrà verificato con la password
if (cleanCode === VALIDATOR_BADGE) { console.log('[FLOW] Transition: waiting-validator -> validator-password');
setPendingValidatorBadge(cleanCode); setPendingValidatorBadge(cleanCode);
setAppState('validator-password'); setAppState('validator-password');
} else {
setError('Badge validatore non riconosciuto');
setTimeout(() => setError(undefined), 3000);
}
break; break;
case 'validator-password': case 'validator-password':
// Ignora badge durante inserimento password // Ignora badge durante inserimento password
console.log('[FLOW] Badge ignorato durante inserimento password');
break; break;
case 'gate-active': case 'gate-active':
case 'showing-user': case 'showing-user':
// Se è il badge del validatore e c'è un utente a schermo // Se è il badge del validatore attuale
if (cleanCode === VALIDATOR_BADGE && currentUser && currentUser.ammesso) { if (validatorSession && cleanCode === validatorSession.badge) {
if (currentUser && currentUser.ammesso) {
console.log('[FLOW] Validator badge detected - confirming entry');
// Conferma ingresso // Conferma ingresso
await handleEntryConfirm(); await handleEntryConfirm();
} else if (cleanCode !== VALIDATOR_BADGE) { } else {
// Nuovo badge partecipante - carica utente // Badge validatore passato senza utente ammesso - mostra notifica
console.log('[FLOW] Validator badge ignored - showing notice');
setShowValidatorBadgeNotice(true);
setTimeout(() => setShowValidatorBadgeNotice(false), 4000);
}
} else {
console.log('[FLOW] Loading participant:', cleanCode);
// Badge partecipante - carica utente
// Se c'era un badge non trovato, cancellalo e carica il nuovo
setNotFoundBadge(null);
await handleLoadUser(cleanCode); await handleLoadUser(cleanCode);
} }
break; break;
@@ -123,7 +131,7 @@ function App() {
default: default:
break; break;
} }
}, [appState, currentUser]); }, [appState, currentUser, validatorSession]);
// ============================================ // ============================================
// Initialize RFID Scanner // Initialize RFID Scanner
@@ -144,17 +152,25 @@ function App() {
const handleLoadUser = useCallback(async (badgeCode: string) => { const handleLoadUser = useCallback(async (badgeCode: string) => {
setLoading(true); setLoading(true);
setError(undefined); setError(undefined);
setNotFoundBadge(null);
setAppState('showing-user'); setAppState('showing-user');
try { try {
const user = await getUserByBadge(badgeCode); const user = await getUserByBadge(badgeCode);
setCurrentUser(user); setCurrentUser(user);
} catch (err) { } catch (err) {
if (err instanceof ApiError && err.statusCode === 404) {
// Utente non trovato - mostra schermata speciale con timeout
console.log('[FLOW] Badge not found:', badgeCode);
setNotFoundBadge(badgeCode);
setCurrentUser(null);
} else {
const message = err instanceof ApiError const message = err instanceof ApiError
? err.detail || err.message ? err.detail || err.message
: 'Errore durante il caricamento utente'; : 'Errore durante il caricamento utente';
setError(message); setError(message);
setCurrentUser(null); setCurrentUser(null);
}
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -165,14 +181,18 @@ function App() {
setLoading(true); setLoading(true);
// Salva il nome utente prima di pulirlo
const userName = `${currentUser.nome} ${currentUser.cognome}`;
try { try {
const response = await requestEntry( const response = await requestEntry(
currentUser.badge_code, currentUser.badge_code,
VALIDATOR_PASSWORD validatorSession.password // Uso la password dalla sessione
); );
if (response.success) { if (response.success) {
setSuccessMessage(response.welcome_message || 'Benvenuto!'); console.log('[FLOW] Entry confirmed for:', userName);
setSuccessUserName(userName);
setShowSuccessModal(true); setShowSuccessModal(true);
setCurrentUser(null); setCurrentUser(null);
setAppState('gate-active'); setAppState('gate-active');
@@ -189,7 +209,7 @@ function App() {
}, [currentUser, validatorSession]); }, [currentUser, validatorSession]);
const handlePasswordSubmit = useCallback(async (password: string) => { const handlePasswordSubmit = useCallback(async (password: string) => {
if (!pendingValidatorBadge) return; if (!pendingValidatorBadge || !roomInfo) return;
setLoading(true); setLoading(true);
setError(undefined); setError(undefined);
@@ -198,11 +218,14 @@ function App() {
const response = await loginValidator(pendingValidatorBadge, password); const response = await loginValidator(pendingValidatorBadge, password);
if (response.success) { if (response.success) {
console.log('[FLOW] Validator authenticated, badge:', pendingValidatorBadge);
const session: ValidatorSession = { const session: ValidatorSession = {
badge: pendingValidatorBadge, badge: pendingValidatorBadge,
password: password, // Salvo la password per le conferme ingresso
token: response.token || '', token: response.token || '',
loginTime: Date.now(), loginTime: Date.now(),
expiresAt: Date.now() + SESSION_DURATION_MS, expiresAt: Date.now() + SESSION_DURATION_MS,
serverStartTime: roomInfo.server_start_time, // Per invalidare se server riparte
}; };
saveSession(session); saveSession(session);
@@ -217,7 +240,7 @@ function App() {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [pendingValidatorBadge, saveSession]); }, [pendingValidatorBadge, saveSession, roomInfo]);
// ============================================ // ============================================
// UI Handlers // UI Handlers
@@ -231,6 +254,7 @@ function App() {
const handleCancelUser = useCallback(() => { const handleCancelUser = useCallback(() => {
setCurrentUser(null); setCurrentUser(null);
setNotFoundBadge(null);
setError(undefined); setError(undefined);
setAppState('gate-active'); setAppState('gate-active');
}, []); }, []);
@@ -238,13 +262,14 @@ function App() {
const handleUserTimeout = useCallback(() => { const handleUserTimeout = useCallback(() => {
console.log('[App] User timeout - tornando in attesa'); console.log('[App] User timeout - tornando in attesa');
setCurrentUser(null); setCurrentUser(null);
setNotFoundBadge(null);
setError(undefined); setError(undefined);
setAppState('gate-active'); setAppState('gate-active');
}, []); }, []);
const handleSuccessModalClose = useCallback(() => { const handleSuccessModalClose = useCallback(() => {
setShowSuccessModal(false); setShowSuccessModal(false);
setSuccessMessage(''); setSuccessUserName(undefined);
}, []); }, []);
const handleErrorModalClose = useCallback(() => { const handleErrorModalClose = useCallback(() => {
@@ -263,8 +288,8 @@ function App() {
const info = await getRoomInfo(); const info = await getRoomInfo();
setRoomInfo(info); setRoomInfo(info);
// Verifica sessione esistente // Verifica sessione esistente (passa serverStartTime per invalidazione)
const existingSession = loadSession(); const existingSession = loadSession(info.server_start_time);
if (existingSession) { if (existingSession) {
setValidatorSession(existingSession); setValidatorSession(existingSession);
@@ -347,20 +372,21 @@ function App() {
rfidState={rfidState} rfidState={rfidState}
rfidBuffer={rfidBuffer} rfidBuffer={rfidBuffer}
currentUser={currentUser} currentUser={currentUser}
notFoundBadge={notFoundBadge}
loading={loading} loading={loading}
error={error} error={error}
onCancelUser={handleCancelUser} onCancelUser={handleCancelUser}
onLogout={clearSession} onLogout={clearSession}
userTimeoutSeconds={USER_TIMEOUT_SECONDS} userTimeoutSeconds={USER_TIMEOUT_SECONDS}
onUserTimeout={handleUserTimeout} onUserTimeout={handleUserTimeout}
showValidatorBadgeNotice={showValidatorBadgeNotice}
/> />
{/* Success Modal */} {/* Success Modal */}
<SuccessModal <SuccessModal
isOpen={showSuccessModal} isOpen={showSuccessModal}
onClose={handleSuccessModalClose} onClose={handleSuccessModalClose}
welcomeMessage={successMessage} userName={successUserName}
userName={currentUser ? `${currentUser.nome} ${currentUser.cognome}` : undefined}
/> />
{/* Error Modal */} {/* Error Modal */}

View File

@@ -2,7 +2,7 @@
* Countdown Timer Component - Focolari Voting System * Countdown Timer Component - Focolari Voting System
*/ */
import { useState, useEffect, useCallback } from 'react'; import {useCallback, useEffect, useState} from 'react';
interface CountdownTimerProps { interface CountdownTimerProps {
/** Secondi totali */ /** Secondi totali */

View File

@@ -2,7 +2,7 @@
* Input Component - Focolari Voting System * Input Component - Focolari Voting System
*/ */
import { forwardRef } from 'react'; import {forwardRef, useState} from 'react';
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> { interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string; label?: string;
@@ -11,8 +11,12 @@ interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
} }
export const Input = forwardRef<HTMLInputElement, InputProps>( export const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, error, fullWidth = true, className = '', ...props }, ref) => { ({label, error, fullWidth = true, className = '', type, ...props}, ref) => {
const widthClass = fullWidth ? 'w-full' : ''; const widthClass = fullWidth ? 'w-full' : '';
const isPassword = type === 'password';
const [showPassword, setShowPassword] = useState(false);
const inputType = isPassword && showPassword ? 'text' : type;
return ( return (
<div className={`${widthClass} ${className}`}> <div className={`${widthClass} ${className}`}>
@@ -21,13 +25,16 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
{label} {label}
</label> </label>
)} )}
<div className="relative">
<input <input
ref={ref} ref={ref}
type={inputType}
className={` className={`
w-full px-4 py-3 text-lg w-full px-4 py-3 text-lg
border-2 rounded-xl border-2 rounded-xl
transition-all duration-200 transition-all duration-200
focus:outline-none focus:ring-4 focus:ring-focolare-blue/30 focus:outline-none focus:ring-4 focus:ring-focolare-blue/30
${isPassword ? 'pr-12' : ''}
${error ${error
? 'border-error focus:border-error' ? 'border-error focus:border-error'
: 'border-gray-300 focus:border-focolare-blue' : 'border-gray-300 focus:border-focolare-blue'
@@ -35,6 +42,32 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
`.trim()} `.trim()}
{...props} {...props}
/> />
{isPassword && (
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 p-1 text-gray-500 hover:text-gray-700 transition-colors"
tabIndex={-1}
aria-label={showPassword ? 'Nascondi password' : 'Mostra password'}
>
{showPassword ? (
// Icona occhio barrato (nascondi)
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"/>
</svg>
) : (
// Icona occhio (mostra)
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg>
)}
</button>
)}
</div>
{error && ( {error && (
<p className="mt-2 text-sm text-error font-medium">{error}</p> <p className="mt-2 text-sm text-error font-medium">{error}</p>
)} )}

View File

@@ -0,0 +1,132 @@
/**
* NumLock Banner Component - Focolari Voting System
*
* Mostra un banner di avviso su browser desktop quando NumLock
* potrebbe essere necessario per il corretto funzionamento del lettore RFID.
* Non viene mostrato su dispositivi touch (tablet, smartphone).
*/
import {useEffect, useState} from 'react';
interface NumLockBannerProps {
className?: string;
}
export function NumLockBanner({className = ''}: NumLockBannerProps) {
const [isDesktop, setIsDesktop] = useState(false);
const [numLockState, setNumLockState] = useState<boolean | null>(null);
const [dismissed, setDismissed] = useState(false);
// Rileva se siamo su desktop (non touch device)
useEffect(() => {
const checkIfDesktop = () => {
// Controlla se è un dispositivo touch
const isTouchDevice =
'ontouchstart' in window ||
navigator.maxTouchPoints > 0 ||
// @ts-expect-error - msMaxTouchPoints è specifico di IE/Edge
navigator.msMaxTouchPoints > 0;
// Controlla user agent per mobile/tablet
const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
const isMobileUA = mobileRegex.test(navigator.userAgent);
// È desktop solo se non è touch E non ha UA mobile
setIsDesktop(!isTouchDevice && !isMobileUA);
};
checkIfDesktop();
}, []);
// Ascolta eventi tastiera per rilevare stato NumLock
useEffect(() => {
if (!isDesktop) return;
const handleKeyDown = (event: KeyboardEvent) => {
// getModifierState restituisce lo stato di NumLock
const numLock = event.getModifierState('NumLock');
setNumLockState(numLock);
};
// Aggiungi listener
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [isDesktop]);
// Non mostrare su mobile/tablet o se dismissato
if (!isDesktop || dismissed) {
return null;
}
return (
<div className={`bg-amber-100 border border-amber-300 rounded-lg p-4 ${className}`}>
<div className="flex items-start gap-3">
{/* Icona */}
<div className="flex-shrink-0 mt-0.5">
<svg
className="w-5 h-5 text-amber-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
</div>
{/* Contenuto */}
<div className="flex-1">
<h4 className="text-sm font-semibold text-amber-800">
Modalità Desktop Rilevata
</h4>
<p className="text-sm text-amber-700 mt-1">
Per il corretto funzionamento del lettore RFID, assicurati che il tasto
<strong> Bloc Num (NumLock) </strong> sia attivo.
</p>
{/* Stato NumLock */}
<div className="mt-2 flex items-center gap-2">
<span className="text-sm text-amber-700">Stato attuale:</span>
{numLockState === null ? (
<span className="text-sm text-amber-600 italic">
Premi un tasto per rilevare...
</span>
) : numLockState ? (
<span
className="inline-flex items-center gap-1 text-sm font-medium text-green-700 bg-green-100 px-2 py-0.5 rounded">
<span className="w-2 h-2 bg-green-500 rounded-full"></span>
NumLock ATTIVO
</span>
) : (
<span
className="inline-flex items-center gap-1 text-sm font-medium text-red-700 bg-red-100 px-2 py-0.5 rounded">
<span className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></span>
NumLock DISATTIVO
</span>
)}
</div>
</div>
{/* Pulsante chiudi */}
<button
onClick={() => setDismissed(true)}
className="flex-shrink-0 text-amber-500 hover:text-amber-700 transition-colors"
aria-label="Chiudi avviso"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
</div>
);
}
export default NumLockBanner;

View File

@@ -0,0 +1,100 @@
/**
* Welcome Carousel Component - Focolari Voting System
*
* Carosello automatico di messaggi di benvenuto multilingua.
* Scorre automaticamente ogni 800ms durante la visualizzazione.
*/
import {useEffect, useState} from 'react';
interface WelcomeMessage {
lang: string;
text: string;
}
const WELCOME_MESSAGES: WelcomeMessage[] = [
{lang: 'it', text: 'Benvenuto!'},
{lang: 'en', text: 'Welcome!'},
{lang: 'fr', text: 'Bienvenue!'},
{lang: 'de', text: 'Willkommen!'},
{lang: 'es', text: '¡Bienvenido!'},
{lang: 'pt', text: 'Bem-vindo!'},
{lang: 'zh', text: '欢迎!'},
{lang: 'ja', text: 'ようこそ!'},
{lang: 'ar', text: '!مرحباً'},
{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;
}
export function WelcomeCarousel({paused = false, userName}: 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);
return () => clearInterval(interval);
}, [paused]);
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>
{/* Nome utente */}
{userName && (
<div className="mt-8 pt-8 border-t border-white/30">
<p className="text-3xl md:text-4xl text-white font-medium">
{userName}
</p>
</div>
)}
{/* Indicatori */}
<div className="flex gap-2 mt-8">
{WELCOME_MESSAGES.map((_, index) => (
<div
key={index}
className={`w-2 h-2 rounded-full transition-all duration-300 ${
index === currentIndex
? 'bg-white w-6'
: 'bg-white/40'
}`}
/>
))}
</div>
</div>
);
}
export default WelcomeCarousel;

View File

@@ -6,3 +6,5 @@ export { Modal } from './Modal';
export {RFIDStatus} from './RFIDStatus'; export {RFIDStatus} from './RFIDStatus';
export {Button} from './Button'; export {Button} from './Button';
export {Input} from './Input'; export {Input} from './Input';
export {WelcomeCarousel} from './WelcomeCarousel';
export {NumLockBanner} from './NumLockBanner';

View File

@@ -1,22 +1,53 @@
/** /**
* Focolari Voting System - RFID Scanner Hook * Focolari Voting System - RFID Scanner Hook (v2 - Multi-Pattern)
* *
* Questo hook gestisce la lettura di badge RFID tramite lettori USB * Questo hook gestisce la lettura di badge RFID tramite lettori USB
* che emulano una tastiera. Il protocollo prevede: * che emulano una tastiera. Supporta pattern multipli per diversi layout:
* - Carattere di inizio: `;` * - Layout US: `;` → `?`
* - Carattere di fine: `?` * - Layout IT: `ò` → `_`
* - Esempio: `;00012345?` *
* NOTA: Il lettore RFID invia Enter (\n) dopo l'ultimo carattere.
* L'hook lo gestisce ignorando l'Enter immediatamente dopo il completamento.
* *
* L'hook funziona indipendentemente dal focus dell'applicazione. * L'hook funziona indipendentemente dal focus dell'applicazione.
*/ */
import { useState, useEffect, useCallback, useRef } from 'react'; import {useCallback, useEffect, useRef, useState} from 'react';
import type {RFIDScannerState, RFIDScanResult} from '../types'; import type {RFIDScannerState, RFIDScanResult} from '../types';
// Costanti // ============================================
const START_SENTINEL = ';'; // CONFIGURAZIONE PATTERN RFID
const END_SENTINEL = '?'; // ============================================
const TIMEOUT_MS = 3000; // 3 secondi di timeout
export interface RFIDPattern {
name: string;
start: string;
end: string;
}
export const VALID_PATTERNS: RFIDPattern[] = [
{name: 'US', start: ';', end: '?'},
{name: 'IT', start: 'ò', end: '_'},
];
const TIMEOUT_MS = 2500; // 2.5 secondi di timeout
const ENTER_GRACE_PERIOD_MS = 100; // Ignora Enter entro 100ms dal completamento
// ============================================
// LOGGING
// ============================================
const log = (message: string, ...args: unknown[]) => {
console.log(`[RFID] ${message}`, ...args);
};
const logWarn = (message: string, ...args: unknown[]) => {
console.warn(`[RFID] ${message}`, ...args);
};
// ============================================
// TIPI
// ============================================
interface UseRFIDScannerOptions { interface UseRFIDScannerOptions {
/** Callback chiamato quando un badge viene letto con successo */ /** Callback chiamato quando un badge viene letto con successo */
@@ -31,17 +62,33 @@ interface UseRFIDScannerOptions {
disabled?: boolean; disabled?: boolean;
} }
interface UseRFIDScannerReturn { export interface UseRFIDScannerReturn {
/** Stato corrente dello scanner */ /** Stato corrente dello scanner */
state: RFIDScannerState; state: RFIDScannerState;
/** Buffer corrente (solo per debug) */ /** Buffer corrente (solo per debug) */
buffer: string; buffer: string;
/** Ultimo codice scansionato */ /** Ultimo codice scansionato */
lastScan: RFIDScanResult | null; lastScan: RFIDScanResult | null;
/** Pattern attualmente in uso (solo durante scanning) */
activePattern: RFIDPattern | null;
/** Reset manuale dello scanner */ /** Reset manuale dello scanner */
reset: () => void; reset: () => void;
/** Ultimi eventi tastiera (per debug) */
keyLog: KeyLogEntry[];
} }
export interface KeyLogEntry {
key: string;
code: string;
timestamp: number;
}
const MAX_KEY_LOG = 20;
// ============================================
// HOOK
// ============================================
export function useRFIDScanner({ export function useRFIDScanner({
onScan, onScan,
onTimeout, onTimeout,
@@ -52,11 +99,15 @@ export function useRFIDScanner({
const [state, setState] = useState<RFIDScannerState>('idle'); const [state, setState] = useState<RFIDScannerState>('idle');
const [buffer, setBuffer] = useState<string>(''); const [buffer, setBuffer] = useState<string>('');
const [lastScan, setLastScan] = useState<RFIDScanResult | null>(null); const [lastScan, setLastScan] = useState<RFIDScanResult | null>(null);
const [activePattern, setActivePattern] = useState<RFIDPattern | null>(null);
const [keyLog, setKeyLog] = useState<KeyLogEntry[]>([]);
// Refs per mantenere i valori aggiornati nei callback // Refs per mantenere i valori aggiornati nei callback
const bufferRef = useRef<string>(''); const bufferRef = useRef<string>('');
const stateRef = useRef<RFIDScannerState>('idle'); const stateRef = useRef<RFIDScannerState>('idle');
const activePatternRef = useRef<RFIDPattern | null>(null);
const timeoutRef = useRef<number | null>(null); const timeoutRef = useRef<number | null>(null);
const lastCompletionRef = useRef<number>(0);
// Sync refs con state // Sync refs con state
useEffect(() => { useEffect(() => {
@@ -67,6 +118,21 @@ export function useRFIDScanner({
stateRef.current = state; stateRef.current = state;
}, [state]); }, [state]);
useEffect(() => {
activePatternRef.current = activePattern;
}, [activePattern]);
/**
* Aggiunge un evento al log tastiera
*/
const addKeyLog = useCallback((key: string, code: string) => {
setKeyLog(prev => {
const newEntry: KeyLogEntry = {key, code, timestamp: Date.now()};
const updated = [newEntry, ...prev].slice(0, MAX_KEY_LOG);
return updated;
});
}, []);
/** /**
* Pulisce il timeout attivo * Pulisce il timeout attivo
*/ */
@@ -84,8 +150,10 @@ export function useRFIDScanner({
clearScanTimeout(); clearScanTimeout();
setState('idle'); setState('idle');
setBuffer(''); setBuffer('');
setActivePattern(null);
bufferRef.current = ''; bufferRef.current = '';
stateRef.current = 'idle'; stateRef.current = 'idle';
activePatternRef.current = null;
}, [clearScanTimeout]); }, [clearScanTimeout]);
/** /**
@@ -94,12 +162,19 @@ export function useRFIDScanner({
const startTimeout = useCallback(() => { const startTimeout = useCallback(() => {
clearScanTimeout(); clearScanTimeout();
timeoutRef.current = window.setTimeout(() => { timeoutRef.current = window.setTimeout(() => {
console.warn('[RFID Scanner] Timeout - lettura incompleta scartata'); logWarn('Buffer timeout - clearing data');
onTimeout?.(); onTimeout?.();
reset(); reset();
}, TIMEOUT_MS); }, TIMEOUT_MS);
}, [clearScanTimeout, onTimeout, reset]); }, [clearScanTimeout, onTimeout, reset]);
/**
* Trova il pattern che corrisponde al carattere start
*/
const findPatternByStart = useCallback((char: string): RFIDPattern | undefined => {
return VALID_PATTERNS.find(p => p.start === char);
}, []);
/** /**
* Handler principale per gli eventi keydown * Handler principale per gli eventi keydown
*/ */
@@ -110,11 +185,37 @@ export function useRFIDScanner({
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
const key = event.key; const key = event.key;
const code = event.code;
// Ignora tasti speciali (frecce, funzione, ecc.) // Log per debug
if (key.length > 1 && key !== START_SENTINEL && key !== END_SENTINEL) { addKeyLog(key, code);
// Eccezione per Backspace in stato scanning: ignora ma non resetta
if (key === 'Backspace' && stateRef.current === 'scanning') { // Gestione Enter dopo completamento (grace period)
if (key === 'Enter' && Date.now() - lastCompletionRef.current < ENTER_GRACE_PERIOD_MS) {
log('Enter ignorato (post-completion grace period)');
if (preventDefaultOnScan) {
event.preventDefault();
}
return;
}
// Gestione ESC: annulla scansione in corso
if (key === 'Escape' && stateRef.current === 'scanning') {
log('Scansione annullata con ESC');
if (preventDefaultOnScan) {
event.preventDefault();
}
reset();
return;
}
// Ignora tasti speciali (frecce, funzione, ecc.) ma non i sentinel
const isStartSentinel = VALID_PATTERNS.some(p => p.start === key);
const isEndSentinel = activePatternRef.current?.end === key;
if (key.length > 1 && !isStartSentinel && !isEndSentinel) {
// Eccezione per Backspace/Enter in stato scanning: ignora ma non resetta
if ((key === 'Backspace' || key === 'Enter') && stateRef.current === 'scanning') {
if (preventDefaultOnScan) { if (preventDefaultOnScan) {
event.preventDefault(); event.preventDefault();
} }
@@ -122,18 +223,22 @@ export function useRFIDScanner({
return; return;
} }
// STATO IDLE: attende il carattere di inizio // STATO IDLE: attende un carattere start di qualsiasi pattern
if (stateRef.current === 'idle') { if (stateRef.current === 'idle') {
if (key === START_SENTINEL) { const pattern = findPatternByStart(key);
console.log('[RFID Scanner] Start sentinel rilevato - inizio scansione');
if (pattern) {
log(`Start sentinel detected: '${key}' (pattern ${pattern.name})`);
if (preventDefaultOnScan) { if (preventDefaultOnScan) {
event.preventDefault(); event.preventDefault();
} }
setState('scanning'); setState('scanning');
setActivePattern(pattern);
setBuffer(''); setBuffer('');
bufferRef.current = ''; bufferRef.current = '';
activePatternRef.current = pattern;
startTimeout(); startTimeout();
onScanStart?.(); onScanStart?.();
} }
@@ -147,14 +252,18 @@ export function useRFIDScanner({
event.preventDefault(); event.preventDefault();
} }
if (key === END_SENTINEL) { const currentPattern = activePatternRef.current;
// Verifica se è l'end sentinel del pattern attivo
if (currentPattern && key === currentPattern.end) {
// Fine della scansione // Fine della scansione
clearScanTimeout(); clearScanTimeout();
lastCompletionRef.current = Date.now();
const scannedCode = bufferRef.current.trim(); const scannedCode = bufferRef.current.trim();
if (scannedCode.length > 0) { if (scannedCode.length > 0) {
console.log('[RFID Scanner] Codice scansionato:', scannedCode); log(`Scan complete (pattern ${currentPattern.name}): ${scannedCode}`);
const result: RFIDScanResult = { const result: RFIDScanResult = {
code: scannedCode, code: scannedCode,
@@ -164,15 +273,18 @@ export function useRFIDScanner({
setLastScan(result); setLastScan(result);
onScan(scannedCode); onScan(scannedCode);
} else { } else {
console.warn('[RFID Scanner] Codice vuoto scartato'); logWarn('Empty code discarded');
} }
reset(); reset();
} else if (key === START_SENTINEL) { } else if (findPatternByStart(key)) {
// Nuovo start sentinel durante scansione: resetta e ricomincia // Nuovo start sentinel durante scansione: resetta e ricomincia con nuovo pattern
console.log('[RFID Scanner] Nuovo start sentinel - reset buffer'); const newPattern = findPatternByStart(key)!;
log(`New start sentinel during scan - switching to pattern ${newPattern.name}`);
setBuffer(''); setBuffer('');
bufferRef.current = ''; bufferRef.current = '';
setActivePattern(newPattern);
activePatternRef.current = newPattern;
startTimeout(); startTimeout();
} else { } else {
// Accumula il carattere nel buffer // Accumula il carattere nel buffer
@@ -199,6 +311,8 @@ export function useRFIDScanner({
clearScanTimeout, clearScanTimeout,
reset, reset,
startTimeout, startTimeout,
findPatternByStart,
addKeyLog,
]); ]);
// Cleanup al unmount // Cleanup al unmount
@@ -212,7 +326,9 @@ export function useRFIDScanner({
state, state,
buffer, buffer,
lastScan, lastScan,
activePattern,
reset, reset,
keyLog,
}; };
} }

View File

@@ -1,10 +1,21 @@
import {StrictMode} from 'react' import {StrictMode} from 'react'
import {createRoot} from 'react-dom/client' import {createRoot} from 'react-dom/client'
import {BrowserRouter, Route, Routes} from 'react-router-dom'
import './index.css' import './index.css'
import App from './App.tsx' import App from './App.tsx'
import {DebugScreen} from './screens'
function DebugWrapper() {
return <DebugScreen onBack={() => window.location.href = '/'}/>
}
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<App /> <BrowserRouter>
<Routes>
<Route path="/" element={<App/>}/>
<Route path="/debug" element={<DebugWrapper/>}/>
</Routes>
</BrowserRouter>
</StrictMode>, </StrictMode>,
) )

View File

@@ -3,20 +3,26 @@
* Schermata principale del varco attivo * Schermata principale del varco attivo
*/ */
import { Logo, Button, RFIDStatus, UserCard, CountdownTimer } from '../components'; import {useEffect, useState} from 'react';
import type { RoomInfo, User, RFIDScannerState } from '../types'; import {Button, CountdownTimer, Logo, NumLockBanner, RFIDStatus, UserCard} from '../components';
import type {RFIDScannerState, RoomInfo, User} from '../types';
// Timeout per badge non trovato (30 secondi)
const NOT_FOUND_TIMEOUT_SECONDS = 30;
interface ActiveGateScreenProps { interface ActiveGateScreenProps {
roomInfo: RoomInfo; roomInfo: RoomInfo;
rfidState: RFIDScannerState; rfidState: RFIDScannerState;
rfidBuffer: string; rfidBuffer: string;
currentUser: User | null; currentUser: User | null;
notFoundBadge: string | null;
loading: boolean; loading: boolean;
error?: string; error?: string;
onCancelUser: () => void; onCancelUser: () => void;
onLogout: () => void; onLogout: () => void;
userTimeoutSeconds?: number; userTimeoutSeconds?: number;
onUserTimeout: () => void; onUserTimeout: () => void;
showValidatorBadgeNotice?: boolean;
} }
export function ActiveGateScreen({ export function ActiveGateScreen({
@@ -24,17 +30,41 @@ export function ActiveGateScreen({
rfidState, rfidState,
rfidBuffer, rfidBuffer,
currentUser, currentUser,
notFoundBadge,
loading, loading,
error, error,
onCancelUser, onCancelUser,
onLogout, onLogout,
userTimeoutSeconds = 60, userTimeoutSeconds = 60,
onUserTimeout, onUserTimeout,
showValidatorBadgeNotice = false,
}: ActiveGateScreenProps) { }: 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 ( return (
<div className="min-h-screen flex flex-col bg-gradient-to-br from-slate-50 to-slate-100"> <div className="min-h-screen flex flex-col bg-gradient-to-br from-slate-50 to-slate-100">
{/* Header */} {/* Header */}
<header className="p-4 md:p-6 flex items-center justify-between border-b bg-white/80 backdrop-blur shadow-sm"> <header
className="p-4 md:p-6 flex items-center justify-between border-b bg-white/80 backdrop-blur shadow-sm">
<Logo size="md"/> <Logo size="md"/>
<div className="flex items-center gap-4 md:gap-8"> <div className="flex items-center gap-4 md:gap-8">
<div className="text-right hidden sm:block"> <div className="text-right hidden sm:block">
@@ -51,12 +81,23 @@ export function ActiveGateScreen({
</div> </div>
</header> </header>
{/* Notifica Badge Validatore Ignorato */}
{showValidatorBadgeNotice && (
<div className="absolute top-20 left-1/2 -translate-x-1/2 z-50 animate-fade-in">
<div className="bg-amber-100 border border-amber-400 text-amber-800 px-6 py-3 rounded-xl shadow-lg">
<p className="font-semibold">Badge validatore rilevato</p>
<p className="text-sm">Se il validatore è cambiato, esci e rilogga con il nuovo badge.</p>
</div>
</div>
)}
{/* Main Content */} {/* Main Content */}
<main className="flex-1 flex items-center justify-center p-4 md:p-8"> <main className="flex-1 flex items-center justify-center p-4 md:p-8">
{loading ? ( {loading ? (
// Loading state // Loading state
<div className="glass rounded-3xl p-12 shadow-xl animate-fade-in text-center"> <div className="glass rounded-3xl p-12 shadow-xl animate-fade-in text-center">
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-focolare-blue/10 mb-6"> <div
className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-focolare-blue/10 mb-6">
<svg <svg
className="animate-spin h-10 w-10 text-focolare-blue" className="animate-spin h-10 w-10 text-focolare-blue"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -79,10 +120,42 @@ export function ActiveGateScreen({
</div> </div>
<p className="text-xl text-gray-600">Caricamento dati...</p> <p className="text-xl text-gray-600">Caricamento dati...</p>
</div> </div>
) : notFoundBadge ? (
// Badge non trovato
<div className="glass rounded-3xl p-12 md:p-16 shadow-xl text-center max-w-2xl w-full">
<div
className="inline-flex items-center justify-center w-32 h-32 rounded-full bg-error/10 mb-8">
<svg
className="h-16 w-16 text-error"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<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 ? (
// Error state // Error state
<div className="glass rounded-3xl p-12 shadow-xl animate-slide-up text-center max-w-md"> <div className="glass rounded-3xl p-12 shadow-xl text-center max-w-md">
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-error/10 mb-6"> <div
className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-error/10 mb-6">
<svg <svg
className="h-10 w-10 text-error" className="h-10 w-10 text-error"
fill="none" fill="none"
@@ -104,7 +177,7 @@ export function ActiveGateScreen({
</div> </div>
) : currentUser ? ( ) : currentUser ? (
// User found - Decision screen // User found - Decision screen
<div className="w-full max-w-4xl animate-slide-up"> <div className="w-full max-w-5xl animate-slide-up">
<div className="glass rounded-3xl p-6 md:p-10 shadow-xl"> <div className="glass rounded-3xl p-6 md:p-10 shadow-xl">
{/* Timer bar */} {/* Timer bar */}
<div className="mb-6"> <div className="mb-6">
@@ -127,7 +200,9 @@ export function ActiveGateScreen({
✓ Utente ammesso all'ingresso ✓ Utente ammesso all'ingresso
</p> </p>
<p className="text-lg text-gray-600"> <p className="text-lg text-gray-600">
Passa il <span className="font-bold text-focolare-blue">badge VALIDATORE</span> per confermare l'accesso Passa il <span
className="font-bold text-focolare-blue">badge VALIDATORE</span> per
confermare l'accesso
</p> </p>
</div> </div>
) : ( ) : (
@@ -156,10 +231,12 @@ export function ActiveGateScreen({
</div> </div>
) : ( ) : (
// Idle - Waiting for participant // Idle - Waiting for participant
<div className="glass rounded-3xl p-12 shadow-xl animate-fade-in text-center max-w-xl"> <div
<div className="inline-flex items-center justify-center w-32 h-32 rounded-full bg-focolare-blue/10 mb-8"> className="glass rounded-3xl p-12 md:p-16 shadow-xl animate-fade-in text-center max-w-3xl w-full">
<div
className="inline-flex items-center justify-center w-40 h-40 rounded-full bg-focolare-blue/10 mb-10">
<svg <svg
className="w-16 h-16 text-focolare-blue" className="w-20 h-20 text-focolare-blue"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
@@ -173,28 +250,33 @@ export function ActiveGateScreen({
</svg> </svg>
</div> </div>
<h1 className="text-4xl font-bold text-focolare-blue mb-4"> <h1 className="text-5xl font-bold text-focolare-blue mb-6">
Varco Attivo Varco Attivo
</h1> </h1>
<p className="text-2xl text-gray-600 mb-8"> <p className="text-3xl text-gray-600 mb-10">
In attesa del partecipante... In attesa del partecipante...
</p> </p>
<div className="py-8 px-6 bg-focolare-orange/10 rounded-2xl border-2 border-dashed border-focolare-orange/40"> <div
className="py-10 px-8 bg-focolare-orange/10 rounded-2xl border-2 border-dashed border-focolare-orange/40">
<div className="flex items-center justify-center gap-4"> <div className="flex items-center justify-center gap-4">
<svg <svg
className="w-10 h-10 text-focolare-orange animate-pulse" className="w-12 h-12 text-focolare-orange animate-pulse"
fill="currentColor" fill="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<path d="M20 4H4c-1.11 0-1.99.89-1.99 2L2 18c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V6c0-1.11-.89-2-2-2zm0 14H4v-6h16v6zm0-10H4V6h16v2z"/> <path
d="M20 4H4c-1.11 0-1.99.89-1.99 2L2 18c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V6c0-1.11-.89-2-2-2zm0 14H4v-6h16v6zm0-10H4V6h16v2z"/>
</svg> </svg>
<span className="text-2xl text-focolare-orange font-medium"> <span className="text-3xl text-focolare-orange font-medium">
Passa il badge Passa il badge
</span> </span>
</div> </div>
</div> </div>
{/* Banner NumLock per desktop */}
<NumLockBanner className="mt-8"/>
</div> </div>
)} )}
</main> </main>

View File

@@ -0,0 +1,208 @@
/**
* Debug Screen - Focolari Voting System
*
* Pagina diagnostica per debug del lettore RFID.
* Mostra in tempo reale gli eventi tastiera e lo stato dello scanner.
*/
import {useRFIDScanner, VALID_PATTERNS} from '../hooks/useRFIDScanner';
import {Button, Logo} from '../components';
interface DebugScreenProps {
onBack: () => void;
}
export function DebugScreen({onBack}: DebugScreenProps) {
const {
state,
buffer,
lastScan,
activePattern,
keyLog,
reset,
} = useRFIDScanner({
onScan: (code) => {
console.log('[DEBUG] Scan received:', code);
},
onTimeout: () => {
console.log('[DEBUG] Timeout occurred');
},
});
const formatTimestamp = (ts: number) => {
const date = new Date(ts);
return date.toLocaleTimeString('it-IT', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3,
});
};
return (
<div className="min-h-screen bg-slate-900 text-white p-4 md:p-6">
{/* Header */}
<header className="flex items-center justify-between mb-6">
<div className="flex items-center gap-4">
<Logo size="sm"/>
<h1 className="text-2xl font-bold text-white">Debug RFID</h1>
</div>
<Button variant="secondary" onClick={onBack}>
Torna all'app
</Button>
</header>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Scanner Status */}
<div className="bg-slate-800 rounded-xl p-6">
<h2 className="text-xl font-semibold mb-4 text-focolare-orange">
📡 Stato Scanner
</h2>
<div className="space-y-4">
{/* State */}
<div className="flex items-center justify-between p-3 bg-slate-700 rounded-lg">
<span className="text-slate-300">Stato</span>
<span className={`px-3 py-1 rounded-full font-mono font-bold ${
state === 'scanning'
? 'bg-focolare-orange text-white animate-pulse'
: 'bg-slate-600 text-slate-300'
}`}>
{state.toUpperCase()}
</span>
</div>
{/* Active Pattern */}
<div className="flex items-center justify-between p-3 bg-slate-700 rounded-lg">
<span className="text-slate-300">Pattern Attivo</span>
<span className="font-mono text-lg">
{activePattern
? `${activePattern.name} (${activePattern.start} → ${activePattern.end})`
: ''
}
</span>
</div>
{/* Buffer */}
<div className="p-3 bg-slate-700 rounded-lg">
<span className="text-slate-300 block mb-2">Buffer Corrente</span>
<div className="font-mono text-2xl bg-slate-900 p-3 rounded min-h-[50px] break-all">
{buffer || <span className="text-slate-500">(vuoto)</span>}
</div>
</div>
{/* Last Scan */}
<div className="p-3 bg-slate-700 rounded-lg">
<span className="text-slate-300 block mb-2">Ultimo Codice Rilevato</span>
{lastScan ? (
<div className="bg-green-900/50 border border-green-500 rounded p-3">
<p className="font-mono text-2xl text-green-400">{lastScan.code}</p>
<p className="text-sm text-green-300 mt-1">
{formatTimestamp(lastScan.timestamp)}
</p>
</div>
) : (
<div className="bg-slate-900 p-3 rounded text-slate-500">
Nessuna scansione
</div>
)}
</div>
{/* Reset Button */}
<Button variant="secondary" onClick={reset} className="w-full">
Reset Scanner
</Button>
</div>
</div>
{/* Key Log */}
<div className="bg-slate-800 rounded-xl p-6">
<h2 className="text-xl font-semibold mb-4 text-focolare-blue">
⌨️ Log Tastiera (ultimi 20)
</h2>
<div className="bg-slate-900 rounded-lg overflow-hidden">
<div className="grid grid-cols-3 gap-2 p-3 bg-slate-700 text-sm font-semibold">
<span>Ora</span>
<span>Key</span>
<span>Code</span>
</div>
<div className="max-h-[400px] overflow-y-auto">
{keyLog.length === 0 ? (
<div className="p-4 text-slate-500 text-center">
Premi un tasto per vedere i log...
</div>
) : (
keyLog.map((entry, index) => {
const isStartSentinel = VALID_PATTERNS.some(p => p.start === entry.key);
const isEndSentinel = VALID_PATTERNS.some(p => p.end === entry.key);
let rowClass = 'bg-slate-900';
if (isStartSentinel) rowClass = 'bg-green-900/30';
if (isEndSentinel) rowClass = 'bg-blue-900/30';
return (
<div
key={`${entry.timestamp}-${index}`}
className={`grid grid-cols-3 gap-2 p-2 border-b border-slate-700 text-sm ${rowClass}`}
>
<span className="text-slate-400 font-mono text-xs">
{formatTimestamp(entry.timestamp)}
</span>
<span className={`font-mono ${
isStartSentinel ? 'text-green-400 font-bold' :
isEndSentinel ? 'text-blue-400 font-bold' :
'text-white'
}`}>
{entry.key === ' ' ? '(space)' :
entry.key === 'Enter' ? ' Enter' :
entry.key}
</span>
<span className="text-slate-400 font-mono text-xs">
{entry.code}
</span>
</div>
);
})
)}
</div>
</div>
</div>
{/* Pattern Reference */}
<div className="bg-slate-800 rounded-xl p-6 lg:col-span-2">
<h2 className="text-xl font-semibold mb-4 text-focolare-yellow">
📋 Pattern Supportati
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{VALID_PATTERNS.map((pattern) => (
<div
key={pattern.name}
className="bg-slate-700 rounded-lg p-4 text-center"
>
<h3 className="text-lg font-bold mb-2">{pattern.name}</h3>
<div className="flex items-center justify-center gap-4 text-3xl font-mono">
<span className="text-green-400">{pattern.start}</span>
<span className="text-slate-500">→</span>
<span className="text-blue-400">{pattern.end}</span>
</div>
<p className="text-sm text-slate-400 mt-2">
Esempio: {pattern.start}123456{pattern.end}
</p>
</div>
))}
</div>
<p className="mt-4 text-sm text-slate-400">
💡 <strong>Nota:</strong> Il lettore RFID invia anche Enter (↵) dopo l'ultimo carattere.
L'hook lo gestisce automaticamente.
</p>
</div>
</div>
</div>
);
}
export default DebugScreen;

View File

@@ -3,7 +3,7 @@
* Modal per errori * Modal per errori
*/ */
import { Modal, Button } from '../components'; import {Button, Modal} from '../components';
interface ErrorModalProps { interface ErrorModalProps {
isOpen: boolean; isOpen: boolean;
@@ -28,7 +28,8 @@ export function ErrorModal({
<div className="text-center text-white p-8 max-w-2xl"> <div className="text-center text-white p-8 max-w-2xl">
{/* Error Icon */} {/* Error Icon */}
<div className="mb-8"> <div className="mb-8">
<div className="inline-flex items-center justify-center w-32 h-32 rounded-full bg-white/20 animate-pulse-error"> <div
className="inline-flex items-center justify-center w-32 h-32 rounded-full bg-white/20 animate-pulse-error">
<svg <svg
className="w-20 h-20 text-white" className="w-20 h-20 text-white"
fill="none" fill="none"

View File

@@ -2,7 +2,9 @@
* Loading Screen - Focolari Voting System * Loading Screen - Focolari Voting System
*/ */
import {useCallback, useEffect, useState} from 'react';
import {Logo} from '../components'; import {Logo} from '../components';
import {checkServerHealth} from '../services/api';
interface LoadingScreenProps { interface LoadingScreenProps {
message?: string; message?: string;
@@ -15,8 +17,51 @@ export function LoadingScreen({
error, error,
onRetry onRetry
}: LoadingScreenProps) { }: LoadingScreenProps) {
const [isRetrying, setIsRetrying] = useState(false);
const [serverStatus, setServerStatus] = useState<'checking' | 'online' | 'offline'>('checking');
// Ping automatico quando c'è un errore
useEffect(() => {
if (!error) return;
const checkServer = async () => {
setServerStatus('checking');
const isOnline = await checkServerHealth();
setServerStatus(isOnline ? 'online' : 'offline');
};
checkServer();
// Ripeti il ping ogni 3 secondi
const interval = setInterval(checkServer, 3000);
return () => clearInterval(interval);
}, [error]);
const handleRetry = useCallback(async () => {
if (!onRetry) return;
setIsRetrying(true);
const isOnline = await checkServerHealth();
if (isOnline) {
onRetry();
} else {
setServerStatus('offline');
}
setIsRetrying(false);
}, [onRetry]);
// Se il server torna online, riprova automaticamente
useEffect(() => {
if (serverStatus === 'online' && error && onRetry) {
console.log('[FLOW] Server tornato online, ricarico...');
onRetry();
}
}, [serverStatus, error, onRetry]);
return ( return (
<div className="min-h-screen flex flex-col items-center justify-center p-8 bg-gradient-to-br from-focolare-blue/5 to-focolare-blue/20"> <div
className="min-h-screen flex flex-col items-center justify-center p-8 bg-gradient-to-br from-focolare-blue/5 to-focolare-blue/20">
<div className="glass rounded-3xl p-12 shadow-xl animate-fade-in max-w-lg w-full text-center"> <div className="glass rounded-3xl p-12 shadow-xl animate-fade-in max-w-lg w-full text-center">
<Logo size="lg" showText={false}/> <Logo size="lg" showText={false}/>
@@ -27,7 +72,8 @@ export function LoadingScreen({
{!error ? ( {!error ? (
<> <>
<div className="mt-8"> <div className="mt-8">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-focolare-blue/10"> <div
className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-focolare-blue/10">
<svg <svg
className="animate-spin h-8 w-8 text-focolare-blue" className="animate-spin h-8 w-8 text-focolare-blue"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -70,14 +116,32 @@ export function LoadingScreen({
</svg> </svg>
</div> </div>
</div> </div>
<p className="mt-4 text-error text-lg font-semibold">{error}</p> <p className="mt-4 text-error text-lg font-semibold">{error}</p>
{/* Server status indicator */}
<div className="mt-4 flex items-center justify-center gap-2">
<span className={`w-3 h-3 rounded-full ${
serverStatus === 'checking' ? 'bg-yellow-500 animate-pulse' :
serverStatus === 'online' ? 'bg-green-500' :
'bg-red-500'
}`}/>
<span className="text-sm text-gray-600">
{serverStatus === 'checking' && 'Verifica connessione...'}
{serverStatus === 'online' && 'Server raggiungibile - riconnessione...'}
{serverStatus === 'offline' && 'Server non raggiungibile'}
</span>
</div>
{onRetry && ( {onRetry && (
<button <button
onClick={onRetry} onClick={handleRetry}
disabled={isRetrying}
className="mt-6 px-8 py-3 bg-focolare-blue text-white rounded-xl className="mt-6 px-8 py-3 bg-focolare-blue text-white rounded-xl
font-semibold hover:bg-focolare-blue-dark transition-colors" font-semibold hover:bg-focolare-blue/90 transition-colors
disabled:opacity-50 disabled:cursor-not-allowed"
> >
Riprova {isRetrying ? 'Verifica in corso...' : 'Riprova'}
</button> </button>
)} )}
</> </>

View File

@@ -1,21 +1,19 @@
/** /**
* Success Modal - Focolari Voting System * Success Modal - Focolari Voting System
* Modal fullscreen per conferma ingresso * Modal fullscreen per conferma ingresso con carosello multilingua
*/ */
import { Modal } from '../components'; import {Modal, WelcomeCarousel} from '../components';
interface SuccessModalProps { interface SuccessModalProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
welcomeMessage: string;
userName?: string; userName?: string;
} }
export function SuccessModal({ export function SuccessModal({
isOpen, isOpen,
onClose, onClose,
welcomeMessage,
userName userName
}: SuccessModalProps) { }: SuccessModalProps) {
return ( return (
@@ -26,10 +24,11 @@ export function SuccessModal({
autoCloseMs={5000} autoCloseMs={5000}
fullscreen fullscreen
> >
<div className="text-center text-white p-8"> <div className="flex flex-col items-center justify-center min-h-screen p-8">
{/* Success Icon */} {/* Success Icon */}
<div className="mb-8"> <div className="mb-8">
<div className="inline-flex items-center justify-center w-32 h-32 rounded-full bg-white/20 animate-pulse-glow"> <div
className="inline-flex items-center justify-center w-32 h-32 rounded-full bg-white/20 animate-pulse-glow">
<svg <svg
className="w-20 h-20 text-white" className="w-20 h-20 text-white"
fill="none" fill="none"
@@ -46,26 +45,17 @@ export function SuccessModal({
</div> </div>
</div> </div>
{/* User Name */} {/* Carosello Messaggi Benvenuto */}
{userName && ( <WelcomeCarousel userName={userName}/>
<h2 className="text-4xl md:text-5xl font-bold mb-4 animate-slide-up">
{userName}
</h2>
)}
{/* Welcome Message */}
<h1 className="text-5xl md:text-7xl font-bold mb-8 animate-slide-up">
{welcomeMessage}
</h1>
{/* Sub text */} {/* Sub text */}
<p className="text-2xl md:text-3xl opacity-80 animate-fade-in"> <p className="text-2xl md:text-3xl text-white/80 mt-8 animate-fade-in">
Ingresso registrato con successo Ingresso registrato con successo
</p> </p>
{/* Auto-close indicator */} {/* Auto-close indicator */}
<div className="mt-12"> <div className="mt-12 w-full max-w-md">
<div className="w-64 h-2 bg-white/30 rounded-full mx-auto overflow-hidden"> <div className="w-full h-2 bg-white/30 rounded-full overflow-hidden">
<div <div
className="h-full bg-white rounded-full" className="h-full bg-white rounded-full"
style={{ style={{

View File

@@ -2,9 +2,9 @@
* Validator Login Screen - Focolari Voting System * Validator Login Screen - Focolari Voting System
*/ */
import { useState, useRef, useEffect } from 'react'; import {useEffect, useRef, useState} from 'react';
import { Logo, Button, Input, RFIDStatus } from '../components'; import {Button, Input, Logo, NumLockBanner, RFIDStatus} from '../components';
import type { RoomInfo, RFIDScannerState } from '../types'; import type {RFIDScannerState, RoomInfo} from '../types';
interface ValidatorLoginScreenProps { interface ValidatorLoginScreenProps {
roomInfo: RoomInfo; roomInfo: RoomInfo;
@@ -62,7 +62,8 @@ export function ValidatorLoginScreen({
// Attesa badge validatore // Attesa badge validatore
<> <>
<div className="text-center"> <div className="text-center">
<div className="inline-flex items-center justify-center w-24 h-24 rounded-full bg-focolare-blue/10 mb-6"> <div
className="inline-flex items-center justify-center w-24 h-24 rounded-full bg-focolare-blue/10 mb-6">
<svg <svg
className="w-12 h-12 text-focolare-blue" className="w-12 h-12 text-focolare-blue"
fill="none" fill="none"
@@ -83,30 +84,44 @@ export function ValidatorLoginScreen({
</h1> </h1>
<p className="text-xl text-gray-600 mb-8"> <p className="text-xl text-gray-600 mb-8">
Passa il badge del <span className="font-semibold text-focolare-blue">Validatore</span> per iniziare Passa il badge del <span
className="font-semibold text-focolare-blue">Validatore</span> per iniziare
</p> </p>
<div className="py-8 px-6 bg-focolare-blue/5 rounded-2xl border-2 border-dashed border-focolare-blue/30"> <div
className="py-8 px-6 bg-focolare-blue/5 rounded-2xl border-2 border-dashed border-focolare-blue/30">
<div className="flex items-center justify-center gap-3"> <div className="flex items-center justify-center gap-3">
<svg <svg
className="w-8 h-8 text-focolare-blue animate-pulse" className="w-8 h-8 text-focolare-blue animate-pulse"
fill="currentColor" fill="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<path d="M20 4H4c-1.11 0-1.99.89-1.99 2L2 18c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V6c0-1.11-.89-2-2-2zm0 14H4v-6h16v6zm0-10H4V6h16v2z"/> <path
d="M20 4H4c-1.11 0-1.99.89-1.99 2L2 18c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V6c0-1.11-.89-2-2-2zm0 14H4v-6h16v6zm0-10H4V6h16v2z"/>
</svg> </svg>
<span className="text-xl text-focolare-blue font-medium"> <span className="text-xl text-focolare-blue font-medium">
In attesa del badge... In attesa del badge...
</span> </span>
</div> </div>
</div> </div>
{/* Banner NumLock per desktop */}
<NumLockBanner className="mt-6"/>
{/* Messaggio errore */}
{error && (
<div className="mt-6 p-4 bg-red-50 border border-red-200 rounded-lg">
<p className="text-red-700">{error}</p>
</div>
)}
</div> </div>
</> </>
) : ( ) : (
// Form password // Form password
<> <>
<div className="text-center mb-8"> <div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-success/10 mb-4"> <div
className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-success/10 mb-4">
<svg <svg
className="w-10 h-10 text-success" className="w-10 h-10 text-success"
fill="none" fill="none"

View File

@@ -4,3 +4,4 @@ export { ValidatorLoginScreen } from './ValidatorLoginScreen';
export {ActiveGateScreen} from './ActiveGateScreen'; export {ActiveGateScreen} from './ActiveGateScreen';
export {SuccessModal} from './SuccessModal'; export {SuccessModal} from './SuccessModal';
export {ErrorModal} from './ErrorModal'; export {ErrorModal} from './ErrorModal';
export {DebugScreen} from './DebugScreen';

View File

@@ -2,28 +2,39 @@
* Focolari Voting System - API Service * Focolari Voting System - API Service
*/ */
import type { import type {EntryRequest, EntryResponse, LoginRequest, LoginResponse, RoomInfo, User} from '../types';
RoomInfo,
User,
LoginRequest,
LoginResponse,
EntryRequest,
EntryResponse
} from '../types';
const API_BASE_URL = 'http://localhost:8000'; // Path relativi: funziona sia in dev (proxy Vite) che in produzione (stesso server)
const API_BASE_URL = '';
// ============================================
// LOGGING
// ============================================
const log = (message: string, ...args: unknown[]) => {
console.log(`[API] ${message}`, ...args);
};
const logError = (message: string, ...args: unknown[]) => {
console.error(`[API] ${message}`, ...args);
};
/** /**
* Custom error class for API errors * Custom error class for API errors
*/ */
export class ApiError extends Error { export class ApiError extends Error {
public statusCode: number;
public detail?: string;
constructor( constructor(
message: string, message: string,
public statusCode: number, statusCode: number,
public detail?: string detail?: string
) { ) {
super(message); super(message);
this.name = 'ApiError'; this.name = 'ApiError';
this.statusCode = statusCode;
this.detail = detail;
} }
} }
@@ -34,6 +45,8 @@ async function apiFetch<T>(
endpoint: string, endpoint: string,
options?: RequestInit options?: RequestInit
): Promise<T> { ): Promise<T> {
log(`Fetching ${options?.method || 'GET'} ${endpoint}`);
try { try {
const response = await fetch(`${API_BASE_URL}${endpoint}`, { const response = await fetch(`${API_BASE_URL}${endpoint}`, {
...options, ...options,
@@ -45,6 +58,7 @@ async function apiFetch<T>(
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); const errorData = await response.json().catch(() => ({}));
logError(`Error ${response.status}: ${errorData.detail || 'Unknown error'}`);
throw new ApiError( throw new ApiError(
errorData.detail || `HTTP Error ${response.status}`, errorData.detail || `HTTP Error ${response.status}`,
response.status, response.status,
@@ -52,11 +66,14 @@ async function apiFetch<T>(
); );
} }
return await response.json(); const data = await response.json();
log(`Response OK from ${endpoint}`);
return data;
} catch (error) { } catch (error) {
if (error instanceof ApiError) { if (error instanceof ApiError) {
throw error; throw error;
} }
logError('Connection error:', error);
throw new ApiError( throw new ApiError(
'Errore di connessione al server', 'Errore di connessione al server',
0, 0,
@@ -85,6 +102,7 @@ export async function loginValidator(
badge: string, badge: string,
password: string password: string
): Promise<LoginResponse> { ): Promise<LoginResponse> {
log(`Login attempt for badge: ${badge}`);
const payload: LoginRequest = {badge, password}; const payload: LoginRequest = {badge, password};
return apiFetch<LoginResponse>('/login-validate', { return apiFetch<LoginResponse>('/login-validate', {
method: 'POST', method: 'POST',
@@ -97,6 +115,7 @@ export async function loginValidator(
* Ottiene i dati anagrafici di un utente tramite badge * Ottiene i dati anagrafici di un utente tramite badge
*/ */
export async function getUserByBadge(badgeCode: string): Promise<User> { export async function getUserByBadge(badgeCode: string): Promise<User> {
log(`Fetching anagrafica for badge: ${badgeCode}`);
return apiFetch<User>(`/anagrafica/${encodeURIComponent(badgeCode)}`); return apiFetch<User>(`/anagrafica/${encodeURIComponent(badgeCode)}`);
} }
@@ -108,6 +127,7 @@ export async function requestEntry(
userBadge: string, userBadge: string,
validatorPassword: string validatorPassword: string
): Promise<EntryResponse> { ): Promise<EntryResponse> {
log(`Entry request for badge: ${userBadge}`);
const payload: EntryRequest = { const payload: EntryRequest = {
user_badge: userBadge, user_badge: userBadge,
validator_password: validatorPassword, validator_password: validatorPassword,

View File

@@ -1,101 +1,103 @@
/** /**
} * Focolari Voting System - TypeScript Types
variant?: 'success' | 'error' | 'info'; */
children: React.ReactNode;
onClose?: () => void;
isOpen: boolean;
export interface ModalProps {
}
paused?: boolean;
onExpire: () => void;
seconds: number;
export interface TimerProps {
}
showWarning?: boolean;
user: User;
export interface UserCardProps {
// ============================================
// Component Props Types
// ============================================
}
timestamp: number;
code: string;
export interface RFIDScanResult {
export type RFIDScannerState = 'idle' | 'scanning';
// ============================================
// RFID Scanner Types
// ============================================
}
expiresAt: number;
loginTime: number;
token: string;
badge: string;
export interface ValidatorSession {
| 'entry-error';
| 'entry-success'
| 'showing-user'
| 'gate-active'
| 'validator-password'
| 'waiting-validator'
| 'loading'
export type AppState =
// ============================================
// Application State Types
// ============================================
}
validator_password: string;
user_badge: string;
export interface EntryRequest {
}
password: string;
badge: string;
export interface LoginRequest {
// ============================================
// Request Types
// ============================================
}
welcome_message?: string;
message: string;
success: boolean;
export interface EntryResponse {
}
token?: string;
message: string;
success: boolean;
export interface LoginResponse {
}
warning?: string;
ammesso: boolean;
ruolo: 'Tecnico' | 'Votante' | 'Ospite';
url_foto: string;
cognome: string;
nome: string;
badge_code: string;
export interface User {
}
meeting_id: string;
room_name: string;
export interface RoomInfo {
// ============================================ // ============================================
// API Response Types // API Response Types
// ============================================ // ============================================
*/ export interface RoomInfo {
* Focolari Voting System - TypeScript Types room_name: string;
meeting_id: string;
server_start_time: number;
}
export interface User {
badge_code: string;
nome: string;
cognome: string;
url_foto: string;
ruolo: 'Tecnico' | 'Votante' | 'Ospite';
ammesso: boolean;
warning?: string;
}
export interface LoginResponse {
success: boolean;
message: string;
token?: string;
}
export interface EntryResponse {
success: boolean;
message: string;
}
// ============================================
// Request Types
// ============================================
export interface LoginRequest {
badge: string;
password: string;
}
export interface EntryRequest {
user_badge: string;
validator_password: string;
}
// ============================================
// Application State Types
// ============================================
export type AppState =
| 'loading'
| 'waiting-validator'
| 'validator-password'
| 'gate-active'
| 'showing-user'
| 'entry-success'
| 'entry-error';
export interface ValidatorSession {
badge: string;
password: string;
token: string;
loginTime: number;
expiresAt: number;
serverStartTime: number; // Per invalidare se il server riparte
}
// ============================================
// RFID Scanner Types
// ============================================
export type RFIDScannerState = 'idle' | 'scanning';
export interface RFIDScanResult {
code: string;
timestamp: number;
}
// ============================================
// Component Props Types
// ============================================
export interface UserCardProps {
user: User;
showWarning?: boolean;
}
export interface TimerProps {
seconds: number;
onExpire: () => void;
paused?: boolean;
}
export interface ModalProps {
isOpen: boolean;
onClose?: () => void;
children: React.ReactNode;
variant?: 'success' | 'error' | 'info';
}

View File

@@ -3,11 +3,16 @@
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022", "target": "ES2022",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"], "lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"module": "ESNext", "module": "ESNext",
"types": ["vite/client"], "types": [
"vite/client"
],
"skipLibCheck": true, "skipLibCheck": true,
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
@@ -15,7 +20,6 @@
"moduleDetection": "force", "moduleDetection": "force",
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
/* Linting */ /* Linting */
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
@@ -24,5 +28,7 @@
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true
}, },
"include": ["src"] "include": [
"src"
]
} }

View File

@@ -1,7 +1,11 @@
{ {
"files": [], "files": [],
"references": [ "references": [
{ "path": "./tsconfig.app.json" }, {
{ "path": "./tsconfig.node.json" } "path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
] ]
} }

View File

@@ -2,18 +2,20 @@
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023", "target": "ES2023",
"lib": ["ES2023"], "lib": [
"ES2023"
],
"module": "ESNext", "module": "ESNext",
"types": ["node"], "types": [
"node"
],
"skipLibCheck": true, "skipLibCheck": true,
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
"moduleDetection": "force", "moduleDetection": "force",
"noEmit": true, "noEmit": true,
/* Linting */ /* Linting */
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
@@ -22,5 +24,7 @@
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true
}, },
"include": ["vite.config.ts"] "include": [
"vite.config.ts"
]
} }

View File

@@ -5,4 +5,36 @@ import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react(), tailwindcss()], plugins: [react(), tailwindcss()],
// Path relativi per funzionare su qualsiasi IP/porta
base: './',
// Build nella cartella dist del frontend
build: {
outDir: 'dist',
emptyOutDir: true,
},
// Proxy API in sviluppo verso il backend (porta 8000)
// In produzione il backend serve direttamente il frontend
server: {
proxy: {
'/info-room': {
target: 'http://127.0.0.1:8000',
changeOrigin: true,
},
'/login-validate': {
target: 'http://127.0.0.1:8000',
changeOrigin: true,
},
'/anagrafica': {
target: 'http://127.0.0.1:8000',
changeOrigin: true,
},
'/entry-request': {
target: 'http://127.0.0.1:8000',
changeOrigin: true,
},
},
},
}) })