- new is_admin column on invites (migration 4) with DEFAULT 0
- requireAdmin middleware returns 404 for non-admins so the route's
existence isn't leaked; path obscured as /fenjaops (not /admin)
- admin/ dir lives outside public/ and protected/; only reachable via
the explicit gated mount + /api/fenjaops/{invites,joins} endpoints
- bin/invite.js gains `admin add|remove|list` subcommands
- OPERATIONS.md + CLAUDE.md + PROJECT.md document the hidden URL
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
61 lines
2.1 KiB
HTML
61 lines
2.1 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">Read-only view. Grant/revoke via <code>bin/invite.js admin</code>.</p>
|
||
</header>
|
||
|
||
<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>
|