422 lines
15 KiB
JavaScript
422 lines
15 KiB
JavaScript
import { formatPairingShortCode } from './js/lib/device-pairing.js';
|
||
|
||
const els = {
|
||
serverLoginInfo: document.querySelector('#server-login-info'),
|
||
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'),
|
||
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'),
|
||
pendingApprovalCard: document.querySelector('#pending-approval-card'),
|
||
pendingApprovalSubtitle: document.querySelector('#pending-approval-subtitle'),
|
||
pendingApprovalDetails: document.querySelector('#pending-approval-details'),
|
||
cancelPendingApprovalBtn: document.querySelector('#cancel-pending-approval-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,
|
||
pendingApproval: 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 renderPendingApproval(pendingApproval) {
|
||
els.pendingApprovalDetails.innerHTML = '';
|
||
if (!pendingApproval) return;
|
||
const summary = pendingApproval.transactionSummary || {};
|
||
const programs = Array.isArray(summary.programs) && summary.programs.length
|
||
? summary.programs.join(', ')
|
||
: 'не определены';
|
||
const details = [
|
||
{ label: 'Сайт', value: pendingApproval.origin || '—', mono: true },
|
||
{ label: 'Кошелёк', value: pendingApproval.publicKeyBase58 || '—', mono: true },
|
||
{ label: 'Комментарий', value: pendingApproval.comment || 'Транзакция запрошена сайтом' },
|
||
{ label: 'Тип', value: summary.kind || 'legacy' },
|
||
{ label: 'Инструкций', value: String(summary.instructionCount ?? 0) },
|
||
{ label: 'Программы', value: programs, mono: true },
|
||
];
|
||
if (summary.feePayer) {
|
||
details.push({ label: 'Fee payer', value: summary.feePayer, mono: true });
|
||
}
|
||
if (summary.recentBlockhash) {
|
||
details.push({ label: 'Blockhash', value: summary.recentBlockhash, mono: true });
|
||
}
|
||
for (const item of details) {
|
||
const row = document.createElement('div');
|
||
row.className = 'detail-row';
|
||
const label = document.createElement('div');
|
||
label.className = 'detail-label';
|
||
label.textContent = item.label;
|
||
const value = document.createElement('div');
|
||
value.className = `detail-value${item.mono ? ' mono' : ''}`;
|
||
value.textContent = item.value;
|
||
row.append(label, value);
|
||
els.pendingApprovalDetails.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 && resolvedServerAddress
|
||
? `Сервер SHiNE: ${resolvedServerLogin} (${resolvedServerAddress})`
|
||
: 'Сервер SHiNE: —';
|
||
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;
|
||
const pendingApproval = state?.pendingApproval || null;
|
||
|
||
els.connectCard.classList.toggle('hidden', !!session);
|
||
els.sessionCard.classList.toggle('hidden', !session);
|
||
els.walletCard.classList.toggle('hidden', !session);
|
||
els.pendingApprovalCard.classList.toggle('hidden', !pendingApproval);
|
||
|
||
if (session) {
|
||
els.sessionLogin.textContent = session.login || '—';
|
||
}
|
||
|
||
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 (pendingApproval) {
|
||
els.pendingApprovalSubtitle.textContent = pendingApproval.origin
|
||
? `Сайт ${pendingApproval.origin} запросил подписание транзакции.`
|
||
: 'Сайт запросил подписание транзакции.';
|
||
renderPendingApproval(pendingApproval);
|
||
} else {
|
||
els.pendingApprovalSubtitle.textContent = 'Сайт запросил подписание транзакции.';
|
||
els.pendingApprovalDetails.innerHTML = '';
|
||
}
|
||
|
||
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');
|
||
}
|
||
}
|
||
|
||
async function cancelPendingApproval() {
|
||
try {
|
||
await sendMessage('wallet:cancelPendingSiteApproval');
|
||
} 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(); });
|
||
els.cancelPendingApprovalBtn.addEventListener('click', () => { void cancelPendingApproval(); });
|
||
}
|
||
|
||
async function init() {
|
||
bindUi();
|
||
await refreshState();
|
||
startUiRefreshLoop();
|
||
}
|
||
|
||
window.addEventListener('beforeunload', () => {
|
||
stopUiRefreshLoop();
|
||
if (saveSettingsTimer) {
|
||
window.clearTimeout(saveSettingsTimer);
|
||
saveSettingsTimer = 0;
|
||
}
|
||
});
|
||
|
||
void init();
|