Add request-and-approve onboarding, alongside the instant self-service path
Two mechanisms now coexist on the same forms, chosen by whether the
optional admin password field is filled in:
- Left blank: the submission becomes a pending request (dynamic_repo /
pending_radicle_peer, approved=0) that does nothing on its own — no
sync, no rad node connect, nothing touches f1/f2 or any Radicle node.
A stranger can propose a peer without ever knowing the admin password.
- Filled in and correct: unchanged from before — goes live immediately
(approved right away for a Forgejo peer/new repo; rad node connect +
rad seed run immediately for a Radicle peer). Still the right tool for
handing the password to a trusted group (a class, a workshop) who
should self-serve without a review step.
- Filled in and wrong: rejected outright, never silently downgraded to
a pending request — an oracle for guessing the password would defeat
the point of the existing rate limiter.
New /admin/pending: the only place the password gates anything now
*for the request path* — a stateless, no-session review list (every
Approve/Reject is its own password-carrying POST, consistent with how
/share and the rest of the admin surface already work) covering both
dynamic_repo requests and pending Radicle peer requests in one list.
Rejecting a Forgejo/repo request also deletes its already-written token
file, so a rejected submission leaves nothing behind.
dynamic_repo gets an `approved` column; UnmaterializedDynamicRepos and
the startup reload (AllDynamicRepos) now both filter on it, so a
pending-but-unapproved row can never be picked up by the sync loop by
accident. New pending_radicle_peer table for the Radicle-peer request
path, which needed no ongoing-pair storage at all — approval just runs
the two rad commands once and the row is gone.
Verified: 9 new functional tests (submission needs no password; wrong
password rejects outright rather than queuing; the pending review page
requires the password and never leaks data without it; approve/reject
only take effect with the correct password; approving activates a row;
rejecting removes it and its token file; the optional-password instant
path still works exactly as before; a Radicle approval fails gracefully
with no real node reachable) plus a live round trip against production
(submit with no password, confirm it's queued and invisible without
the admin password, reject it, confirm it's gone) and a Playwright pass
confirming zero CSP violations on the new review page.