import { showToast } from '../services/channels-ux.js'; import { createTopBar } from '../components/topbar.js'; import { isSessionInvalidError, refreshSessions, setAuthError, setAuthInfo, state, terminateCurrentSession, } from '../state.js'; export const pageMeta = { id: 'device-view', title: 'Устройства' }; function formatSessionType(sessionType) { if (Number(sessionType) === 100) return 'Домашний сервер'; if (Number(sessionType) === 50) return 'Кошелёк'; if (Number(sessionType) === 1) return 'Приложение'; return `Тип ${Number(sessionType) || 0}`; } // «Mozilla/5.0 (X11; Linux x86_64) … Chrome/139 …» → «Chrome · Linux». function describeClient(userAgent) { const ua = String(userAgent || ''); if (!ua) return 'Неизвестное устройство'; const browser = /Edg\//.test(ua) ? 'Edge' : /OPR\/|Opera/.test(ua) ? 'Opera' : /Firefox\//.test(ua) ? 'Firefox' : /YaBrowser\//.test(ua) ? 'Яндекс Браузер' : /Chrome\//.test(ua) ? 'Chrome' : /Safari\//.test(ua) ? 'Safari' : ''; const os = /iPhone/.test(ua) ? 'iPhone' : /iPad/.test(ua) ? 'iPad' : /Android/.test(ua) ? 'Android' : /Windows/.test(ua) ? 'Windows' : /Mac OS X|Macintosh/.test(ua) ? 'macOS' : /Linux|X11/.test(ua) ? 'Linux' : ''; const parts = [browser, os].filter(Boolean); return parts.length ? parts.join(' · ') : ua.slice(0, 40); } // «сегодня, 02:20» / «вчера, 23:10» / «24 сентября, 18:05». function formatSessionTime(ms) { const date = new Date(ms); const time = date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }); const startOf = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime(); const days = Math.round((startOf(new Date()) - startOf(date)) / 86400000); if (days === 0) return `сегодня, ${time}`; if (days === 1) return `вчера, ${time}`; const sameYear = date.getFullYear() === new Date().getFullYear(); const day = date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', ...(sameYear ? {} : { year: 'numeric' }) }); return `${day}, ${time}`; } function formatOnlineStatus(onlineOnThisServer) { return onlineOnThisServer ? 'в сети' : 'не в сети'; } function sortSessionsByOnline(sessions = []) { return [...sessions].sort((a, b) => { const aOnline = a?.onlineOnThisServer ? 1 : 0; const bOnline = b?.onlineOnThisServer ? 1 : 0; if (aOnline !== bOnline) return bOnline - aOnline; return Number(b?.lastAuthenticatedAtMs || 0) - Number(a?.lastAuthenticatedAtMs || 0); }); } export function render({navigate, chrome}) { const screen = document.createElement('section'); screen.className = 'stack'; chrome?.setTopbar(createTopBar({ title: 'Устройства', back: { label: '←', onClick: () => navigate('settings-view') }, actions: [{ icon: 'refresh', title: 'Обновить список сеансов', ariaLabel: 'Обновить список сеансов', id: 'reload-sessions', onClick: () => reloadSessions() }], })); const actions = document.createElement('div'); actions.className = 'nav-list'; actions.innerHTML = ` `; actions.querySelector('#pair-by-code-btn').addEventListener('click', () => navigate('device-pairing-view')); actions.querySelector('#connect-device-btn').addEventListener('click', () => navigate('connect-device-view')); actions.querySelector('#show-keys-btn').addEventListener('click', () => navigate('show-keys-view')); const sessionsBlock = document.createElement('div'); sessionsBlock.className = 'stack device-sessions'; const buildList = () => { sessionsBlock.innerHTML = ''; const sessions = sortSessionsByOnline(state.sessions || []); const current = sessions.find((s) => s.sessionId === state.session.sessionId) || sessions[0]; const others = sortSessionsByOnline(sessions.filter((s) => s.sessionId !== current?.sessionId)); const createSessionItem = (session, isCurrent) => { const item = document.createElement('button'); item.className = 'ui-button session-item'; item.type = 'button'; const isWebClient = Number(session.sessionType) === 1 && /web/i.test(String(session.clientPlatform || '')); const sessionTypeText = isWebClient ? 'Браузер' : formatSessionType(session.sessionType); const sessionPlatformText = !isWebClient && session.clientPlatform ? ` · ${session.clientPlatform}` : ''; const onlineStatusText = formatOnlineStatus(!!session.onlineOnThisServer); const onlineStatusClass = session.onlineOnThisServer ? 'session-status session-status--online' : 'session-status'; item.innerHTML = ` ${sessionTypeText}${sessionPlatformText} · ${onlineStatusText} ${session.geo && session.geo !== 'unknown' ? '' : ''} ${formatSessionTime(session.lastAuthenticatedAtMs || Date.now())} `; item.querySelector('.session-item__title').textContent = describeClient(session.clientInfoFromClient); item.title = String(session.clientInfoFromClient || ''); const geoEl = item.querySelector('.session-item__geo'); if (geoEl) geoEl.textContent = session.geo; item.addEventListener('click', () => navigate(`device-session-view/${session.sessionId}`)); return item; }; if (!current) { const empty = document.createElement('p'); empty.className = 'meta-muted'; empty.textContent = 'Активные сессии не найдены.'; sessionsBlock.append(empty); return; } const groupTitle = (text) => { const title = document.createElement('p'); title.className = 'nav-list__title'; title.textContent = text; return title; }; const currentList = document.createElement('div'); currentList.className = 'nav-list'; currentList.append(createSessionItem(current, true)); sessionsBlock.append(groupTitle('Это устройство'), currentList, groupTitle('Другие сеансы')); if (others.length === 0) { const empty = document.createElement('p'); empty.className = 'empty-note'; empty.textContent = 'Других сеансов нет.'; sessionsBlock.append(empty); } else { const othersList = document.createElement('div'); othersList.className = 'nav-list'; others.forEach((session) => othersList.append(createSessionItem(session, false))); sessionsBlock.append(othersList); } }; async function reloadSessions() { try { await refreshSessions(); buildList(); setAuthInfo('Список сеансов обновлён.'); showToast('Список сеансов обновлён'); } catch (error) { if (isSessionInvalidError(error)) { await terminateCurrentSession({ infoMessage: 'Сессия на этом устройстве уже завершена. Выполните вход заново.', }); return; } setAuthError(error.message); showToast(error.message, { kind: 'error' }); } } buildList(); screen.append(actions, sessionsBlock); return screen; }