SHA256
Палитра «Сияние» по умолчанию, новые роли цветов. Лента канала: плоские записи, ряд действий с равным шагом иконок, дата у записи. Обсуждение со строкой ввода, личный чат, шторки. Вход и регистрация: стартовый экран (надпись «СИЯНИЕ», сияние мандалы), настройки входа со статусами серверов, статус логина в одну строку, вход с другого устройства (код, таймер, шаги), сохранение ключей с переключателями. Корневые разделы – заголовок по центру; фильтр чатов – переключатель; уведомления – список как в чатах; фото – просмотр на весь экран. Общее: один зазор под шапкой (--page-top-gap), переключатели высотой с поле ввода, второстепенные кнопки по одной роли (в том числе на экранах устройств и серверов), иконки только из ui-icon.js, полоса прокрутки скрыта, среднее тире в текстах интерфейса. Что нужно от сервера и разработчика – docs/UI-Design/UI-v4-for-dev.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
219 lines
8.3 KiB
JavaScript
219 lines
8.3 KiB
JavaScript
import { readShineUserPdaByRef } from '../../js/services/shine-user-pda-service.js';
|
||
import { bytesToBase58 } from '../../js/services/crypto-utils.js';
|
||
import { $, clearStatus, formatBigInt, formatTimestamp, setStatus } from './server-ui-shared.js';
|
||
import { defaultSolanaEndpoint } from '../../js/deploy-config.js';
|
||
|
||
function hex(bytes) {
|
||
const data = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || []);
|
||
return Array.from(data).map((x) => x.toString(16).padStart(2, '0')).join('');
|
||
}
|
||
|
||
function base58(bytes) {
|
||
const data = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || []);
|
||
return bytesToBase58(data);
|
||
}
|
||
|
||
if ($('endpoint')) {
|
||
$('endpoint').value = defaultSolanaEndpoint;
|
||
}
|
||
|
||
function renderRows(rows) {
|
||
const container = $('summaryRows');
|
||
container.innerHTML = '';
|
||
for (const row of rows) {
|
||
const el = document.createElement('div');
|
||
el.className = 'field-row';
|
||
const label = document.createElement('div');
|
||
label.className = 'field-label';
|
||
label.textContent = row.label;
|
||
const value = document.createElement('div');
|
||
value.className = `field-value${row.muted ? ' muted' : ''}`;
|
||
value.textContent = row.value;
|
||
el.append(label, value);
|
||
container.appendChild(el);
|
||
}
|
||
}
|
||
|
||
function renderMiniGrid(items) {
|
||
const wrap = document.createElement('div');
|
||
wrap.className = 'mini-grid';
|
||
for (const item of items) {
|
||
const node = document.createElement('div');
|
||
node.className = 'mini-item';
|
||
const label = document.createElement('div');
|
||
label.className = 'mini-label';
|
||
label.textContent = item.label;
|
||
const value = document.createElement('div');
|
||
value.className = 'mini-value';
|
||
value.textContent = item.value;
|
||
node.append(label, value);
|
||
wrap.appendChild(node);
|
||
}
|
||
return wrap;
|
||
}
|
||
|
||
function renderBlock(title, subtitle, items) {
|
||
const card = document.createElement('div');
|
||
card.className = 'block-card';
|
||
const ttl = document.createElement('div');
|
||
ttl.className = 'block-title';
|
||
ttl.textContent = title;
|
||
card.appendChild(ttl);
|
||
if (subtitle) {
|
||
const sub = document.createElement('div');
|
||
sub.className = 'block-subtitle';
|
||
sub.textContent = subtitle;
|
||
card.appendChild(sub);
|
||
}
|
||
card.appendChild(renderMiniGrid(items));
|
||
return card;
|
||
}
|
||
|
||
function renderArrayLine(values, emptyLabel = '–') {
|
||
const arr = Array.isArray(values) ? values : [];
|
||
return arr.length ? arr.join(', ') : emptyLabel;
|
||
}
|
||
|
||
function renderSessionList(sessions) {
|
||
const list = document.createElement('div');
|
||
list.className = 'details-wrap';
|
||
const details = document.createElement('details');
|
||
details.open = true;
|
||
const summary = document.createElement('summary');
|
||
summary.textContent = `Сессии (${Array.isArray(sessions) ? sessions.length : 0})`;
|
||
const body = document.createElement('div');
|
||
body.className = 'details-body';
|
||
if (!Array.isArray(sessions) || sessions.length === 0) {
|
||
body.innerHTML = '<div class="field-value muted">Сессий нет.</div>';
|
||
} else {
|
||
const grid = document.createElement('div');
|
||
grid.className = 'block-list';
|
||
sessions.forEach((session, idx) => {
|
||
grid.appendChild(renderBlock(
|
||
`Session #${idx + 1}`,
|
||
`type=${session.sessionType}, version=${session.sessionVersion}`,
|
||
[
|
||
{ label: 'Имя', value: session.sessionName || '–' },
|
||
{ label: 'PubKey', value: base58(session.sessionPubKey32) },
|
||
],
|
||
));
|
||
});
|
||
body.appendChild(grid);
|
||
}
|
||
details.append(summary, body);
|
||
list.appendChild(details);
|
||
return list;
|
||
}
|
||
|
||
function renderParsed(parsed) {
|
||
$('summaryCard').style.display = 'block';
|
||
$('blocksCard').style.display = 'block';
|
||
$('rawCard').style.display = 'block';
|
||
|
||
renderRows([
|
||
{ label: 'PDA адрес', value: parsed.pdaAddress },
|
||
{ label: 'Формат', value: `${parsed.formatMajor}.${parsed.formatMinor}` },
|
||
{ label: 'Логин', value: parsed.login },
|
||
{ label: 'Статус', value: parsed.isServer ? 'server' : 'not server' },
|
||
{ label: 'recordNumber', value: String(parsed.recordNumber) },
|
||
{ label: 'createdAtMs', value: `${parsed.createdAtMs} · ${formatTimestamp(parsed.createdAtMs)}` },
|
||
{ label: 'updatedAtMs', value: `${parsed.updatedAtMs} · ${formatTimestamp(parsed.updatedAtMs)}` },
|
||
{ label: 'recordLen', value: String(parsed.recordLen) },
|
||
{ label: 'prevRecordHash32', value: hex(parsed.prevRecordHash) },
|
||
{ label: 'signature64', value: base58(parsed.signature) },
|
||
{ label: 'unsignedBytesLen', value: String(parsed.unsignedBytes?.length || 0) },
|
||
]);
|
||
|
||
const badgeLine = $('badgeLine');
|
||
badgeLine.innerHTML = '';
|
||
const badges = [
|
||
{ label: 'format=1.2', kind: 'ok' },
|
||
{ label: `server=${parsed.isServer ? '1' : '0'}`, kind: parsed.isServer ? 'ok' : 'warn' },
|
||
{ label: `forks=${Array.isArray(parsed.forks) ? parsed.forks.length : 0}`, kind: 'info' },
|
||
{ label: `access=${Array.isArray(parsed.accessServers) ? parsed.accessServers.length : 0}`, kind: 'info' },
|
||
];
|
||
badges.forEach((item) => {
|
||
const badge = document.createElement('span');
|
||
badge.className = `badge ${item.kind}`;
|
||
badge.textContent = item.label;
|
||
badgeLine.appendChild(badge);
|
||
});
|
||
|
||
const blocks = $('blocksList');
|
||
blocks.innerHTML = '';
|
||
blocks.appendChild(renderBlock('RootKeyBlock', 'block_type=1, version=0', [
|
||
{ label: 'rootKey32', value: base58(parsed.rootKey) },
|
||
]));
|
||
blocks.appendChild(renderBlock('ClientKeyBlock', 'block_type=2, version=0', [
|
||
{ label: 'clientKey32', value: base58(parsed.clientKey) },
|
||
]));
|
||
|
||
const forkLines = (parsed.forks || []).map((fork, idx) =>
|
||
`#${idx}: ${base58(fork.blockchainKey)} · created=${formatTimestamp(fork.createdAtMs)} · limit=${formatBigInt(fork.paidLimitBytes)}`,
|
||
);
|
||
blocks.appendChild(renderBlock('BlockchainRegistryBlock', 'block_type=3, version=0', [
|
||
{ label: 'forkCount', value: String(parsed.forks?.length || 0) },
|
||
{ label: 'forks', value: forkLines.length ? forkLines.join('\n') : '–' },
|
||
]));
|
||
|
||
if (parsed.isServer) {
|
||
const addressLines = (parsed.serverAddresses || []).map((item) =>
|
||
`type=${item.addressFormatType}, v=${item.addressFormatVersion}: ${item.address}`,
|
||
);
|
||
blocks.appendChild(renderBlock('ServerProfileBlock', 'block_type=30, version=0', [
|
||
{ label: 'addressCount', value: String(parsed.serverAddresses?.length || 0) },
|
||
{ label: 'address', value: addressLines.length ? addressLines.join('\n') : '–' },
|
||
]));
|
||
}
|
||
if (Array.isArray(parsed.accessServers) && parsed.accessServers.length) {
|
||
blocks.appendChild(renderBlock('AccessServersBlock', 'block_type=40, version=0', [
|
||
{ label: 'accessServersCount', value: String(parsed.accessServers.length) },
|
||
{ label: 'accessServer', value: renderArrayLine(parsed.accessServers) },
|
||
]));
|
||
}
|
||
|
||
const raw = {
|
||
pdaAddress: parsed.pdaAddress,
|
||
format: `${parsed.formatMajor}.${parsed.formatMinor}`,
|
||
login: parsed.login,
|
||
isServer: parsed.isServer,
|
||
recordNumber: parsed.recordNumber,
|
||
createdAtMs: parsed.createdAtMs.toString(),
|
||
updatedAtMs: parsed.updatedAtMs.toString(),
|
||
recordLen: parsed.recordLen,
|
||
prevRecordHash: hex(parsed.prevRecordHash),
|
||
rootKey: base58(parsed.rootKey),
|
||
clientKey: base58(parsed.clientKey),
|
||
serverAddresses: (parsed.serverAddresses || []).map((a) => ({ ...a })),
|
||
accessServers: parsed.accessServers || [],
|
||
signature: base58(parsed.signature),
|
||
};
|
||
|
||
$('rawJson').textContent = JSON.stringify(raw, null, 2);
|
||
}
|
||
|
||
$('btnLoad').addEventListener('click', async () => {
|
||
clearStatus($('status'));
|
||
$('btnLoad').disabled = true;
|
||
$('summaryCard').style.display = 'none';
|
||
$('blocksCard').style.display = 'none';
|
||
$('rawCard').style.display = 'none';
|
||
try {
|
||
const endpoint = String($('endpoint').value || '').trim();
|
||
const ref = String($('ref').value || '').trim();
|
||
if (!endpoint) throw new Error('Укажите Solana endpoint');
|
||
if (!ref) throw new Error('Укажите логин или адрес PDA');
|
||
|
||
setStatus($('status'), 'Чтение PDA...', 'info');
|
||
const parsed = await readShineUserPdaByRef({ value: ref, solanaEndpoint: endpoint });
|
||
renderParsed(parsed);
|
||
setStatus($('status'), 'PDA загружена.', 'success');
|
||
} catch (error) {
|
||
setStatus($('status'), error?.message || String(error), 'error');
|
||
} finally {
|
||
$('btnLoad').disabled = false;
|
||
}
|
||
});
|
||
|
||
document.body.dataset.ready = '1';
|