customer-presentation/admin/index.html
Arlind Ukshini 2d06f8e513 /fenjaops: add Remove button for non-admin invites
- New endpoint: DELETE /api/fenjaops/invites/:email, behind
  requireAuth + requireAdmin. Guardrails:
    * refuses if the target email is an admin (demote first via
      bin/invite.js admin remove) — preserves the invariant that
      a compromised admin session can't lock everyone out;
    * refuses if the target email equals the caller's own —
      prevents self-inflicted lockouts from the UI;
    * deletes active sessions for the target email so the user
      is kicked out immediately instead of holding their 30-day
      cookie.
- Admin page: Invites table gains an "Action" column. Non-admin,
  non-self rows show a Remove button (quiet ink outline; crimson
  on hover to cue destructive intent). Admin and self rows show
  an em-dash. Click → browser confirm() → DELETE → load() to
  refresh counts + tables.
- admin.js fetches /auth/me alongside the other payloads so
  render can compare each row's email against the viewer's.
- PROJECT.md and CLAUDE.md updated: the "no web deletion"
  invariant is narrowed to "no web deletion of admins or self"
  to reflect the new capability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 11:23:12 +02:00

76 lines
2.6 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>
</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><th class="num">Action</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>