package services import ( "bytes" "encoding/json" "net/http" "net/url" "os" "strconv" "strings" "time" "fotbal-club/internal/config" "fotbal-club/internal/models" "gorm.io/gorm" ) type erMonitor struct { ID uint `json:"id,omitempty"` Name string `json:"name"` URL string `json:"url"` Enabled bool `json:"enabled"` CheckIntervalSec int `json:"check_interval_sec"` TimeoutMs int `json:"timeout_ms"` ExpectStatusMin int `json:"expect_status_min"` ExpectStatusMax int `json:"expect_status_max"` } type erListResp struct { Items []erMonitor `json:"items"` } func StartErrorReviewAutoRegister(db *gorm.DB) { go func() { defer func() { _ = recover() }() time.Sleep(2 * time.Second) for i := 0; i < 60; i++ { if tryAutoRegister(db) { return } time.Sleep(10 * time.Second) } }() } func tryAutoRegister(db *gorm.DB) bool { if db == nil || config.AppConfig == nil { return false } var s models.Settings _ = db.First(&s).Error // Domain is managed via environment only; fallback based on ERROR_LOCAL adminURL := strings.TrimSpace(os.Getenv("ERROR_REVIEW_ADMIN_URL")) if adminURL == "" { errorLocal := false if b, err := strconv.ParseBool(strings.TrimSpace(os.Getenv("ERROR_LOCAL"))); err == nil && b { errorLocal = true } if !errorLocal { if b, err := strconv.ParseBool(strings.TrimSpace(os.Getenv("error_local"))); err == nil && b { errorLocal = true } } if errorLocal { adminURL = "http://127.0.0.1:8083/api/v1/admin" } else { adminURL = "https://errors.tdvorak.dev/api/v1/admin" } } // Prefer env token only token := strings.TrimSpace(os.Getenv("ERROR_REVIEW_ADMIN_TOKEN")) if token == "" { return false } // Only register after setup is complete (club name present) if strings.TrimSpace(s.ClubName) == "" { return false } if strings.HasSuffix(adminURL, "/") { adminURL = strings.TrimRight(adminURL, "/") } base := strings.TrimRight(strings.TrimSpace(s.APIBaseURL), "/") if base == "" { base = strings.TrimRight(config.AppConfig.PublicAPIBaseURL, "/") } if base == "" { // Fallback for local dev errorLocal := false if b, err := strconv.ParseBool(strings.TrimSpace(os.Getenv("ERROR_LOCAL"))); err == nil && b { errorLocal = true } if !errorLocal { if b, err := strconv.ParseBool(strings.TrimSpace(os.Getenv("error_local"))); err == nil && b { errorLocal = true } } if errorLocal { base = "http://127.0.0.1:8080/api/v1" } else { return false } } // Ensure API suffix present if !strings.Contains(base, "/api/") { base = strings.TrimRight(base, "/") + "/api/v1" } monURL := base + "/health" if u, err := url.Parse(monURL); err == nil { h := u.Hostname() if h == "127.0.0.1" || h == "localhost" { u.Host = strings.Replace(u.Host, h, "host.docker.internal", 1) monURL = u.String() } } disp := "Fotbal Club" if strings.TrimSpace(s.ClubName) != "" { disp = strings.TrimSpace(s.ClubName) } host := "" if u, err := url.Parse(monURL); err == nil { host = u.Hostname() } name := disp if host != "" { name = name + " (" + host + ")" } name = name + " API Health" mon := erMonitor{ Name: name, URL: monURL, Enabled: true, CheckIntervalSec: 60, TimeoutMs: 4000, ExpectStatusMin: 200, ExpectStatusMax: 399, } client := &http.Client{Timeout: 5 * time.Second} effectiveAdmin := strings.TrimRight(adminURL, "/") req, _ := http.NewRequest(http.MethodGet, effectiveAdmin+"/monitors", nil) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("X-Admin-Token", token) res, err := client.Do(req) if err != nil || res.StatusCode != http.StatusOK { if res != nil { res.Body.Close() } if u, e := url.Parse(effectiveAdmin); e == nil { h := u.Hostname() if h == "127.0.0.1" || h == "localhost" { u.Host = strings.Replace(u.Host, h, "host.docker.internal", 1) effectiveAdmin = strings.TrimRight(u.String(), "/") req2, _ := http.NewRequest(http.MethodGet, effectiveAdmin+"/monitors", nil) req2.Header.Set("Authorization", "Bearer "+token) req2.Header.Set("X-Admin-Token", token) res2, err2 := client.Do(req2) if err2 != nil || res2.StatusCode != http.StatusOK { if res2 != nil { res2.Body.Close() } return false } defer res2.Body.Close() var list erListResp if err := json.NewDecoder(res2.Body).Decode(&list); err != nil { return false } var existing *erMonitor for i := range list.Items { if strings.TrimSpace(list.Items[i].URL) == monURL { existing = &list.Items[i] break } } b, _ := json.Marshal(mon) if existing == nil { req3, _ := http.NewRequest(http.MethodPost, effectiveAdmin+"/monitors", bytes.NewReader(b)) req3.Header.Set("Authorization", "Bearer "+token) req3.Header.Set("Content-Type", "application/json") req3.Header.Set("X-Admin-Token", token) res3, err3 := client.Do(req3) if err3 != nil { return false } defer res3.Body.Close() if res3.StatusCode == http.StatusCreated || res3.StatusCode == http.StatusOK { return true } return false } req4, _ := http.NewRequest(http.MethodPut, effectiveAdmin+"/monitors/"+itoa(existing.ID), bytes.NewReader(b)) req4.Header.Set("Authorization", "Bearer "+token) req4.Header.Set("Content-Type", "application/json") req4.Header.Set("X-Admin-Token", token) res4, err4 := client.Do(req4) if err4 != nil { return false } defer res4.Body.Close() if res4.StatusCode == http.StatusOK { return true } return false } } return false } var list erListResp if err := json.NewDecoder(res.Body).Decode(&list); err != nil { return false } var existing *erMonitor for i := range list.Items { if strings.TrimSpace(list.Items[i].URL) == monURL { existing = &list.Items[i] break } } b, _ := json.Marshal(mon) if existing == nil { req2, _ := http.NewRequest(http.MethodPost, effectiveAdmin+"/monitors", bytes.NewReader(b)) req2.Header.Set("Authorization", "Bearer "+token) req2.Header.Set("Content-Type", "application/json") req2.Header.Set("X-Admin-Token", token) res2, err2 := client.Do(req2) if err2 != nil { return false } defer res2.Body.Close() if res2.StatusCode == http.StatusCreated || res2.StatusCode == http.StatusOK { return true } return false } req3, _ := http.NewRequest(http.MethodPut, effectiveAdmin+"/monitors/"+itoa(existing.ID), bytes.NewReader(b)) req3.Header.Set("Authorization", "Bearer "+token) req3.Header.Set("Content-Type", "application/json") req3.Header.Set("X-Admin-Token", token) res3, err3 := client.Do(req3) if err3 != nil { return false } defer res3.Body.Close() if res3.StatusCode == http.StatusOK { return true } return false } func itoa(u uint) string { return strconv.FormatUint(uint64(u), 10) }