7 Commits

Author SHA1 Message Date
davide bd90a2efe4 chore(deps): risolve vulnerabilità npm audit e aggiorna a Node 24 LTS
Aggiorna vitest/@vitest/ui alla 4.1.10 (azzera le 14 vulnerabilità
segnalate da npm audit, incluse 2 critiche). Node 20 era ormai a fine
supporto: Dockerfile, .nvmrc ed engines in package.json passano a
Node 24 LTS; il README documenta i requisiti in una sezione dedicata
in cima al file, senza duplicarli altrove.
2026-07-15 10:58:52 +02:00
davide 93372a1299 fix(referto): rimuove il riepilogo set vinti ridondante
Il blocco "3 – 0 / SET VINTI" accanto alla tabella "Risultato finale"
duplicava l'informazione già mostrata in "Vince <squadra> 3 – 0".
2026-07-15 10:36:26 +02:00
davide 82c997d475 Merge branch 'issue#19'
Migliora il referto di gara: nomi squadra al posto di A/B, icona
servizio a inizio set, allineamento "vinto da" e colori squadra
coerenti col segnapunti live.
2026-07-15 10:26:50 +02:00
davide 35c49247a8 style(referto): allinea i colori squadra a quelli del segnapunti live
Punti in striscia e nomi in "Formazione di partenza" ora usano lo
stesso giallo (#f5c518) per la squadra di casa e blu (#2196f3) per
gli ospiti già usati in display/controller, invece di una palette
scollegata dal resto dell'app.
2026-07-15 10:25:57 +02:00
davide fec140df62 style(referto): allinea "vinto da" a destra nell'header del set
Separa punteggio ed etichetta vincitore su un flex con
justify-content: space-between per migliorare la leggibilità.
2026-07-15 10:11:40 +02:00
davide c6eb493f25 feat(referto): mostra l'icona del servizio a inizio set
Aggiunge un'icona nera (nuovo asset serv-nero.png, più visibile del
serv.png bianco su sfondo chiaro) accanto al nome della squadra che
serviva all'inizio di ogni set nel referto stampabile.

