/**
* Per-repo Peers tab — `/repos/:name/peers`.
*
* Shows the replication state for a single repo:
* - The daemon's own server key (what peers see)
* - The repo's swarm topic (the discovery key; peers join on this)
* - Whether this daemon is actively serving the repo
* - Total peers on the shared swarm (daemon-wide; per-repo split lives in
* hyper-sync internals that aren't exposed yet — expand later)
*
* Deliberately read-only. Peer add/remove flows live at the daemon-wide
* `/peers` page (P.4).
*/
import { layout, escapeHtml } from '../helpers.js'
import { renderRepoTabs, REPO_TAB_STYLES } from '../components/repo-tabs.js'
import { isAdmin } from '../../auth/check.js'
export async function renderRepoPeers (svc, ctx, repoName) {
const prefix = (ctx && ctx.prefix) || ''
const canAdmin = await isAdmin(ctx, svc.identityRegistry)
const sync = svc._sync
if (!sync) return errorPage('Sync service not running', ctx, repoName)
const info = sync.info(repoName)
if (!info) return errorPage('Repository "' + repoName + '" not found', ctx, repoName)
const serverKey = sync.serverKey || null
const topic = info.topic || null
const serving = !!info.serving
const swarmPeers = svc.swarm ? svc.swarm.connections : 0
const swarmTopic = svc.swarm ? escapeHtml(Buffer.from(svc.swarm.topic).toString('hex')) : null
// Repo writers = the per-repo issue-tracker autobase writer set (gip push
// authority). Mirrors the org members page. Best-effort: a peer that
// hasn't mounted the tracker yet just shows none.
let writers = []
try {
const tracker = await svc.issueRegistry.getForRepo(repoName)
writers = await tracker.listWriters()
} catch {}
const body = `
${REPO_TAB_STYLES}
${renderRepoTabs(repoName, 'peers', {}, prefix)}
<div class="card">
<h2><a href="${prefix}/repos/${encodeURIComponent(repoName)}">← ${escapeHtml(repoName)}</a> / peers</h2>
</div>
<div class="card">
<h2>This daemon</h2>
<div class="row">
<div class="label">server key</div>
<div class="mono" style="font-size:0.7rem">${escapeHtml(serverKey || '(unavailable)')}</div>
</div>
<div class="row">
<div class="label">serving</div>
<div>${serving ? '<span class="badge badge-open">yes</span>' : '<span class="badge badge-closed">no</span>'}</div>
</div>
${topic ? `<div class="row">
<div class="label">repo topic</div>
<div class="mono" style="font-size:0.7rem">${escapeHtml(topic)}</div>
</div>` : ''}
</div>
<div class="card">
<h2>Swarm</h2>
<div class="row">
<div class="label">connections</div>
<div>${swarmPeers} peer${swarmPeers === 1 ? '' : 's'}</div>
</div>
${swarmTopic ? `<div class="row">
<div class="label">daemon topic</div>
<div class="mono" style="font-size:0.7rem">${swarmTopic}</div>
</div>` : '<div class="row"><div class="label">daemon topic</div><div class="meta">(swarm disabled)</div></div>'}
<div class="meta" style="margin-top:0.5rem;font-size:0.7rem">
Per-repo peer breakdown is daemon-wide for now; richer accounting lives in hyper-sync (P.4 / P.10).
</div>
</div>
<div class="card">
<h2>Writers (push authority)</h2>
<div class="meta" style="margin-bottom:0.5rem;font-size:0.8rem">
The repo's per-repo autobase writer set — who can push (gip authority). Writers are added by accepting a contributor+ invite; remove revokes push access.
</div>
${writers.length === 0 ? '<div class="empty">(no writers tracked yet)</div>' : `
<div class="member-list">
${writers.map(w => `
<div class="member-row" style="display:flex;align-items:center;gap:0.5rem">
<div style="flex:1">
<div class="mono" style="font-size:0.75rem">${escapeHtml(String(w.userId || w.id || '').slice(0, 32))}${String(w.userId || w.id || '').length > 32 ? '…' : ''}</div>
${w.role ? `<div class="meta">${escapeHtml(w.role)}</div>` : ''}
</div>
${canAdmin ? `<form action="${prefix}/repos/${encodeURIComponent(repoName)}/writer-revoke" method="POST" style="margin:0" onsubmit="return confirm('Revoke push access for ${escapeHtml(String(w.userId || w.id || '').slice(0, 16))}… on ${escapeHtml(repoName)}?')">
<input type="hidden" name="userId" value="${escapeHtml(String(w.userId || w.id || ''))}">
<button class="btn-secondary" type="submit" style="font-size:0.7rem;padding:0.2rem 0.5rem" aria-label="Revoke writer ${escapeHtml(String(w.userId || w.id || '').slice(0, 16))}">revoke</button>
</form>` : ''}
</div>`).join('')}
</div>`}
</div>
<div class="card">
<h2>How to join</h2>
<div class="meta" style="font-size:0.8rem">
To replicate this repo from another daemon, clone with the P2P URL (<code>punch://<server-key>/${escapeHtml(repoName)}</code>)
or HTTP URL shown on the <a href="${prefix}/repos/${encodeURIComponent(repoName)}">Overview tab</a>.
</div>
</div>
${canAdmin ? `
<div class="card">
<h2>Invite collaborator (issue tracker)</h2>
<div class="meta" style="margin-bottom:0.5rem">Generates a P2P invite code for this repo's issue tracker. Share it — the holder pairs over the swarm and (for contributor+) becomes a writer. No URL, no HTTP.</div>
<form action="${prefix}/repos/${encodeURIComponent(repoName)}/invite" method="POST">
<div class="field"><label for="repo-invite-role">Role</label>
<select id="repo-invite-role" name="role">
<option value="reader">reader (read-only mirror)</option>
<option value="contributor">contributor (can write)</option>
<option value="maintainer">maintainer</option>
<option value="admin">admin</option>
</select>
</div>
<button type="submit">Generate invite</button>
</form>
</div>` : ''}
`
return layout('peers · ' + repoName, '/repos', body, ctx)
}
/**
* Render the result of minting a repo invite — shows the P2P invite code.
* The recipient runs `pgh repo join <code>` or pastes it into the Join box;
* they pair over the swarm (no URL, no HTTP).
*/
export function renderRepoInviteResult (repoName, invitation, role, ctx) {
const prefix = (ctx && ctx.prefix) || ''
const body = `
<h2><a href="${prefix}/repos/${encodeURIComponent(repoName)}">← ${escapeHtml(repoName)}</a> / invite</h2>
<div class="card">
<h2 style="margin-top:0">Invite created — role: ${escapeHtml(role)}</h2>
<div class="meta" style="margin-bottom:0.5rem">Share this code. The recipient runs <code>pgh repo join <code></code> or pastes it into the Join box on the repos page — they pair over the swarm and (for contributor+) become a writer.</div>
<textarea readonly rows="4" style="width:100%;font-family:monospace;font-size:0.8rem" onclick="this.select()">${escapeHtml(invitation)}</textarea>
</div>`
return layout('invite · ' + repoName, '/repos', body, ctx)
}
function errorPage (msg, ctx, repoName) {
const body = `
${REPO_TAB_STYLES}
${repoName ? renderRepoTabs(repoName, 'peers', {}, prefix) : ''}
<div class="card"><h2>Error</h2><div class="empty">${escapeHtml(msg)}</div></div>`
return layout('Error', '/repos', body, ctx)
}