/** * HTML templates for the admin area. * * Deliberately plain. Every action is a form submission that redirects back, * so a JavaScript-off session works — the only script we ship is a tiny * convenience to copy the sign-this string on the login page. * * Pages follow the same {title, description, body, here?} shape as the public * pages, so the shared layout wraps them unchanged. The admin topnav sits * inside each body (so the public topnav still appears above it for continuity). */ import { escapeHtml } from './layout.js'; import { CATEGORIES } from './pages-browse.js'; import { MODES } from './modes.js'; import { adminPublisher } from './admin-session.js'; /* ------------------------------- login ------------------------------- */ export function loginPage({ challenge, error = null, name }) { const message = `PROMETHEUS-ADMIN-LOGIN\nname=${name}\nchallenge=${challenge}`; return { title: 'Admin — sign in', description: 'Sign in to the Prometheus admin.', here: '', body: `

Admin

Sign in with the key that owns ${escapeHtml(name)}.

${error ? `
Sign-in failed. ${escapeHtml(error)}
` : ''}
  1. 1. Copy the challenge below into your wallet's "sign message" box. The entire block, exactly as shown.
  2. 2. Sign with the key for the address that owns the admin name. Theseus's Aegis panel, Electron Cash, Deviant — any BIP-137 wallet works.
  3. 3. Paste the signature below and submit. The signature authenticates you as the admin for the next 24 hours.

Message to sign

${escapeHtml(message)}
`, }; } /* ------------------------------- shared topnav ------------------------------- */ function adminNav(session, here = '') { const link = (href, label) => `${label}`; return ``; } /* ------------------------------- dashboard ------------------------------- */ export function dashboardPage({ session, counts, total, modes, recentListings, recentAudit }) { const modeChip = (m) => `${escapeHtml(m)}`; return { title: 'Admin — dashboard', description: 'Prometheus admin dashboard.', here: '/admin', body: ` ${adminNav(session, '/admin')}

Dashboard

${total}

active listings across ${Object.values(counts).filter((n) => n > 0).length} categories

${modeChip(modes.global)}

global publish mode

${Object.values(modes.categories).filter(Boolean).length}

per-category overrides in effect

Recent listings

${recentListings.length === 0 ? `

No listings yet.

` : ` ${recentListings.map((r) => ``).join('')}
idcategorysizeat
${escapeHtml(r.id)} ${escapeHtml(r.category)} ${formatBytes(r.download_size)} ${escapeHtml(r.created_at)} manage →
`}

Recent audit

${recentAudit.length === 0 ? `

No admin actions yet.

` : ` ${recentAudit.slice(0, 10).map((r) => ``).join('')}
atactoractiontarget
${escapeHtml(r.at)} ${escapeHtml(r.actor)} ${escapeHtml(r.action)} ${escapeHtml(r.target || '')}

Full audit log →

`}
`, }; } /* ------------------------------- listings ------------------------------- */ export function listingsPage({ session, rows, notice = null }) { return { title: 'Admin — listings', description: 'All listings on Prometheus.', here: '/admin/listings', body: ` ${adminNav(session, '/admin/listings')}
${notice ? `
${escapeHtml(notice)}
` : ''}

Listings (${rows.length})

${rows.length === 0 ? `

No listings yet.

` : ` ${rows.map((r) => renderListingRow(r)).join('\n')}
idnamecategorysize statuscreated
`}
`, }; } function renderListingRow(r) { const withdrawn = !!r.withdrawn_at; const statusCell = withdrawn ? `withdrawn` : `active`; const actions = withdrawn ? `${escapeHtml(r.withdrawn_at)}` : `
`; return ` ${escapeHtml(r.id)} ${escapeHtml(r.name)} ${escapeHtml(r.category)} ${formatBytes(r.download_size)} ${statusCell} ${escapeHtml(r.created_at)} ${actions} `; } /* ------------------------------- modes ------------------------------- */ export function modesPage({ session, modes, notice = null }) { const modeOptions = (selected) => MODES.map((m) => ``).join(''); return { title: 'Admin — modes', description: 'Publish-mode policy per category.', here: '/admin/modes', body: ` ${adminNav(session, '/admin/modes')}
${notice ? `
${escapeHtml(notice)}
` : ''}

Publish modes

The three modes describe how new listings get accepted. The global default applies everywhere unless a category has its own override. Clear an override by picking "inherits global" from the per-category dropdown.

append — any valid listing goes in; readers see the signed scan report and decide.
scan-first — infected uploads are rejected before they land. Clean ones publish.
review — clean listings sit in a human-review queue before browse shows them. Toggle persists; enforcement lands in a later milestone.
${CATEGORIES.map((c) => { const override = modes.categories[c.slug]; return ``; }).join('')}
ScopeCurrent mode
Global
${c.icon} ${escapeHtml(c.title)} ${c.slug}
`, }; } /* ------------------------------- audit ------------------------------- */ export function auditPage({ session, entries }) { return { title: 'Admin — audit log', description: 'Hash-chained log of admin actions.', here: '/admin/audit', body: ` ${adminNav(session, '/admin/audit')}

Audit log

Append-only. Each row's this_hash is computed over its own fields plus the previous row's hash — a retroactive edit anywhere in the chain shows up as a mismatch in the next row.

${entries.length === 0 ? `

No admin actions yet.

` : ` ${entries.map((e) => ``).join('')}
seqatactoraction targetdetailsthis_hash
${e.seq} ${escapeHtml(e.at)} ${escapeHtml(e.actor)} ${escapeHtml(e.action)} ${escapeHtml(e.target || '')} ${escapeHtml(e.details || '')} ${escapeHtml(e.this_hash.slice(0, 10))}…
`}
`, }; } /* ------------------------------- helpers ------------------------------- */ function formatBytes(n) { const num = Number(n) || 0; if (num < 1024) return `${num} B`; if (num < 1024 * 1024) return `${(num / 1024).toFixed(1)} KiB`; if (num < 1024 * 1024 * 1024) return `${(num / 1024 / 1024).toFixed(1)} MiB`; return `${(num / 1024 / 1024 / 1024).toFixed(2)} GiB`; }