Corregge inoltre generaReferto: la stampa partiva subito dopo
document.close(), prima che l'icona (risorsa esterna asincrona)
finisse di caricare, risultando in un riquadro vuoto. Ora attende
l'evento 'load' della finestra popup prima di chiamare print().
2026-07-15 10:05:47 +02:00
davide 7cf84a94d9 fix(referto): mostra i nomi delle squadre al posto delle etichette A/B
L'header e la tabella del risultato finale usavano segnaposto generici
"A"/"B"; ora indicano esplicitamente casa/ospiti e i nomi reali delle
squadre.
2026-07-15 09:55:51 +02:00
8 changed files with 479 additions and 417 deletions
+1
View File
@@ -0,0 +1 @@
24
+2 -2
View File
@@ -1,5 +1,5 @@
# Stage 1: dipendenze condivise (build + dev) # Stage 1: dipendenze condivise (build + dev)
FROM node:20-alpine AS deps FROM node:24-alpine AS deps
WORKDIR /app WORKDIR /app
COPY package*.json ./ COPY package*.json ./
RUN npm ci RUN npm ci
@@ -16,7 +16,7 @@ COPY . .
RUN npm run build RUN npm run build
# Stage 4: runtime di produzione # Stage 4: runtime di produzione
FROM node:20-alpine AS runtime FROM node:24-alpine AS runtime
WORKDIR /app WORKDIR /app
ENV NODE_ENV=production PORT=3000 ENV NODE_ENV=production PORT=3000
+16 -10
View File
@@ -1,7 +1,7 @@
# Segnapunti # Segnapunti
![Version](https://img.shields.io/badge/versione-2.1.0-blue) ![Version](https://img.shields.io/badge/versione-2.1.0-blue)
![Node](https://img.shields.io/badge/node-%3E%3D18-green) ![Node](https://img.shields.io/badge/node-%3E%3D24-green)
![Docker](https://img.shields.io/badge/docker-ready-2496ED?logo=docker&logoColor=white) ![Docker](https://img.shields.io/badge/docker-ready-2496ED?logo=docker&logoColor=white)
![License](https://img.shields.io/badge/licenza-privata-lightgrey) ![License](https://img.shields.io/badge/licenza-privata-lightgrey)
@@ -9,8 +9,21 @@ Segnapunti digitale in tempo reale per partite di pallavolo. Un server centrale
--- ---
## Requisiti
| Strumento | Versione minima | Uso |
|---|---|---|
| Node.js | >= 24 | Sviluppo locale (`npm run dev`) |
| npm | >= 11 | Sviluppo locale |
| Docker + Docker Compose | — | Deploy in produzione; alternativa containerizzata allo sviluppo locale |
Versione Node pinnata in `.nvmrc` e in `engines` di `package.json`. Con [nvm](https://github.com/nvm-sh/nvm): `nvm use`.
---
## Indice ## Indice
- [Requisiti](#requisiti)
- [Architettura](#architettura) - [Architettura](#architettura)
- [Guida utente](#guida-utente) - [Guida utente](#guida-utente)
- [Funzionalità](#funzionalità) - [Funzionalità](#funzionalità)
@@ -18,6 +31,7 @@ Segnapunti digitale in tempo reale per partite di pallavolo. Un server centrale
- [Sviluppo](#sviluppo) - [Sviluppo](#sviluppo)
- [Sviluppo con Docker](#sviluppo-con-docker) - [Sviluppo con Docker](#sviluppo-con-docker)
- [Test](#test) - [Test](#test)
- [Changelog](#changelog)
--- ---
@@ -166,7 +180,6 @@ La **partita** è vinta da chi raggiunge 3 set su 5 (modalità `3/5`) o 2 set su
--- ---
## Deploy con Docker ## Deploy con Docker
### Prima installazione ### Prima installazione
@@ -209,13 +222,6 @@ docker run --rm -p 3000:3000 -v ./.segnapunti:/app/.segnapunti segnapunti:local
## Sviluppo ## Sviluppo
### Requisiti
| Strumento | Versione minima |
|---|---|
| Node.js | >= 18 |
| npm | >= 9 |
### Avvio ### Avvio
```bash ```bash
@@ -253,7 +259,7 @@ docker compose -f docker-compose.dev.yml up --build
| `http://localhost:5173/display` | Display | | `http://localhost:5173/display` | Display |
| `http://localhost:5173/controller` | Controller | | `http://localhost:5173/controller` | Controller |
Le modifiche ai file sorgente sull'host si riflettono subito nel container (bind-mount); `node_modules` resta invece un volume anonimo del container, per non farsi sovrascrivere dal bind-mount e per evitare incompatibilità tra i binari nativi installati sull'host e quelli richiesti dal container (es. host ARM / container `node:20-alpine` su x86). Le modifiche ai file sorgente sull'host si riflettono subito nel container (bind-mount); `node_modules` resta invece un volume anonimo del container, per non farsi sovrascrivere dal bind-mount e per evitare incompatibilità tra i binari nativi installati sull'host e quelli richiesti dal container (es. host ARM / container `node:24-alpine` su x86).
Per fermare e rimuovere il container: Per fermare e rimuovere il container:
+426 -382
View File
File diff suppressed because it is too large Load Diff
+5 -2
View File
@@ -3,6 +3,9 @@
"private": true, "private": true,
"version": "2.1.0", "version": "2.1.0",
"type": "module", "type": "module",
"engines": {
"node": ">=24.0.0"
},
"scripts": { "scripts": {
"dev": "vite --host", "dev": "vite --host",
"build": "vite build", "build": "vite build",
@@ -30,12 +33,12 @@
"@playwright/test": "^1.58.2", "@playwright/test": "^1.58.2",
"@types/node": "^25.2.3", "@types/node": "^25.2.3",
"@vitejs/plugin-vue": "^6.0.5", "@vitejs/plugin-vue": "^6.0.5",
"@vitest/ui": "^4.0.18", "@vitest/ui": "^4.1.10",
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.6",
"happy-dom": "^20.6.1", "happy-dom": "^20.6.1",
"jsdom": "^28.0.0", "jsdom": "^28.0.0",
"vite": "^7.3.1", "vite": "^7.3.1",
"vite-plugin-pwa": "^1.2.0", "vite-plugin-pwa": "^1.2.0",
"vitest": "^4.0.18" "vitest": "^4.1.10"
} }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

+23 -20
View File
@@ -34,11 +34,11 @@ export function buildRefertoHtml(state, now = new Date()) {
return ` return `
<div class="form-row"> <div class="form-row">
<div class="form-team"> <div class="form-team">
<div class="form-team-name">${nomi.home}</div> <div class="form-team-name form-team-name-h">${nomi.home}</div>
<div class="giocatori">${formazioneSet.home.map(giocatoreHtml).join('')}</div> <div class="giocatori">${formazioneSet.home.map(giocatoreHtml).join('')}</div>
</div> </div>
<div class="form-team"> <div class="form-team">
<div class="form-team-name">${nomi.guest}</div> <div class="form-team-name form-team-name-g">${nomi.guest}</div>
<div class="giocatori">${formazioneSet.guest.map(giocatoreHtml).join('')}</div> <div class="giocatori">${formazioneSet.guest.map(giocatoreHtml).join('')}</div>
</div> </div>
</div>` </div>`
@@ -79,7 +79,11 @@ export function buildRefertoHtml(state, now = new Date()) {
const vinc = vincitoreSet(s) const vinc = vincitoreSet(s)
const nomeVinc = vinc === 'h' ? nomi.home : vinc === 'g' ? nomi.guest : '' const nomeVinc = vinc === 'h' ? nomi.home : vinc === 'g' ? nomi.guest : ''
const etichettaVinc = nomeVinc ? ` &nbsp;·&nbsp; vinto da <strong>${nomeVinc}</strong>` : '' const etichettaVinc = nomeVinc ? `<span class="set-vinc">vinto da <strong>${nomeVinc}</strong></span>` : ''
const servIcon = '<img src="/serv-nero.png" class="serv-icon" alt="Servizio">'
const servHomeIcon = s.serv === 'h' ? servIcon : ''
const servGuestIcon = s.serv === 'g' ? servIcon : ''
const puntiHtml = punti.map(p => const puntiHtml = punti.map(p =>
`<span class="punto punto-${p.chi}">${p.h}-${p.g}</span>` `<span class="punto punto-${p.chi}">${p.h}-${p.g}</span>`
@@ -90,7 +94,8 @@ export function buildRefertoHtml(state, now = new Date()) {
<div class="set-barra"><span>SET</span><span class="set-numero">${i + 1}</span></div> <div class="set-barra"><span>SET</span><span class="set-numero">${i + 1}</span></div>
<div class="set-corpo"> <div class="set-corpo">
<div class="set-header"> <div class="set-header">
${nomi.home} <strong>${h}</strong> · <strong>${g}</strong> ${nomi.guest}${etichettaVinc} <span>${servHomeIcon}${nomi.home} <strong>${h}</strong> · <strong>${g}</strong> ${nomi.guest}${servGuestIcon}</span>
${etichettaVinc}
</div> </div>
<div class="form-inizio"> <div class="form-inizio">
<div class="form-inizio-label">Formazione di partenza</div> <div class="form-inizio-label">Formazione di partenza</div>
@@ -130,25 +135,23 @@ export function buildRefertoHtml(state, now = new Date()) {
.meta { font-size: 11px; color: #333; margin-top: 3px; text-transform: uppercase; letter-spacing: 0.5px; } .meta { font-size: 11px; color: #333; margin-top: 3px; text-transform: uppercase; letter-spacing: 0.5px; }
.squadre-row { display: flex; align-items: center; gap: 14px; } .squadre-row { display: flex; align-items: center; gap: 14px; }
.badge-ab { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 50%; border: 2px solid #111; font-weight: bold; font-size: 12px; flex-shrink: 0; } .badge-ab { display: inline-flex; align-items: center; justify-content: center; height: 22px; padding: 0 8px; border-radius: 11px; border: 2px solid #111; font-weight: bold; font-size: 12px; flex-shrink: 0; }
.nome-sq { font-size: 18px; font-weight: bold; } .nome-sq { font-size: 18px; font-weight: bold; }
.vs { font-size: 12px; color: #666; font-weight: bold; } .vs { font-size: 12px; color: #666; font-weight: bold; }
.risultato-riepilogo { display: flex; align-items: center; gap: 10px; }
.risultato { font-size: 30px; font-weight: bold; letter-spacing: 3px; }
.modalita-label { font-size: 10px; color: #555; text-transform: uppercase; letter-spacing: 0.5px; }
.set-section { display: flex; margin-bottom: 10px; border: 2px solid #111; } .set-section { display: flex; margin-bottom: 10px; border: 2px solid #111; }
.set-barra { flex: 0 0 26px; background: var(--teal); color: #fff; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; border-right: 2px solid #111; } .set-barra { flex: 0 0 26px; background: var(--teal); color: #fff; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; border-right: 2px solid #111; }
.set-barra span:first-child { writing-mode: vertical-rl; transform: rotate(180deg); font-weight: bold; font-size: 11px; letter-spacing: 2px; } .set-barra span:first-child { writing-mode: vertical-rl; transform: rotate(180deg); font-weight: bold; font-size: 11px; letter-spacing: 2px; }
.set-numero { font-weight: bold; font-size: 15px; } .set-numero { font-weight: bold; font-size: 15px; }
.set-corpo { flex: 1; min-width: 0; } .set-corpo { flex: 1; min-width: 0; }
.set-header { background: #eaf7fb; padding: 6px 12px; font-size: 13px; border-bottom: 1px solid #111; } .set-header { background: #eaf7fb; padding: 6px 12px; font-size: 13px; border-bottom: 1px solid #111; display: flex; justify-content: space-between; align-items: center; gap: 10px; }
.set-vinc { flex-shrink: 0; }
.serv-icon { width: 12px; height: 12px; vertical-align: middle; margin: 0 3px; }
.punti-grid { display: flex; flex-wrap: wrap; gap: 0; padding: 0; border-top: 1px solid #111; } .punti-grid { display: flex; flex-wrap: wrap; gap: 0; padding: 0; border-top: 1px solid #111; }
.punto { display: inline-flex; align-items: center; justify-content: center; min-width: 34px; padding: 3px 4px; font-size: 11px; font-family: 'Courier New', monospace; white-space: nowrap; border-right: 1px solid #ccc; border-bottom: 1px solid #ccc; } .punto { display: inline-flex; align-items: center; justify-content: center; min-width: 34px; padding: 3px 4px; font-size: 11px; font-family: 'Courier New', monospace; white-space: nowrap; border-right: 1px solid #ccc; border-bottom: 1px solid #ccc; }
.punto-h { background: #d0e8ff; color: #003a6e; } .punto-h { background: #f5c518; color: #111; }
.punto-g { background: #ffddc0; color: #6e2700; } .punto-g { background: #2196f3; color: #fff; }
.eventi-set { padding: 6px 12px; border-bottom: 1px solid #111; } .eventi-set { padding: 6px 12px; border-bottom: 1px solid #111; }
.eventi-label { font-size: 10px; font-weight: bold; letter-spacing: 0.5px; text-transform: uppercase; color: #0f8faf; margin-bottom: 4px; } .eventi-label { font-size: 10px; font-weight: bold; letter-spacing: 0.5px; text-transform: uppercase; color: #0f8faf; margin-bottom: 4px; }
@@ -160,6 +163,8 @@ export function buildRefertoHtml(state, now = new Date()) {
.form-row { display: flex; gap: 40px; } .form-row { display: flex; gap: 40px; }
.form-team { flex: 1; } .form-team { flex: 1; }
.form-team-name { font-weight: bold; font-size: 12px; margin-bottom: 5px; } .form-team-name { font-weight: bold; font-size: 12px; margin-bottom: 5px; }
.form-team-name-h { color: #f5c518; }
.form-team-name-g { color: #2196f3; }
.giocatori { display: flex; gap: 5px; flex-wrap: wrap; } .giocatori { display: flex; gap: 5px; flex-wrap: wrap; }
.giocatore { background: #fff; border-radius: 50%; width: 26px; height: 26px; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 11px; border: 2px solid #111; } .giocatore { background: #fff; border-radius: 50%; width: 26px; height: 26px; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 11px; border: 2px solid #111; }
@@ -181,11 +186,11 @@ export function buildRefertoHtml(state, now = new Date()) {
<div class="meta">${dataOra} &nbsp;·&nbsp; Modalità: ${modalitaPartita}</div> <div class="meta">${dataOra} &nbsp;·&nbsp; Modalità: ${modalitaPartita}</div>
</div> </div>
<div class="squadre-row"> <div class="squadre-row">
<span class="badge-ab">A</span> <span class="badge-ab">Casa</span>
<span class="nome-sq">${nomi.home}</span> <span class="nome-sq">${nomi.home}</span>
<span class="vs">vs</span> <span class="vs">vs</span>
<span class="nome-sq">${nomi.guest}</span> <span class="nome-sq">${nomi.guest}</span>
<span class="badge-ab">B</span> <span class="badge-ab">Ospiti</span>
</div> </div>
</div> </div>
@@ -195,15 +200,11 @@ export function buildRefertoHtml(state, now = new Date()) {
<div class="finale-box"> <div class="finale-box">
<div class="finale-box-header">Risultato finale</div> <div class="finale-box-header">Risultato finale</div>
<table class="finale-tabella"> <table class="finale-tabella">
<thead><tr><th>Set</th><th>A</th><th>B</th></tr></thead> <thead><tr><th>Set</th><th>${nomi.home}</th><th>${nomi.guest}</th></tr></thead>
<tbody>${risultatoSetHtml}</tbody> <tbody>${risultatoSetHtml}</tbody>
</table> </table>
<div class="finale-vince">Vince ${setVinti.home >= setVinti.guest ? nomi.home : nomi.guest} &nbsp;${setVinti.home} ${setVinti.guest}</div> <div class="finale-vince">Vince ${setVinti.home >= setVinti.guest ? nomi.home : nomi.guest} &nbsp;${setVinti.home} ${setVinti.guest}</div>
</div> </div>
<div class="risultato-riepilogo">
<div class="risultato">${setVinti.home} ${setVinti.guest}</div>
<div class="modalita-label">set vinti</div>
</div>
</div> </div>
</div> </div>
</body> </body>
@@ -219,5 +220,7 @@ export function generaReferto(state) {
const w = window.open('', '_blank') const w = window.open('', '_blank')
w.document.write(html) w.document.write(html)
w.document.close() w.document.close()
w.print() // Aspetta il caricamento delle immagini (icona servizio) prima di stampare,
// altrimenti la stampa parte con risorse ancora in caricamento e appaiono vuote.
w.addEventListener('load', () => w.print())
} }
@@ -11,10 +11,12 @@ describe('generaReferto (referto.js)', () => {
vi.restoreAllMocks() vi.restoreAllMocks()
}) })
it('apre una nuova scheda, scrive il referto e avvia la stampa', () => { it('apre una nuova scheda, scrive il referto e avvia la stampa dopo il caricamento', () => {
const listeners = {}
const popup = { const popup = {
document: { write: vi.fn(), close: vi.fn() }, document: { write: vi.fn(), close: vi.fn() },
print: vi.fn(), print: vi.fn(),
addEventListener: vi.fn((evt, cb) => { listeners[evt] = cb }),
} }
const openSpy = vi.spyOn(window, 'open').mockReturnValue(popup) const openSpy = vi.spyOn(window, 'open').mockReturnValue(popup)
@@ -26,6 +28,9 @@ describe('generaReferto (referto.js)', () => {
expect(popup.document.write).toHaveBeenCalledTimes(1) expect(popup.document.write).toHaveBeenCalledTimes(1)
expect(popup.document.write.mock.calls[0][0]).toContain('Antoniana') expect(popup.document.write.mock.calls[0][0]).toContain('Antoniana')
expect(popup.document.close).toHaveBeenCalledTimes(1) expect(popup.document.close).toHaveBeenCalledTimes(1)
expect(popup.print).not.toHaveBeenCalled()
listeners.load()
expect(popup.print).toHaveBeenCalledTimes(1) expect(popup.print).toHaveBeenCalledTimes(1)
}) })
}) })