144 lines
6.7 KiB
JavaScript
144 lines
6.7 KiB
JavaScript
const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadcastActive = false, technicalPeers = [] }) => {
|
|
const { form, button, div, h2, p, section, a, hr, input, label, br, span, table, tr, td, textarea } = require("../server/node_modules/hyperaxe");
|
|
const { template, i18n } = require('./main_views');
|
|
|
|
const startButton = form({ action: "/settings/conn/start", method: "post" }, button({ type: "submit" }, i18n.startNetworking));
|
|
const restartButton = form({ action: "/settings/conn/restart", method: "post" }, button({ type: "submit" }, i18n.restartNetworking));
|
|
const stopButton = form({ action: "/settings/conn/stop", method: "post" }, button({ type: "submit" }, i18n.stopNetworking));
|
|
const syncButton = form({ action: "/settings/conn/sync", method: "post" }, button({ type: "submit" }, i18n.sync));
|
|
const connButtons = [startButton, restartButton, stopButton, syncButton];
|
|
|
|
const deduplicatePeers = (peers) => {
|
|
const seen = new Set();
|
|
return peers.filter(p => {
|
|
const key = p[1]?.key;
|
|
if (!key || seen.has(key)) return false;
|
|
seen.add(key);
|
|
return true;
|
|
});
|
|
};
|
|
|
|
const sourceLabel = (src) => {
|
|
const s = String(src || '').toLowerCase();
|
|
if (s === 'rpc') return i18n.peerSourceRpc || 'RPC';
|
|
if (s === 'gossip') return i18n.peerSourceGossip || 'Gossip';
|
|
if (s === 'ebt') return i18n.peerSourceEbt || 'EBT';
|
|
if (s === 'recent') return i18n.peerSourceRecent || 'Recent';
|
|
if (s === 'lan') return i18n.peerSourceLan || 'LAN';
|
|
return null;
|
|
};
|
|
|
|
const renderPeerRow = (peerData) => {
|
|
const peer = peerData[1];
|
|
const { name, users, key } = peer;
|
|
const peerUrl = `/author/${encodeURIComponent(key)}`;
|
|
const filteredUsers = (users || []).filter(u => u.id !== key);
|
|
const userCount = filteredUsers.length;
|
|
return tr(
|
|
td({ 'data-label': i18n.peerHost || 'Pub' }, a({ href: peerUrl, class: "user-link" }, name || key.slice(0, 20) + '…')),
|
|
td({ 'data-label': 'Key' }, a({ href: peerUrl, class: 'user-link peer-key' }, key)),
|
|
td({ 'data-label': i18n.peersReplicatedFeeds || 'Feeds' }, String(userCount))
|
|
);
|
|
};
|
|
|
|
const dedupOnline = deduplicatePeers(onlinePeers);
|
|
const dedupDiscovered = deduplicatePeers(discoveredPeers);
|
|
const dedupUnknown = deduplicatePeers(unknownPeers);
|
|
|
|
const onlineCount = dedupOnline.length;
|
|
const discoveredCount = dedupDiscovered.length;
|
|
const unknownCount = dedupUnknown.length;
|
|
|
|
const renderPeerTable = (peers, emptyKey) => {
|
|
if (peers.length === 0) return p(i18n[emptyKey] || i18n.noConnections);
|
|
return table({ class: 'block-info-table' },
|
|
tr(
|
|
td({ class: 'card-label' }, i18n.peerHost || 'Pub'),
|
|
td({ class: 'card-label' }, 'Key'),
|
|
td({ class: 'card-label' }, i18n.peersReplicatedFeeds || 'Replicated feeds')
|
|
),
|
|
...peers.map(renderPeerRow)
|
|
);
|
|
};
|
|
|
|
const technicalRows = (technicalPeers || []).map(tp => {
|
|
const k = tp.key || '';
|
|
const connected = tp.state === 'connected';
|
|
const action = connected ? 'disconnect' : 'connect';
|
|
const btnLabel = connected ? (i18n.peerDisconnect || 'Disconnect') : (i18n.peerConnect || 'Connect');
|
|
return tr(
|
|
td({ 'data-label': 'Key' }, a({ href: `/author/${encodeURIComponent(k)}`, class: 'user-link peer-key' }, k ? k.slice(0, 20) + '…' : '—')),
|
|
td({ 'data-label': 'Host' }, String(tp.host || '—')),
|
|
td({ 'data-label': i18n.peerPort || 'Port' }, String(tp.port || '—')),
|
|
td({ 'data-label': 'State' }, String(tp.state || tp.source || '—')),
|
|
td({ 'data-label': i18n.peerLastChange || 'Last change' }, String(tp.stateChange ? new Date(tp.stateChange).toISOString().slice(0, 16).replace('T', ' ') : '—')),
|
|
td({ 'data-label': '' },
|
|
form({ method: "POST", action: `/peers/${action}`, class: "inline-form" },
|
|
input({ type: "hidden", name: "key", value: k }),
|
|
input({ type: "hidden", name: "host", value: String(tp.host || '') }),
|
|
input({ type: "hidden", name: "port", value: String(tp.port || 8008) }),
|
|
button({ type: "submit", class: "filter-btn" }, btnLabel)
|
|
)
|
|
)
|
|
);
|
|
});
|
|
const refreshButton = form({ action: "/peers/refresh", method: "post" }, button({ type: "submit" }, i18n.peerRefresh || 'Refresh'));
|
|
const pruneButton = form({ action: "/peers/prune", method: "post" }, button({ type: "submit" }, i18n.peerPruneIdle || 'Remove idle'));
|
|
const exportButton = form({ action: "/peers/export", method: "get" }, button({ type: "submit" }, i18n.peerExport || 'Export'));
|
|
const importForm = form(
|
|
{ action: "/peers/import", method: "post", enctype: "multipart/form-data", class: "peers-import-form" },
|
|
label({ class: 'peers-import-label' }, i18n.peerImportTitle || 'Import peer list'),
|
|
br(),
|
|
textarea({ name: "peerList", rows: "4", placeholder: i18n.peerImportPlaceholder || 'Paste one multiserver address per line…' }),
|
|
br(),
|
|
input({ type: "file", name: "peerFile", accept: ".txt,text/plain" }),
|
|
br(),
|
|
button({ type: "submit", class: "filter-btn" }, i18n.peerImport || 'Import')
|
|
);
|
|
|
|
const peersTechnicalBlock = div({ class: 'tags-header peers-technical-block' },
|
|
h2(i18n.peerConnectionsTitle || 'Connections'),
|
|
div({ class: "conn-actions peers-conn-actions" }, refreshButton, pruneButton, exportButton),
|
|
technicalPeers.length
|
|
? table({ class: 'block-info-table' },
|
|
tr(
|
|
td({ class: 'card-label' }, 'Key'),
|
|
td({ class: 'card-label' }, 'Host'),
|
|
td({ class: 'card-label' }, i18n.peerPort || 'Port'),
|
|
td({ class: 'card-label' }, 'State'),
|
|
td({ class: 'card-label' }, 'Last change'),
|
|
td({ class: 'card-label' }, '')
|
|
),
|
|
...technicalRows
|
|
)
|
|
: p(i18n.peersTechnicalEmpty || 'No peers registered yet.'),
|
|
importForm
|
|
);
|
|
|
|
return template(
|
|
i18n.peers,
|
|
section(
|
|
div({ class: 'tags-header' },
|
|
h2(i18n.peers),
|
|
p(i18n.peerConnectionsIntro)
|
|
),
|
|
div({ class: "conn-actions" }, ...connButtons),
|
|
(onlineCount + discoveredCount + unknownCount) > 0
|
|
? div({ class: "peers-list" },
|
|
div({ class: "tags-header" }, h2(`${i18n.online} (${onlineCount})`)),
|
|
renderPeerTable(dedupOnline, 'noConnections'),
|
|
hr(),
|
|
div({ class: "tags-header" }, h2(`${i18n.discovered} (${discoveredCount})`)),
|
|
renderPeerTable(dedupDiscovered, 'noDiscovered'),
|
|
hr(),
|
|
div({ class: "tags-header" }, h2(`${i18n.unknown} (${unknownCount})`)),
|
|
renderPeerTable(dedupUnknown, 'noUnknownPeers')
|
|
)
|
|
: null,
|
|
peersTechnicalBlock,
|
|
p(i18n.connectionActionIntro)
|
|
)
|
|
);
|
|
};
|
|
|
|
exports.peersView = peersView;
|