Riscrittura più concisa (elenchi puntati al posto di prosa, frasi accorciate); tutti i concetti tecnici restano invariati.
7.4 KiB
CLAUDE.md
Guida per Claude Code su questo repository.
Scopo del progetto
Segnapunti Anto: PWA segnapunti pallavolo in tempo reale. Server Express/WebSocket gestisce lo stato di gioco; due interfacce Vue — display (tabellone pubblico) e controller (pannello operatore) — sincronizzate via WebSocket.
Comandi
npm run dev # Vite dev — display: :5173/display, controller: :5173/controller
npm run serve # Build + avvio produzione — display: :3000/display, controller: :3000/controller
npm run test # Vitest watch
npm run test:all # Tutte le suite in una sola esecuzione
npm run test:unit # Solo unit + integration
npm run test:component # Componenti Vue (happy-dom)
npm run test:stress # Load test (50+ client concorrenti)
npm run test:e2e # Playwright E2E (richiede `npm run serve` attivo)
npm run test:e2e:ui # Playwright con UI interattiva
Singolo file di test: npx vitest run tests/unit/gameState.test.js
Architettura
Controller (Vue) ──WebSocket──┐
Display (Vue) ──WebSocket──┤── websocket-handler.js ── gameState.js
│ │
│ └── persist.js ── .segnapunti/state.json
src/gameState.js — logica di gioco, funzioni pure:
createInitialState()— stato inizialeapplyAction(state, action)— reducer immutabile (deep-clone viastructuredClone)checkVittoria(state)— vittoria set: 25 punti/vantaggio 2 (15 nel set decisivo)punteggio/setVinti/servizio— derivati dallastriscia, NON memorizzati direttamente nello stato
Punteggio, set e servizio si ricavano sempre dalla striscia. Al primo punto di ogni set, applyAction('incPunt') salva in formInizio uno snapshot della formazione di partenza (usato dal referto); decPunt lo rimuove se il set torna a 0-0.
Ogni voce della striscia accumula anche lo storico di cambi/time out del set, popolato lazy (nessun campo se il set non ha eventi), senza timestamp — il "quando" è il punteggio al momento dell'evento, non l'orologio:
set.timeouts:{ team, punteggio: { home, guest } }, appeso dastartTimeout(solo se non già attivo, per non duplicare l'evento su un restart).set.cambi:{ team, in, out, punteggio }, uno per sostituzione applicata daconfermaCambi(nulla se la validazione fallisce).
src/websocket-handler.js — riceve i messaggi, valida che il mittente sia controller (non display), chiama applyAction, fa broadcast del nuovo stato, poi invoca onStateChange per persistere.
src/wsMixin.js — mixin Vue lato client: connessione/riconnessione WebSocket (backoff esponenziale), espone i computed punt/servHome/set.
src/referto.js — genera il referto di gara: buildRefertoHtml(state, now) è pura (testabile, ritorna HTML); generaReferto(state) apre la finestra (window.open) e avvia la stampa. Per ogni set mostra formazione di partenza, griglia punti e (se presenti) set.timeouts/set.cambi ordinati per punteggio crescente.
src/persist.js — carica/salva lo stato su .segnapunti/state.json.
src/server-utils.js — getNetworkIPs (sorgente iniettabile), stampa gli URL all'avvio.
src/spot-utils.js — listSpotVideos(spotDir): elenca i .mp4 (ordinati) di spot/; [] se la cartella manca.
Time out / spot video — startTimeout (team, video opzionale) e stopTimeout impostano/azzerano state.timeout, timeoutTeam, timeoutVideo, timeoutStartedAt. startedAt è aggiunto da websocket-handler.js (non dal client), così gameState.js resta puro/deterministico; lo stesso handler arma un timer server-side di 30s che invia automaticamente stopTimeout (sopravvive a refresh/disconnessione del controller). Sul controller, il pulsante Time Out apre una modal per scegliere squadra + un .mp4 da spot/ (cartella in root, non versionata) o nessun video; durante il time out mostra il countdown, ripremerlo chiude in anticipo. Sul display, un overlay fullscreen con countdown copre il tabellone: riproduce il video (riallineandosi al punto giusto su riconnessione a metà time out) o mostra "TIME OUT " se manca il video o finisce prima dei 30s, con dissolvenza nell'ultimo secondo. Lista esposta da /spot/list (JSON), file serviti da /spot/<file>.mp4 sia in produzione (server.js) che in dev (vite-plugin-websocket.js). I video partono con audio; se l'autoplay con suono è bloccato si ripiega su muto — per audio reale il browser del display va avviato in kiosk con autoplay consentito (es. Chrome --autoplay-policy=no-user-gesture-required).
server.js — Express: espone createApp(distDir) (solo routing, testabile) e startServer(port) (HTTP + WebSocket). Serve /display → dist/index.html, /controller → dist/controller.html sulla porta 3000; endpoint WebSocket unico /ws. Si auto-avvia solo se eseguito come entrypoint.
vite-plugin-websocket.js — in dev, incorpora il server WebSocket nel dev server Vite con rewrite URL per /display e /controller.
Vincoli di design
- Logica solo server-side — i client sono pura UI; il server è l'unica source of truth.
- WebSocket role-based — i client si registrano come
displayocontroller; solo i controller inviano azioni. - Stato immutabile —
applyActionnon muta mai lo stato, restituisce sempre un nuovo oggetto. - Un solo controller/display attivi — nessuna conflict resolution per controller simultanei.
- Stato persistente —
src/persist.jssalva in.segnapunti/state.jsondopo ogni azione e ricarica all'avvio.
Layout dei test
| Suite | Percorso | Runner |
|---|---|---|
| Unit | tests/unit/ |
Vitest + Node |
| Integration | tests/integration/ |
Vitest + Node |
| Component | tests/component/ |
Vitest + Happy-DOM |
| Stress | tests/stress/ |
Vitest + Node |
| E2E | tests/e2e/ |
Playwright (Chromium, Firefox, Mobile Chrome) |
E2E gira in serie (workers: 1) per evitare race condition sullo stato WebSocket; richiede npm run serve attivo prima di npm run test:e2e.
Copertura statement/linee Vitest oltre il 99%. Per testare startServer() (server.js) o i rami platform-dependent di getNetworkIPs() (server-utils.js) senza effetti collaterali reali (scrittura su .segnapunti/state.json, dipendenza dalla piattaforma), mockare il modulo con vi.mock/vi.doMock (vedi tests/integration/startServer.test.js, tests/unit/server-utils-platform.test.js) invece di eseguire il codice reale.
Deploy
Docker, immagine pubblicata sul registry Gitea self-hosted (santantonio.sytes.net):
docker compose up -d # Avvia con immagine dal registry privato (porta 3000)
Volume ./.segnapunti persiste lo stato tra i riavvii del container.
Istruzioni operative
- Nuova funzionalità: valutare come si inserisce nel flusso
action → applyAction → broadcast, poi decidere se serve un nuovo action type o estendere uno esistente. - Regole di gioco → solo
src/gameState.js. Protocollo WebSocket → solosrc/websocket-handler.js. - Nuovo action type o regola di gioco → aggiungere test in
tests/unit/gameState.test.js. - Commenti e nomi di dominio in italiano (es.
servHome,striscia,nomi); nomi tecnici standard in inglese (state,action,handler). - Niente emoji in codice, UI (label, testi bottoni, banner) o commenti, salvo richiesta esplicita dell'utente.