Обновить pairing устройств и доработать ESP32 UI

This commit is contained in:
AidarKC
2026-06-19 20:47:56 +04:00
parent cc074a941f
commit a788d8bcf5
18 changed files with 1020 additions and 126 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ import { state } from '../state.js';
import { openAuthRequiredModal } from '../services/auth-required-modal.js';
const ITEMS = [
{ pageId: 'messages-list', label: 'Личные сообщения', icon: '💬' },
{ pageId: 'messages-list', label: 'личные', icon: '💬' },
{ pageId: 'channels-list', label: 'Каналы', icon: '📢' },
{ pageId: 'network-view', label: 'Связи', icon: '🕸' },
{ pageId: 'notifications-view', label: 'Уведомления', icon: '🔔' },
+19 -30
View File
@@ -14,6 +14,7 @@ import {
buildSessionAttachPayload,
deriveEspPairingPasswordHash,
encryptPairingPayloadForRequester,
formatPairingShortCode,
} from '../services/device-pairing-service.js';
import { loadEncryptedUserSecrets } from '../services/key-vault.js';
import { toUserMessage } from '../services/ui-error-texts.js';
@@ -29,8 +30,10 @@ function setStatus(statusEl, message, kind = 'info') {
statusEl.style.display = message ? '' : 'none';
}
function normalizeCode(value) {
return String(value || '').replace(/\D+/g, '').slice(0, 7);
const SESSION_TYPE_WALLET = 50;
function pairingSessionKindLabel(sessionType) {
return Number(sessionType || 0) === SESSION_TYPE_WALLET ? 'Wallet session' : 'Client session';
}
function buildTransferKeys(savedKeys, { withExtras = false }) {
@@ -54,19 +57,19 @@ function buildTransferKeys(savedKeys, { withExtras = false }) {
}
function requestCardHtml(request) {
const shortCode = String(request?.shortCode || '').trim() || '0000000';
const shortCode = formatPairingShortCode(request?.shortCode || '');
const client = String(request?.requesterClientPlatform || 'unknown');
const requesterSessionType = Number(request?.requesterSessionType || 0);
const expiresText = request?.expiresAtMs ? formatRelativeTime(request.expiresAtMs) : '—';
const sessionOnly = requesterSessionType === 50;
const sessionOnly = requesterSessionType === SESSION_TYPE_WALLET;
const sessionKind = pairingSessionKindLabel(requesterSessionType);
return `
<div class="card stack" data-pairing-id="${String(request?.pairingId || '')}">
<div class="row" style="align-items:flex-start;">
<div class="stack" style="gap:4px;">
<div style="font-size:28px; font-weight:700; letter-spacing:0.14em;">${shortCode}</div>
<span class="meta-muted">Платформа: ${client}</span>
<span class="meta-muted">Тип сессии: ${requesterSessionType || '—'}</span>
<span class="meta-muted">Тип payload: ${Number(request?.payloadType || 0)}</span>
<span class="meta-muted">Session name: ${client}</span>
<span class="meta-muted">Session kind: ${sessionKind}</span>
<span class="meta-muted">Истекает: ${expiresText}</span>
</div>
</div>
@@ -198,14 +201,11 @@ export function render({ navigate }) {
const requestsCard = document.createElement('div');
requestsCard.className = 'card stack';
requestsCard.innerHTML = `
<div class="row" style="align-items:flex-end; gap:10px; flex-wrap:wrap;">
<label class="stack" style="flex:1 1 180px;">
<span class="field-label">Код нового устройства</span>
<input class="input" id="pairing-code-filter" inputmode="numeric" maxlength="7" placeholder="7 цифр" />
</label>
<div class="row" style="justify-content:space-between; align-items:center; gap:10px; flex-wrap:wrap;">
<span class="field-label">Активные заявки</span>
<button class="ghost-btn" type="button" id="refresh-pairing-requests">Обновить заявки</button>
</div>
<p class="meta-muted">Если код не введён, показываются все активные заявки. Для wallet-заявки можно выпустить отдельную session-only сессию без передачи постоянных ключей.</p>
<p class="meta-muted">Показываются все активные заявки. Для wallet-заявки можно выпустить отдельную session-only сессию без передачи постоянных ключей.</p>
<div class="stack" id="pairing-requests-list"></div>
`;
@@ -213,7 +213,6 @@ export function render({ navigate }) {
status.className = 'status-line is-unavailable';
status.style.display = 'none';
const keySummaryEl = keySummaryCard.querySelector('#pairing-key-summary');
const codeFilterInput = requestsCard.querySelector('#pairing-code-filter');
const refreshBtn = requestsCard.querySelector('#refresh-pairing-requests');
const requestsListEl = requestsCard.querySelector('#pairing-requests-list');
@@ -413,23 +412,17 @@ export function render({ navigate }) {
};
const renderRequests = () => {
const filterCode = normalizeCode(codeFilterInput.value);
const filtered = filterCode
? requests.filter((item) => normalizeCode(item?.shortCode) === filterCode)
: requests;
requestsListEl.innerHTML = '';
if (!filtered.length) {
if (!requests.length) {
const empty = document.createElement('p');
empty.className = 'meta-muted';
empty.textContent = filterCode
? 'Заявка с таким кодом пока не найдена.'
: 'Активных заявок сейчас нет.';
empty.textContent = 'Активных заявок сейчас нет.';
requestsListEl.append(empty);
return;
}
filtered.forEach((request) => {
requests.forEach((request) => {
const wrapper = document.createElement('div');
wrapper.innerHTML = requestCardHtml(request);
requestsListEl.append(wrapper.firstElementChild);
@@ -470,12 +463,12 @@ export function render({ navigate }) {
const approveRequest = async (request, mode) => {
const withExtras = mode === 'with-extras';
let payload;
if (!withExtras && Number(request?.requesterSessionType || 0) === 50) {
if (!withExtras && Number(request?.requesterSessionType || 0) === SESSION_TYPE_WALLET) {
const delegatedSession = await authService.createDelegatedSessionWithDeviceKey({
login: state.session.login,
devicePrivPkcs8: String(savedKeys?.deviceKey || '').trim(),
sessionKey: String(request?.requesterSessionKey || '').trim(),
sessionType: Number(request?.requesterSessionType || 50) || 50,
sessionType: Number(request?.requesterSessionType || SESSION_TYPE_WALLET) || SESSION_TYPE_WALLET,
clientPlatform: String(request?.requesterClientPlatform || '').trim() || 'Wallet plugin',
clientInfo: 'Wallet session approved via device pairing',
});
@@ -493,7 +486,7 @@ export function render({ navigate }) {
}
const encryptedPayload = await encryptPairingPayloadForRequester(request?.requesterSessionKey, payload);
await runPairingOpWithSessionRestore(() => authService.approveTrustedDeviceLogin(request?.pairingId, encryptedPayload));
const sessionOnly = !withExtras && Number(request?.requesterSessionType || 0) === 50;
const sessionOnly = !withExtras && Number(request?.requesterSessionType || 0) === SESSION_TYPE_WALLET;
showToast(
withExtras
? 'Ключи переданы на новое устройство'
@@ -600,10 +593,6 @@ export function render({ navigate }) {
refreshBtn.addEventListener('click', () => {
void reloadRequests();
});
codeFilterInput.addEventListener('input', () => {
codeFilterInput.value = normalizeCode(codeFilterInput.value);
renderRequests();
});
requestsListEl.addEventListener('click', async (event) => {
const target = event.target;
+9 -4
View File
@@ -12,7 +12,12 @@ import {
terminateCurrentSession,
} from '../state.js';
import { showToast } from '../services/channels-ux.js';
import { decryptPairingPayloadFromEnvelope, deriveEspPairingPasswordHash, createRequesterPairingMaterial } from '../services/device-pairing-service.js';
import {
decryptPairingPayloadFromEnvelope,
deriveEspPairingPasswordHash,
createRequesterPairingMaterial,
formatPairingShortCode,
} from '../services/device-pairing-service.js';
import { clearClientAuthData, saveEncryptedUserSecrets } from '../services/key-vault.js';
import { clearStoredMessages } from '../services/message-store.js';
import { toUserMessage } from '../services/ui-error-texts.js';
@@ -30,7 +35,7 @@ function codeCardHtml() {
return `
<div class="card stack">
<p class="field-label">Код подключения</p>
<div id="pairing-short-code" style="font-size:34px; font-weight:700; letter-spacing:0.18em;">0000000</div>
<div id="pairing-short-code" style="font-size:34px; font-weight:700; letter-spacing:0.12em;">00 00 00 00 00</div>
<p class="meta-muted" id="pairing-status-hint">Покажите этот код на уже подключённом устройстве в разделе «Подключить по коду».</p>
<p class="meta-muted" id="pairing-online-hint"></p>
<p class="meta-muted" id="pairing-expire-hint"></p>
@@ -47,7 +52,7 @@ function formatRemaining(ms) {
function resetCodeCard(resultWrap, shortCodeEl, statusHintEl, onlineHintEl, expireHintEl) {
resultWrap.style.display = 'none';
shortCodeEl.textContent = '0000000';
shortCodeEl.textContent = formatPairingShortCode('');
statusHintEl.textContent = 'Покажите этот код на уже подключённом устройстве в разделе «Подключить по коду».';
onlineHintEl.textContent = '';
expireHintEl.textContent = '';
@@ -327,7 +332,7 @@ export function render({ navigate }) {
throw new Error('Сервер не вернул pairingId.');
}
shortCodeEl.textContent = String(payload?.shortCode || '0000000');
shortCodeEl.textContent = formatPairingShortCode(payload?.shortCode || '');
statusHintEl.textContent = 'Откройте на доверенном устройстве: Настройки -> Устройства -> Подключить устройство -> Подключить по коду.';
onlineHintEl.textContent = payload?.trustedSessionOnline
? 'Сейчас есть хотя бы одна онлайн доверенная сессия, которая может принять заявку.'
+48 -11
View File
@@ -362,6 +362,7 @@ function startTone(nextToneName) {
function getCallStateSnapshot() {
const call = getActiveCall();
if (!call) return null;
if (call.localUiDismissed) return null;
const callPhase = String(call.phase || '').trim();
return {
callId: call.callId,
@@ -386,6 +387,12 @@ function notifyCallState() {
});
}
function dismissCallUiLocally(call) {
if (!call) return;
call.localUiDismissed = true;
notifyCallState();
}
function setStatus(call, statusText, phase = '') {
if (!call) return;
call.statusText = String(statusText || '').trim();
@@ -1453,10 +1460,18 @@ export async function acceptIncomingCall() {
export async function declineIncomingCall() {
const call = getActiveCall();
if (!call || call.direction !== 'in' || call.phase !== 'incoming') return;
try {
await sendSignal(call, TYPES.DECLINE_BUSY, 'decline');
} catch {}
await finalizeCall(call, { localReasonCode: 'declined', debugReason: 'declined_by_user' });
dismissCallUiLocally(call);
const declinePromise = (async () => {
try {
await sendSignal(call, TYPES.DECLINE_BUSY, 'decline');
} catch {}
})();
await finalizeCall(call, {
localReasonCode: 'declined',
debugReason: 'declined_by_user',
suppressRemoteSignal: true,
});
await declinePromise;
}
export async function handleIncomingCallSignal(evt) {
@@ -1499,14 +1514,35 @@ export async function handleIncomingCallSignal(evt) {
}
}
} else {
const remoteSessionLocked = Boolean(
call.initialOfferSent
|| call.connectedAtMs
|| call.phase === 'connecting'
|| call.phase === 'active'
|| call.phase === 'reconnecting',
);
const terminalSignalFromAnotherSession =
Boolean(call.remoteSessionId)
&& Boolean(fromSessionId)
&& call.remoteSessionId !== fromSessionId
&& (type === TYPES.DECLINE_BUSY || type === TYPES.TIMEOUT || type === TYPES.HANGUP);
if (call.remoteSessionId && fromSessionId && call.remoteSessionId !== fromSessionId) {
await emitDebug(
call,
'info',
'signal_from_non_selected_session_ignored',
`type=${type}; selected=${call.remoteSessionId}; from=${fromSessionId}`,
);
return;
if (terminalSignalFromAnotherSession && !remoteSessionLocked) {
await emitDebug(
call,
'info',
'terminal_signal_from_non_selected_session_allowed_before_lock',
`type=${type}; selected=${call.remoteSessionId}; from=${fromSessionId}`,
);
} else {
await emitDebug(
call,
'info',
'signal_from_non_selected_session_ignored',
`type=${type}; selected=${call.remoteSessionId}; from=${fromSessionId}`,
);
return;
}
}
if (!call.remoteSessionId && fromSessionId) {
call.remoteSessionId = fromSessionId;
@@ -1632,6 +1668,7 @@ export async function handleIncomingCallSignal(evt) {
export async function hangupActiveCall() {
if (!activeCallId) return;
const call = getCall(activeCallId);
dismissCallUiLocally(call);
await finalizeCall(call, {
localReasonCode: call?.connectedAtMs ? 'completed' : 'no_answer',
debugReason: 'hangup_by_user',
@@ -101,6 +101,15 @@ export async function createRequesterPairingMaterial() {
};
}
export function normalizePairingShortCode(value, digits = 10) {
return String(value || '').replace(/\D+/g, '').slice(0, digits).padStart(digits, '0');
}
export function formatPairingShortCode(value) {
const normalized = normalizePairingShortCode(value, 10);
return normalized.match(/.{1,2}/g)?.join(' ') || normalized;
}
export async function encryptPairingPayloadForRequester(requesterSessionKey, payload) {
const requesterPublicB64 = extractSessionPublicKeyB64(requesterSessionKey);
const requesterMontPub = edwardsToMontgomeryPub(base64ToBytes(requesterPublicB64));