feat(client): migliora robustezza connessioni WebSocket su display e controller
Aggiunge gestione riconnessione con backoff esponenziale e protezione da reconnect multipli. Migliora cleanup su unmount/HMR per evitare listener e timeout pendenti. Uniforma gestione errori e stato connessione lato client. Semplifica etichette pulsanti controller rimuovendo emoji e aggiorna commenti.
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="controller-page">
|
<section class="controller-page">
|
||||||
<!-- Connection status bar -->
|
<!-- Barra di stato connessione -->
|
||||||
<div class="conn-bar" :class="{ connected: wsConnected }">
|
<div class="conn-bar" :class="{ connected: wsConnected }">
|
||||||
<span class="dot"></span>
|
<span class="dot"></span>
|
||||||
{{ wsConnected ? 'Connesso' : 'Connessione...' }}
|
{{ wsConnected ? 'Connesso' : 'Connessione...' }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Score preview -->
|
<!-- Anteprima punteggio -->
|
||||||
<div class="score-preview">
|
<div class="score-preview">
|
||||||
<div class="team-score home-bg" @click="sendAction({ type: 'incPunt', team: 'home' })">
|
<div class="team-score home-bg" @click="sendAction({ type: 'incPunt', team: 'home' })">
|
||||||
<div class="team-name">{{ state.sp.nomi.home }}</div>
|
<div class="team-name">{{ state.sp.nomi.home }}</div>
|
||||||
@@ -22,14 +22,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Undo row -->
|
<!-- Riga annulla punto -->
|
||||||
<div class="undo-row">
|
<div class="undo-row">
|
||||||
<button class="btn btn-undo" @click="sendAction({ type: 'decPunt' })">
|
<button class="btn btn-undo" @click="sendAction({ type: 'decPunt' })">
|
||||||
↩ ANNULLA PUNTO
|
ANNULLA PUNTO
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Set buttons -->
|
<!-- Pulsanti set -->
|
||||||
<div class="action-row">
|
<div class="action-row">
|
||||||
<button class="btn btn-set home-bg" @click="sendAction({ type: 'incSet', team: 'home' })">
|
<button class="btn btn-set home-bg" @click="sendAction({ type: 'incSet', team: 'home' })">
|
||||||
SET {{ state.sp.nomi.home }}
|
SET {{ state.sp.nomi.home }}
|
||||||
@@ -39,35 +39,35 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Controls -->
|
<!-- Controlli principali -->
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button class="btn btn-ctrl" @click="sendAction({ type: 'cambiaPalla' })" :disabled="!isPunteggioZeroZero">
|
<button class="btn btn-ctrl" @click="sendAction({ type: 'cambiaPalla' })" :disabled="!isPunteggioZeroZero">
|
||||||
🏐 Cambio Palla
|
Cambio Palla
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-ctrl" @click="sendAction({ type: 'toggleFormazione' })">
|
<button class="btn btn-ctrl" @click="sendAction({ type: 'toggleFormazione' })">
|
||||||
{{ state.visuForm ? '🔢 Punteggio' : '📋 Formazioni' }}
|
{{ state.visuForm ? 'Punteggio' : 'Formazioni' }}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-ctrl" @click="sendAction({ type: 'toggleStriscia' })">
|
<button class="btn btn-ctrl" @click="sendAction({ type: 'toggleStriscia' })">
|
||||||
📊 Striscia
|
Striscia
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-ctrl" @click="sendAction({ type: 'toggleOrder' })">
|
<button class="btn btn-ctrl" @click="sendAction({ type: 'toggleOrder' })">
|
||||||
🔄 Inverti
|
Inverti
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-ctrl" @click="speak()">
|
<button class="btn btn-ctrl" @click="speak()">
|
||||||
🔊 Voce
|
Voce
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-ctrl" @click="openConfig()">
|
<button class="btn btn-ctrl" @click="openConfig()">
|
||||||
⚙️ Config
|
Config
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-ctrl" @click="openCambiTeam()">
|
<button class="btn btn-ctrl" @click="openCambiTeam()">
|
||||||
🔀 Cambi
|
Cambi
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger" @click="confirmReset = true">
|
<button class="btn btn-danger" @click="confirmReset = true">
|
||||||
🗑️ Reset
|
Reset
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Reset confirmation -->
|
<!-- Finestra conferma reset -->
|
||||||
<div class="overlay" v-if="confirmReset" @click.self="confirmReset = false">
|
<div class="overlay" v-if="confirmReset" @click.self="confirmReset = false">
|
||||||
<div class="dialog">
|
<div class="dialog">
|
||||||
<div class="dialog-title">Azzero punteggio?</div>
|
<div class="dialog-title">Azzero punteggio?</div>
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Config dialog -->
|
<!-- Finestra configurazione -->
|
||||||
<div class="overlay" v-if="showConfig" @click.self="showConfig = false">
|
<div class="overlay" v-if="showConfig" @click.self="showConfig = false">
|
||||||
<div class="dialog dialog-config">
|
<div class="dialog dialog-config">
|
||||||
<div class="dialog-title">Configurazione</div>
|
<div class="dialog-title">Configurazione</div>
|
||||||
@@ -143,7 +143,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Cambi team selection -->
|
<!-- Selezione squadra per cambi -->
|
||||||
<div class="overlay" v-if="showCambiTeam" @click.self="showCambiTeam = false">
|
<div class="overlay" v-if="showCambiTeam" @click.self="showCambiTeam = false">
|
||||||
<div class="dialog">
|
<div class="dialog">
|
||||||
<div class="dialog-title">Scegli squadra</div>
|
<div class="dialog-title">Scegli squadra</div>
|
||||||
@@ -154,7 +154,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Cambi dialog -->
|
<!-- Finestra gestione cambi -->
|
||||||
<div class="overlay" v-if="showCambi" @click.self="closeCambi()">
|
<div class="overlay" v-if="showCambi" @click.self="closeCambi()">
|
||||||
<div class="dialog">
|
<div class="dialog">
|
||||||
<div class="dialog-title">{{ state.sp.nomi[cambiTeam] }}: CAMBIO</div>
|
<div class="dialog-title">{{ state.sp.nomi[cambiTeam] }}: CAMBIO</div>
|
||||||
@@ -181,6 +181,10 @@ export default {
|
|||||||
return {
|
return {
|
||||||
ws: null,
|
ws: null,
|
||||||
wsConnected: false,
|
wsConnected: false,
|
||||||
|
isConnecting: false,
|
||||||
|
reconnectTimeout: null,
|
||||||
|
reconnectAttempts: 0,
|
||||||
|
maxReconnectDelay: 30000,
|
||||||
confirmReset: false,
|
confirmReset: false,
|
||||||
showConfig: false,
|
showConfig: false,
|
||||||
showCambiTeam: false,
|
showCambiTeam: false,
|
||||||
@@ -236,44 +240,185 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.connectWebSocket()
|
this.connectWebSocket()
|
||||||
|
|
||||||
|
// Gestisce l'HMR di Vite evitando riconnessioni durante la ricarica a caldo.
|
||||||
|
if (import.meta.hot) {
|
||||||
|
import.meta.hot.on('vite:beforeUpdate', () => {
|
||||||
|
// Annulla eventuali tentativi di riconnessione pianificati.
|
||||||
|
if (this.reconnectTimeout) {
|
||||||
|
clearTimeout(this.reconnectTimeout)
|
||||||
|
this.reconnectTimeout = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
if (this.ws) this.ws.close()
|
// Pulisce il timeout di riconnessione.
|
||||||
|
if (this.reconnectTimeout) {
|
||||||
|
clearTimeout(this.reconnectTimeout)
|
||||||
|
this.reconnectTimeout = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chiude il WebSocket con il codice di chiusura appropriato.
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.onclose = null // Rimuove il listener per evitare nuove riconnessioni pianificate.
|
||||||
|
this.ws.onerror = null
|
||||||
|
this.ws.onmessage = null
|
||||||
|
this.ws.onopen = null
|
||||||
|
|
||||||
|
// Usa il codice 1000 (chiusura normale) se la connessione e aperta.
|
||||||
|
try {
|
||||||
|
if (this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
this.ws.close(1000, 'Component unmounting')
|
||||||
|
} else if (this.ws.readyState === WebSocket.CONNECTING) {
|
||||||
|
// Se la connessione e ancora in fase di apertura, chiude direttamente.
|
||||||
|
this.ws.close()
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Controller] Error closing WebSocket:', err)
|
||||||
|
}
|
||||||
|
this.ws = null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
connectWebSocket() {
|
connectWebSocket() {
|
||||||
|
// Evita connessioni simultanee multiple.
|
||||||
|
if (this.isConnecting) {
|
||||||
|
console.log('[Controller] Already connecting, skipping...')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chiude la connessione precedente, se presente.
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.onclose = null
|
||||||
|
this.ws.onerror = null
|
||||||
|
this.ws.onmessage = null
|
||||||
|
this.ws.onopen = null
|
||||||
|
try {
|
||||||
|
if (this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
this.ws.close(1000, 'Reconnecting')
|
||||||
|
} else if (this.ws.readyState === WebSocket.CONNECTING) {
|
||||||
|
this.ws.close()
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Controller] Error closing previous WebSocket:', err)
|
||||||
|
}
|
||||||
|
this.ws = null
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isConnecting = true
|
||||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||||
const wsUrl = `${protocol}//${location.host}`
|
const wsUrl = `${protocol}//${location.host}`
|
||||||
this.ws = new WebSocket(wsUrl)
|
|
||||||
|
try {
|
||||||
|
this.ws = new WebSocket(wsUrl)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Controller] Failed to create WebSocket:', err)
|
||||||
|
this.isConnecting = false
|
||||||
|
this.scheduleReconnect()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.ws.onopen = () => {
|
this.ws.onopen = () => {
|
||||||
|
this.isConnecting = false
|
||||||
this.wsConnected = true
|
this.wsConnected = true
|
||||||
this.ws.send(JSON.stringify({ type: 'register', role: 'controller' }))
|
this.reconnectAttempts = 0
|
||||||
|
console.log('[Controller] Connected to server')
|
||||||
|
|
||||||
|
// Invia la registrazione solo se la connessione e realmente aperta.
|
||||||
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
try {
|
||||||
|
this.ws.send(JSON.stringify({ type: 'register', role: 'controller' }))
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Controller] Failed to register:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ws.onmessage = (event) => {
|
this.ws.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse(event.data)
|
const msg = JSON.parse(event.data)
|
||||||
|
|
||||||
if (msg.type === 'state') {
|
if (msg.type === 'state') {
|
||||||
this.state = msg.state
|
this.state = msg.state
|
||||||
|
} else if (msg.type === 'error') {
|
||||||
|
console.error('[Controller] Server error:', msg.message)
|
||||||
|
// Fornisce feedback di errore all'utente.
|
||||||
|
this.showErrorFeedback(msg.message)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('WS parse error:', e)
|
console.error('[Controller] Parse error:', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ws.onclose = () => {
|
this.ws.onclose = (event) => {
|
||||||
|
this.isConnecting = false
|
||||||
this.wsConnected = false
|
this.wsConnected = false
|
||||||
setTimeout(() => this.connectWebSocket(), 2000)
|
console.log('[Controller] Disconnected from server', event.code, event.reason)
|
||||||
|
|
||||||
|
// Non riconnette durante HMR (codice 1001, "going away")
|
||||||
|
// ne in caso di chiusura pulita (codice 1000).
|
||||||
|
if (event.code === 1000 || event.code === 1001) {
|
||||||
|
console.log('[Controller] Clean close, not reconnecting')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scheduleReconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ws.onerror = () => { this.wsConnected = false }
|
this.ws.onerror = (err) => {
|
||||||
|
console.error('[Controller] WebSocket error:', err)
|
||||||
|
this.isConnecting = false
|
||||||
|
this.wsConnected = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
scheduleReconnect() {
|
||||||
|
// Evita pianificazioni multiple della riconnessione.
|
||||||
|
if (this.reconnectTimeout) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Applica backoff esponenziale: 1s, 2s, 4s, 8s, 16s, fino a 30s.
|
||||||
|
const delay = Math.min(
|
||||||
|
1000 * Math.pow(2, this.reconnectAttempts),
|
||||||
|
this.maxReconnectDelay
|
||||||
|
)
|
||||||
|
this.reconnectAttempts++
|
||||||
|
|
||||||
|
console.log(`[Controller] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`)
|
||||||
|
|
||||||
|
this.reconnectTimeout = setTimeout(() => {
|
||||||
|
this.reconnectTimeout = null
|
||||||
|
this.connectWebSocket()
|
||||||
|
}, delay)
|
||||||
},
|
},
|
||||||
|
|
||||||
sendAction(action) {
|
sendAction(action) {
|
||||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
if (!this.wsConnected || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||||
this.ws.send(JSON.stringify({ type: 'action', action }))
|
console.warn('[Controller] Cannot send action: not connected')
|
||||||
|
this.showErrorFeedback('Non connesso al server')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Valida l'azione prima dell'invio.
|
||||||
|
if (!action || !action.type) {
|
||||||
|
console.error('[Controller] Invalid action format:', action)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.ws.send(JSON.stringify({ type: 'action', action }))
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Controller] Failed to send action:', err)
|
||||||
|
this.showErrorFeedback('Errore invio comando')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
showErrorFeedback(message) {
|
||||||
|
// Feedback visivo degli errori: attualmente solo log su console.
|
||||||
|
// In futuro puo essere esteso con notifiche a comparsa (toast).
|
||||||
|
console.error('[Controller] Error:', message)
|
||||||
},
|
},
|
||||||
|
|
||||||
doReset() {
|
doReset() {
|
||||||
@@ -356,7 +501,7 @@ export default {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Connection bar */
|
/* Barra stato connessione */
|
||||||
.conn-bar {
|
.conn-bar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -384,7 +529,7 @@ export default {
|
|||||||
background: white;
|
background: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Score preview */
|
/* Anteprima punteggio */
|
||||||
.score-preview {
|
.score-preview {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -443,7 +588,7 @@ export default {
|
|||||||
height: 20px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Undo row */
|
/* Riga annulla punto */
|
||||||
.undo-row {
|
.undo-row {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
@@ -462,7 +607,7 @@ export default {
|
|||||||
background: rgba(255,100,50,0.2);
|
background: rgba(255,100,50,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Set buttons */
|
/* Pulsanti set */
|
||||||
.action-row {
|
.action-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -482,7 +627,7 @@ export default {
|
|||||||
transform: scale(0.97);
|
transform: scale(0.97);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Controls grid */
|
/* Griglia controlli */
|
||||||
.controls {
|
.controls {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
@@ -526,7 +671,7 @@ export default {
|
|||||||
background: rgba(198, 40, 40, 0.45);
|
background: rgba(198, 40, 40, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Overlays & Dialogs */
|
/* Overlay e finestre modali */
|
||||||
.overlay {
|
.overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -593,7 +738,7 @@ export default {
|
|||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Form groups */
|
/* Gruppi form */
|
||||||
.form-group {
|
.form-group {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
@@ -637,7 +782,7 @@ export default {
|
|||||||
border-color: #64b5f6;
|
border-color: #64b5f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Form grid */
|
/* Griglia formazione */
|
||||||
.form-grid {
|
.form-grid {
|
||||||
background: rgba(205, 133, 63, 0.15);
|
background: rgba(205, 133, 63, 0.15);
|
||||||
border: 2px solid rgba(255,255,255,0.15);
|
border: 2px solid rgba(255,255,255,0.15);
|
||||||
@@ -655,7 +800,7 @@ export default {
|
|||||||
margin: 4px 0;
|
margin: 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mode buttons */
|
/* Pulsanti modalita */
|
||||||
.mode-buttons {
|
.mode-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -675,7 +820,7 @@ export default {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Cambi */
|
/* Sezione cambi */
|
||||||
.cambi-container {
|
.cambi-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<section class="display-page">
|
<section class="display-page">
|
||||||
<div class="campo">
|
<div class="campo">
|
||||||
<span v-if="state.order">
|
<span v-if="state.order">
|
||||||
<!-- home guest -->
|
<!-- Ordine visualizzazione: home / guest -->
|
||||||
<div class="hea home">
|
<div class="hea home">
|
||||||
<span :style="{ 'float': 'left' }">
|
<span :style="{ 'float': 'left' }">
|
||||||
{{ state.sp.nomi.home }}
|
{{ state.sp.nomi.home }}
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span v-else>
|
<span v-else>
|
||||||
<!-- guest home -->
|
<!-- Ordine visualizzazione: guest / home -->
|
||||||
<div class="hea guest">
|
<div class="hea guest">
|
||||||
<span :style="{ 'float': 'left' }">
|
<span :style="{ 'float': 'left' }">
|
||||||
{{ state.sp.nomi.guest }}
|
{{ state.sp.nomi.guest }}
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Connection status indicator -->
|
<!-- Indicatore stato connessione -->
|
||||||
<div class="connection-status" :class="{ connected: wsConnected, disconnected: !wsConnected }">
|
<div class="connection-status" :class="{ connected: wsConnected, disconnected: !wsConnected }">
|
||||||
<span class="dot"></span>
|
<span class="dot"></span>
|
||||||
{{ wsConnected ? '' : 'Disconnesso' }}
|
{{ wsConnected ? '' : 'Disconnesso' }}
|
||||||
@@ -120,6 +120,10 @@ export default {
|
|||||||
return {
|
return {
|
||||||
ws: null,
|
ws: null,
|
||||||
wsConnected: false,
|
wsConnected: false,
|
||||||
|
isConnecting: false,
|
||||||
|
reconnectTimeout: null,
|
||||||
|
reconnectAttempts: 0,
|
||||||
|
maxReconnectDelay: 30000, // Ritardo massimo di riconnessione: 30 secondi
|
||||||
state: {
|
state: {
|
||||||
order: true,
|
order: true,
|
||||||
visuForm: false,
|
visuForm: false,
|
||||||
@@ -142,14 +146,48 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.connectWebSocket()
|
this.connectWebSocket()
|
||||||
// Fullscreen on mobile
|
// Attiva la modalita fullscreen su dispositivi mobili.
|
||||||
if (this.isMobile()) {
|
if (this.isMobile()) {
|
||||||
try { document.documentElement.requestFullscreen() } catch (e) {}
|
try { document.documentElement.requestFullscreen() } catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gestisce l'HMR di Vite evitando riconnessioni durante la ricarica a caldo.
|
||||||
|
if (import.meta.hot) {
|
||||||
|
import.meta.hot.on('vite:beforeUpdate', () => {
|
||||||
|
// Annulla eventuali tentativi di riconnessione pianificati.
|
||||||
|
if (this.reconnectTimeout) {
|
||||||
|
clearTimeout(this.reconnectTimeout)
|
||||||
|
this.reconnectTimeout = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
|
// Pulisce il timeout di riconnessione.
|
||||||
|
if (this.reconnectTimeout) {
|
||||||
|
clearTimeout(this.reconnectTimeout)
|
||||||
|
this.reconnectTimeout = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chiude il WebSocket con il codice di chiusura appropriato.
|
||||||
if (this.ws) {
|
if (this.ws) {
|
||||||
this.ws.close()
|
this.ws.onclose = null // Rimuove il listener per evitare nuove riconnessioni pianificate.
|
||||||
|
this.ws.onerror = null
|
||||||
|
this.ws.onmessage = null
|
||||||
|
this.ws.onopen = null
|
||||||
|
|
||||||
|
// Usa il codice 1000 (chiusura normale) se la connessione e aperta.
|
||||||
|
try {
|
||||||
|
if (this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
this.ws.close(1000, 'Component unmounting')
|
||||||
|
} else if (this.ws.readyState === WebSocket.CONNECTING) {
|
||||||
|
// Se la connessione e ancora in fase di apertura, chiude direttamente.
|
||||||
|
this.ws.close()
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Display] Error closing WebSocket:', err)
|
||||||
|
}
|
||||||
|
this.ws = null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -157,36 +195,113 @@ export default {
|
|||||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
|
||||||
},
|
},
|
||||||
connectWebSocket() {
|
connectWebSocket() {
|
||||||
|
// Evita connessioni simultanee multiple.
|
||||||
|
if (this.isConnecting) {
|
||||||
|
console.log('[Display] Already connecting, skipping...')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chiude la connessione precedente, se presente.
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.onclose = null
|
||||||
|
this.ws.onerror = null
|
||||||
|
this.ws.onmessage = null
|
||||||
|
this.ws.onopen = null
|
||||||
|
try {
|
||||||
|
if (this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
this.ws.close(1000, 'Reconnecting')
|
||||||
|
} else if (this.ws.readyState === WebSocket.CONNECTING) {
|
||||||
|
this.ws.close()
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Display] Error closing previous WebSocket:', err)
|
||||||
|
}
|
||||||
|
this.ws = null
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isConnecting = true
|
||||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||||
const wsUrl = `${protocol}//${location.host}`
|
const wsUrl = `${protocol}//${location.host}`
|
||||||
this.ws = new WebSocket(wsUrl)
|
|
||||||
|
try {
|
||||||
|
this.ws = new WebSocket(wsUrl)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Display] Failed to create WebSocket:', err)
|
||||||
|
this.isConnecting = false
|
||||||
|
this.scheduleReconnect()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.ws.onopen = () => {
|
this.ws.onopen = () => {
|
||||||
|
this.isConnecting = false
|
||||||
this.wsConnected = true
|
this.wsConnected = true
|
||||||
// Register as display
|
this.reconnectAttempts = 0
|
||||||
this.ws.send(JSON.stringify({ type: 'register', role: 'display' }))
|
console.log('[Display] Connected to server')
|
||||||
|
|
||||||
|
// Registra il client come display solo con connessione effettivamente aperta.
|
||||||
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
try {
|
||||||
|
this.ws.send(JSON.stringify({ type: 'register', role: 'display' }))
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Display] Failed to register:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ws.onmessage = (event) => {
|
this.ws.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse(event.data)
|
const msg = JSON.parse(event.data)
|
||||||
|
|
||||||
if (msg.type === 'state') {
|
if (msg.type === 'state') {
|
||||||
this.state = msg.state
|
this.state = msg.state
|
||||||
|
} else if (msg.type === 'error') {
|
||||||
|
console.error('[Display] Server error:', msg.message)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error parsing WS message:', e)
|
console.error('[Display] Error parsing message:', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ws.onclose = () => {
|
this.ws.onclose = (event) => {
|
||||||
|
this.isConnecting = false
|
||||||
this.wsConnected = false
|
this.wsConnected = false
|
||||||
// Auto-reconnect after 2 seconds
|
console.log('[Display] Disconnected from server', event.code, event.reason)
|
||||||
setTimeout(() => this.connectWebSocket(), 2000)
|
|
||||||
|
// Non riconnette durante HMR (codice 1001, "going away")
|
||||||
|
// ne in caso di chiusura pulita (codice 1000).
|
||||||
|
if (event.code === 1000 || event.code === 1001) {
|
||||||
|
console.log('[Display] Clean close, not reconnecting')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scheduleReconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ws.onerror = () => {
|
this.ws.onerror = (err) => {
|
||||||
|
console.error('[Display] WebSocket error:', err)
|
||||||
|
this.isConnecting = false
|
||||||
this.wsConnected = false
|
this.wsConnected = false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
scheduleReconnect() {
|
||||||
|
// Evita pianificazioni multiple della riconnessione.
|
||||||
|
if (this.reconnectTimeout) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Applica backoff esponenziale: 1s, 2s, 4s, 8s, 16s, fino a 30s.
|
||||||
|
const delay = Math.min(
|
||||||
|
1000 * Math.pow(2, this.reconnectAttempts),
|
||||||
|
this.maxReconnectDelay
|
||||||
|
)
|
||||||
|
this.reconnectAttempts++
|
||||||
|
|
||||||
|
console.log(`[Display] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`)
|
||||||
|
|
||||||
|
this.reconnectTimeout = setTimeout(() => {
|
||||||
|
this.reconnectTimeout = null
|
||||||
|
this.connectWebSocket()
|
||||||
|
}, delay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ button:focus-visible {
|
|||||||
font-size: xx-large;
|
font-size: xx-large;
|
||||||
}
|
}
|
||||||
.hea span {
|
.hea span {
|
||||||
/* border: 1px solid #f3fb00; */
|
/* Bordo di debug: border: 1px solid #f3fb00; */
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|||||||
Reference in New Issue
Block a user