ec19eb62c3
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.
112 lines
8.2 KiB
Markdown
112 lines
8.2 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
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.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 <squadra>" 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 `display` o `controller`; solo i controller inviano azioni.
|
|
- **Stato immutabile** — `applyAction` non muta mai lo stato, restituisce sempre un nuovo oggetto.
|
|
- **Un solo controller/display attivi** — nessuna conflict resolution per controller simultanei.
|
|
- **Stato persistente** — `src/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).
|
|
|
|
**Produzione** — `docker-compose.yml` usa l'immagine già pubblicata sul registry Gitea self-hosted (`santantonio.sytes.net`), niente build locale:
|
|
|
|
```bash
|
|
docker compose up -d # Avvia con immagine dal registry privato (porta 3000)
|
|
```
|
|
|
|
**Sviluppo** — `docker-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):
|
|
|
|
```bash
|
|
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.
|