customer-presentation/admin/index.html
Arlind Ukshini cbfb187d16 /fenjaops: admin-only form to invite non-admin users
- 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>
2026-04-23 18:07:47 +02:00

77 lines
2.7 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>