- 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>
240 lines
5.7 KiB
CSS
240 lines
5.7 KiB
CSS
/* ─────────────────────────────────────────────────────────────
|
|
admin/admin.css — utilitarian read-only admin styles. Shares
|
|
Fenja's paper/ink palette with the public site but drops the
|
|
editorial serif in favour of system-sans for scannability.
|
|
───────────────────────────────────────────────────────────── */
|
|
|
|
:root {
|
|
--paper: #faf6ee;
|
|
--paper-2: #f6f2e8;
|
|
--ink: #2e2e28;
|
|
--ink-soft: #5f5e5e;
|
|
--ink-dim: #8a887f;
|
|
--line: rgba(46, 46, 40, 0.12);
|
|
--accent: #b96b58;
|
|
--admin: #8a3a2f;
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
html, body {
|
|
margin: 0;
|
|
padding: 0;
|
|
background: var(--paper);
|
|
color: var(--ink);
|
|
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.masthead {
|
|
padding: 32px 48px 8px;
|
|
border-bottom: 1px solid var(--line);
|
|
}
|
|
.masthead h1 {
|
|
margin: 0 0 4px;
|
|
font-family: "Newsreader", Georgia, serif;
|
|
font-size: 34px;
|
|
font-weight: 400;
|
|
letter-spacing: -0.015em;
|
|
}
|
|
.masthead .dim { color: var(--ink-dim); font-style: italic; }
|
|
.masthead .meta {
|
|
margin: 0;
|
|
color: var(--ink-dim);
|
|
font-size: 13px;
|
|
}
|
|
.masthead code {
|
|
background: var(--paper-2);
|
|
padding: 1px 6px;
|
|
border-radius: 3px;
|
|
font-size: 12.5px;
|
|
}
|
|
|
|
.panel {
|
|
padding: 28px 48px;
|
|
border-bottom: 1px solid var(--line);
|
|
}
|
|
.panel h2 {
|
|
margin: 0 0 16px;
|
|
font-family: "Newsreader", Georgia, serif;
|
|
font-size: 20px;
|
|
font-weight: 500;
|
|
letter-spacing: -0.005em;
|
|
color: var(--ink);
|
|
}
|
|
.panel h2 .dim { color: var(--ink-dim); font-weight: 400; font-style: italic; }
|
|
|
|
/* Stats cards */
|
|
.stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
gap: 16px;
|
|
max-width: 900px;
|
|
}
|
|
.stat {
|
|
background: var(--paper-2);
|
|
padding: 16px 20px;
|
|
border-radius: 6px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
.stat-k {
|
|
font-size: 11.5px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.12em;
|
|
color: var(--ink-dim);
|
|
font-weight: 600;
|
|
}
|
|
.stat-v {
|
|
font-family: "Newsreader", Georgia, serif;
|
|
font-size: 32px;
|
|
font-weight: 500;
|
|
color: var(--ink);
|
|
line-height: 1;
|
|
}
|
|
|
|
/* Tables */
|
|
.t {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
.t th,
|
|
.t td {
|
|
text-align: left;
|
|
padding: 10px 14px;
|
|
border-bottom: 1px solid var(--line);
|
|
vertical-align: top;
|
|
}
|
|
.t thead th {
|
|
font-size: 11.5px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.12em;
|
|
color: var(--ink-dim);
|
|
font-weight: 600;
|
|
border-bottom: 1px solid var(--line);
|
|
background: var(--paper-2);
|
|
}
|
|
.t tbody tr:hover { background: var(--paper-2); }
|
|
.t .num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
.t .mono { font-family: ui-monospace, "SF Mono", Consolas, monospace; font-size: 12px; color: var(--ink-soft); }
|
|
.t .when { white-space: nowrap; color: var(--ink-soft); font-size: 13px; }
|
|
.t .badge {
|
|
display: inline-block;
|
|
font-size: 10.5px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.12em;
|
|
color: var(--admin);
|
|
font-weight: 700;
|
|
}
|
|
.t .dim { color: var(--ink-dim); }
|
|
|
|
/* Row action buttons (Remove, …). Quiet by default, crimson on hover
|
|
so the destructive intent reads clearly before the click. */
|
|
.row-action {
|
|
all: unset;
|
|
cursor: pointer;
|
|
font-size: 11.5px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.06em;
|
|
text-transform: uppercase;
|
|
padding: 5px 10px;
|
|
border-radius: 3px;
|
|
background: transparent;
|
|
color: var(--ink-soft);
|
|
box-shadow: inset 0 0 0 1px var(--line);
|
|
transition:
|
|
background 140ms ease,
|
|
color 140ms ease,
|
|
box-shadow 140ms ease;
|
|
}
|
|
.row-action:hover {
|
|
color: #fff;
|
|
background: var(--admin);
|
|
box-shadow: inset 0 0 0 1px var(--admin);
|
|
}
|
|
.row-action:disabled {
|
|
opacity: 0.55;
|
|
cursor: progress;
|
|
}
|
|
|
|
.empty {
|
|
margin: 12px 2px 0;
|
|
color: var(--ink-dim);
|
|
font-style: italic;
|
|
font-size: 13px;
|
|
}
|
|
|
|
/* Invite form */
|
|
.invite-form {
|
|
display: grid;
|
|
grid-template-columns: minmax(220px, 1fr) minmax(160px, 1fr) auto;
|
|
gap: 14px;
|
|
align-items: end;
|
|
max-width: 900px;
|
|
}
|
|
.invite-form label {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
.invite-form .lbl {
|
|
font-size: 11.5px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.12em;
|
|
color: var(--ink-dim);
|
|
font-weight: 600;
|
|
}
|
|
.invite-form .opt {
|
|
text-transform: none;
|
|
letter-spacing: 0;
|
|
font-weight: 400;
|
|
font-style: italic;
|
|
color: var(--ink-dim);
|
|
}
|
|
.invite-form input {
|
|
background: var(--paper-2);
|
|
border: 1px solid var(--line);
|
|
border-radius: 4px;
|
|
padding: 9px 11px;
|
|
font: inherit;
|
|
color: var(--ink);
|
|
}
|
|
.invite-form input:focus {
|
|
outline: none;
|
|
border-color: var(--accent);
|
|
background: var(--paper);
|
|
}
|
|
.invite-form button {
|
|
background: var(--ink);
|
|
color: var(--paper);
|
|
border: none;
|
|
border-radius: 4px;
|
|
padding: 10px 20px;
|
|
font: inherit;
|
|
font-weight: 600;
|
|
letter-spacing: 0.02em;
|
|
cursor: pointer;
|
|
}
|
|
.invite-form button:hover:not(:disabled) { background: #000; }
|
|
.invite-form button:disabled { opacity: 0.55; cursor: progress; }
|
|
.invite-form .form-msg {
|
|
grid-column: 1 / -1;
|
|
margin: 4px 0 0;
|
|
padding: 9px 12px;
|
|
border-radius: 4px;
|
|
font-size: 13px;
|
|
}
|
|
.invite-form .form-msg.ok { background: #e4eadf; color: #3a5330; }
|
|
.invite-form .form-msg.err { background: #f2dcd6; color: var(--admin); }
|
|
|
|
@media (max-width: 640px) {
|
|
.masthead, .panel { padding-left: 20px; padding-right: 20px; }
|
|
.masthead h1 { font-size: 26px; }
|
|
.t th, .t td { padding: 8px 10px; }
|
|
.t .mono { display: none; }
|
|
.invite-form { grid-template-columns: 1fr; }
|
|
.invite-form button { width: 100%; }
|
|
}
|