package console import ( "net/http" "sort" ) // The armed-regions endpoint (issue #346): one authoritative answer to // "which regions can serve right now", for consumer pickers — titogo's // signup picker and move doors consume it through a 60s cache and hide // themselves when it is unreachable, so it is never in a request path. // // GET /api/cli/apps/{account}/{app}/regions // {"regions":[{"code":"ie","armed":true},{"code":"uk","armed":true}], // "default":"ie"} // // Codes are CARLOS placement codes, never AWS region strings. armed is // EXACTLY the create door's predicate (#329's armedPlacements — a picker // built on this can never offer what create would refuse): a box publishes // the code, has a dial address, and can host (#110 — see armedPlacements // for why reachability alone advertised five regions that served 502). // The home region appears under its NAME // (Config.HomeRegion) with armed true — create accepts it by normalizing // the name to the record's "" — and is also the default. A deployment with // no named home and no placement-publishing boxes answers empty lists, and // consumers hide their pickers. // // Nearest-region additions (§9.3): each entry may carry aws_region and // name, and the answer carries app_default (regiondefault.go). regions and // default are unchanged for every older reader. // // The instance family's gate, deliberately: the answer is deployment // topology (which regions exist), the same information the create door's // refusal message already hands the same bearer. func (s *Server) handleAPIRegions(w http.ResponseWriter, r *http.Request) { _, acct, app, ok := s.instanceGate(w, r) if !ok { return } type region struct { Code string `json:"code"` Armed bool `json:"armed"` AWSRegion string `json:"aws_region,omitempty"` Name string `json:"name,omitempty"` } set, _ := armedPlacements(r.Context(), s.cfg.Releases) cands := s.candsOrHome(r.Context()) named := func(code string) region { c, _ := findCandidate(cands, s.homeRegionIn(code)) return region{Code: code, AWSRegion: c.AWSRegion, Name: s.placeOf(s.homeRegionIn(code), cands)} } out := make([]region, 0, len(set)+1) if s.cfg.HomeRegion != "" { h := named(s.cfg.HomeRegion) h.Armed = true out = append(out, h) // A box that publishes the home's code explicitly (a future coded // home) must not appear twice. delete(set, s.cfg.HomeRegion) } for code, armed := range set { e := named(code) e.Armed = armed out = append(out, e) } sort.Slice(out, func(i, j int) bool { return out[i].Code < out[j].Code }) // regions and default are what older clients and titogo read; they stay // as they were. app_default is the nearest-region addition (§9.3). writeJSON(w, http.StatusOK, map[string]any{ "regions": out, "default": s.cfg.HomeRegion, "app_default": s.appDefault(r.Context(), acct, app), }) }