feat(update): server push-команда на массовое обновление UI и формат версий

This commit is contained in:
AidarKC
2026-04-23 16:19:00 +03:00
parent 630ba30c27
commit f213e9aa43
10 changed files with 252 additions and 14 deletions
+6
View File
@@ -1,5 +1,11 @@
self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', (event) => event.waitUntil(self.clients.claim()));
self.addEventListener('message', (event) => {
const data = event?.data || {};
if (data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
async function broadcastToClients(payload) {
const clients = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
+1 -1
View File
@@ -7,7 +7,7 @@
<title>Shine UI Demo</title>
<script>
window.__SHINE_BUILD_HASH__ = '20260413151200';
window.__SHINE_CLIENT_VERSION__ = '1.2.1';
window.__SHINE_CLIENT_VERSION__ = '1.2.2';
</script>
<script>
(function attachStylesWithBuildHash() {
+60 -4
View File
@@ -118,6 +118,7 @@ let connectionNextRetryAtMs = 0;
let connectionCheckInFlight = false;
let wsSessionRestoreInFlight = null;
let uiUpdateReloadScheduled = false;
let pwaUpdateCheckAttempted = false;
setClientErrorTransport((payload) => authService.reportClientError(payload));
initPwaInstallPromptHandling();
@@ -235,20 +236,62 @@ async function triggerImmediateConnectionRetry() {
}
function checkAndReloadIfUiUpdated(remoteHashRaw) {
if (uiUpdateReloadScheduled) return;
const remoteHash = String(remoteHashRaw || '').trim();
if (!remoteHash || !CURRENT_BUILD_HASH || remoteHash === CURRENT_BUILD_HASH) return;
scheduleUiReload({
source: 'version-check',
message: `Обнаружена новая версия UI (через Ping): ${CURRENT_BUILD_HASH} -> ${remoteHash}`,
delayMs: 600,
activateWaitingWorker: true,
});
}
async function refreshServiceWorkers({ activateWaitingWorker = false } = {}) {
if (!('serviceWorker' in navigator)) return;
try {
const registrations = await navigator.serviceWorker.getRegistrations();
await Promise.all(registrations.map(async (registration) => {
try {
await registration.update();
} catch {}
if (activateWaitingWorker && registration.waiting) {
try {
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
} catch {}
}
}));
} catch {
// ignore service worker update failures
}
}
function scheduleUiReload({
source = 'ui-update',
message = 'Запрошено обновление интерфейса',
delayMs = 700,
activateWaitingWorker = true,
} = {}) {
if (uiUpdateReloadScheduled) return;
uiUpdateReloadScheduled = true;
addAppLogEntry({
level: 'info',
source: 'version-check',
message: `Обнаружена новая версия UI (через Ping): ${CURRENT_BUILD_HASH} -> ${remoteHash}`,
source,
message,
});
setConnectionStatus('updating');
void refreshServiceWorkers({ activateWaitingWorker });
const ms = Math.min(15_000, Math.max(200, Number(delayMs || 700)));
window.setTimeout(() => {
window.location.reload();
}, 600);
}, ms);
}
async function tryUpdatePwaOnFirstConnectedPing() {
if (pwaUpdateCheckAttempted) return;
pwaUpdateCheckAttempted = true;
await refreshServiceWorkers({ activateWaitingWorker: false });
}
async function checkConnectionHealth() {
@@ -271,6 +314,7 @@ async function checkConnectionHealth() {
const pingResp = await authService.ws.request('Ping', { ts: Date.now() }, 7000);
const remoteUiBuildHash = pingResp?.payload?.uiBuildHash || pingResp?.uiBuildHash || '';
checkAndReloadIfUiUpdated(remoteUiBuildHash);
await tryUpdatePwaOnFirstConnectedPing();
setConnectionStatus('connected');
} catch {
connectionStatusText = '';
@@ -618,6 +662,18 @@ async function init() {
await terminateCurrentSession({ infoMessage: 'Сессия закрыта с другого устройства.' });
});
authService.onEvent('ForceUiReload', async (evt) => {
const payload = evt?.payload || {};
const reason = String(payload.reason || 'server_debug_api').trim() || 'server_debug_api';
const reloadAfterMs = Number(payload.reloadAfterMs || 700);
scheduleUiReload({
source: 'server-ui-reload',
message: `Сервер запросил обновление UI (${reason})`,
delayMs: reloadAfterMs,
activateWaitingWorker: true,
});
});
authService.onEvent('SignedMessageArrived', async (evt) => {
const payload = evt?.payload || {};
const messageKey = String(payload.messageKey || '').trim();
+36 -6
View File
@@ -10,6 +10,28 @@ import { initPwaPush } from '../services/pwa-push-service.js';
export const pageMeta = { id: 'settings-view', title: 'Настройки' };
function formatBuildStamp(rawValue) {
const value = String(rawValue || '').trim();
if (!/^\d{14}$/.test(value)) return value;
const yyyy = value.slice(0, 4);
const mm = value.slice(4, 6);
const dd = value.slice(6, 8);
const hh = value.slice(8, 10);
const min = value.slice(10, 12);
const ss = value.slice(12, 14);
return `${yyyy}-${mm}-${dd}/${hh}:${min}__${ss}`;
}
function formatVersionForUi(rawValue) {
const value = String(rawValue || '').trim();
if (!value) return 'n/a';
const formatted = formatBuildStamp(value);
if (formatted && formatted !== value) {
return `${formatted} (${value})`;
}
return value;
}
export function render({ navigate }) {
const screen = document.createElement('section');
screen.className = 'stack';
@@ -129,11 +151,11 @@ export function render({ navigate }) {
const clientVersion = document.createElement('p');
clientVersion.className = 'meta-muted';
clientVersion.textContent = `Клиент: ${String(window.__SHINE_CLIENT_VERSION__ || 'n/a').trim() || 'n/a'}`;
clientVersion.textContent = `Клиент: ${formatVersionForUi(window.__SHINE_CLIENT_VERSION__)}`;
const uiBuild = document.createElement('p');
uiBuild.className = 'meta-muted';
uiBuild.textContent = `Сборка UI: ${String(window.__SHINE_BUILD_HASH__ || 'n/a').trim() || 'n/a'}`;
uiBuild.textContent = `Сборка UI: ${formatVersionForUi(window.__SHINE_BUILD_HASH__)}`;
const serverVersion = document.createElement('p');
serverVersion.className = 'meta-muted';
@@ -143,11 +165,19 @@ export function render({ navigate }) {
void (async () => {
try {
const resp = await authService.ws.request('GetServerInfo', {});
const value = String(resp?.payload?.version || '').trim() || 'n/a';
if (!isDisposed) {
serverVersion.textContent = `Сервер: ${value}`;
let value = '';
try {
const pingResp = await authService.ws.request('Ping', { ts: Date.now() }, 7000);
value = String(pingResp?.payload?.serverVersion || pingResp?.serverVersion || '').trim();
} catch {
// fallback below
}
if (!value) {
const infoResp = await authService.ws.request('GetServerInfo', {});
value = String(infoResp?.payload?.version || '').trim();
}
if (!isDisposed) serverVersion.textContent = `Сервер: ${formatVersionForUi(value)}`;
} catch {
if (!isDisposed) {
serverVersion.textContent = 'Сервер: недоступно';