Closes the gap where all of this session's new server-side capability
(live replication, coordinated promotion, identical signing keys) had
no UI at all -- an operator using the Master Site modal had no way to
know any of it existed or was working.
- Master Site modal: new "Live Replication" row (spoke) shows whether
this join actually registered for live updates or is stuck on a
one-time snapshot; new "Registered Spokes" row (master) shows how
many spokes are receiving live pushes.
- Join form: new "this site's own reachable URL" field, prefilled from
window.location.origin, wired to the selfUrl the join API already
supported but the UI never sent -- a UI-driven join previously NEVER
registered for live replication, only the setup.sh bootstrap path did.
The success toast now reports whether live replication actually
activated, not just "joined".
- Promote button: success toast now surfaces the handoff result (old
master demoted / unreachable / no previous master), so the operator
sees immediately whether the coordinated demotion actually happened.
- GET /api/site/config no longer returns masterJoinKey or
replicationPushToken in the response -- found while wiring this up:
live credentials were being sent straight to the browser for every
admin session. Replaced with boolean derivatives
(hasMasterJoinKey, liveReplication).
- GET /api/directory-admin/site-status gained liveReplication (spoke)
and registeredSpokesCount (master) so the modal has something to render.
Verified by actually driving it in a real browser against a live
container (not just code review): logged in, opened the modal, saw the
new rows, minted a real join key end-to-end, no console errors.
docs/site-join.md rewritten to cover live replication, signing-key sync,
coordinated promotion/demote, and the new endpoints -- it previously only
described the v2.2.0-v2.3.0 one-time-snapshot behavior.