hub: offsite multi-endpoint management UI (v0.47.0 part 5)

- /offsite lists ALL wg_endpoints rows as cards (id, address, pubkey, subnet,
  PBS addr, peers-in-subnet count) + add/edit/delete forms
- store: ListWGEndpoints (id order) + DeleteWGEndpoint (plain delete; the
  peers-in-subnet guard lives in the handler where the refusal is built);
  SetWGEndpoint upsert reused, single-expected comment updated
- guards: subnet edit refused 409 while peers sit in the current subnet;
  endpoint delete refused 409 while peers sit in its subnet; full form
  validation (CIDR, pbs ip in subnet, port 1-65535, pubkey, id charset) -> 400
- peer table gains an Endpoint column (first id-ordered subnet match; em dash
  when none); pubkey-change edit gets a type-to-confirm noting pull-based
  convergence
- allocation/reconciler/desired-state STAY lowest-endpoint-id (page notes the
  deferral); GetWGEndpoint semantics untouched
- tests: E1-E6 incl. guard red-proofs; store list/delete test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vvz1NCu22p8dGkRCpeX9re
This commit is contained in:
2026-07-11 21:35:24 +02:00
parent 068427a729
commit 0daddcd1c4
6 changed files with 723 additions and 46 deletions
+15 -1
View File
@@ -248,9 +248,23 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case strings.HasPrefix(path, "/apps/"):
appName := strings.TrimPrefix(path, "/apps/")
s.handleAppDetail(w, r, appName)
// Offsite — read-only WG endpoint + peer registry (S2). Mutations stay on the admin API.
// Offsite — WG endpoint + peer registry (S2), plus endpoint management forms
// (v0.47.0; peer mutations stay on the admin API, allocation stays lowest-endpoint-id).
case path == "/offsite":
s.handleOffsite(w, r)
case path == "/offsite/endpoints":
if r.Method == http.MethodPost {
s.handleOffsiteEndpointSave(w, r)
} else {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
case strings.HasPrefix(path, "/offsite/endpoints/") && strings.HasSuffix(path, "/delete"):
endpointID := strings.TrimSuffix(strings.TrimPrefix(path, "/offsite/endpoints/"), "/delete")
if r.Method == http.MethodPost {
s.handleOffsiteEndpointDelete(w, r, endpointID)
} else {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
// Hosts — read-only fleet view (audit F-M1) + the v0.46.0 log-bundle actions.
case path == "/hosts" || path == "/hosts/":
s.handleHostsList(w, r)