diff --git a/shopping_app/static/css/split/components.css b/shopping_app/static/css/split/components.css index d4fca88..f1024d2 100644 --- a/shopping_app/static/css/split/components.css +++ b/shopping_app/static/css/split/components.css @@ -315,6 +315,53 @@ letter-spacing: .08em; } + +.app-ws-status { + display: inline-flex; + align-items: center; + justify-content: center; + gap: .38rem; + min-height: 2rem; + padding: .34rem .58rem; + border-radius: 999px; + border: 1px solid rgba(255,255,255,.10); + background: rgba(255,255,255,.05); + color: var(--app-text-muted); + font-size: .7rem; + font-weight: 700; + line-height: 1; + letter-spacing: .05em; + user-select: none; +} + +.app-ws-status__dot { + width: .58rem; + height: .58rem; + flex: 0 0 .58rem; + border-radius: 50%; + background: #f5b942; + box-shadow: 0 0 0 .18rem rgba(245,185,66,.13); + transition: background-color .15s ease, box-shadow .15s ease; +} + +.app-ws-status[data-state="connected"] .app-ws-status__dot { + background: #27d17f; + box-shadow: 0 0 0 .18rem rgba(39,209,127,.14); +} + +.app-ws-status[data-state="connecting"] .app-ws-status__dot, +.app-ws-status[data-state="syncing"] .app-ws-status__dot, +.app-ws-status[data-state="reconnecting"] .app-ws-status__dot { + background: #f5b942; + box-shadow: 0 0 0 .18rem rgba(245,185,66,.13); +} + +.app-ws-status[data-state="offline"] .app-ws-status__dot, +.app-ws-status[data-state="error"] .app-ws-status__dot { + background: #ef6461; + box-shadow: 0 0 0 .18rem rgba(239,100,97,.14); +} + .app-navbar-toggler { border-color: rgba(255,255,255,.28); padding: .3rem .55rem; diff --git a/shopping_app/static/css/split/responsive.css b/shopping_app/static/css/split/responsive.css index d23c74c..4fdb2d2 100644 --- a/shopping_app/static/css/split/responsive.css +++ b/shopping_app/static/css/split/responsive.css @@ -64,6 +64,25 @@ display: inline-flex; } + .app-ws-status--mobile { + width: 2.15rem; + min-width: 2.15rem; + height: 2.15rem; + min-height: 2.15rem; + padding: 0; + gap: 0; + } + + .app-ws-status--mobile .app-ws-status__label { + display: none; + } + + .app-ws-status--mobile .app-ws-status__dot { + width: .68rem; + height: .68rem; + flex-basis: .68rem; + } + .app-mobile-menu { grid-column: 3; justify-self: end; @@ -919,3 +938,18 @@ padding: .85rem .9rem; } } + +@media (max-width: 991.98px) { + .endpoint-view_list .app-navbar__meta--mobile, + .endpoint-shared_list .app-navbar__meta--mobile { + max-width: min(50vw, 14rem); + flex-wrap: nowrap; + } +} + +@media (max-width: 430px) { + .endpoint-view_list .app-navbar__meta--mobile, + .endpoint-shared_list .app-navbar__meta--mobile { + max-width: min(46vw, 10rem); + } +} diff --git a/shopping_app/static/js/live.js b/shopping_app/static/js/live.js index 985b20b..1b1c244 100644 --- a/shopping_app/static/js/live.js +++ b/shopping_app/static/js/live.js @@ -1,9 +1,10 @@ const socket = io({ reconnection: true, reconnectionAttempts: Infinity, - reconnectionDelay: 500, - reconnectionDelayMax: 5000, - timeout: 8000 + reconnectionDelay: 350, + reconnectionDelayMax: 2000, + randomizationFactor: 0.2, + timeout: 5000 }); /*──────────────── placeholder pustej listy ────────────────*/ @@ -38,6 +39,9 @@ function setupList(listId, username) { if (typeof window.ensureCurrentListJoined === 'function') { window.ensureCurrentListJoined(); } + if (typeof window.refreshListSocketStatus === 'function') { + window.refreshListSocketStatus(); + } const newItemInput = document.getElementById('newItem'); diff --git a/shopping_app/static/js/sockets.js b/shopping_app/static/js/sockets.js index e3476e5..7260c04 100644 --- a/shopping_app/static/js/sockets.js +++ b/shopping_app/static/js/sockets.js @@ -14,17 +14,74 @@ let currentRoomUsers = []; // wbudowanemu mechanizmowi Socket.IO zestawić nowe połączenie. Nie wywołujemy // socket.disconnect(), bo jest to ręczne rozłączenie i może wejść w konflikt // z automatycznym reconnectem. -const WAKE_CHECK_DEBOUNCE_MS = 250; -const WAKE_ACK_TIMEOUT_MS = 4000; -const WAKE_WATCHDOG_INTERVAL_MS = 5000; -const WAKE_WATCHDOG_GAP_MS = 15000; -const WAKE_RETRY_CONNECT_MS = 1200; +const WAKE_CHECK_DEBOUNCE_MS = 80; +const WAKE_ACK_TIMEOUT_MS = 1800; +const WAKE_WATCHDOG_INTERVAL_MS = 3000; +const WAKE_WATCHDOG_GAP_MS = 9000; +const WAKE_RETRY_CONNECT_MS = 500; let wakeCheckTimer = null; let wakeCheckInProgress = false; let reconnectFallbackTimer = null; let lastWatchdogTick = Date.now(); + +const LIST_SOCKET_STATUS_LABELS = { + connected: 'WebSocket: połączono i zsynchronizowano', + connecting: 'WebSocket: łączenie', + syncing: 'WebSocket: synchronizacja listy', + reconnecting: 'WebSocket: ponowne łączenie', + offline: 'WebSocket: brak połączenia z internetem', + error: 'WebSocket: błąd połączenia' +}; + +let listSocketStatus = 'connecting'; + +function setListSocketStatus(state, detail = '') { + listSocketStatus = state; + const baseLabel = LIST_SOCKET_STATUS_LABELS[state] || LIST_SOCKET_STATUS_LABELS.connecting; + const label = detail ? `${baseLabel} (${detail})` : baseLabel; + + document.querySelectorAll('[data-ws-status]').forEach(el => { + el.dataset.state = state; + el.setAttribute('title', label); + el.setAttribute('aria-label', label); + }); +} + +function currentSocketTransportName() { + try { + return socket.io && socket.io.engine && socket.io.engine.transport + ? socket.io.engine.transport.name + : ''; + } catch (_) { + return ''; + } +} + +function refreshListSocketStatus() { + if (!hasActiveListRoom()) return; + + if (typeof navigator !== 'undefined' && navigator.onLine === false) { + setListSocketStatus('offline'); + return; + } + + if (!socket.connected) { + setListSocketStatus('reconnecting'); + return; + } + + if (joinedSocketId === socket.id && joinedRoomId === String(window.LIST_ID) && didReceiveFirstFullList) { + setListSocketStatus('connected', currentSocketTransportName()); + return; + } + + setListSocketStatus('syncing'); +} + +window.refreshListSocketStatus = refreshListSocketStatus; + function hasActiveListRoom() { return Boolean(window.LIST_ID && window.usernameForReconnect); } @@ -56,6 +113,7 @@ function ensureCurrentListJoined() { joinedSocketId = socketId; joinedRoomId = roomId; + setListSocketStatus('syncing'); socket.emit('join_list', { room: window.LIST_ID, @@ -77,6 +135,7 @@ function forceCloseStaleTransport() { wakeCheckInProgress = false; joinedSocketId = null; joinedRoomId = null; + setListSocketStatus('reconnecting'); if (window.LIST_ID) disableCheckboxes(true); @@ -106,9 +165,14 @@ function forceCloseStaleTransport() { function runWakeCheck() { if (document.hidden || !hasActiveListRoom()) return; - if (typeof navigator !== 'undefined' && navigator.onLine === false) return; + if (typeof navigator !== 'undefined' && navigator.onLine === false) { + setListSocketStatus('offline'); + disableCheckboxes(true); + return; + } if (!socket.connected) { + setListSocketStatus('reconnecting'); disableCheckboxes(true); socket.connect(); return; @@ -122,6 +186,7 @@ function runWakeCheck() { if (wakeCheckInProgress) return; wakeCheckInProgress = true; + setListSocketStatus('syncing'); // request_full_list pełni jednocześnie rolę health-checka i synchronizacji. socket.timeout(WAKE_ACK_TIMEOUT_MS).emit( @@ -133,6 +198,7 @@ function runWakeCheck() { if (!err && response && response.ok === true) { // Jeśli połączenie jest zdrowe, nie wykonujemy reconnectu i nie emitujemy // ponownego join_list. Samo przełączenie karty pozostaje więc całkowicie ciche. + setListSocketStatus('connected', currentSocketTransportName()); return; } @@ -159,7 +225,20 @@ window.addEventListener('pageshow', event => { if (event.persisted) recoverListSocket(); }); -window.addEventListener('online', recoverListSocket); +window.addEventListener('focus', () => { + if (!document.hidden) recoverListSocket(); +}); + +window.addEventListener('online', () => { + if (hasActiveListRoom()) setListSocketStatus('reconnecting'); + recoverListSocket(); +}); + +window.addEventListener('offline', () => { + if (!hasActiveListRoom()) return; + setListSocketStatus('offline'); + disableCheckboxes(true); +}); // Dla środowisk emitujących zdarzenie resume (np. webview/PWA wrapper). document.addEventListener('resume', recoverListSocket); @@ -182,6 +261,7 @@ socket.on('connect', function () { joinedRoomId = null; if (hasActiveListRoom()) { + setListSocketStatus('syncing'); disableCheckboxes(true); ensureCurrentListJoined(); } @@ -191,18 +271,35 @@ socket.on('disconnect', function () { wakeCheckInProgress = false; joinedSocketId = null; joinedRoomId = null; - if (window.LIST_ID) disableCheckboxes(true); + if (window.LIST_ID) { + setListSocketStatus( + typeof navigator !== 'undefined' && navigator.onLine === false ? 'offline' : 'reconnecting' + ); + disableCheckboxes(true); + } }); socket.on('connect_error', function () { wakeCheckInProgress = false; - if (window.LIST_ID) disableCheckboxes(true); + if (window.LIST_ID) { + setListSocketStatus( + typeof navigator !== 'undefined' && navigator.onLine === false ? 'offline' : 'error' + ); + disableCheckboxes(true); + } }); +if (socket.io && typeof socket.io.on === 'function') { + socket.io.on('reconnect_attempt', () => { + if (hasActiveListRoom()) setListSocketStatus('reconnecting'); + }); +} + socket.off('joined_confirmation'); socket.on('joined_confirmation', function (data) { const isInitialVisibleJoin = !didCompleteInitialJoin && !data.silent; didCompleteInitialJoin = true; + setListSocketStatus('syncing'); if (Array.isArray(data.users)) { currentRoomUsers = data.users; @@ -289,6 +386,7 @@ socket.on('full_list', function (data) { updateListSmoothly(data.items); disableCheckboxes(false); wakeCheckInProgress = false; + setListSocketStatus('connected', currentSocketTransportName()); if (typeof window.syncSortModeUI === 'function') { window.syncSortModeUI(); } diff --git a/shopping_app/templates/base.html b/shopping_app/templates/base.html index 70dc0cc..f2b811b 100644 --- a/shopping_app/templates/base.html +++ b/shopping_app/templates/base.html @@ -20,6 +20,7 @@ {% set hide_login_ui = request.path == '/system-auth' %} + {% set is_live_list_view = request.endpoint in ['view_list', 'shared_list'] %} {% set exclude_paths = ['/system-auth'] %} {% if (exclude_paths | select("in", request.path) | list | length == 0) @@ -68,6 +69,12 @@ {% endif %} {% endif %} + {% if is_live_list_view %} +
+ + +
+ {% endif %}
@@ -84,6 +91,12 @@
{% endif %} {% endif %} + {% if is_live_list_view %} +
+ + +
+ {% endif %}