package main import ( "encoding/json" "net/http" "strconv" "strings" "time" "hamlog/internal/applog" ) // updateCheckURL is the GitHub Releases "latest" endpoint for the public OpsLog // build (the exe lives there; source stays on Gitea). Adjust the repo if needed. const updateCheckURL = "https://api.github.com/repos/GregTroar/OpsLog/releases/latest" // UpdateInfo is the result of the startup version check. type UpdateInfo struct { Current string `json:"current"` // this build's version (appVersion) Latest string `json:"latest"` // newest published release, "" if unknown Available bool `json:"available"` // Latest > Current URL string `json:"url"` // release page to open } // CheckForUpdate asks GitHub for the latest release and compares it to this // build. Best effort — on any failure it reports "no update" so the app never // nags about a check it couldn't complete. func (a *App) CheckForUpdate() UpdateInfo { out := UpdateInfo{Current: appVersion} client := &http.Client{Timeout: 8 * time.Second} req, err := http.NewRequest(http.MethodGet, updateCheckURL, nil) if err != nil { return out } req.Header.Set("Accept", "application/vnd.github+json") resp, err := client.Do(req) if err != nil { applog.Printf("update: check failed: %v", err) return out } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return out // no release yet (404) or rate-limited — treat as up to date } var r struct { TagName string `json:"tag_name"` HTMLURL string `json:"html_url"` } if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { return out } out.Latest = strings.TrimPrefix(strings.TrimSpace(r.TagName), "v") out.URL = r.HTMLURL out.Available = versionLess(appVersion, out.Latest) if out.Available { applog.Printf("update: newer version available — current=%s latest=%s", appVersion, out.Latest) } return out } // versionLess reports whether version a is older than b. Compares dot-separated // numeric parts ("0.9" < "0.10" < "1.0"); non-numeric junk in a part counts as 0. func versionLess(a, b string) bool { pa := strings.Split(a, ".") pb := strings.Split(b, ".") n := len(pa) if len(pb) > n { n = len(pb) } for i := 0; i < n; i++ { ai, bi := 0, 0 if i < len(pa) { ai = leadingInt(pa[i]) } if i < len(pb) { bi = leadingInt(pb[i]) } if ai != bi { return ai < bi } } return false } // leadingInt parses the leading digits of s (e.g. "2beta" → 2), 0 if none. func leadingInt(s string) int { s = strings.TrimSpace(s) end := 0 for end < len(s) && s[end] >= '0' && s[end] <= '9' { end++ } if end == 0 { return 0 } n, _ := strconv.Atoi(s[:end]) return n }