diff --git a/internal/agent/telemetry.go b/internal/agent/telemetry.go index 087c58c..fe16539 100644 --- a/internal/agent/telemetry.go +++ b/internal/agent/telemetry.go @@ -35,7 +35,9 @@ type telemetryPayload struct { // collectTelemetry samples every source independently; a failing source // only blanks its own field, never the whole report. func (a *Agent) collectTelemetry() *telemetryPayload { - t := &telemetryPayload{AgentVersion: config.GetBaseVersion()} + // Agents report the A-series display version; the master shows it + // verbatim on agent node rows. + t := &telemetryPayload{AgentVersion: config.GetAgentDisplayVersion()} if v, ok := a.cachedXrayVersion(); ok { t.XrayVersion = v } diff --git a/internal/config/config.go b/internal/config/config.go index b9bf4a8..a6d43ee 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -35,6 +35,13 @@ var ( buildCommit string buildDate string buildVersion string + // displayVersion is this fork's own user-facing release number ("1.0.0" + // in "V1.0.0" / "A1.0.0"), injected via `-ldflags -X` by release builds. + // It is deliberately separate from buildVersion (the upstream-shaped + // internal version): display surfaces (panel header, node list, agent + // telemetry) read the V/A display version, while internal comparisons + // and advertisements keep using GetBaseVersion/GetPanelVersion. + displayVersion string ) // LogLevel represents the logging level for the application. @@ -103,6 +110,96 @@ func GetPanelVersion() string { return "dev+" + commit } +// defaultDisplayVersion is the display version reported when a build does +// not inject one (plain `go build` / `go run`). +const defaultDisplayVersion = "1.0.0" + +// GetDisplayVersion returns the bare numeric display version ("1.0.0"): +// the injected display version when the build provided one, else the +// default. Display versions advance in single-digit carry steps +// (1.0.9 -> 1.1.0 -> ... -> 1.9.9 -> 2.0.0); each segment stays 0-9. +func GetDisplayVersion() string { + if v := strings.TrimSpace(displayVersion); v != "" { + return v + } + return defaultDisplayVersion +} + +// GetMasterDisplayVersion is the master's display version ("V1.0.0"), +// shown in the panel's version slot. +func GetMasterDisplayVersion() string { + return "V" + GetDisplayVersion() +} + +// GetAgentDisplayVersion is the agent's display version ("A1.0.0"), +// reported in agent telemetry and shown on agent node rows. +func GetAgentDisplayVersion() string { + return "A" + GetDisplayVersion() +} + +// ParseDisplayVersion parses a display version into its three numeric +// segments. It accepts the bare form ("1.0.0") and the letter-prefixed +// display forms ("V1.0.0", "A1.0.0", "v1.0.0"). Anything else — dev labels, +// suffixed builds like "3.7.0-agent.1", wrong segment counts, non-numeric +// segments — reports ok=false so callers can fall back instead of guessing. +func ParseDisplayVersion(s string) (parts [3]int, ok bool) { + s = strings.TrimSpace(s) + if parsed, good := parseDisplayParts(s); good { + return parsed, true + } + // Retry once without a single leading display letter (V/A, any case). + if len(s) > 1 && (s[0] == 'V' || s[0] == 'v' || s[0] == 'A' || s[0] == 'a') { + if parsed, good := parseDisplayParts(s[1:]); good { + return parsed, true + } + } + return [3]int{}, false +} + +func parseDisplayParts(s string) ([3]int, bool) { + var parts [3]int + segs := strings.Split(s, ".") + if len(segs) != 3 { + return parts, false + } + for i, seg := range segs { + if seg == "" { + return parts, false + } + for _, r := range seg { + if r < '0' || r > '9' { + return parts, false + } + } + n, err := strconv.Atoi(seg) + if err != nil { + return parts, false + } + parts[i] = n + } + return parts, true +} + +// CompareDisplayVersion compares two display versions segment by segment +// (numeric, so the single-digit carry rule 1.0.9 < 1.1.0 holds naturally). +// ok=false when either side is not a parseable display version. +func CompareDisplayVersion(a, b string) (cmp int, ok bool) { + pa, okA := ParseDisplayVersion(a) + pb, okB := ParseDisplayVersion(b) + if !okA || !okB { + return 0, false + } + for i := range 3 { + if pa[i] != pb[i] { + if pa[i] < pb[i] { + return -1, true + } + return 1, true + } + } + return 0, true +} + // GetLogLevel returns the current logging level based on environment variables or defaults to Info. func GetLogLevel() LogLevel { if IsDebug() { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index f31820f..34bd017 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -53,6 +53,78 @@ func TestGetBaseVersionBuildVersionOverride(t *testing.T) { } } +func TestDisplayVersionDefaultsAndOverride(t *testing.T) { + orig := displayVersion + t.Cleanup(func() { displayVersion = orig }) + + displayVersion = "" + if got := GetDisplayVersion(); got != "1.0.0" { + t.Fatalf("default display version = %q, want 1.0.0", got) + } + if got := GetMasterDisplayVersion(); got != "V1.0.0" { + t.Fatalf("default master display version = %q, want V1.0.0", got) + } + if got := GetAgentDisplayVersion(); got != "A1.0.0" { + t.Fatalf("default agent display version = %q, want A1.0.0", got) + } + + displayVersion = "1.0.1" + if got := GetMasterDisplayVersion(); got != "V1.0.1" { + t.Fatalf("injected master display version = %q, want V1.0.1", got) + } + if got := GetAgentDisplayVersion(); got != "A1.0.1" { + t.Fatalf("injected agent display version = %q, want A1.0.1", got) + } +} + +func TestParseDisplayVersion(t *testing.T) { + valid := map[string][3]int{ + "1.0.0": {1, 0, 0}, + "V1.0.0": {1, 0, 0}, + "A1.0.9": {1, 0, 9}, + "v2.0.0": {2, 0, 0}, + " 1.2.3 ": {1, 2, 3}, + } + for in, want := range valid { + got, ok := ParseDisplayVersion(in) + if !ok || got != want { + t.Fatalf("ParseDisplayVersion(%q) = %v, %v; want %v, true", in, got, ok, want) + } + } + invalid := []string{"", "3.7.0-agent.1", "dev+1a2b3c4d", "1.0", "1.0.0.1", "1.x.0", "V", "1..0"} + for _, in := range invalid { + if _, ok := ParseDisplayVersion(in); ok { + t.Fatalf("ParseDisplayVersion(%q) ok=true, want false", in) + } + } +} + +func TestCompareDisplayVersion(t *testing.T) { + cases := []struct { + a, b string + want int + }{ + {"1.0.0", "1.0.0", 0}, + {"V1.0.0", "A1.0.0", 0}, // prefix letters are flavour, not value + {"1.0.9", "1.1.0", -1}, // single-digit carry step + {"1.1.0", "1.0.9", 1}, // carry compares numerically, not lexically + {"1.9.9", "2.0.0", -1}, // full carry + {"A1.0.1", "A1.0.0", 1}, // agent series + {"V1.0.1", "1.0.0", 1}, // release tag vs current + {"2.0.0", "1.9.9", 1}, + {"1.0.0", "1.0.1", -1}, + } + for _, tc := range cases { + got, ok := CompareDisplayVersion(tc.a, tc.b) + if !ok || got != tc.want { + t.Fatalf("CompareDisplayVersion(%q, %q) = %d, %v; want %d, true", tc.a, tc.b, got, ok, tc.want) + } + } + if _, ok := CompareDisplayVersion("3.7.0-agent.1", "1.0.0"); ok { + t.Fatal("unparseable side must report ok=false") + } +} + func TestGetPortOverride(t *testing.T) { tests := []struct { name string diff --git a/internal/web/controller/dist.go b/internal/web/controller/dist.go index 67e9ff2..e188d10 100644 --- a/internal/web/controller/dist.go +++ b/internal/web/controller/dist.go @@ -86,7 +86,9 @@ func serveDistPage(c *gin.Context, name string) { } script := `