import { formatPairingShortCode } from './js/lib/device-pairing.js'; const els = { serverLoginInfo: document.querySelector('#server-login-info'), serverAddress: document.querySelector('#server-address'), loginInput: document.querySelector('#login-input'), usePassword: document.querySelector('#use-password'), passwordField: document.querySelector('#password-field'), passwordInput: document.querySelector('#password-input'), startBtn: document.querySelector('#start-btn'), connectCard: document.querySelector('#connect-card'), pairingCard: document.querySelector('#pairing-card'), shortCode: document.querySelector('#short-code'), pairingHint: document.querySelector('#pairing-hint'), pairingExpire: document.querySelector('#pairing-expire'), cancelBtn: document.querySelector('#cancel-btn'), status: document.querySelector('#status'), sessionCard: document.querySelector('#session-card'), sessionLogin: document.querySelector('#session-login'), sessionId: document.querySelector('#session-id'), sessionType: document.querySelector('#session-type'), clientKeyShort: document.querySelector('#client-key-short'), resumeBtn: document.querySelector('#resume-btn'), refreshDevicesBtn: document.querySelector('#refresh-devices-btn'), disconnectBtn: document.querySelector('#disconnect-btn'), walletCard: document.querySelector('#wallet-card'), deviceSelect: document.querySelector('#device-select'), homeserverList: document.querySelector('#homeserver-list'), requestWalletBtn: document.querySelector('#request-wallet-btn'), walletResultCard: document.querySelector('#wallet-result-card'), walletType: document.querySelector('#wallet-type'), walletPubkey: document.querySelector('#wallet-pubkey'), walletVerify: document.querySelector('#wallet-verify'), copyWalletBtn: document.querySelector('#copy-wallet-btn'), connectionPill: document.querySelector('#connection-pill'), }; let state = { settings: { serverLogin: 'shineupme', serverHttp: 'https://shineup.me', login: '', }, pairing: { active: false, expiresAtMs: 0, shortCode: '', }, session: null, walletProfile: null, signing: { selectedDeviceName: '', }, currentWallet: null, status: { text: '', kind: 'info', }, }; let refreshTimer = 0; let saveSettingsTimer = 0; function setStatus(message, kind = 'info') { els.status.textContent = String(message || ''); els.status.className = `status ${kind === 'error' ? 'error' : 'info'}`; els.status.classList.toggle('hidden', !message); } function setConnectedPill(connected) { els.connectionPill.textContent = connected ? 'подключено' : 'не подключено'; els.connectionPill.className = connected ? 'pill pill-online' : 'pill pill-offline'; } function formatRemaining(ms) { const safe = Math.max(0, Math.floor(Number(ms || 0) / 1000)); const minutes = Math.floor(safe / 60); const seconds = safe % 60; return `${minutes} мин ${seconds} сек`; } function shortKey(value, size = 10) { const raw = String(value || '').trim(); return raw ? `${raw.slice(0, size)}...` : '—'; } function renderHomeserverList(items = []) { els.homeserverList.innerHTML = ''; if (!items.length) { const empty = document.createElement('p'); empty.className = 'muted small'; empty.textContent = 'В PDA пока нет опубликованных homeserver-сессий.'; els.homeserverList.append(empty); return; } items.forEach((item) => { const row = document.createElement('div'); row.className = 'summary-row device-row'; const label = document.createElement('span'); label.textContent = `${item.sessionName} (${shortKey(item.sessionPubKeyBase58, 8)})`; const badge = document.createElement('strong'); const stateValue = String(item.onlineState || 'unknown'); badge.textContent = stateValue; badge.className = `device-state device-state-${stateValue}`; row.append(label, badge); els.homeserverList.append(row); }); } function applyState(nextState) { state = nextState || state; const loginValue = String(state?.settings?.login || ''); const resolvedServerLogin = String(state?.settings?.serverLogin || '').trim(); const resolvedServerAddress = String(state?.settings?.serverHttp || '').trim(); els.serverLoginInfo.textContent = resolvedServerLogin ? `Сервер SHiNE: ${resolvedServerLogin}` : 'Сервер SHiNE: —'; els.serverAddress.textContent = resolvedServerAddress ? `Адрес: ${resolvedServerAddress}` : 'Адрес: —'; if (document.activeElement !== els.loginInput) { els.loginInput.value = loginValue; } setConnectedPill(!!state?.session); setStatus(state?.status?.text || '', state?.status?.kind || 'info'); const session = state?.session; const walletProfile = state?.walletProfile; const signing = state?.signing || {}; const currentWallet = state?.currentWallet || null; els.connectCard.classList.toggle('hidden', !!session); els.sessionCard.classList.toggle('hidden', !session); els.walletCard.classList.toggle('hidden', !session); if (session) { els.sessionLogin.textContent = session.login || '—'; els.sessionId.textContent = session.sessionId || '—'; els.sessionType.textContent = String(session.sessionType || 50) === '50' ? 'wallet' : String(session.sessionType || '—'); els.clientKeyShort.textContent = shortKey(walletProfile?.publicKeys?.clientKeyBase58 || ''); } const homeservers = Array.isArray(walletProfile?.homeserverSessions) ? walletProfile.homeserverSessions : []; els.deviceSelect.innerHTML = ''; homeservers.forEach((item) => { const option = document.createElement('option'); option.value = item.sessionName; option.textContent = `${item.sessionName} [${item.onlineState || 'offline'}]`; option.selected = item.sessionName === signing.selectedDeviceName; els.deviceSelect.append(option); }); renderHomeserverList(homeservers); els.requestWalletBtn.disabled = !session || !signing.selectedDeviceName; if (currentWallet?.publicKeyBase58) { els.walletResultCard.classList.remove('hidden'); els.walletType.textContent = currentWallet.type || '—'; els.walletPubkey.textContent = currentWallet.publicKeyBase58 || '—'; els.walletVerify.textContent = currentWallet.verificationText || '—'; } else { els.walletResultCard.classList.add('hidden'); els.walletType.textContent = '—'; els.walletPubkey.textContent = '—'; els.walletVerify.textContent = '—'; } const pairing = state?.pairing || {}; if (pairing.active) { els.pairingCard.classList.remove('hidden'); els.shortCode.textContent = formatPairingShortCode(String(pairing.shortCode || '')); const leftMs = Number(pairing.expiresAtMs || 0) - Date.now(); els.pairingExpire.textContent = leftMs > 0 ? `Код действителен ещё ${formatRemaining(leftMs)}.` : 'Время ожидания истекло.'; els.startBtn.disabled = true; } else { els.pairingCard.classList.add('hidden'); els.shortCode.textContent = formatPairingShortCode(''); els.pairingExpire.textContent = ''; els.startBtn.disabled = false; } } function normalizeError(response, fallback) { return response?.error || fallback || 'Unknown error'; } function sendMessage(type, payload = {}) { return new Promise((resolve, reject) => { chrome.runtime.sendMessage({ type, payload }, (response) => { if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message || 'Runtime message failed')); return; } if (!response?.ok) { reject(new Error(normalizeError(response, 'Wallet operation failed'))); return; } if (response?.state) applyState(response.state); resolve(response); }); }); } async function refreshState() { const response = await sendMessage('wallet:getState'); applyState(response.state); } async function saveSettings() { await sendMessage('wallet:saveSettings', { login: String(els.loginInput.value || '').trim(), }); } async function resolveServerInfo() { const login = String(els.loginInput.value || '').trim(); if (!login) { await sendMessage('wallet:saveSettings', { login: '' }); return; } try { await sendMessage('wallet:resolveServerInfo', { login }); } catch (error) { setStatus(error.message || 'Не удалось определить сервер SHiNE по PDA.', 'error'); } } function scheduleSaveSettings() { if (saveSettingsTimer) { window.clearTimeout(saveSettingsTimer); } saveSettingsTimer = window.setTimeout(() => { saveSettingsTimer = 0; void saveSettings(); }, 250); } async function startPairing() { const login = String(els.loginInput.value || '').trim(); if (!login) { setStatus('Введите логин.', 'error'); return; } setStatus('Создаём wallet-session заявку...', 'info'); try { await sendMessage('wallet:startPairing', { login, usePassword: !!els.usePassword.checked, password: String(els.passwordInput.value || ''), }); } catch (error) { setStatus(error.message || 'Не удалось начать pairing.', 'error'); } } async function cancelPairing() { try { await sendMessage('wallet:cancelPairing'); } catch (error) { setStatus(error.message || 'Не удалось отменить pairing.', 'error'); } } async function resumeSession() { setStatus('Проверяем сохранённую wallet-session...', 'info'); try { await sendMessage('wallet:resumeSession'); } catch (error) { setStatus(error.message || 'Не удалось восстановить session.', 'error'); } } async function disconnectSession() { try { await sendMessage('wallet:disconnectSession'); } catch (error) { setStatus(error.message || 'Не удалось удалить session.', 'error'); } } async function refreshDevices() { setStatus('Обновляем trusted homeserver-устройства...', 'info'); try { await sendMessage('wallet:refreshWalletDevices'); } catch (error) { setStatus(error.message || 'Не удалось обновить список устройств.', 'error'); } } async function updateDeviceSelection() { try { await sendMessage('wallet:updateSigningSelection', { selectedDeviceName: String(els.deviceSelect.value || '').trim(), }); } catch (error) { setStatus(error.message || 'Не удалось обновить выбор homeserver.', 'error'); } } async function requestCurrentWallet() { setStatus('Запрашиваем текущий кошелёк с ESP32...', 'info'); try { await sendMessage('wallet:requestCurrentWallet'); } catch (error) { setStatus(error.message || 'Не удалось получить кошелёк с ESP32.', 'error'); } } async function copyWalletKey() { const value = String(els.walletPubkey.textContent || '').trim(); if (!value || value === '—') return; try { await navigator.clipboard.writeText(value); setStatus('Публичный ключ скопирован.', 'info'); } catch (error) { setStatus(error.message || 'Не удалось скопировать ключ.', 'error'); } } function startUiRefreshLoop() { stopUiRefreshLoop(); refreshTimer = window.setInterval(() => { void refreshState(); }, 1000); } function stopUiRefreshLoop() { if (refreshTimer) { window.clearInterval(refreshTimer); refreshTimer = 0; } } function bindUi() { els.usePassword.addEventListener('change', () => { els.passwordField.classList.toggle('hidden', !els.usePassword.checked); if (!els.usePassword.checked) { els.passwordInput.value = ''; } }); els.loginInput.addEventListener('input', () => { scheduleSaveSettings(); }); els.loginInput.addEventListener('change', () => { void saveSettings(); void resolveServerInfo(); }); els.startBtn.addEventListener('click', () => { void startPairing(); }); els.cancelBtn.addEventListener('click', () => { void cancelPairing(); }); els.resumeBtn.addEventListener('click', () => { void resumeSession(); }); els.refreshDevicesBtn.addEventListener('click', () => { void refreshDevices(); }); els.disconnectBtn.addEventListener('click', () => { void disconnectSession(); }); els.deviceSelect.addEventListener('change', () => { void updateDeviceSelection(); }); els.requestWalletBtn.addEventListener('click', () => { void requestCurrentWallet(); }); els.copyWalletBtn.addEventListener('click', () => { void copyWalletKey(); }); } async function init() { bindUi(); await refreshState(); startUiRefreshLoop(); } window.addEventListener('beforeunload', () => { stopUiRefreshLoop(); if (saveSettingsTimer) { window.clearTimeout(saveSettingsTimer); saveSettingsTimer = 0; } }); void init();