fix: correzioni critiche e checklist test manuali

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

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

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

View File

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

View File

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

View File

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

View File

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