// The public read-only page — https://<server host>/ and /status (hyper-social #80; operator 2026-10-13:
// "frontends for each bridge so that you could see what posts really show up, like a webui for a relay").
//
// It shows bsky's OWN view of what this gateway serves — the relay's status for our host, each account's
// last posts as bsky's AppView shows them, each community feed as bsky's getFeed returns it — beside our
// side of it (in sync or not). Everything comes from the cached probes (5 min); a page view never makes a
// request to bsky.
//
// ⛔ PUBLIC: alert TITLES only. No fix commands, no alert details, no process stats, no peer internals —
// those are for the operator page on the authenticated admin surface (/admin/status).
const esc = (s) => String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
const ago = (ts, now) => {
if (!ts) return 'never'
const s = Math.max(0, Math.round((now - (typeof ts === 'string' ? Date.parse(ts) : ts)) / 1000))
if (!Number.isFinite(s)) return '?'
if (s < 90) return s + 's ago'
if (s < 90 * 60) return Math.round(s / 60) + 'm ago'
if (s < 36 * 3600) return Math.round(s / 3600) + 'h ago'
return Math.round(s / 86400) + 'd ago'
}
// at://did/app.bsky.feed.post/rkey → where bsky shows it.
const bskyLink = (uri) => {
const m = /^at:\/\/([^/]+)\/app\.bsky\.feed\.post\/([^/]+)$/.exec(String(uri || ''))
return m ? `https://bsky.app/profile/${m[1]}/post/${m[2]}` : null
}
const STYLE = `body{font:15px/1.5 system-ui,sans-serif;margin:0 auto;padding:16px;max-width:820px;color:#111;background:#fff}
h1{font-size:22px;margin:8px 0}h2{font-size:16px;margin-top:28px;border-bottom:1px solid #ddd;padding-bottom:4px}
.who{color:#666;font-size:13px}.bad{color:#a00;font-weight:600}.good{color:#070}
.post{border-left:3px solid #ddd;padding:2px 10px;margin:8px 0}.post a{color:inherit;text-decoration:none}
.post .meta{color:#666;font-size:12px}code{font:12px ui-monospace,monospace;word-break:break-all}
@media (prefers-color-scheme: dark){body{background:#111;color:#eee}.post{border-color:#444}h2{border-color:#333}.who,.post .meta{color:#999}}`
const post = (p, now) => {
const link = bskyLink(p.uri)
const text = esc(p.text || '(no text)')
return '<div class="post">' + (link ? '<a href="' + esc(link) + '">' + text + '</a>' : text) +
'<div class="meta">' + (p.author ? '@' + esc(p.author) + ' · ' : '') + esc(ago(p.createdAt, now)) + '</div></div>'
}
export function renderPublicPage (m, { now = Date.now() } = {}) {
const b = m.bsky
const host = b ? b.hostname : '?'
const parts = []
parts.push('<h1>' + esc(host) + '</h1><p class="who">A favela gateway: it serves these accounts to bsky and the fediverse. This page shows what bsky itself reports — checked ' + esc(b ? ago(b.at, now) : 'not yet') + '.</p>')
const open = (m.alerts && m.alerts.open) || []
if (open.length) parts.push('<h2>Known problems</h2><ul>' + open.map((a) => '<li class="' + (a.severity === 'critical' ? 'bad' : '') + '">' + esc(a.title) + ' <span class="who">(since ' + esc(ago(a.since, now)) + ')</span></li>').join('') + '</ul>')
if (!b) {
parts.push('<p class="bad">bsky has not been checked yet.</p>')
} else {
parts.push('<h2>bsky</h2><p>bsky\'s relay sees this host as <span class="' + (b.relay.status === 'active' ? 'good' : 'bad') + '">' + esc(b.relay.error ? 'unreachable' : b.relay.status) + '</span>.</p>')
for (const d of b.dids || []) {
const own = ((m.membrane && m.membrane.dids) || []).find((x) => x.did === d.did) || {}
const name = d.handle || own.handle || d.did
const sync = d.inSync === true ? '<span class="good">in sync</span>' : d.inSync === false ? '<span class="bad">bsky has a different version</span>' : 'not compared'
const av = d.appview && d.appview.resolves ? esc(d.appview.postsCount) + ' posts on bsky' : '<span class="bad">bsky does not show this account</span>'
parts.push('<h2><a href="https://bsky.app/profile/' + esc(d.did) + '">@' + esc(name) + '</a></h2><p class="who">' + sync + ' · ' + av +
(d.plcPds && d.plcPds !== 'https://' + host ? ' · <span class="bad">its identity points elsewhere</span>' : '') + '</p>' +
((d.appview && d.appview.recent) || []).map((p) => post(p, now)).join(''))
}
for (const f of b.feeds || []) {
const lens = ((m.lens && m.lens.feeds) || []).find((x) => x.feedUri === f.uri) || {}
const name = lens.id || f.uri.split('/').pop()
const parsed = /^at:\/\/([^/]+)\/app\.bsky\.feed\.generator\/([^/]+)$/.exec(f.uri)
const feedLink = parsed ? `https://bsky.app/profile/${parsed[1]}/feed/${parsed[2]}` : null
parts.push('<h2>Feed: ' + (feedLink ? '<a href="' + esc(feedLink) + '">' + esc(name) + '</a>' : esc(name)) + '</h2>' +
(f.error
? '<p class="bad">bsky cannot load this feed (' + esc(f.error) + ').</p>'
: '<p class="who">As bsky shows it' + (lens.members ? ' — posts from ' + esc(lens.members) + ' members' : '') + '.</p>' + f.posts.map((p) => post(p, now)).join('')))
}
}
if (m.fediverse) {
parts.push('<h2>Fediverse</h2><ul>' + m.fediverse.users.map((u) => '<li><code>@' + esc(u.username) + '@ap.7rnx.net</code> — ' + esc(u.followers) + ' follower' + (u.followers === 1 ? '' : 's') + '</li>').join('') +
'</ul><p><a href="https://ap.7rnx.net/timeline">What the fediverse is sent</a></p>')
}
return '<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>' + esc(host) + '</title><style>' + STYLE + '</style>' + parts.join('')
}
export default renderPublicPage