Files
segnapunti/CLAUDE.md
T
davide ec19eb62c3 feat(docker): aggiungi flusso di sviluppo con hot reload via compose
Dockerfile diventa multi-stage con target `dev` (Vite + bind-mount) oltre
a `builder`/`runtime`; docker-compose.dev.yml lo usa per un ambiente di
sviluppo containerizzato equivalente a `npm run dev`, senza toccare il
compose di produzione (che resta pull-only dal registry). Documentato in
README e CLAUDE.md.
2026-07-08 10:23:27 +02:00

8.2 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 iniziale
  • applyAction(state, action) — reducer immutabile (deep-clone via structuredClone)
  • checkVittoria(state) — vittoria set: 25 punti/vantaggio 2 (15 nel set decisivo)
  • punteggio / setVinti / servizio — derivati dalla striscia, 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 da startTimeout (solo se non già attivo, per non duplicare l'evento su un restart).
  • set.cambi: { team, in, out, punteggio }, uno per sostituzione applicata da confermaCambi (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.jsgetNetworkIPs (sorgente iniettabile), stampa gli URL all'avvio. src/spot-utils.jslistSpotVideos(spotDir): elenca i .mp4 (ordinati) di spot/; [] se la cartella manca.

Time out / spot videostartTimeout (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 /displaydist/index.html, /controllerdist/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 display o controller; solo i controller inviano azioni.
  • Stato immutabileapplyAction non muta mai lo stato, restituisce sempre un nuovo oggetto.
  • Un solo controller/display attivi — nessuna conflict resolution per controller simultanei.
  • Stato persistentesrc/persist.js salva in .segnapunti/state.json dopo 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

Dockerfile è multi-stage con 4 target: deps (dipendenze condivise), dev (hot reload Vite), builder (build frontend), runtime (immagine di produzione, target di default).

Produzionedocker-compose.yml usa l'immagine già pubblicata sul registry Gitea self-hosted (santantonio.sytes.net), niente build locale:

docker compose up -d   # Avvia con immagine dal registry privato (porta 3000)

Sviluppodocker-compose.dev.yml builda il target dev in locale e monta il codice sorgente per l'hot reload (Vite su porta 5173); node_modules resta un volume anonimo del container per non farsi sovrascrivere dal bind-mount (evita anche problemi di binari nativi arch-specific tra host e container):

docker compose -f docker-compose.dev.yml up --build

Per buildare in locale l'immagine di produzione (es. per testarla prima del push sul registry): docker build -t segnapunti:local . (usa il target runtime di default).

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 → solo src/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.