feat(ui): индикатор connected в тулбаре и панель переподключения
This commit is contained in:
parent
29a07a9a8b
commit
0159dd9074
@ -113,53 +113,126 @@ let versionCheckIntervalId = null;
|
|||||||
let versionCheckInFlight = false;
|
let versionCheckInFlight = false;
|
||||||
let reconnectIntervalId = null;
|
let reconnectIntervalId = null;
|
||||||
let sessionRuntimeStarted = false;
|
let sessionRuntimeStarted = false;
|
||||||
let connectionStatusEl = null;
|
|
||||||
let connectionState = '';
|
let connectionState = '';
|
||||||
|
let connectionStatusText = '';
|
||||||
|
let connectionRetryBannerEl = null;
|
||||||
|
let connectionStatusCountdownId = null;
|
||||||
|
let connectionNextRetryAtMs = 0;
|
||||||
|
let connectionCheckInFlight = false;
|
||||||
|
|
||||||
setClientErrorTransport((payload) => authService.reportClientError(payload));
|
setClientErrorTransport((payload) => authService.reportClientError(payload));
|
||||||
initPwaInstallPromptHandling();
|
initPwaInstallPromptHandling();
|
||||||
initCallUiOverlay();
|
initCallUiOverlay();
|
||||||
setCallDebugReporter((payload) => authService.reportClientDebug(payload));
|
setCallDebugReporter((payload) => authService.reportClientDebug(payload));
|
||||||
|
|
||||||
function ensureConnectionStatusEl() {
|
function ensureConnectionIndicatorEl() {
|
||||||
if (connectionStatusEl) return connectionStatusEl;
|
return document.getElementById('toolbar-connection-indicator');
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureConnectionRetryBannerEl() {
|
||||||
|
if (connectionRetryBannerEl) return connectionRetryBannerEl;
|
||||||
if (!appShellEl) return null;
|
if (!appShellEl) return null;
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.id = 'connection-status-slot';
|
el.id = 'connection-retry-banner';
|
||||||
el.className = 'connection-status-slot is-connecting';
|
el.className = 'connection-retry-banner is-connecting';
|
||||||
el.textContent = 'Подключение к серверу...';
|
el.textContent = 'Соединяюсь…';
|
||||||
|
el.addEventListener('click', () => {
|
||||||
|
void triggerImmediateConnectionRetry();
|
||||||
|
});
|
||||||
appShellEl.append(el);
|
appShellEl.append(el);
|
||||||
connectionStatusEl = el;
|
connectionRetryBannerEl = el;
|
||||||
return 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 = '') {
|
function setConnectionStatus(nextState, text = '') {
|
||||||
const el = ensureConnectionStatusEl();
|
|
||||||
if (!el) return;
|
|
||||||
const state = String(nextState || '').trim();
|
const state = String(nextState || '').trim();
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
if (state === connectionState && !text) return;
|
|
||||||
connectionState = state;
|
connectionState = state;
|
||||||
el.classList.remove('is-connected', 'is-connecting', 'is-disconnected', 'is-updating');
|
connectionStatusText = String(text || '').trim();
|
||||||
el.classList.add(`is-${state}`);
|
|
||||||
|
|
||||||
if (text) {
|
|
||||||
el.textContent = text;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (state === 'connected') {
|
|
||||||
el.textContent = 'Подключено к серверу';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (state === 'disconnected') {
|
if (state === 'disconnected') {
|
||||||
el.textContent = 'Нет соединения с сервером';
|
if (!connectionNextRetryAtMs || connectionNextRetryAtMs <= Date.now()) {
|
||||||
return;
|
connectionNextRetryAtMs = Date.now() + CONNECTION_CHECK_INTERVAL_MS;
|
||||||
}
|
}
|
||||||
if (state === 'updating') {
|
startConnectionCountdown();
|
||||||
el.textContent = 'Найдена новая версия, обновляю приложение...';
|
} else {
|
||||||
return;
|
stopConnectionCountdown();
|
||||||
}
|
}
|
||||||
el.textContent = 'Подключение к серверу...';
|
refreshConnectionUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function triggerImmediateConnectionRetry() {
|
||||||
|
if (connectionCheckInFlight) return;
|
||||||
|
connectionNextRetryAtMs = Date.now() + CONNECTION_CHECK_INTERVAL_MS;
|
||||||
|
setConnectionStatus('connecting', 'Соединяюсь…');
|
||||||
|
await checkConnectionHealth();
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseBuildHashFromHtml(html) {
|
function parseBuildHashFromHtml(html) {
|
||||||
@ -207,14 +280,22 @@ function startVersionMonitor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function checkConnectionHealth() {
|
async function checkConnectionHealth() {
|
||||||
|
if (connectionCheckInFlight) return;
|
||||||
|
connectionCheckInFlight = true;
|
||||||
if (connectionState !== 'connected') {
|
if (connectionState !== 'connected') {
|
||||||
setConnectionStatus('connecting');
|
setConnectionStatus('connecting');
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
if (!wsIsOpen()) {
|
||||||
|
await authService.ws.open();
|
||||||
|
}
|
||||||
await authService.ws.request('Ping', { ts: Date.now() }, 7000);
|
await authService.ws.request('Ping', { ts: Date.now() }, 7000);
|
||||||
setConnectionStatus('connected');
|
setConnectionStatus('connected');
|
||||||
} catch {
|
} catch {
|
||||||
|
connectionStatusText = '';
|
||||||
setConnectionStatus('disconnected');
|
setConnectionStatus('disconnected');
|
||||||
|
} finally {
|
||||||
|
connectionCheckInFlight = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,8 +304,15 @@ function startConnectionMonitor() {
|
|||||||
window.clearInterval(pingIntervalId);
|
window.clearInterval(pingIntervalId);
|
||||||
pingIntervalId = null;
|
pingIntervalId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connectionNextRetryAtMs = Date.now() + CONNECTION_CHECK_INTERVAL_MS;
|
||||||
void checkConnectionHealth();
|
void checkConnectionHealth();
|
||||||
|
|
||||||
pingIntervalId = window.setInterval(() => {
|
pingIntervalId = window.setInterval(() => {
|
||||||
|
connectionNextRetryAtMs = Date.now() + CONNECTION_CHECK_INTERVAL_MS;
|
||||||
|
if (connectionState === 'disconnected') {
|
||||||
|
refreshConnectionUi();
|
||||||
|
}
|
||||||
void checkConnectionHealth();
|
void checkConnectionHealth();
|
||||||
}, CONNECTION_CHECK_INTERVAL_MS);
|
}, CONNECTION_CHECK_INTERVAL_MS);
|
||||||
}
|
}
|
||||||
@ -355,6 +443,7 @@ function renderPageFailureFallback(pageId, error) {
|
|||||||
|
|
||||||
screenEl.classList.toggle('no-app-chrome', false);
|
screenEl.classList.toggle('no-app-chrome', false);
|
||||||
toolbarEl.innerHTML = '';
|
toolbarEl.innerHTML = '';
|
||||||
|
refreshConnectionUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderApp() {
|
function renderApp() {
|
||||||
@ -395,6 +484,7 @@ function renderApp() {
|
|||||||
if (showAppChrome) {
|
if (showAppChrome) {
|
||||||
toolbarEl.append(renderToolbar(page.pageMeta.id, navigate));
|
toolbarEl.append(renderToolbar(page.pageMeta.id, navigate));
|
||||||
}
|
}
|
||||||
|
refreshConnectionUi();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[renderApp] controlled fallback', error);
|
console.error('[renderApp] controlled fallback', error);
|
||||||
renderPageFailureFallback(pageId, error);
|
renderPageFailureFallback(pageId, error);
|
||||||
|
|||||||
@ -15,8 +15,22 @@ export function renderToolbar(currentPageId, navigate) {
|
|||||||
|
|
||||||
ITEMS.forEach((item) => {
|
ITEMS.forEach((item) => {
|
||||||
const btn = document.createElement('button');
|
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.innerHTML = `<span>${item.icon}</span><span>${item.label}</span>`;
|
||||||
|
}
|
||||||
btn.addEventListener('click', () => navigate(item.pageId));
|
btn.addEventListener('click', () => navigate(item.pageId));
|
||||||
root.append(btn);
|
root.append(btn);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -611,6 +611,62 @@
|
|||||||
font-size: 11px;
|
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 {
|
.toolbar-btn.active {
|
||||||
background: rgba(83, 216, 251, 0.14);
|
background: rgba(83, 216, 251, 0.14);
|
||||||
color: var(--text);
|
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%);
|
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;
|
position: absolute;
|
||||||
left: 12px;
|
left: 12px;
|
||||||
right: 12px;
|
right: 12px;
|
||||||
bottom: calc(62px + env(safe-area-inset-bottom));
|
bottom: calc(74px + env(safe-area-inset-bottom));
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
border-radius: 11px;
|
border-radius: 11px;
|
||||||
border: 1px solid rgba(133, 156, 201, 0.3);
|
border: 1px solid rgba(133, 156, 201, 0.3);
|
||||||
@ -55,26 +55,27 @@ body {
|
|||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
pointer-events: none;
|
pointer-events: auto;
|
||||||
|
cursor: pointer;
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.connection-status-slot.is-connected {
|
.connection-retry-banner.is-connected {
|
||||||
border-color: rgba(124, 235, 171, 0.4);
|
border-color: rgba(124, 235, 171, 0.4);
|
||||||
color: #d8ffe9;
|
color: #d8ffe9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connection-status-slot.is-connecting {
|
.connection-retry-banner.is-connecting {
|
||||||
border-color: rgba(238, 196, 107, 0.42);
|
border-color: rgba(238, 196, 107, 0.42);
|
||||||
color: #ffe8bb;
|
color: #ffe8bb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connection-status-slot.is-disconnected {
|
.connection-retry-banner.is-disconnected {
|
||||||
border-color: rgba(228, 127, 145, 0.44);
|
border-color: rgba(228, 127, 145, 0.44);
|
||||||
color: #ffdce3;
|
color: #ffdce3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connection-status-slot.is-updating {
|
.connection-retry-banner.is-updating {
|
||||||
border-color: rgba(144, 201, 255, 0.44);
|
border-color: rgba(144, 201, 255, 0.44);
|
||||||
color: #d9eeff;
|
color: #d9eeff;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user