SHA256
feat(ui): индикатор connected в тулбаре и панель переподключения
This commit is contained in:
+118
-28
@@ -113,53 +113,126 @@ let versionCheckIntervalId = null;
|
||||
let versionCheckInFlight = false;
|
||||
let reconnectIntervalId = null;
|
||||
let sessionRuntimeStarted = false;
|
||||
let connectionStatusEl = null;
|
||||
let connectionState = '';
|
||||
let connectionStatusText = '';
|
||||
let connectionRetryBannerEl = null;
|
||||
let connectionStatusCountdownId = null;
|
||||
let connectionNextRetryAtMs = 0;
|
||||
let connectionCheckInFlight = false;
|
||||
|
||||
setClientErrorTransport((payload) => authService.reportClientError(payload));
|
||||
initPwaInstallPromptHandling();
|
||||
initCallUiOverlay();
|
||||
setCallDebugReporter((payload) => authService.reportClientDebug(payload));
|
||||
|
||||
function ensureConnectionStatusEl() {
|
||||
if (connectionStatusEl) return connectionStatusEl;
|
||||
function ensureConnectionIndicatorEl() {
|
||||
return document.getElementById('toolbar-connection-indicator');
|
||||
}
|
||||
|
||||
function ensureConnectionRetryBannerEl() {
|
||||
if (connectionRetryBannerEl) return connectionRetryBannerEl;
|
||||
if (!appShellEl) return null;
|
||||
const el = document.createElement('div');
|
||||
el.id = 'connection-status-slot';
|
||||
el.className = 'connection-status-slot is-connecting';
|
||||
el.textContent = 'Подключение к серверу...';
|
||||
el.id = 'connection-retry-banner';
|
||||
el.className = 'connection-retry-banner is-connecting';
|
||||
el.textContent = 'Соединяюсь…';
|
||||
el.addEventListener('click', () => {
|
||||
void triggerImmediateConnectionRetry();
|
||||
});
|
||||
appShellEl.append(el);
|
||||
connectionStatusEl = el;
|
||||
connectionRetryBannerEl = el;
|
||||
return el;
|
||||
}
|
||||
|
||||
function stopConnectionCountdown() {
|
||||
if (connectionStatusCountdownId) {
|
||||
window.clearInterval(connectionStatusCountdownId);
|
||||
connectionStatusCountdownId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function getConnectionRetrySecondsLeft() {
|
||||
const leftMs = Math.max(0, Number(connectionNextRetryAtMs || 0) - Date.now());
|
||||
return Math.ceil(leftMs / 1000);
|
||||
}
|
||||
|
||||
function refreshConnectionUi() {
|
||||
const state = String(connectionState || 'connecting').trim();
|
||||
|
||||
const indicatorEl = ensureConnectionIndicatorEl();
|
||||
if (indicatorEl) {
|
||||
indicatorEl.classList.remove('is-connected', 'is-disconnected', 'is-connecting', 'is-updating', 'is-unknown');
|
||||
indicatorEl.classList.add(`is-${state || 'unknown'}`);
|
||||
}
|
||||
|
||||
const bannerEl = ensureConnectionRetryBannerEl();
|
||||
if (!bannerEl) return;
|
||||
|
||||
if (state === 'connected' || state === 'updating') {
|
||||
bannerEl.hidden = true;
|
||||
stopConnectionCountdown();
|
||||
return;
|
||||
}
|
||||
|
||||
bannerEl.hidden = false;
|
||||
bannerEl.classList.remove('is-connected', 'is-disconnected', 'is-connecting', 'is-updating');
|
||||
bannerEl.classList.add(`is-${state}`);
|
||||
|
||||
if (connectionStatusText) {
|
||||
bannerEl.textContent = connectionStatusText;
|
||||
return;
|
||||
}
|
||||
|
||||
if (state === 'connecting') {
|
||||
bannerEl.textContent = 'Соединяюсь… Нажмите, чтобы попробовать сразу';
|
||||
return;
|
||||
}
|
||||
|
||||
if (state === 'disconnected') {
|
||||
const secs = getConnectionRetrySecondsLeft();
|
||||
bannerEl.textContent = `Нет соединения. Повтор через ${secs}с. Нажмите для попытки сейчас`;
|
||||
return;
|
||||
}
|
||||
|
||||
bannerEl.textContent = 'Проблема с соединением. Нажмите для повтора';
|
||||
}
|
||||
|
||||
function startConnectionCountdown() {
|
||||
if (connectionStatusCountdownId) return;
|
||||
connectionStatusCountdownId = window.setInterval(() => {
|
||||
if (connectionState !== 'disconnected') return;
|
||||
const secs = getConnectionRetrySecondsLeft();
|
||||
if (secs <= 0 && !connectionCheckInFlight) {
|
||||
connectionStatusText = '';
|
||||
setConnectionStatus('connecting', 'Соединяюсь…');
|
||||
void checkConnectionHealth();
|
||||
return;
|
||||
}
|
||||
refreshConnectionUi();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
connectionStatusText = String(text || '').trim();
|
||||
if (state === 'disconnected') {
|
||||
el.textContent = 'Нет соединения с сервером';
|
||||
return;
|
||||
if (!connectionNextRetryAtMs || connectionNextRetryAtMs <= Date.now()) {
|
||||
connectionNextRetryAtMs = Date.now() + CONNECTION_CHECK_INTERVAL_MS;
|
||||
}
|
||||
startConnectionCountdown();
|
||||
} else {
|
||||
stopConnectionCountdown();
|
||||
}
|
||||
if (state === 'updating') {
|
||||
el.textContent = 'Найдена новая версия, обновляю приложение...';
|
||||
return;
|
||||
}
|
||||
el.textContent = 'Подключение к серверу...';
|
||||
refreshConnectionUi();
|
||||
}
|
||||
|
||||
async function triggerImmediateConnectionRetry() {
|
||||
if (connectionCheckInFlight) return;
|
||||
connectionNextRetryAtMs = Date.now() + CONNECTION_CHECK_INTERVAL_MS;
|
||||
setConnectionStatus('connecting', 'Соединяюсь…');
|
||||
await checkConnectionHealth();
|
||||
}
|
||||
|
||||
function parseBuildHashFromHtml(html) {
|
||||
@@ -207,14 +280,22 @@ function startVersionMonitor() {
|
||||
}
|
||||
|
||||
async function checkConnectionHealth() {
|
||||
if (connectionCheckInFlight) return;
|
||||
connectionCheckInFlight = true;
|
||||
if (connectionState !== 'connected') {
|
||||
setConnectionStatus('connecting');
|
||||
}
|
||||
try {
|
||||
if (!wsIsOpen()) {
|
||||
await authService.ws.open();
|
||||
}
|
||||
await authService.ws.request('Ping', { ts: Date.now() }, 7000);
|
||||
setConnectionStatus('connected');
|
||||
} catch {
|
||||
connectionStatusText = '';
|
||||
setConnectionStatus('disconnected');
|
||||
} finally {
|
||||
connectionCheckInFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,8 +304,15 @@ function startConnectionMonitor() {
|
||||
window.clearInterval(pingIntervalId);
|
||||
pingIntervalId = null;
|
||||
}
|
||||
|
||||
connectionNextRetryAtMs = Date.now() + CONNECTION_CHECK_INTERVAL_MS;
|
||||
void checkConnectionHealth();
|
||||
|
||||
pingIntervalId = window.setInterval(() => {
|
||||
connectionNextRetryAtMs = Date.now() + CONNECTION_CHECK_INTERVAL_MS;
|
||||
if (connectionState === 'disconnected') {
|
||||
refreshConnectionUi();
|
||||
}
|
||||
void checkConnectionHealth();
|
||||
}, CONNECTION_CHECK_INTERVAL_MS);
|
||||
}
|
||||
@@ -355,6 +443,7 @@ function renderPageFailureFallback(pageId, error) {
|
||||
|
||||
screenEl.classList.toggle('no-app-chrome', false);
|
||||
toolbarEl.innerHTML = '';
|
||||
refreshConnectionUi();
|
||||
}
|
||||
|
||||
function renderApp() {
|
||||
@@ -395,6 +484,7 @@ function renderApp() {
|
||||
if (showAppChrome) {
|
||||
toolbarEl.append(renderToolbar(page.pageMeta.id, navigate));
|
||||
}
|
||||
refreshConnectionUi();
|
||||
} catch (error) {
|
||||
console.error('[renderApp] controlled fallback', error);
|
||||
renderPageFailureFallback(pageId, error);
|
||||
|
||||
@@ -15,8 +15,22 @@ export function renderToolbar(currentPageId, navigate) {
|
||||
|
||||
ITEMS.forEach((item) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = `toolbar-btn${item.pageId === active ? ' active' : ''}`;
|
||||
btn.innerHTML = `<span>${item.icon}</span><span>${item.label}</span>`;
|
||||
const isProfile = item.pageId === 'profile-view';
|
||||
btn.className = `toolbar-btn${item.pageId === active ? ' active' : ''}${isProfile ? ' toolbar-btn-profile' : ''}`;
|
||||
if (isProfile) {
|
||||
btn.innerHTML = `
|
||||
<span>${item.icon}</span>
|
||||
<span class="toolbar-label-wrap">
|
||||
<span>${item.label}</span>
|
||||
<span id="toolbar-connection-indicator" class="toolbar-connection-indicator is-unknown">
|
||||
<span class="toolbar-connection-text">connected</span>
|
||||
<span class="toolbar-connection-dot" aria-hidden="true"></span>
|
||||
</span>
|
||||
</span>
|
||||
`;
|
||||
} else {
|
||||
btn.innerHTML = `<span>${item.icon}</span><span>${item.label}</span>`;
|
||||
}
|
||||
btn.addEventListener('click', () => navigate(item.pageId));
|
||||
root.append(btn);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user