feat(update): проверка версии UI через Ping без периодических опросов
This commit is contained in:
parent
1a8d1c70fd
commit
78d6124f2a
@ -103,14 +103,11 @@ const screenEl = document.getElementById('app-screen');
|
|||||||
const toolbarEl = document.getElementById('toolbar-slot');
|
const toolbarEl = document.getElementById('toolbar-slot');
|
||||||
const appShellEl = document.querySelector('.app-shell');
|
const appShellEl = document.querySelector('.app-shell');
|
||||||
|
|
||||||
const VERSION_CHECK_INTERVAL_MS = 10 * 60 * 1000;
|
|
||||||
const CONNECTION_CHECK_INTERVAL_MS = 20 * 1000;
|
const CONNECTION_CHECK_INTERVAL_MS = 20 * 1000;
|
||||||
const CURRENT_BUILD_HASH = String(window.__SHINE_BUILD_HASH__ || '').trim();
|
const CURRENT_BUILD_HASH = String(window.__SHINE_BUILD_HASH__ || '').trim();
|
||||||
|
|
||||||
let currentCleanup = null;
|
let currentCleanup = null;
|
||||||
let pingIntervalId = null;
|
let pingIntervalId = null;
|
||||||
let versionCheckIntervalId = null;
|
|
||||||
let versionCheckInFlight = false;
|
|
||||||
let reconnectIntervalId = null;
|
let reconnectIntervalId = null;
|
||||||
let sessionRuntimeStarted = false;
|
let sessionRuntimeStarted = false;
|
||||||
let connectionState = '';
|
let connectionState = '';
|
||||||
@ -120,6 +117,7 @@ let connectionStatusCountdownId = null;
|
|||||||
let connectionNextRetryAtMs = 0;
|
let connectionNextRetryAtMs = 0;
|
||||||
let connectionCheckInFlight = false;
|
let connectionCheckInFlight = false;
|
||||||
let wsSessionRestoreInFlight = null;
|
let wsSessionRestoreInFlight = null;
|
||||||
|
let uiUpdateReloadScheduled = false;
|
||||||
|
|
||||||
setClientErrorTransport((payload) => authService.reportClientError(payload));
|
setClientErrorTransport((payload) => authService.reportClientError(payload));
|
||||||
initPwaInstallPromptHandling();
|
initPwaInstallPromptHandling();
|
||||||
@ -236,48 +234,21 @@ async function triggerImmediateConnectionRetry() {
|
|||||||
await checkConnectionHealth();
|
await checkConnectionHealth();
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseBuildHashFromHtml(html) {
|
function checkAndReloadIfUiUpdated(remoteHashRaw) {
|
||||||
const text = String(html || '');
|
if (uiUpdateReloadScheduled) return;
|
||||||
const m = text.match(/window\.__SHINE_BUILD_HASH__\s*=\s*'([^']+)'/);
|
const remoteHash = String(remoteHashRaw || '').trim();
|
||||||
return String(m?.[1] || '').trim();
|
if (!remoteHash || !CURRENT_BUILD_HASH || remoteHash === CURRENT_BUILD_HASH) return;
|
||||||
}
|
|
||||||
|
|
||||||
async function checkUiVersionAndReload() {
|
uiUpdateReloadScheduled = true;
|
||||||
if (versionCheckInFlight) return;
|
addAppLogEntry({
|
||||||
versionCheckInFlight = true;
|
level: 'info',
|
||||||
try {
|
source: 'version-check',
|
||||||
const resp = await fetch(`./index.html?versionCheckTs=${Date.now()}`, { cache: 'no-store' });
|
message: `Обнаружена новая версия UI (через Ping): ${CURRENT_BUILD_HASH} -> ${remoteHash}`,
|
||||||
if (!resp.ok) return;
|
});
|
||||||
const html = await resp.text();
|
setConnectionStatus('updating');
|
||||||
const remoteHash = parseBuildHashFromHtml(html);
|
window.setTimeout(() => {
|
||||||
if (!remoteHash || !CURRENT_BUILD_HASH) return;
|
window.location.reload();
|
||||||
if (remoteHash === CURRENT_BUILD_HASH) return;
|
}, 600);
|
||||||
|
|
||||||
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() {
|
async function checkConnectionHealth() {
|
||||||
@ -297,7 +268,9 @@ async function checkConnectionHealth() {
|
|||||||
return;
|
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');
|
setConnectionStatus('connected');
|
||||||
} catch {
|
} catch {
|
||||||
connectionStatusText = '';
|
connectionStatusText = '';
|
||||||
@ -817,7 +790,6 @@ async function init() {
|
|||||||
|
|
||||||
await tryAutoLogin();
|
await tryAutoLogin();
|
||||||
await hydrateMessagesFromStore();
|
await hydrateMessagesFromStore();
|
||||||
startVersionMonitor();
|
|
||||||
startConnectionMonitor();
|
startConnectionMonitor();
|
||||||
await ensureSessionRuntimeStarted();
|
await ensureSessionRuntimeStarted();
|
||||||
|
|
||||||
@ -830,7 +802,6 @@ async function init() {
|
|||||||
window.addEventListener('hashchange', renderApp);
|
window.addEventListener('hashchange', renderApp);
|
||||||
document.addEventListener('visibilitychange', () => {
|
document.addEventListener('visibilitychange', () => {
|
||||||
if (document.visibilityState !== 'visible') return;
|
if (document.visibilityState !== 'visible') return;
|
||||||
void checkUiVersionAndReload();
|
|
||||||
void checkConnectionHealth();
|
void checkConnectionHealth();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,49 +1,4 @@
|
|||||||
const LS_KEY = 'shine-ui-webpush-subscription-v1';
|
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) {
|
function urlBase64ToUint8Array(base64String) {
|
||||||
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
|
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
|
||||||
@ -81,16 +36,13 @@ export async function initPwaPush({ authService, onLog = null }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const registration = await navigator.serviceWorker.register('./firebase-messaging-sw.js', {
|
const registration = await navigator.serviceWorker.register('./firebase-messaging-sw.js');
|
||||||
updateViaCache: 'none',
|
|
||||||
});
|
|
||||||
log({
|
log({
|
||||||
level: 'info',
|
level: 'info',
|
||||||
source: 'web-push',
|
source: 'web-push',
|
||||||
message: 'Service Worker зарегистрирован',
|
message: 'Service Worker зарегистрирован',
|
||||||
details: { scope: registration.scope },
|
details: { scope: registration.scope },
|
||||||
});
|
});
|
||||||
setupServiceWorkerAutoUpdate(registration, log);
|
|
||||||
|
|
||||||
const permission = await Notification.requestPermission();
|
const permission = await Notification.requestPermission();
|
||||||
if (permission !== 'granted') {
|
if (permission !== 'granted') {
|
||||||
|
|||||||
@ -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_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_Ping_Response;
|
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_Ping_Response;
|
||||||
import server.logic.ws_protocol.WireCodes;
|
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.
|
* Ping — keep-alive.
|
||||||
* В ответ кладём только ts (текущее время сервера в мс).
|
* В ответ кладём:
|
||||||
|
* - ts (текущее время сервера в мс)
|
||||||
|
* - uiBuildHash (текущий build hash UI, если удалось определить)
|
||||||
*/
|
*/
|
||||||
public class Net_Ping_Handler implements JsonMessageHandler {
|
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
|
@Override
|
||||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
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.setTs(System.currentTimeMillis());
|
||||||
|
resp.setUiBuildHash(resolveUiBuildHash());
|
||||||
|
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -8,13 +8,20 @@ import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
|||||||
* "op": "Ping",
|
* "op": "Ping",
|
||||||
* "requestId": "req-1",
|
* "requestId": "req-1",
|
||||||
* "status": 200,
|
* "status": 200,
|
||||||
* "payload": { "ts": 1700000000123 }
|
* "payload": {
|
||||||
|
* "ts": 1700000000123,
|
||||||
|
* "uiBuildHash": "20260422164933"
|
||||||
|
* }
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
public class Net_Ping_Response extends Net_Response {
|
public class Net_Ping_Response extends Net_Response {
|
||||||
|
|
||||||
private long ts;
|
private long ts;
|
||||||
|
private String uiBuildHash;
|
||||||
|
|
||||||
public long getTs() { return ts; }
|
public long getTs() { return ts; }
|
||||||
public void setTs(long ts) { this.ts = ts; }
|
public void setTs(long ts) { this.ts = ts; }
|
||||||
|
|
||||||
|
public String getUiBuildHash() { return uiBuildHash; }
|
||||||
|
public void setUiBuildHash(String uiBuildHash) { this.uiBuildHash = uiBuildHash; }
|
||||||
}
|
}
|
||||||
@ -12,6 +12,8 @@ server.info.physicalRegion=
|
|||||||
server.info.description=
|
server.info.description=
|
||||||
server.info.origin=
|
server.info.origin=
|
||||||
server.info.extraInfo=
|
server.info.extraInfo=
|
||||||
|
server.ui.indexPath=/home/user/docker/caddyFile/sites/shine-UI/index.html
|
||||||
|
server.ui.buildHash=
|
||||||
|
|
||||||
# Web Push (VAPID)
|
# Web Push (VAPID)
|
||||||
webpush.vapid.public=BOdoWZndZRaNe9kyUFsJ5-xEfFABXNKennAKg15Z7ycAwUIQ7yDV_sIWWYJCwJriN4g9oU-CyJPrn1U6lfxuDbI
|
webpush.vapid.public=BOdoWZndZRaNe9kyUFsJ5-xEfFABXNKennAKg15Z7ycAwUIQ7yDV_sIWWYJCwJriN4g9oU-CyJPrn1U6lfxuDbI
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user