- POST /api/fenjaops/invites on server.js (requireAuth+requireAdmin). Ignores any is_admin field in the body — always stores 0. Records the acting admin's email in invited_by so the audit trail shows who added whom (CLI adds still record "cli"). - admin/index.html: new "Invite a new user" form panel at the top (email + optional first name). - admin/admin.js: wires the form submit to the POST, shows inline success/error, refreshes the tables on success. - admin/admin.css: form styling matching the existing paper/ink palette; mobile stacks. - Docs: CLAUDE.md, PROJECT.md, OPERATIONS.md, CHECKLIST.md, README.md all updated. New non-negotiable property in PROJECT.md: no web endpoint can set is_admin=1 or delete an invite — promotion + removal stay on bin/invite.js. New CHECKLIST.md section H2 covers the page's gating, the invite form, and an escalation-path audit. Admin promotion and invite deletion remain CLI-only so a compromised admin session cannot escalate or evict. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
77 lines
2.7 KiB
HTML
77 lines
2.7 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<meta name="robots" content="noindex, nofollow" />
|
||
<title>Admin — Fenja AI</title>
|
||
<link rel="stylesheet" href="/fenjaops/admin.css" />
|
||
</head>
|
||
<body>
|
||
<header class="masthead">
|
||
<h1>Fenja AI <span class="dim">— Admin</span></h1>
|
||
<p class="meta">Invite regular users below. Admin promotion is CLI-only via <code>bin/invite.js admin</code>.</p>
|
||
</header>
|
||
|
||
<section class="panel">
|
||
<h2>Invite a new user <span class="dim">— non-admin only</span></h2>
|
||
<form class="invite-form" id="invite-form" novalidate>
|
||
<label>
|
||
<span class="lbl">Email</span>
|
||
<input type="email" name="email" autocomplete="off" required />
|
||
</label>
|
||
<label>
|
||
<span class="lbl">First name <span class="opt">(optional)</span></span>
|
||
<input type="text" name="first_name" maxlength="64" autocomplete="off" />
|
||
</label>
|
||
<button type="submit">Send invite</button>
|
||
<p class="form-msg" id="invite-msg" hidden></p>
|
||
</form>
|
||
</section>
|
||
|
||
<section class="panel">
|
||
<h2>Stats</h2>
|
||
<div class="stats" id="stats">
|
||
<div class="stat"><span class="stat-k">Total clicks</span><span class="stat-v" id="stat-clicks">–</span></div>
|
||
<div class="stat"><span class="stat-k">Unique users</span><span class="stat-v" id="stat-unique">–</span></div>
|
||
<div class="stat"><span class="stat-k">Invites</span><span class="stat-v" id="stat-invites">–</span></div>
|
||
<div class="stat"><span class="stat-k">Admins</span><span class="stat-v" id="stat-admins">–</span></div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="panel">
|
||
<h2>Per-user join summary</h2>
|
||
<table class="t" id="t-summary">
|
||
<thead><tr>
|
||
<th>Email</th><th class="num">Clicks</th><th>First click</th><th>Last click</th>
|
||
</tr></thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
<p class="empty" id="empty-summary" hidden>No join clicks yet.</p>
|
||
</section>
|
||
|
||
<section class="panel">
|
||
<h2>Invites</h2>
|
||
<table class="t" id="t-invites">
|
||
<thead><tr>
|
||
<th>Email</th><th>Name</th><th>Invited</th><th>By</th><th class="num">Admin</th>
|
||
</tr></thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
<p class="empty" id="empty-invites" hidden>No invites yet.</p>
|
||
</section>
|
||
|
||
<section class="panel">
|
||
<h2>Raw join log <span class="dim">(newest first)</span></h2>
|
||
<table class="t" id="t-clicks">
|
||
<thead><tr>
|
||
<th>When</th><th>Email</th><th class="mono">Session</th>
|
||
</tr></thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
<p class="empty" id="empty-clicks" hidden>No clicks logged yet.</p>
|
||
</section>
|
||
|
||
<script src="/fenjaops/admin.js" defer></script>
|
||
</body>
|
||
</html>
|