// ─────────────────────────────────────────────────────────────
// admin/admin.js — pulls invites + joins from the gated
// /api/fenjaops endpoints and renders them into the three tables
// in index.html. Read-only: no mutations, no nav links elsewhere.
//
// Note: the public URL path is `/fenjaops` (not `/admin`) as a
// small obscurity measure. Internal names stay as "admin".
// ─────────────────────────────────────────────────────────────
function iso(ms) {
return new Date(ms).toISOString().replace('T', ' ').slice(0, 19) + 'Z';
}
function shortSession(id) {
if (!id) return '';
return id.slice(0, 10) + '…';
}
function escapeHtml(s) {
return String(s ?? '').replace(/[&<>"']/g, (c) => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
}[c]));
}
function renderInvites(rows) {
const tbody = document.querySelector('#t-invites tbody');
const empty = document.getElementById('empty-invites');
if (!rows.length) {
tbody.innerHTML = '';
empty.hidden = false;
return;
}
empty.hidden = true;
tbody.innerHTML = rows.map((r) => {
const when = new Date(r.invited_at).toISOString().slice(0, 10);
const admin = r.is_admin ? 'Admin' : '';
return `
| ${escapeHtml(r.email)} |
${escapeHtml(r.first_name || '')} |
${when} |
${escapeHtml(r.invited_by || '')} |
${admin} |
`;
}).join('');
}
function renderSummary(rows) {
const tbody = document.querySelector('#t-summary tbody');
const empty = document.getElementById('empty-summary');
if (!rows.length) {
tbody.innerHTML = '';
empty.hidden = false;
return;
}
empty.hidden = true;
tbody.innerHTML = rows.map((r) => `
| ${escapeHtml(r.email)} |
${r.click_count} |
${iso(r.first_clicked_at)} |
${iso(r.last_clicked_at)} |
`).join('');
}
function renderClicks(rows) {
const tbody = document.querySelector('#t-clicks tbody');
const empty = document.getElementById('empty-clicks');
if (!rows.length) {
tbody.innerHTML = '';
empty.hidden = false;
return;
}
empty.hidden = true;
tbody.innerHTML = rows.map((r) => `
| ${iso(r.clicked_at)} |
${escapeHtml(r.email)} |
${escapeHtml(shortSession(r.session_id))} |
`).join('');
}
async function load() {
try {
const [invitesRes, joinsRes] = await Promise.all([
fetch('/api/fenjaops/invites', { credentials: 'same-origin' }),
fetch('/api/fenjaops/joins', { credentials: 'same-origin' }),
]);
if (!invitesRes.ok || !joinsRes.ok) {
// Session expired or admin flag revoked while the page was open —
// bounce to the front page rather than leaving stale tables.
window.location.href = '/';
return;
}
const invites = await invitesRes.json();
const joins = await joinsRes.json();
document.getElementById('stat-clicks').textContent = joins.total_clicks;
document.getElementById('stat-unique').textContent = joins.unique_users;
document.getElementById('stat-invites').textContent = invites.length;
document.getElementById('stat-admins').textContent = invites.filter((r) => r.is_admin).length;
renderInvites(invites);
renderSummary(joins.summary);
renderClicks(joins.clicks);
} catch (err) {
// Network failure — show a soft error rather than a blank page.
const stats = document.getElementById('stats');
stats.innerHTML = 'Failed to load admin data — check the server logs.
';
console.error('[admin]', err);
}
}
load();