diff --git a/shine-UI/js/app.js b/shine-UI/js/app.js index aa7a321..6a4ba4f 100644 --- a/shine-UI/js/app.js +++ b/shine-UI/js/app.js @@ -94,14 +94,131 @@ const routes = { const screenEl = document.getElementById('app-screen'); const toolbarEl = document.getElementById('toolbar-slot'); +const appShellEl = document.querySelector('.app-shell'); + +const VERSION_CHECK_INTERVAL_MS = 10 * 60 * 1000; +const CONNECTION_CHECK_INTERVAL_MS = 20 * 1000; +const CURRENT_BUILD_HASH = String(window.__SHINE_BUILD_HASH__ || '').trim(); let currentCleanup = null; let pingIntervalId = null; +let versionCheckIntervalId = null; +let versionCheckInFlight = false; let sessionRuntimeStarted = false; +let connectionStatusEl = null; +let connectionState = ''; setClientErrorTransport((payload) => authService.reportClientError(payload)); initPwaInstallPromptHandling(); +function ensureConnectionStatusEl() { + if (connectionStatusEl) return connectionStatusEl; + if (!appShellEl) return null; + const el = document.createElement('div'); + el.id = 'connection-status-slot'; + el.className = 'connection-status-slot is-connecting'; + el.textContent = 'Подключение к серверу...'; + appShellEl.append(el); + connectionStatusEl = el; + return el; +} + +function setConnectionStatus(nextState, text = '') { + const el = ensureConnectionStatusEl(); + if (!el) return; + const state = String(nextState || '').trim(); + if (!state) return; + if (state === connectionState && !text) return; + connectionState = state; + el.classList.remove('is-connected', 'is-connecting', 'is-disconnected', 'is-updating'); + el.classList.add(`is-${state}`); + + if (text) { + el.textContent = text; + return; + } + if (state === 'connected') { + el.textContent = 'Подключено к серверу'; + return; + } + if (state === 'disconnected') { + el.textContent = 'Нет соединения с сервером'; + return; + } + if (state === 'updating') { + el.textContent = 'Найдена новая версия, обновляю приложение...'; + return; + } + el.textContent = 'Подключение к серверу...'; +} + +function parseBuildHashFromHtml(html) { + const text = String(html || ''); + const m = text.match(/window\.__SHINE_BUILD_HASH__\s*=\s*'([^']+)'/); + return String(m?.[1] || '').trim(); +} + +async function checkUiVersionAndReload() { + if (versionCheckInFlight) return; + versionCheckInFlight = true; + try { + const resp = await fetch(`./index.html?versionCheckTs=${Date.now()}`, { cache: 'no-store' }); + if (!resp.ok) return; + const html = await resp.text(); + const remoteHash = parseBuildHashFromHtml(html); + if (!remoteHash || !CURRENT_BUILD_HASH) return; + if (remoteHash === CURRENT_BUILD_HASH) return; + + addAppLogEntry({ + level: 'info', + source: 'version-check', + message: `Обнаружена новая версия UI: ${CURRENT_BUILD_HASH} -> ${remoteHash}`, + }); + setConnectionStatus('updating'); + window.setTimeout(() => { + window.location.reload(); + }, 600); + } catch { + // ignore transient network/version-check errors + } finally { + versionCheckInFlight = false; + } +} + +function startVersionMonitor() { + if (versionCheckIntervalId) { + window.clearInterval(versionCheckIntervalId); + versionCheckIntervalId = null; + } + void checkUiVersionAndReload(); + versionCheckIntervalId = window.setInterval(() => { + void checkUiVersionAndReload(); + }, VERSION_CHECK_INTERVAL_MS); +} + +async function checkConnectionHealth() { + if (connectionState !== 'connected') { + setConnectionStatus('connecting'); + } + try { + await authService.ws.request('Ping', { ts: Date.now() }, 7000); + setConnectionStatus('connected'); + } catch { + setConnectionStatus('disconnected'); + } +} + +function startConnectionMonitor() { + if (pingIntervalId) { + window.clearInterval(pingIntervalId); + pingIntervalId = null; + } + void checkConnectionHealth(); + pingIntervalId = window.setInterval(() => { + void checkConnectionHealth(); + }, CONNECTION_CHECK_INTERVAL_MS); +} + function showGlobalErrorAlert(title, details = {}) { const lines = [title]; if (details.message) lines.push(`Сообщение: ${details.message}`); @@ -293,18 +410,7 @@ async function ensureSessionRuntimeStarted() { onLog: (entry) => addAppLogEntry(entry), }); - if (pingIntervalId) { - window.clearInterval(pingIntervalId); - pingIntervalId = null; - } - pingIntervalId = window.setInterval(async () => { - if (!state.session.isAuthorized) return; - try { - await authService.ws.request('Ping', { ts: Date.now() }); - } catch { - // silent keep-alive - } - }, 60_000); + startConnectionMonitor(); } async function init() { @@ -316,10 +422,7 @@ async function init() { setSessionResetHandler(() => { sessionRuntimeStarted = false; - if (pingIntervalId) { - window.clearInterval(pingIntervalId); - pingIntervalId = null; - } + startConnectionMonitor(); navigate('start-view'); }); @@ -463,6 +566,8 @@ async function init() { await tryAutoLogin(); await hydrateMessagesFromStore(); + startVersionMonitor(); + startConnectionMonitor(); await ensureSessionRuntimeStarted(); if (!window.location.hash) { @@ -472,6 +577,11 @@ async function init() { } window.addEventListener('hashchange', renderApp); + document.addEventListener('visibilitychange', () => { + if (document.visibilityState !== 'visible') return; + void checkUiVersionAndReload(); + void checkConnectionHealth(); + }); } init(); diff --git a/shine-UI/styles/layout.css b/shine-UI/styles/layout.css index 19144be..c7efd84 100644 --- a/shine-UI/styles/layout.css +++ b/shine-UI/styles/layout.css @@ -41,6 +41,44 @@ body { background: linear-gradient(180deg, rgba(7, 12, 23, 0) 0%, rgba(6, 11, 22, 0.96) 44%); } +.connection-status-slot { + position: absolute; + left: 12px; + right: 12px; + bottom: calc(62px + env(safe-area-inset-bottom)); + z-index: 5; + border-radius: 11px; + border: 1px solid rgba(133, 156, 201, 0.3); + background: rgba(10, 19, 37, 0.86); + color: #c6d6f7; + font-size: 12px; + line-height: 1.2; + text-align: center; + padding: 7px 10px; + pointer-events: none; + backdrop-filter: blur(10px); +} + +.connection-status-slot.is-connected { + border-color: rgba(124, 235, 171, 0.4); + color: #d8ffe9; +} + +.connection-status-slot.is-connecting { + border-color: rgba(238, 196, 107, 0.42); + color: #ffe8bb; +} + +.connection-status-slot.is-disconnected { + border-color: rgba(228, 127, 145, 0.44); + color: #ffdce3; +} + +.connection-status-slot.is-updating { + border-color: rgba(144, 201, 255, 0.44); + color: #d9eeff; +} + @media (min-width: 900px) { .app-shell { margin: 16px 0;