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.
Fix: the approved column silently unapproved every pre-existing peer
The migration adding dynamic_repo.approved defaulted every row to 0,
including rows that were already materialized (live) under the old
model — federation-x-f3 among them. AllDynamicRepos now filters on
approved=1, so on the very next restart after this deploy, f3 silently
dropped out of live.syncers entirely: no error, no log line, just gone.
Caught live: its README stayed stuck for ~35 minutes across a restart
while f1/f2/bimr kept moving, and it sat invisible in /admin/pending
(a peer that had been live for hours suddenly asking to be
"re-approved" is exactly the tell).
Fixed with a startup backfill — UPDATE dynamic_repo SET approved = 1
WHERE materialized = 1 AND approved = 0 — idempotent (materialized
never reverts to 0), so safe to run unconditionally every startup, not
gated on "is this the first run after the column was added." Verified
with a test that closes and reopens a store with a pre-existing
materialized row and confirms it survives migration approved; verified
live: f3 reloaded on restart, dropped out of the pending queue, and
resumed picking up new commits.
Also: dropped the Radicle-side sentence from /add-peer's lead
paragraph (redundant with the field-level hint already there).
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.
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.