Adds GET /api/fenjaops/events and three new panels: per-type totals
+ device breakdown, per-user summary (logins / timeline views / last
seen), and a raw event log capped at the newest 500 rows.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>
The form panel's own subheading already says "non-admin only", so the
separate admin-page subtitle was redundant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>
- 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>