5270bad76e
gates / gates (push) Successful in 23s
Slice 1 translated the dashboard's markup. The sentences the program BUILDS were still Hungarian literals in Go, so an English household clicked an English button and was answered in Hungarian. 226 of them move into the bundle here. A flash was the hard part: it travels inside the redirect URL and is rendered by a DIFFERENT request, so it now carries a bundle key plus its parameters. A link minted by an older controller carries prose and is shown verbatim — never a raw key, never dropped. Also converted: page data and view-model text, the internal/api JSON answers, the alert banners (Alert.MessageKey, rendered on the way out of GetAlerts), 237 country names at display, and the four page titles built around an app name (R-566 closed). Hungarian is byte-identical, and that is measured rather than read: scripts/i18n_go_parity.py freezes every Go literal at the base commit (7 467) and refuses a key whose Hungarian is not that text, byte for byte. Three decoys, each seen to convict. Its own first version filtered the capture through an ASCII-Hungarian word list and missed seven real literals — the R-565 class. The filter is gone. Nothing on the wire moved, and wire goldens now hold it there: the report's health warnings and every notify event message stay Hungarian, because the hub MAILS the controller's sentence when it has no entry of its own. Slice 3 (R-558) owns those. MinAgent: 0.131.0 (unchanged). No hub release needed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
203 lines
6.6 KiB
Go
203 lines
6.6 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"sort"
|
|
|
|
cf "gitea.dooplex.hu/admin/felhom-controller/internal/cloudflare"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
|
)
|
|
|
|
func (r *Router) geoStatus(w http.ResponseWriter, _ *http.Request) {
|
|
geo := r.sett.GetGeoRestriction()
|
|
|
|
data := map[string]interface{}{
|
|
"cf_configured": r.cfg.Infrastructure.CFAPIToken != "",
|
|
"geo_available": r.geoSync != nil,
|
|
}
|
|
|
|
if geo != nil {
|
|
data["enabled"] = geo.Enabled
|
|
data["allowed_countries"] = geo.AllowedCountries
|
|
data["app_overrides"] = geo.AppOverrides
|
|
data["last_sync"] = geo.LastSync
|
|
data["last_sync_error"] = geo.LastSyncError
|
|
data["syncing"] = r.geoSync != nil && r.geoSync.IsRunning()
|
|
} else {
|
|
data["enabled"] = false
|
|
data["allowed_countries"] = []string{"HU"}
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: data})
|
|
}
|
|
|
|
func (r *Router) geoUpdateSettings(w http.ResponseWriter, req *http.Request) {
|
|
limitBody(w, req)
|
|
r.dbg("geoUpdateSettings: contentLength=%d", req.ContentLength)
|
|
|
|
var body struct {
|
|
Enabled bool `json:"enabled"`
|
|
AllowedCountries []string `json:"allowed_countries"`
|
|
}
|
|
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, apiResponse{OK: false, Error: "invalid request body"})
|
|
return
|
|
}
|
|
|
|
// Validate country codes
|
|
for _, code := range body.AllowedCountries {
|
|
if !cf.ValidCountryCode(code) {
|
|
writeJSON(w, http.StatusBadRequest, apiResponse{OK: false, Error: "invalid country code: " + code})
|
|
return
|
|
}
|
|
}
|
|
|
|
// Get existing settings to preserve app overrides and sync state
|
|
existing := r.sett.GetGeoRestriction()
|
|
geo := &settings.GeoRestriction{
|
|
Enabled: body.Enabled,
|
|
AllowedCountries: body.AllowedCountries,
|
|
}
|
|
if existing != nil {
|
|
geo.AppOverrides = existing.AppOverrides
|
|
geo.ZoneID = existing.ZoneID
|
|
geo.RulesetID = existing.RulesetID
|
|
}
|
|
|
|
if err := r.sett.SetGeoRestriction(geo); err != nil {
|
|
r.logger.Printf("[ERROR] [api] Failed to save geo settings: %v", err)
|
|
writeJSON(w, http.StatusInternalServerError, apiResponse{OK: false, Error: err.Error()})
|
|
return
|
|
}
|
|
|
|
r.logger.Printf("[INFO] [api] Geo settings updated: enabled=%v, countries=%v", body.Enabled, body.AllowedCountries)
|
|
|
|
// Trigger async CF sync
|
|
if r.geoSync != nil {
|
|
go func() {
|
|
if err := r.geoSync.Sync(context.Background()); err != nil {
|
|
r.logger.Printf("[ERROR] [api] Geo sync after settings update failed: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Push a fresh hub report out-of-band so the hub reflects the new geo state right
|
|
// away instead of after the next ~15-min cycle.
|
|
r.reportPushNow()
|
|
|
|
writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: r.msg(req, "api.geo.set")})
|
|
}
|
|
|
|
func (r *Router) geoTriggerSync(w http.ResponseWriter, req *http.Request) {
|
|
if r.geoSync == nil {
|
|
writeJSON(w, http.StatusBadRequest, apiResponse{OK: false, Error: r.msg(req, "api.geo.no_cloudflare")})
|
|
return
|
|
}
|
|
|
|
go func() {
|
|
if err := r.geoSync.Sync(context.Background()); err != nil {
|
|
r.logger.Printf("[ERROR] [api] Manual geo sync failed: %v", err)
|
|
return
|
|
}
|
|
// On success the sync clears any stale last_sync_error — push a report so the hub
|
|
// reflects the cleared state immediately rather than after the next cycle.
|
|
r.reportPushNow()
|
|
}()
|
|
|
|
writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: r.msg(req, "api.geo.sync_started")})
|
|
}
|
|
|
|
// geoCountries serves the picker's country list in the request's language.
|
|
//
|
|
// The TABLE in internal/cloudflare is untouched and stays Hungarian: it is also the validator, and
|
|
// internal/report puts country CODES on the wire. Only the NAME shown to the customer is translated,
|
|
// and the list is re-sorted afterwards -- alphabetical order is per language, and a list sorted by
|
|
// the Hungarian names would read as shuffled in English.
|
|
func (r *Router) geoCountries(w http.ResponseWriter, req *http.Request) {
|
|
lang := r.langFor(req)
|
|
list := cf.AllCountries()
|
|
for i := range list {
|
|
list[i].Name = r.countryName(lang, list[i].Code, list[i].Name)
|
|
}
|
|
sort.Slice(list, func(i, j int) bool { return list[i].Name < list[j].Name })
|
|
writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: list})
|
|
}
|
|
|
|
func (r *Router) geoSetAppOverride(w http.ResponseWriter, req *http.Request, appName string) {
|
|
if appName == "" {
|
|
writeJSON(w, http.StatusBadRequest, apiResponse{OK: false, Error: "invalid app name"})
|
|
return
|
|
}
|
|
limitBody(w, req)
|
|
|
|
var body struct {
|
|
AllowedCountries []string `json:"allowed_countries"`
|
|
}
|
|
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, apiResponse{OK: false, Error: "invalid request body"})
|
|
return
|
|
}
|
|
|
|
// Validate country codes
|
|
for _, code := range body.AllowedCountries {
|
|
if !cf.ValidCountryCode(code) {
|
|
writeJSON(w, http.StatusBadRequest, apiResponse{OK: false, Error: "invalid country code: " + code})
|
|
return
|
|
}
|
|
}
|
|
|
|
// Verify app exists
|
|
if _, ok := r.stackMgr.GetStack(appName); !ok {
|
|
writeJSON(w, http.StatusNotFound, apiResponse{OK: false, Error: "app not found: " + appName})
|
|
return
|
|
}
|
|
|
|
override := &settings.AppGeoOverride{AllowedCountries: body.AllowedCountries}
|
|
if err := r.sett.SetGeoAppOverride(appName, override); err != nil {
|
|
r.logger.Printf("[ERROR] [api] Failed to save geo override for %s: %v", appName, err)
|
|
writeJSON(w, http.StatusInternalServerError, apiResponse{OK: false, Error: err.Error()})
|
|
return
|
|
}
|
|
|
|
r.logger.Printf("[INFO] [api] Geo override set for %s: countries=%v", appName, body.AllowedCountries)
|
|
|
|
// Trigger async CF sync
|
|
if r.geoSync != nil {
|
|
go func() {
|
|
if err := r.geoSync.Sync(context.Background()); err != nil {
|
|
r.logger.Printf("[ERROR] [api] Geo sync after app override failed: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: r.msg(req, "api.geo.app_set")})
|
|
}
|
|
|
|
func (r *Router) geoRemoveAppOverride(w http.ResponseWriter, req *http.Request, appName string) {
|
|
if appName == "" {
|
|
writeJSON(w, http.StatusBadRequest, apiResponse{OK: false, Error: "invalid app name"})
|
|
return
|
|
}
|
|
|
|
if err := r.sett.RemoveGeoAppOverride(appName); err != nil {
|
|
r.logger.Printf("[ERROR] [api] Failed to remove geo override for %s: %v", appName, err)
|
|
writeJSON(w, http.StatusInternalServerError, apiResponse{OK: false, Error: err.Error()})
|
|
return
|
|
}
|
|
|
|
r.logger.Printf("[INFO] [api] Geo override removed for %s", appName)
|
|
|
|
// Trigger async CF sync
|
|
if r.geoSync != nil {
|
|
go func() {
|
|
if err := r.geoSync.Sync(context.Background()); err != nil {
|
|
r.logger.Printf("[ERROR] [api] Geo sync after override removal failed: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: r.msg(req, "api.geo.app_removed")})
|
|
}
|