federation/network: fetch peer's own /federation/explore/data on click + render their self-view inline (their peers, vouches in/out, transitive, translog head)
This commit is contained in:
@@ -1798,3 +1798,29 @@ body.wide #federation-network-graph { height: 720px; }
|
|||||||
}
|
}
|
||||||
.fe-walk-cta { width: 100%; justify-content: center; }
|
.fe-walk-cta { width: 100%; justify-content: center; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* peer self-view section inside the detail panel — fetched cross-origin */
|
||||||
|
.fn-remote-sec { grid-column: 1 / -1; }
|
||||||
|
.fn-remote-status {
|
||||||
|
font-size: 11px; color: var(--muted); margin-left: 8px; font-weight: 400;
|
||||||
|
text-transform: lowercase; letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
.fn-remote-meta {
|
||||||
|
display: flex; flex-wrap: wrap; gap: 12px 18px;
|
||||||
|
font-size: 12px; color: var(--muted); margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.fn-remote-meta b { color: var(--text); font-weight: 600; }
|
||||||
|
.fn-remote-meta code { font-size: 11px; color: var(--accent); }
|
||||||
|
.fn-remote-ok { color: rgba(74,222,128,0.95); }
|
||||||
|
.fn-remote-warn { color: rgba(251,191,36,0.95); }
|
||||||
|
.fn-remote-cols {
|
||||||
|
display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
|
gap: 14px; margin: 8px 0 12px;
|
||||||
|
}
|
||||||
|
.fn-remote-h { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 6px; }
|
||||||
|
.fn-remote-list { list-style: none; padding: 0; margin: 0; font-size: 12px; line-height: 1.55; }
|
||||||
|
.fn-remote-list li { padding: 2px 0; border-bottom: 1px dashed rgba(125,133,151,0.18); }
|
||||||
|
.fn-remote-list li:last-child { border-bottom: 0; }
|
||||||
|
.fn-remote-list code { font-size: 11px; color: var(--accent); }
|
||||||
|
.fn-remote-list .muted { color: var(--muted); margin-left: 6px; font-size: 11px; }
|
||||||
|
.fn-remote-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 6px; }
|
||||||
|
|||||||
@@ -581,6 +581,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
|
// Placeholder for the peer's-own-view, populated by an async fetch below.
|
||||||
|
const remote = (!n.is_self && n.domain)
|
||||||
|
? `<div class="fn-detail-sec fn-remote-sec" data-remote-fp="${esc(n.fp)}">
|
||||||
|
<h4>peer's self-view <span class="fn-remote-status">fetching…</span></h4>
|
||||||
|
<div class="fn-remote-body">loading from <code>${esc(n.domain)}</code>…</div>
|
||||||
|
</div>`
|
||||||
|
: "";
|
||||||
|
|
||||||
const html = `
|
const html = `
|
||||||
<div class="td-head">
|
<div class="td-head">
|
||||||
<span class="td-kind ${kindCls}">${esc(kindLabel)}</span>
|
<span class="td-kind ${kindCls}">${esc(kindLabel)}</span>
|
||||||
@@ -594,12 +602,20 @@
|
|||||||
${vouches}
|
${vouches}
|
||||||
${quorum}
|
${quorum}
|
||||||
${translog}
|
${translog}
|
||||||
|
${remote}
|
||||||
${actions}
|
${actions}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
detailEl.innerHTML = html;
|
detailEl.innerHTML = html;
|
||||||
detailEl.classList.add("has-selection");
|
detailEl.classList.add("has-selection");
|
||||||
|
|
||||||
|
// Async-fetch the peer's own /federation/explore/data and render their
|
||||||
|
// self-view inline. CORS is set on that endpoint, so the browser can hit
|
||||||
|
// any psyc node directly. Falls back to /federation/network on older nodes.
|
||||||
|
if (!n.is_self && n.domain) {
|
||||||
|
fetchPeerSelfView(n.domain, n.fp);
|
||||||
|
}
|
||||||
|
|
||||||
const close = detailEl.querySelector(".td-close");
|
const close = detailEl.querySelector(".td-close");
|
||||||
if (close) close.addEventListener("click", clearSelection);
|
if (close) close.addEventListener("click", clearSelection);
|
||||||
|
|
||||||
@@ -628,6 +644,99 @@
|
|||||||
detailEl.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
detailEl.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- peer self-view (cross-origin fetch) ----------------------
|
||||||
|
// Each psyc node exposes /federation/explore/data (rich) and falls back to
|
||||||
|
// /federation/network (slim). Both carry CORS=*, so the browser can hit
|
||||||
|
// them from any cockpit page. The fetch is best-effort — if the peer is
|
||||||
|
// unreachable, blocked by CSP, or older, we render whatever we got.
|
||||||
|
|
||||||
|
async function fetchPeerSelfView(domain, expectedFp) {
|
||||||
|
const sec = detailEl.querySelector(`.fn-remote-sec[data-remote-fp="${expectedFp}"]`);
|
||||||
|
if (!sec) return;
|
||||||
|
const statusEl = sec.querySelector(".fn-remote-status");
|
||||||
|
const bodyEl = sec.querySelector(".fn-remote-body");
|
||||||
|
const base = `https://${domain}`;
|
||||||
|
let data = null;
|
||||||
|
let kind = "";
|
||||||
|
try {
|
||||||
|
const r = await fetch(`${base}/federation/explore/data`, { mode: "cors", cache: "no-store" });
|
||||||
|
if (r.ok) { data = await r.json(); kind = "explore"; }
|
||||||
|
} catch (e) { /* fall through */ }
|
||||||
|
if (!data) {
|
||||||
|
try {
|
||||||
|
const r = await fetch(`${base}/federation/network`, { mode: "cors", cache: "no-store" });
|
||||||
|
if (r.ok) { data = await r.json(); kind = "network"; }
|
||||||
|
} catch (e) { /* fall through */ }
|
||||||
|
}
|
||||||
|
if (!data) {
|
||||||
|
statusEl.textContent = "unreachable";
|
||||||
|
bodyEl.innerHTML = `<span class="muted">couldn't reach ${esc(domain)} — peer may be offline, blocking cross-origin, or on an older psyc.</span>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The detail panel may have moved on to another node by now — re-check
|
||||||
|
// the section is still in the DOM before mutating.
|
||||||
|
if (!detailEl.contains(sec)) return;
|
||||||
|
|
||||||
|
const gen = (data.generated_at || (data.node && data.node.generated_at) || "").slice(0, 19).replace("T", " ");
|
||||||
|
const declaredFp = data.fingerprint || (data.node && data.node.fingerprint) || "";
|
||||||
|
const peers = data.peers || [];
|
||||||
|
const vouchesOut = data.vouches_out || data.vouches || [];
|
||||||
|
const vouchesIn = data.vouches_in || [];
|
||||||
|
const transitive = data.transitive_peers || [];
|
||||||
|
const corr = (typeof data.corroboration_count_24h === "number") ? data.corroboration_count_24h : null;
|
||||||
|
const logHead = data.node && data.node.transparency_log_head_hash;
|
||||||
|
const logCount = data.node && data.node.translog_entry_count;
|
||||||
|
|
||||||
|
const fpMatch = declaredFp === expectedFp;
|
||||||
|
const fpBadge = fpMatch
|
||||||
|
? `<span class="fn-remote-ok">fingerprint matches</span>`
|
||||||
|
: `<span class="fn-remote-warn">fingerprint mismatch (${esc((declaredFp || "—").slice(0, 12))}…)</span>`;
|
||||||
|
|
||||||
|
const peersList = peers.length
|
||||||
|
? `<ul class="fn-remote-list">${peers.slice(0, 12).map(p => {
|
||||||
|
const fp = p.fingerprint || "";
|
||||||
|
const dom = p.domain || "—";
|
||||||
|
const sig24 = (typeof p.signal_count_24h === "number") ? ` · ${p.signal_count_24h} sig/24h` : "";
|
||||||
|
return `<li><code>${esc(shortFp(fp))}</code> <span class="muted">${esc(dom)}</span>${sig24}</li>`;
|
||||||
|
}).join("")}</ul>`
|
||||||
|
: `<span class="muted">no trusted peers exposed</span>`;
|
||||||
|
|
||||||
|
const vouchesOutList = vouchesOut.length
|
||||||
|
? `<ul class="fn-remote-list">${vouchesOut.slice(0, 12).map(v => {
|
||||||
|
const tfp = v.target_fingerprint || v.target_fp || "";
|
||||||
|
return `<li><code>${esc(shortFp(tfp))}</code></li>`;
|
||||||
|
}).join("")}</ul>`
|
||||||
|
: `<span class="muted">no outbound vouches</span>`;
|
||||||
|
|
||||||
|
const vouchesInList = vouchesIn.length
|
||||||
|
? `<ul class="fn-remote-list">${vouchesIn.slice(0, 12).map(v => {
|
||||||
|
const vfp = v.voucher_fingerprint || v.voucher_fp || "";
|
||||||
|
return `<li><code>${esc(shortFp(vfp))}</code></li>`;
|
||||||
|
}).join("")}</ul>`
|
||||||
|
: `<span class="muted">no inbound vouches</span>`;
|
||||||
|
|
||||||
|
statusEl.textContent = kind === "explore" ? "explore feed" : "network feed";
|
||||||
|
bodyEl.innerHTML = `
|
||||||
|
<div class="fn-remote-meta">
|
||||||
|
<span>generated <code>${esc(gen || "—")}</code></span>
|
||||||
|
${fpBadge}
|
||||||
|
${corr !== null ? `<span>corroborations 24h: <b>${corr}</b></span>` : ""}
|
||||||
|
${logCount !== undefined ? `<span>translog: <b>${logCount}</b> entries</span>` : ""}
|
||||||
|
${logHead ? `<span>head <code>${esc(String(logHead).slice(0, 12))}…</code></span>` : ""}
|
||||||
|
</div>
|
||||||
|
<div class="fn-remote-cols">
|
||||||
|
<div><div class="fn-remote-h">their peers <span class="muted">(${peers.length})</span></div>${peersList}</div>
|
||||||
|
<div><div class="fn-remote-h">vouches out <span class="muted">(${vouchesOut.length})</span></div>${vouchesOutList}</div>
|
||||||
|
<div><div class="fn-remote-h">vouches in <span class="muted">(${vouchesIn.length})</span></div>${vouchesInList}</div>
|
||||||
|
${transitive.length ? `<div><div class="fn-remote-h">they know <span class="muted">(${transitive.length})</span></div><ul class="fn-remote-list">${transitive.slice(0, 12).map(t => `<li><code>${esc(shortFp(t.fingerprint || ""))}</code></li>`).join("")}</ul></div>` : ""}
|
||||||
|
</div>
|
||||||
|
<div class="fn-remote-actions">
|
||||||
|
<a class="fn-action-btn" href="${esc(base)}/federation/explore" target="_blank" rel="noopener">open their explorer →</a>
|
||||||
|
<a class="fn-action-btn" href="${esc(base)}/federation/explore/data" target="_blank" rel="noopener">raw JSON</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- idle animation ------------------------------------------
|
// ---------- idle animation ------------------------------------------
|
||||||
let energyBudget = 40;
|
let energyBudget = 40;
|
||||||
function loop() {
|
function loop() {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
// This makes the cockpit installable as a PWA and survives flaky connections,
|
// This makes the cockpit installable as a PWA and survives flaky connections,
|
||||||
// without serving stale operational data behind the operator's back.
|
// without serving stale operational data behind the operator's back.
|
||||||
|
|
||||||
const CACHE_VERSION = "psyc-v8";
|
const CACHE_VERSION = "psyc-v9";
|
||||||
const STATIC_ASSETS = [
|
const STATIC_ASSETS = [
|
||||||
"/static/cockpit.css",
|
"/static/cockpit.css",
|
||||||
"/static/psyc-tokens.css",
|
"/static/psyc-tokens.css",
|
||||||
|
|||||||
Reference in New Issue
Block a user