UI: добавить просмотр любого PDA
This commit is contained in:
parent
f1c1132690
commit
08628704c7
@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.244
|
client.version=1.2.245
|
||||||
server.version=1.2.229
|
server.version=1.2.230
|
||||||
|
|||||||
@ -623,6 +623,38 @@ export async function readShineUserPda({ login, solanaEndpoint }) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function readShineUserPdaByAddress({ pdaAddress, solanaEndpoint }) {
|
||||||
|
const address = String(pdaAddress || '').trim();
|
||||||
|
const endpoint = String(solanaEndpoint || '').trim();
|
||||||
|
if (!address) throw new Error('Не указан адрес PDA');
|
||||||
|
if (!endpoint) throw new Error('Не указан Solana RPC endpoint');
|
||||||
|
const solana = await loadSolanaLib();
|
||||||
|
const connection = new solana.Connection(endpoint, 'confirmed');
|
||||||
|
const accountAddress = new solana.PublicKey(address);
|
||||||
|
const accountInfo = await connection.getAccountInfo(accountAddress, 'confirmed');
|
||||||
|
if (!accountInfo?.data) throw new Error(`PDA не найдена: ${address}`);
|
||||||
|
return {
|
||||||
|
...parseShineUserPda(accountInfo.data),
|
||||||
|
userPda: accountAddress.toBase58(),
|
||||||
|
pdaAddress: accountAddress.toBase58(),
|
||||||
|
endpoint,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readShineUserPdaByRef({ value, solanaEndpoint }) {
|
||||||
|
const ref = String(value || '').trim();
|
||||||
|
if (!ref) throw new Error('Не указан логин или адрес PDA');
|
||||||
|
try {
|
||||||
|
const bytes = base58ToBytes(ref);
|
||||||
|
if (bytes.length === 32) {
|
||||||
|
return await readShineUserPdaByAddress({ pdaAddress: ref, solanaEndpoint });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Если это не адрес, читаем как логин.
|
||||||
|
}
|
||||||
|
return readShineUserPda({ login: ref, solanaEndpoint });
|
||||||
|
}
|
||||||
|
|
||||||
export async function getShineBlockchainUsage({ login, solanaEndpoint }) {
|
export async function getShineBlockchainUsage({ login, solanaEndpoint }) {
|
||||||
const parsed = await readShineUserPda({ login, solanaEndpoint });
|
const parsed = await readShineUserPda({ login, solanaEndpoint });
|
||||||
const bch = parsed.blockchain;
|
const bch = parsed.blockchain;
|
||||||
|
|||||||
@ -28,6 +28,13 @@
|
|||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="margin-top: 12px;">
|
||||||
|
<a href="server-ui/read-pda.html">
|
||||||
|
<button class="btn-secondary" style="width:100%">
|
||||||
|
Просмотреть любой PDA
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|||||||
@ -38,6 +38,7 @@
|
|||||||
<div class="nav-links">
|
<div class="nav-links">
|
||||||
<a href="../server-ui.html">← Назад</a>
|
<a href="../server-ui.html">← Назад</a>
|
||||||
<a href="update-server-pda.html">Обновить PDA</a>
|
<a href="update-server-pda.html">Обновить PDA</a>
|
||||||
|
<a href="read-pda.html">Просмотреть PDA</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1>Регистрация серверного аккаунта</h1>
|
<h1>Регистрация серверного аккаунта</h1>
|
||||||
|
|||||||
242
shine-UI/server-ui/js/read-pda-page.js
Normal file
242
shine-UI/server-ui/js/read-pda-page.js
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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: `server=${parsed.isServer ? '1' : '0'}`, kind: parsed.isServer ? 'ok' : 'warn' },
|
||||||
|
{ label: `trusted=${Number(parsed.trustedCount || 0)}`, kind: Number(parsed.trustedCount || 0) > 0 ? 'ok' : 'warn' },
|
||||||
|
{ label: `sessions=${Array.isArray(parsed.sessions) ? parsed.sessions.length : 0}`, kind: 'info' },
|
||||||
|
{ label: `access=${Array.isArray(parsed.accessServers) ? parsed.accessServers.length : 0}`, kind: 'info' },
|
||||||
|
{ label: `sync=${Array.isArray(parsed.syncServers) ? parsed.syncServers.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('RecoveryKeyBlock', 'block_type=0', [
|
||||||
|
{ label: 'recoveryKey32', value: base58(parsed.recoveryKey) },
|
||||||
|
]));
|
||||||
|
blocks.appendChild(renderBlock('RootKeyBlock', 'block_type=1', [
|
||||||
|
{ label: 'rootKey32', value: base58(parsed.rootKey) },
|
||||||
|
]));
|
||||||
|
blocks.appendChild(renderBlock('ClientKeyBlock', 'block_type=2', [
|
||||||
|
{ label: 'clientKey32', value: base58(parsed.clientKey) },
|
||||||
|
]));
|
||||||
|
blocks.appendChild(renderBlock('BlockchainRegistryBlock', 'block_type=3', [
|
||||||
|
{ label: 'blockchainType', value: String(parsed.blockchain?.blockchainType ?? 0) },
|
||||||
|
{ label: 'blockchainName', value: parsed.blockchain?.blockchainName || '—' },
|
||||||
|
{ label: 'blockchainPublicKey32', value: base58(parsed.blockchain?.blockchainPublicKey) },
|
||||||
|
{ label: 'paidLimitBytes', value: formatBigInt(parsed.blockchain?.paidLimitBytes || 0n) },
|
||||||
|
{ label: 'usedBytes', value: formatBigInt(parsed.blockchain?.usedBytes || 0n) },
|
||||||
|
{ label: 'lastBlockNumber', value: String(parsed.blockchain?.lastBlockNumber ?? 0) },
|
||||||
|
{ label: 'lastBlockHash32', value: hex(parsed.blockchain?.lastBlockHash) },
|
||||||
|
{ label: 'lastBlockSignature64', value: base58(parsed.blockchain?.lastBlockSignature) },
|
||||||
|
{ label: 'arweaveTxId', value: parsed.blockchain?.arweaveTxId || '—' },
|
||||||
|
]));
|
||||||
|
blocks.appendChild(renderBlock('ServerProfileBlock', 'block_type=30', [
|
||||||
|
{ label: 'isServer', value: parsed.isServer ? '1' : '0' },
|
||||||
|
{ label: 'addressFormatType', value: String(parsed.addressFormatType ?? 0) },
|
||||||
|
{ label: 'addressFormatVersion', value: String(parsed.addressFormatVersion ?? 0) },
|
||||||
|
{ label: 'serverAddress', value: parsed.serverAddress || '—' },
|
||||||
|
{ label: 'syncServersCount', value: String(parsed.syncServers?.length || 0) },
|
||||||
|
{ label: 'syncServers', value: renderArrayLine(parsed.syncServers) },
|
||||||
|
]));
|
||||||
|
blocks.appendChild(renderBlock('AccessServersBlock', 'block_type=40', [
|
||||||
|
{ label: 'accessServersCount', value: String(parsed.accessServers?.length || 0) },
|
||||||
|
{ label: 'accessServers', value: renderArrayLine(parsed.accessServers) },
|
||||||
|
]));
|
||||||
|
blocks.appendChild(renderBlock('SessionsBlock', 'block_type=50', [
|
||||||
|
{ label: 'sessionsMode', value: String(parsed.sessionsMode ?? 0) },
|
||||||
|
{ label: 'sessionsCount', value: String(parsed.sessions?.length || 0) },
|
||||||
|
{ label: 'sessions', value: parsed.sessions?.length ? 'ниже' : '—' },
|
||||||
|
]));
|
||||||
|
blocks.appendChild(renderBlock('TrustedStateBlock', 'block_type=70', [
|
||||||
|
{ label: 'trustedCount', value: String(parsed.trustedCount ?? 0) },
|
||||||
|
]));
|
||||||
|
blocks.appendChild(renderSessionList(parsed.sessions));
|
||||||
|
|
||||||
|
$('rawJson').textContent = JSON.stringify({
|
||||||
|
pdaAddress: parsed.pdaAddress,
|
||||||
|
login: parsed.login,
|
||||||
|
isServer: parsed.isServer,
|
||||||
|
recordNumber: parsed.recordNumber,
|
||||||
|
createdAtMs: parsed.createdAtMs.toString(),
|
||||||
|
updatedAtMs: parsed.updatedAtMs.toString(),
|
||||||
|
recordLen: parsed.recordLen,
|
||||||
|
trustedCount: parsed.trustedCount,
|
||||||
|
addressFormatType: parsed.addressFormatType,
|
||||||
|
addressFormatVersion: parsed.addressFormatVersion,
|
||||||
|
serverAddress: parsed.serverAddress,
|
||||||
|
syncServers: parsed.syncServers,
|
||||||
|
accessServers: parsed.accessServers,
|
||||||
|
sessionsMode: parsed.sessionsMode,
|
||||||
|
sessions: parsed.sessions.map((s) => ({
|
||||||
|
sessionType: s.sessionType,
|
||||||
|
sessionVersion: s.sessionVersion,
|
||||||
|
sessionName: s.sessionName,
|
||||||
|
sessionPubKey32: base58(s.sessionPubKey32),
|
||||||
|
})),
|
||||||
|
recoveryKey: base58(parsed.recoveryKey),
|
||||||
|
rootKey: base58(parsed.rootKey),
|
||||||
|
clientKey: base58(parsed.clientKey),
|
||||||
|
blockchain: {
|
||||||
|
blockchainType: parsed.blockchain.blockchainType,
|
||||||
|
blockchainName: parsed.blockchain.blockchainName,
|
||||||
|
blockchainPublicKey: base58(parsed.blockchain.blockchainPublicKey),
|
||||||
|
paidLimitBytes: parsed.blockchain.paidLimitBytes.toString(),
|
||||||
|
usedBytes: parsed.blockchain.usedBytes.toString(),
|
||||||
|
lastBlockNumber: parsed.blockchain.lastBlockNumber,
|
||||||
|
lastBlockHash: hex(parsed.blockchain.lastBlockHash),
|
||||||
|
lastBlockSignature: base58(parsed.blockchain.lastBlockSignature),
|
||||||
|
arweaveTxId: parsed.blockchain.arweaveTxId || '',
|
||||||
|
},
|
||||||
|
}, 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';
|
||||||
91
shine-UI/server-ui/read-pda.html
Normal file
91
shine-UI/server-ui/read-pda.html
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Просмотр PDA — SHiNE Server Admin</title>
|
||||||
|
<link rel="stylesheet" href="styles.css" />
|
||||||
|
<style>
|
||||||
|
.field-row { display: grid; grid-template-columns: 180px 1fr; gap: 12px; padding: 6px 0; border-bottom: 1px solid var(--border); }
|
||||||
|
.field-row:last-child { border-bottom: 0; }
|
||||||
|
.field-label { color: var(--text-muted); font-size: 12px; text-transform: uppercase; letter-spacing: .04em; }
|
||||||
|
.field-value { font-family: monospace; font-size: 12px; word-break: break-all; white-space: pre-wrap; }
|
||||||
|
.field-value.muted { color: var(--text-muted); }
|
||||||
|
.block-list { display: grid; gap: 12px; }
|
||||||
|
.block-card { border: 1px solid var(--border); border-radius: var(--radius); padding: 14px; background: #121212; }
|
||||||
|
.block-title { font-size: 13px; font-weight: 700; color: var(--accent); margin-bottom: 10px; }
|
||||||
|
.block-subtitle { font-size: 11px; color: var(--text-muted); margin-bottom: 10px; }
|
||||||
|
.mini-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 12px; }
|
||||||
|
.mini-item { min-width: 0; }
|
||||||
|
.mini-label { font-size: 11px; color: var(--text-muted); margin-bottom: 2px; text-transform: uppercase; letter-spacing: .04em; }
|
||||||
|
.mini-value { font-family: monospace; font-size: 12px; word-break: break-all; white-space: pre-wrap; }
|
||||||
|
.mono-box { font-family: monospace; font-size: 12px; white-space: pre-wrap; word-break: break-all; background: #0d0d0d; border: 1px solid var(--border); border-radius: var(--radius); padding: 12px; }
|
||||||
|
.summary-grid { display: grid; gap: 0; }
|
||||||
|
.badge-line { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||||
|
.badge { display: inline-flex; align-items: center; border: 1px solid var(--border); border-radius: 999px; padding: 4px 10px; font-size: 11px; color: var(--text-muted); background: #0d0d0d; }
|
||||||
|
.badge.ok { color: #7dcc7d; border-color: #2a4a2a; background: #1a2e1a; }
|
||||||
|
.badge.warn { color: #ffd37a; border-color: #5f4b22; background: #2f2614; }
|
||||||
|
.badge.err { color: #f08080; border-color: #5a2a2a; background: #2e1a1a; }
|
||||||
|
.details-wrap { margin-top: 12px; }
|
||||||
|
.details-wrap details { border: 1px solid var(--border); border-radius: var(--radius); background: #121212; }
|
||||||
|
.details-wrap summary { cursor: pointer; padding: 12px 14px; color: var(--accent); font-weight: 600; }
|
||||||
|
.details-body { padding: 0 14px 14px; }
|
||||||
|
.tight { margin-bottom: 8px; }
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.field-row { grid-template-columns: 1fr; gap: 4px; }
|
||||||
|
.mini-grid { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="nav-links">
|
||||||
|
<a href="../server-ui.html">← Назад</a>
|
||||||
|
<a href="create-server-pda.html">Создать PDA</a>
|
||||||
|
<a href="update-server-pda.html">Обновить PDA</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1>Просмотр PDA</h1>
|
||||||
|
<p class="subtitle">Читает user_pda или server PDA по логину либо по адресу и показывает все поля</p>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Параметры Solana</h2>
|
||||||
|
<div class="field">
|
||||||
|
<label>Solana Endpoint</label>
|
||||||
|
<input type="text" id="endpoint" value="https://api.devnet.solana.com" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Источник PDA</h2>
|
||||||
|
<div class="field">
|
||||||
|
<label>Логин или адрес PDA</label>
|
||||||
|
<input type="text" id="ref" placeholder="Логин сервера / пользователя или base58-адрес PDA" />
|
||||||
|
<div class="hint">Если введён base58-адрес длиной 32 байта, он читается напрямую. Иначе значение трактуется как логин.</div>
|
||||||
|
</div>
|
||||||
|
<div class="btn-row">
|
||||||
|
<button class="btn-primary" id="btnLoad">Загрузить PDA</button>
|
||||||
|
</div>
|
||||||
|
<div class="status" id="status"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" id="summaryCard" style="display:none">
|
||||||
|
<h2>Сводка</h2>
|
||||||
|
<div class="summary-grid" id="summaryRows"></div>
|
||||||
|
<div class="badge-line" style="margin-top:12px" id="badgeLine"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" id="blocksCard" style="display:none">
|
||||||
|
<h2>Блоки и поля</h2>
|
||||||
|
<div class="block-list" id="blocksList"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" id="rawCard" style="display:none">
|
||||||
|
<h2>Raw JSON</h2>
|
||||||
|
<pre class="mono-box" id="rawJson"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="./js/read-pda-page.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -46,6 +46,7 @@
|
|||||||
<div class="nav-links">
|
<div class="nav-links">
|
||||||
<a href="../server-ui.html">← Назад</a>
|
<a href="../server-ui.html">← Назад</a>
|
||||||
<a href="create-server-pda.html">Создать PDA</a>
|
<a href="create-server-pda.html">Создать PDA</a>
|
||||||
|
<a href="read-pda.html">Просмотреть PDA</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1>Обновление PDA сервера</h1>
|
<h1>Обновление PDA сервера</h1>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user