OASIS_MOBILE/src/views/peers_view.js

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;