OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / lib / web / pages / peers.js

179 lines · javascript ref:
/**
 * GET /peers — swarm peers + sync remotes.
 *
 * Orchestrator-level view: combines state from all services that share
 * the daemon's Hyperswarm. Shows:
 *
 *   - Swarm summary (topic + peer count)
 *   - Live connections (remote public keys from the shared swarm)
 *   - Sync remotes — named peer serverKeys for pulling other servers' repos
 *   - Form to add a new sync remote by serverKey
 *
 * "Remotes" in sync-speak are server identities this daemon knows how to
 * pull from; they're not the same as live connections.
 */

import b4a from 'b4a'

import { layout, escapeHtml, identicon } from '../helpers.js'
import { isAdmin } from '../../auth/check.js'

const PEERS_STYLES = `
<style>
  .peer-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px; }
  .peer-card { display: flex; align-items: center; gap: 0.6rem; background: #0d1117; border: 1px solid #30363d; border-radius: 6px; padding: 0.6rem 0.8rem; }
  .peer-key { font-family: monospace; font-size: 0.75rem; color: #c9d1d9; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
  .peer-sub { font-size: 0.7rem; color: #8b949e; margin-top: 2px; }
</style>`

export async function renderPeersIndex (svc, ctx) {
  const prefix = (ctx && ctx.prefix) || ''
  const hasSwarm = !!svc.swarm
  const swarm = hasSwarm ? svc.swarm.swarm : null
  const canAdmin = await isAdmin(ctx, svc.identityRegistry)
  const topicHex = hasSwarm ? b4a.toString(svc.swarm.topic, 'hex') : null
  const connectionCount = hasSwarm ? svc.swarm.connections : 0

  // Try to extract live peer pubkeys from the underlying hyperswarm's
  // connection set. Hyperswarm exposes .connections as a Set of Noise
  // streams; each stream has a `.remotePublicKey` buffer. Where available
  // we also pull the underlying UDX stream's remote address so we can show
  // something human-sized beyond the noise pubkey.
  const livePeers = []
  if (swarm && swarm.connections) {
    try {
      for (const conn of swarm.connections) {
        if (!conn.remotePublicKey) continue
        const rawStream = conn.rawStream || null
        const remoteAddr = rawStream && rawStream.remoteHost
          ? rawStream.remoteHost + ':' + rawStream.remotePort
          : null
        livePeers.push({
          pubkey: b4a.toString(conn.remotePublicKey, 'hex'),
          remoteAddr
        })
      }
    } catch {}
  }

  // Sync remotes (server keys we know how to pull from)
  const syncRemotes = []
  if (svc._sync && svc._sync.remotes) {
    for (const [name, info] of svc._sync.remotes) {
      syncRemotes.push({
        name,
        serverKey: typeof info === 'string' ? info : (info.serverKey || info)
      })
    }
  }

  // Repos currently served — read hyper-sync's topic map so we can render
  // per-repo topics + a link back to each repo's own Peers tab. Each
  // _topics entry is { topic: Buffer, discovery, name }.
  const servedRepos = []
  if (svc._sync && svc._sync._topics) {
    for (const [name, entry] of svc._sync._topics) {
      servedRepos.push({
        name,
        topic: entry.topic ? Buffer.from(entry.topic).toString('hex') : null
      })
    }
  }

  // Daemon's own serverKey + identity — handy for giving peers our address.
  const serverKey = svc._sync && svc._sync.serverKey ? svc._sync.serverKey : null
  const identityHex = svc.identity ? svc.identity.publicKeyHex : null

  const body = `
${PEERS_STYLES}
<h1>peers</h1>

<div class="card">
  <h2>This daemon</h2>
  ${identityHex ? `<div class="row"><div class="label">identity</div><div class="meta mono" style="font-size:0.7rem">${escapeHtml(identityHex)}</div></div>` : ''}
  ${serverKey ? `<div class="row"><div class="label">server key</div><div class="meta mono" style="font-size:0.7rem">${escapeHtml(serverKey)}</div></div>` : ''}
</div>

<div class="card">
  <h2>Swarm</h2>
  ${hasSwarm ? `
  <div class="row"><div class="label">status</div><div><span class="badge badge-open">connected</span></div></div>
  <div class="row"><div class="label">topic</div><div class="meta mono" style="font-size:0.7rem">${escapeHtml(topicHex)}</div></div>
  <div class="row"><div class="label">connections</div><div>${connectionCount}</div></div>
  ` : `
  <div class="empty">Swarm is not enabled. Start the daemon with a swarm config to peer with others.</div>`}
</div>

${hasSwarm && livePeers.length > 0 ? `
<div class="card">
  <h2>Live connections (${livePeers.length})</h2>
  <div class="peer-grid">
    ${livePeers.map(p => `
    <div class="peer-card">
      ${identicon(p.pubkey, 32)}
      <div style="flex:1;min-width:0">
        <div class="peer-key">${escapeHtml(p.pubkey.slice(0, 20))}…</div>
        <div class="peer-sub">${p.remoteAddr ? escapeHtml(p.remoteAddr) : 'remote pubkey'}</div>
      </div>
    </div>`).join('')}
  </div>
</div>` : hasSwarm ? `
<div class="card">
  <h2>Live connections (0)</h2>
  <div class="empty">No active connections. Run the topic on another daemon to peer up.</div>
</div>` : ''}

${svc._sync ? `
<div class="card">
  <h2>Repos served (${servedRepos.length})</h2>
  <div class="meta" style="margin-bottom:0.5rem">Repos this daemon is actively serving on the swarm. Each has its own per-repo topic.</div>
  ${servedRepos.length === 0 ? '<div class="empty">No repos in the repos directory yet. Push one to start serving.</div>' : `
  <div class="peer-grid">
    ${servedRepos.map(r => `
    <div class="peer-card">
      <div style="flex:1;min-width:0">
        <div class="peer-key"><a href="${prefix}/repos/${encodeURIComponent(r.name)}/peers">${escapeHtml(r.name)}</a></div>
        <div class="peer-sub">${r.topic ? escapeHtml(r.topic.slice(0, 16)) + '…' : '(no topic)'}</div>
      </div>
    </div>`).join('')}
  </div>`}
</div>` : ''}

${svc._sync ? `
<div class="card">
  <h2>Sync remotes (${syncRemotes.length})</h2>
  <div class="meta" style="margin-bottom:0.5rem">Known servers you can <code>git pull</code> from via punch://.</div>
  ${syncRemotes.length === 0
    ? `<div class="empty">${canAdmin ? '(no remotes — add one below)' : 'No sync remotes — only owners can add them.'}</div>`
    : `
  <div class="peer-grid">
    ${syncRemotes.map(r => `
    <div class="peer-card">
      ${identicon(r.serverKey, 32)}
      <div style="flex:1;min-width:0">
        <div class="peer-key">${escapeHtml(r.name)}</div>
        <div class="peer-sub">${escapeHtml(r.serverKey.slice(0, 20))}…</div>
      </div>
      ${canAdmin ? `
      <form action="${prefix}/peers/remote-remove" method="POST" style="margin:0">
        <input type="hidden" name="name" value="${escapeHtml(r.name)}">
        <button class="btn-secondary" type="submit" style="font-size:0.7rem;padding:0.2rem 0.5rem" onclick="return confirm('Remove remote ${escapeHtml(r.name)}?')">remove</button>
      </form>` : ''}
    </div>`).join('')}
  </div>`}
  ${canAdmin ? `
  <form action="${prefix}/peers/remote-add" method="POST" style="margin-top:1rem">
    <h2 style="margin-top:0">Add a sync remote</h2>
    <div class="field"><label for="add-remote-name">Name (alias)</label><input id="add-remote-name" name="name" placeholder="my-mirror" required></div>
    <div class="field"><label for="add-remote-serverkey">Server key (hex)</label><input id="add-remote-serverkey" name="serverKey" placeholder="64-char hex pubkey" required></div>
    <button type="submit">Add</button>
  </form>` : ''}
</div>` : `
<div class="card">
  <h2>Sync remotes</h2>
  <div class="empty">Sync service is not running.</div>
</div>`}
`
  return layout('peers', '/peers', body, ctx)
}