feat(ui): индикатор connected в тулбаре и панель переподключения
This commit is contained in:
parent
29a07a9a8b
commit
0159dd9074
@ -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;
|
||||
}
|
||||
if (state === 'updating') {
|
||||
el.textContent = 'Найдена новая версия, обновляю приложение...';
|
||||
return;
|
||||
startConnectionCountdown();
|
||||
} else {
|
||||
stopConnectionCountdown();
|
||||
}
|
||||
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' : ''}`;
|
||||
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);
|
||||
});
|
||||
|
||||
@ -611,6 +611,62 @@
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.toolbar-label-wrap {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
line-height: 1.05;
|
||||
}
|
||||
|
||||
.toolbar-btn-profile {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.toolbar-connection-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
margin-top: 1px;
|
||||
min-height: 9px;
|
||||
}
|
||||
|
||||
.toolbar-connection-text {
|
||||
font-size: 8px;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: lowercase;
|
||||
color: rgba(191, 213, 255, 0.8);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.toolbar-connection-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 999px;
|
||||
background: rgba(148, 162, 195, 0.65);
|
||||
box-shadow: 0 0 0 2px rgba(97, 116, 156, 0.25);
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
|
||||
.toolbar-connection-indicator.is-connected .toolbar-connection-text {
|
||||
opacity: 1;
|
||||
color: rgba(145, 255, 192, 0.9);
|
||||
}
|
||||
|
||||
.toolbar-connection-indicator.is-connected .toolbar-connection-dot {
|
||||
background: #71e9a5;
|
||||
box-shadow: 0 0 0 2px rgba(72, 201, 134, 0.28);
|
||||
}
|
||||
|
||||
.toolbar-connection-indicator.is-connecting .toolbar-connection-dot {
|
||||
background: #f0c56b;
|
||||
box-shadow: 0 0 0 2px rgba(240, 197, 107, 0.22);
|
||||
}
|
||||
|
||||
.toolbar-connection-indicator.is-disconnected .toolbar-connection-dot {
|
||||
background: #e48792;
|
||||
box-shadow: 0 0 0 2px rgba(228, 135, 146, 0.24);
|
||||
}
|
||||
|
||||
.toolbar-btn.active {
|
||||
background: rgba(83, 216, 251, 0.14);
|
||||
color: var(--text);
|
||||
|
||||
@ -41,11 +41,11 @@ body {
|
||||
background: linear-gradient(180deg, rgba(7, 12, 23, 0) 0%, rgba(6, 11, 22, 0.96) 44%);
|
||||
}
|
||||
|
||||
.connection-status-slot {
|
||||
.connection-retry-banner {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
bottom: calc(62px + env(safe-area-inset-bottom));
|
||||
bottom: calc(74px + env(safe-area-inset-bottom));
|
||||
z-index: 5;
|
||||
border-radius: 11px;
|
||||
border: 1px solid rgba(133, 156, 201, 0.3);
|
||||
@ -55,26 +55,27 @@ body {
|
||||
line-height: 1.2;
|
||||
text-align: center;
|
||||
padding: 7px 10px;
|
||||
pointer-events: none;
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.connection-status-slot.is-connected {
|
||||
.connection-retry-banner.is-connected {
|
||||
border-color: rgba(124, 235, 171, 0.4);
|
||||
color: #d8ffe9;
|
||||
}
|
||||
|
||||
.connection-status-slot.is-connecting {
|
||||
.connection-retry-banner.is-connecting {
|
||||
border-color: rgba(238, 196, 107, 0.42);
|
||||
color: #ffe8bb;
|
||||
}
|
||||
|
||||
.connection-status-slot.is-disconnected {
|
||||
.connection-retry-banner.is-disconnected {
|
||||
border-color: rgba(228, 127, 145, 0.44);
|
||||
color: #ffdce3;
|
||||
}
|
||||
|
||||
.connection-status-slot.is-updating {
|
||||
.connection-retry-banner.is-updating {
|
||||
border-color: rgba(144, 201, 255, 0.44);
|
||||
color: #d9eeff;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user