diff --git a/public/index.html b/public/index.html
index 601bccc..bd0acca 100644
--- a/public/index.html
+++ b/public/index.html
@@ -628,6 +628,30 @@
/* Touch highlight */
.option-card { -webkit-tap-highlight-color: rgba(0,212,255,0.12); }
+
+ /* ── Connection overlay ─────────────────────────────────── */
+ #wsOverlay {
+ display: none;
+ position: fixed;
+ inset: 0;
+ background: rgba(11,13,20,0.92);
+ z-index: 900;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ gap: 14px;
+ }
+ #wsOverlay.show { display: flex; }
+ #wsOverlay .spinner {
+ width: 40px; height: 40px;
+ border: 3px solid var(--border);
+ border-top-color: var(--accent);
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+ }
+ @keyframes spin { to { transform: rotate(360deg); } }
+ #wsOverlay p { color: var(--text-muted); font-size: 0.85rem; text-align: center; }
+ #wsOverlay .sub { font-size: 0.72rem; color: var(--text-muted); opacity: 0.6; margin-top: -8px; }
@@ -691,6 +715,13 @@
+
+
+
+
Connection lost
+
Attempting to reconnect…
+
+
@@ -745,7 +776,14 @@
}
// ── WebSocket ──────────────────────────────────────────────
+ const RECONNECT_BASE = 1000; // 1s initial
+ const RECONNECT_MAX = 30000; // 30s cap
+ let reconnectDelay = RECONNECT_BASE;
+ let reconnectTimer = null;
+ let offlineVoteQueue = []; // votes cast while disconnected
+
function connectWS() {
+ if (ws) { ws.onclose = ws.onerror = null; ws.close(); }
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${location.host}`;
ws = new WebSocket(wsUrl);
@@ -754,6 +792,17 @@
state.wsConnected = true;
updateWsStatus('Connected');
document.getElementById('wsDot').classList.remove('offline');
+ document.getElementById('wsOverlay').classList.remove('show');
+ reconnectDelay = RECONNECT_BASE; // reset backoff
+
+ // Replay queued votes
+ if (offlineVoteQueue.length > 0) {
+ const queue = [...offlineVoteQueue];
+ offlineVoteQueue = [];
+ queue.forEach(msg => wsSend(msg));
+ }
+
+ showToast('✓ Reconnected', 'success');
};
ws.onmessage = (event) => {
@@ -768,10 +817,7 @@
} else if (msg.type === 'vote_update') {
msg.results.forEach(r => {
const opt = state.options.find(o => o.id === r.id);
- if (opt) {
- opt.votes = r.votes;
- opt.voters = r.voters;
- }
+ if (opt) { opt.votes = r.votes; opt.voters = r.voters; }
});
render();
} else if (msg.type === 'option_added' || msg.type === 'option_approved') {
@@ -794,12 +840,25 @@
state.wsConnected = false;
updateWsStatus('Reconnecting…');
document.getElementById('wsDot').classList.add('offline');
- setTimeout(connectWS, 3000);
+ document.getElementById('wsOverlay').classList.add('show');
+ clearTimeout(reconnectTimer);
+ reconnectTimer = setTimeout(() => {
+ reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX);
+ connectWS();
+ }, reconnectDelay);
};
}
function wsSend(obj) {
- if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(obj));
+ if (ws && ws.readyState === WebSocket.OPEN) {
+ ws.send(JSON.stringify(obj));
+ } else {
+ // Queue vote if offline
+ if (obj.type === 'vote' && !offlineVoteQueue.find(m =>
+ m.type === 'vote' && m.optionId === obj.optionId && m.voterName === obj.voterName)) {
+ offlineVoteQueue.push(obj);
+ }
+ }
}
function updateWsStatus(text) {