194 lines
7.9 KiB
JavaScript
194 lines
7.9 KiB
JavaScript
window.BackOnePeerPicker = function PeerPicker(container, options) {
|
|
options = options || {};
|
|
const onSelect = options.onSelect || (() => {});
|
|
const onNetworkChange = options.onNetworkChange || (() => {});
|
|
const onAction = options.onAction || null;
|
|
const actionLabel = options.actionLabel || 'Select';
|
|
const showActionButton = options.showActionButton !== false;
|
|
const useSharedNetwork = options.useSharedNetwork !== false;
|
|
const layout = options.layout || 'default';
|
|
|
|
let selectedPeer = null;
|
|
let peers = [];
|
|
let currentNetwork = null;
|
|
let unsub = null;
|
|
|
|
if (layout === 'sidebar') {
|
|
container.innerHTML = `
|
|
<div class="peer-picker">
|
|
<div id="pp-network-banner" class="peer-info-box"></div>
|
|
<label class="peer-picker-row">
|
|
<input id="pp-search" type="search" placeholder="Name or peer ID" />
|
|
</label>
|
|
<ul id="pp-list" class="peer-list"></ul>
|
|
</div>`;
|
|
} else {
|
|
container.innerHTML = `
|
|
<div class="peer-picker">
|
|
<div class="peer-picker-core">BackOne core · same network as <code>backone-cli</code></div>
|
|
<div id="pp-network-banner" class="peer-network-banner"></div>
|
|
<label class="peer-picker-row">Search
|
|
<input id="pp-search" type="search" placeholder="Name or peer ID" />
|
|
</label>
|
|
<ul id="pp-list" class="peer-list"></ul>
|
|
<div id="pp-selected" class="peer-selected"></div>
|
|
${showActionButton ? `<button id="pp-action" type="button" disabled>${actionLabel}</button>` : ''}
|
|
</div>`;
|
|
}
|
|
|
|
const netBanner = container.querySelector('#pp-network-banner');
|
|
const searchInput = container.querySelector('#pp-search');
|
|
const listEl = container.querySelector('#pp-list');
|
|
const selectedEl = container.querySelector('#pp-selected');
|
|
const actionBtn = container.querySelector('#pp-action');
|
|
|
|
function networkLabel(n) {
|
|
if (!n) return '';
|
|
return n.name ? `${n.name} (${n.id})` : n.id;
|
|
}
|
|
|
|
function renderNetworkBanner() {
|
|
if (!useSharedNetwork) return;
|
|
const id = BackOneNetworkSession.getNetworkId();
|
|
const core = BackOneNetworkSession.getCoreStatus();
|
|
if (!core.online) {
|
|
netBanner.innerHTML = '<span class="peer-network-warn">BackOne offline — peers unavailable</span>';
|
|
return;
|
|
}
|
|
if (!currentNetwork || !id) {
|
|
netBanner.innerHTML = '<span class="peer-network-warn">No network — join one in the header bar</span>';
|
|
return;
|
|
}
|
|
const labelSuffix = `${peers.length} contact(s) · ${core.leafPeerCount} VL1 peer(s) from core`;
|
|
if (layout === 'sidebar') {
|
|
netBanner.textContent = `${networkLabel(currentNetwork)} · ${labelSuffix}`;
|
|
} else {
|
|
netBanner.textContent = `Network: ${networkLabel(currentNetwork)} · ${textSuffix}`;
|
|
}
|
|
}
|
|
|
|
function renderList() {
|
|
const q = searchInput.value;
|
|
const filtered = BackOnePeerDirectory.searchPeers(q, peers);
|
|
const netTag = currentNetwork ? (currentNetwork.name || currentNetwork.id) : '';
|
|
listEl.innerHTML = filtered.map(p => {
|
|
const lat = p.latency != null ? `${p.latency}ms` : '—';
|
|
const link = p.link || 'DIRECT';
|
|
const meta = [p.role || 'LEAF', lat, link, netTag].filter(Boolean).join(' · ');
|
|
return `
|
|
<li class="peer-item ${p.online ? 'online' : 'offline'} ${selectedPeer && selectedPeer.peerId === p.peerId ? 'active' : ''}" data-id="${p.peerId}">
|
|
<span class="peer-dot" title="${p.online ? 'Online' : 'Offline'}"></span>
|
|
<span class="peer-main">
|
|
<strong class="peer-name">${escapeHtml(p.displayName || p.peerId)}</strong>
|
|
<small class="peer-sub">${escapeHtml(meta)}</small>
|
|
</span>
|
|
<button type="button" class="peer-edit" title="Edit nickname">✎</button>
|
|
</li>`;
|
|
}).join('') || '<li class="peer-empty">No peers on this network</li>';
|
|
|
|
listEl.querySelectorAll('.peer-item').forEach(li => {
|
|
li.addEventListener('click', (ev) => {
|
|
if (ev.target.classList.contains('peer-edit')) return;
|
|
const id = li.dataset.id;
|
|
selectedPeer = peers.find(x => x.peerId === id) || null;
|
|
if (selectedPeer) BackOneNetworkSession.setSelectedPeer(selectedPeer);
|
|
renderList();
|
|
renderSelected();
|
|
if (selectedPeer) onSelect(selectedPeer, currentNetwork);
|
|
});
|
|
const editBtn = li.querySelector('.peer-edit');
|
|
if (editBtn) {
|
|
editBtn.addEventListener('click', async (ev) => {
|
|
ev.stopPropagation();
|
|
const id = li.dataset.id;
|
|
const peer = peers.find(x => x.peerId === id);
|
|
if (!peer) return;
|
|
const nick = prompt('Nickname for ' + peer.peerId, peer.nickname || peer.controllerName || '');
|
|
if (nick === null) return;
|
|
await BackOneContacts.setNickname(peer.peerId, nick);
|
|
BackOneContacts.invalidate();
|
|
await refreshPeers();
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderSelected() {
|
|
if (layout === 'sidebar') return;
|
|
if (!selectedPeer) {
|
|
if (selectedEl) selectedEl.textContent = '';
|
|
if (actionBtn) actionBtn.disabled = true;
|
|
return;
|
|
}
|
|
const net = currentNetwork ? networkLabel(currentNetwork) : BackOneNetworkSession.getNetworkId();
|
|
if (selectedEl) {
|
|
selectedEl.innerHTML = `Selected: <strong>${escapeHtml(selectedPeer.displayName)}</strong> (${escapeHtml(selectedPeer.peerId)})${net ? ` on <em>${escapeHtml(net)}</em>` : ''}`;
|
|
}
|
|
if (actionBtn) actionBtn.disabled = !BackOneNetworkSession.getNetworkId();
|
|
}
|
|
|
|
function escapeHtml(s) {
|
|
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
|
|
async function refreshPeers() {
|
|
currentNetwork = BackOneNetworkSession.getActiveNetwork();
|
|
if (useSharedNetwork && BackOneNetworkSession.getNetworkId() && !currentNetwork) {
|
|
currentNetwork = await BackOneNetworkSession.refresh();
|
|
}
|
|
renderNetworkBanner();
|
|
const nwid = BackOneNetworkSession.getNetworkId();
|
|
await BackOneContacts.load();
|
|
peers = nwid ? await BackOnePeerDirectory.loadPeersForNetwork(nwid) : [];
|
|
const lastPeerId = BackOneNetworkSession.getSelectedPeerId();
|
|
if (lastPeerId) {
|
|
selectedPeer = peers.find(p => p.peerId === lastPeerId) || BackOneNetworkSession.getSelectedPeer();
|
|
}
|
|
renderList();
|
|
renderSelected();
|
|
onNetworkChange(currentNetwork, peers);
|
|
}
|
|
|
|
searchInput.addEventListener('input', renderList);
|
|
if (actionBtn) {
|
|
actionBtn.addEventListener('click', () => {
|
|
if (selectedPeer && onAction) onAction(selectedPeer, currentNetwork);
|
|
});
|
|
}
|
|
|
|
if (useSharedNetwork) {
|
|
unsub = BackOneNetworkSession.subscribe(() => refreshPeers());
|
|
}
|
|
|
|
refreshPeers();
|
|
|
|
return {
|
|
getSelectedPeer: () => selectedPeer,
|
|
getSelectedNetwork: () => currentNetwork,
|
|
getPeers: () => peers,
|
|
refresh: refreshPeers,
|
|
destroy: () => { if (unsub) unsub(); }
|
|
};
|
|
};
|
|
|
|
window.BackOneCommLayout = {
|
|
connectionBarHtml(peer, network) {
|
|
const netLabel = network
|
|
? (network.name ? `${network.name} (${network.id})` : network.id)
|
|
: BackOneNetworkSession.getNetworkId() || '—';
|
|
const lat = peer && peer.latency != null ? `${peer.latency}ms` : '—';
|
|
const latClass = peer && peer.latency != null && peer.latency < 100 ? 'latency-ok' : '';
|
|
return `
|
|
<div class="comm-stat"><label>Network ID</label><span>${escapeHtml(netLabel)}</span></div>
|
|
<div class="comm-stat"><label>Peer ID</label><span>${peer ? escapeHtml(peer.peerId) : '—'}</span></div>
|
|
<div class="comm-stat"><label>Role</label><span>${peer ? escapeHtml(peer.role || 'LEAF') : '—'}</span></div>
|
|
<div class="comm-stat"><label>Transport</label><span>${peer ? escapeHtml(peer.link || 'DIRECT') : '—'}</span></div>
|
|
<div class="comm-stat"><label>Latency</label><span class="${latClass}">${lat}</span></div>
|
|
<div class="comm-stat"><label>Quality</label><span class="comm-quality"><span></span><span></span><span></span><span></span></span></div>`;
|
|
}
|
|
};
|
|
|
|
function escapeHtml(s) {
|
|
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|