diff --git a/shine-UI/js/app.js b/shine-UI/js/app.js index bd84128..d1795f1 100644 --- a/shine-UI/js/app.js +++ b/shine-UI/js/app.js @@ -103,14 +103,11 @@ 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 reconnectIntervalId = null; let sessionRuntimeStarted = false; let connectionState = ''; @@ -120,6 +117,7 @@ let connectionStatusCountdownId = null; let connectionNextRetryAtMs = 0; let connectionCheckInFlight = false; let wsSessionRestoreInFlight = null; +let uiUpdateReloadScheduled = false; setClientErrorTransport((payload) => authService.reportClientError(payload)); initPwaInstallPromptHandling(); @@ -236,48 +234,21 @@ async function triggerImmediateConnectionRetry() { await checkConnectionHealth(); } -function parseBuildHashFromHtml(html) { - const text = String(html || ''); - const m = text.match(/window\.__SHINE_BUILD_HASH__\s*=\s*'([^']+)'/); - return String(m?.[1] || '').trim(); -} +function checkAndReloadIfUiUpdated(remoteHashRaw) { + if (uiUpdateReloadScheduled) return; + const remoteHash = String(remoteHashRaw || '').trim(); + if (!remoteHash || !CURRENT_BUILD_HASH || remoteHash === CURRENT_BUILD_HASH) return; -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); + uiUpdateReloadScheduled = true; + addAppLogEntry({ + level: 'info', + source: 'version-check', + message: `Обнаружена новая версия UI (через Ping): ${CURRENT_BUILD_HASH} -> ${remoteHash}`, + }); + setConnectionStatus('updating'); + window.setTimeout(() => { + window.location.reload(); + }, 600); } async function checkConnectionHealth() { @@ -297,7 +268,9 @@ async function checkConnectionHealth() { return; } } - await authService.ws.request('Ping', { ts: Date.now() }, 7000); + const pingResp = await authService.ws.request('Ping', { ts: Date.now() }, 7000); + const remoteUiBuildHash = pingResp?.payload?.uiBuildHash || pingResp?.uiBuildHash || ''; + checkAndReloadIfUiUpdated(remoteUiBuildHash); setConnectionStatus('connected'); } catch { connectionStatusText = ''; @@ -817,7 +790,6 @@ async function init() { await tryAutoLogin(); await hydrateMessagesFromStore(); - startVersionMonitor(); startConnectionMonitor(); await ensureSessionRuntimeStarted(); @@ -830,7 +802,6 @@ async function init() { window.addEventListener('hashchange', renderApp); document.addEventListener('visibilitychange', () => { if (document.visibilityState !== 'visible') return; - void checkUiVersionAndReload(); void checkConnectionHealth(); }); } diff --git a/shine-UI/js/services/pwa-push-service.js b/shine-UI/js/services/pwa-push-service.js index ff5d971..2e2eb1b 100644 --- a/shine-UI/js/services/pwa-push-service.js +++ b/shine-UI/js/services/pwa-push-service.js @@ -1,49 +1,4 @@ const LS_KEY = 'shine-ui-webpush-subscription-v1'; -const SW_UPDATE_CHECK_INTERVAL_MS = 5 * 60 * 1000; -let swUpdateIntervalId = null; -let controllerChangeHandled = false; - -function setupServiceWorkerAutoUpdate(registration, log) { - if (!registration) return; - - if (!controllerChangeHandled && 'serviceWorker' in navigator) { - controllerChangeHandled = true; - navigator.serviceWorker.addEventListener('controllerchange', () => { - log({ - level: 'info', - source: 'web-push', - message: 'Service Worker обновился, перезагружаем UI', - }); - window.location.reload(); - }); - } - - registration.addEventListener('updatefound', () => { - const installing = registration.installing; - if (!installing) return; - log({ - level: 'info', - source: 'web-push', - message: 'Найдено обновление Service Worker', - }); - }); - - if (swUpdateIntervalId) { - window.clearInterval(swUpdateIntervalId); - swUpdateIntervalId = null; - } - - const checkNow = () => { - registration.update().catch(() => {}); - }; - - checkNow(); - swUpdateIntervalId = window.setInterval(checkNow, SW_UPDATE_CHECK_INTERVAL_MS); - document.addEventListener('visibilitychange', () => { - if (document.visibilityState !== 'visible') return; - checkNow(); - }); -} function urlBase64ToUint8Array(base64String) { const padding = '='.repeat((4 - (base64String.length % 4)) % 4); @@ -81,16 +36,13 @@ export async function initPwaPush({ authService, onLog = null }) { } try { - const registration = await navigator.serviceWorker.register('./firebase-messaging-sw.js', { - updateViaCache: 'none', - }); + const registration = await navigator.serviceWorker.register('./firebase-messaging-sw.js'); log({ level: 'info', source: 'web-push', message: 'Service Worker зарегистрирован', details: { scope: registration.scope }, }); - setupServiceWorkerAutoUpdate(registration, log); const permission = await Notification.requestPermission(); if (permission !== 'granted') { diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/Net_Ping_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/Net_Ping_Handler.java index e106a41..afb122d 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/Net_Ping_Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/Net_Ping_Handler.java @@ -7,12 +7,26 @@ import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_Ping_Request; import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_Ping_Response; import server.logic.ws_protocol.WireCodes; +import utils.config.AppConfig; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Ping — keep-alive. - * В ответ кладём только ts (текущее время сервера в мс). + * В ответ кладём: + * - ts (текущее время сервера в мс) + * - uiBuildHash (текущий build hash UI, если удалось определить) */ public class Net_Ping_Handler implements JsonMessageHandler { + private static final AppConfig CONFIG = AppConfig.getInstance(); + private static final String DEFAULT_UI_INDEX_PATH = "/home/user/docker/caddyFile/sites/shine-UI/index.html"; + private static final Pattern UI_HASH_PATTERN = Pattern.compile("window\\.__SHINE_BUILD_HASH__\\s*=\\s*'([^']+)'"); + private static volatile String cachedUiBuildHash = ""; + private static volatile long cachedUiIndexMtimeMs = -1L; @Override public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) { @@ -25,7 +39,33 @@ public class Net_Ping_Handler implements JsonMessageHandler { // ничего не проверяем, просто отдаём серверное время resp.setTs(System.currentTimeMillis()); + resp.setUiBuildHash(resolveUiBuildHash()); return resp; } -} \ No newline at end of file + + private static String resolveUiBuildHash() { + String indexPathRaw = CONFIG.getParam("server.ui.indexPath"); + String indexPath = (indexPathRaw == null || indexPathRaw.isBlank()) + ? DEFAULT_UI_INDEX_PATH + : indexPathRaw.trim(); + try { + Path path = Path.of(indexPath); + long mtime = Files.getLastModifiedTime(path).toMillis(); + if (mtime == cachedUiIndexMtimeMs && !cachedUiBuildHash.isBlank()) { + return cachedUiBuildHash; + } + + String html = Files.readString(path, StandardCharsets.UTF_8); + Matcher matcher = UI_HASH_PATTERN.matcher(html); + String found = matcher.find() ? String.valueOf(matcher.group(1)).trim() : ""; + + cachedUiIndexMtimeMs = mtime; + cachedUiBuildHash = found; + return found; + } catch (Exception ignored) { + String fallback = CONFIG.getParam("server.ui.buildHash"); + return fallback == null ? "" : fallback.trim(); + } + } +} diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/entyties/Net_Ping_Response.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/entyties/Net_Ping_Response.java index 9f443cd..f85145a 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/entyties/Net_Ping_Response.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/entyties/Net_Ping_Response.java @@ -8,13 +8,20 @@ import server.logic.ws_protocol.JSON.entyties.Net_Response; * "op": "Ping", * "requestId": "req-1", * "status": 200, - * "payload": { "ts": 1700000000123 } + * "payload": { + * "ts": 1700000000123, + * "uiBuildHash": "20260422164933" + * } * } */ public class Net_Ping_Response extends Net_Response { private long ts; + private String uiBuildHash; public long getTs() { return ts; } public void setTs(long ts) { this.ts = ts; } -} \ No newline at end of file + + public String getUiBuildHash() { return uiBuildHash; } + public void setUiBuildHash(String uiBuildHash) { this.uiBuildHash = uiBuildHash; } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b1e9e86..7143818 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -12,6 +12,8 @@ server.info.physicalRegion= server.info.description= server.info.origin= server.info.extraInfo= +server.ui.indexPath=/home/user/docker/caddyFile/sites/shine-UI/index.html +server.ui.buildHash= # Web Push (VAPID) webpush.vapid.public=BOdoWZndZRaNe9kyUFsJ5-xEfFABXNKennAKg15Z7ycAwUIQ7yDV_sIWWYJCwJriN4g9oU-CyJPrn1U6lfxuDbI