From a13179ed9b8099f7e0569fe98a8de2c6bc384e3d Mon Sep 17 00:00:00 2001 From: Muse Date: Fri, 23 Oct 2026 15:02:12 +0000 Subject: [PATCH] =?UTF-8?q?v221=20=E2=91=A6=20public=20views:=20node=20nam?= =?UTF-8?q?e=20prefers=20the=20public=20remark?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buildProbeNodeView now sources its 'name' from probeDisplayName: the node's public remark (probe_meta.public_remark) when non-empty after trimming, else the node name as before. Contract field name unchanged, theme packages untouched, both audiences (anonymous and admin frames share the builder); the panel's internal node list is a different surface and keeps the plain node name. Tests: display-name unit cases (remark / empty / whitespace) and view-level assertions for both audiences; the public contract test's seeded expectation updated to the remark (its fixture sets public_remark='shown publicly' on node 'PubNode'). --- internal/web/controller/probe_public_test.go | 4 +- internal/web/service/probe_public.go | 15 ++++++- .../web/service/probe_public_name_test.go | 40 +++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 internal/web/service/probe_public_name_test.go diff --git a/internal/web/controller/probe_public_test.go b/internal/web/controller/probe_public_test.go index a9319fb..183a52b 100644 --- a/internal/web/controller/probe_public_test.go +++ b/internal/web/controller/probe_public_test.go @@ -323,7 +323,9 @@ func TestProbePublicContract(t *testing.T) { assertKeys(t, keySet(t, nodeBody), anonNodeKeys) var node map[string]any _ = json.Unmarshal(nodeBody, &node) - if node["id"] != float64(pubID) || node["name"] != "PubNode" || node["online"] != true || + // V2.2.1 ⑦: the public view's name is the public remark when one + // is set (the seed sets "shown publicly"), not the node name. + if node["id"] != float64(pubID) || node["name"] != "shown publicly" || node["online"] != true || node["month_rx"] != float64(33) || node["month_used"] != float64(77) || node["total_tx"] != float64(222) || node["price"] != float64(10) { t.Fatalf("anon node values wrong: %v", node) diff --git a/internal/web/service/probe_public.go b/internal/web/service/probe_public.go index c361f5a..55cb943 100644 --- a/internal/web/service/probe_public.go +++ b/internal/web/service/probe_public.go @@ -268,6 +268,19 @@ func expiresInDays(expires *string, now time.Time) any { return days } +// probeDisplayName is the name a node carries in the public views +// (V2.2.1 ⑦): the operator's public remark when one is set — it is +// the label written for public consumption — else the node name. The +// contract field stays "name"; themes and the public API keep their +// shape, only the value's source changes. The panel's internal node +// list is a different surface and keeps the plain node name. +func probeDisplayName(name, publicRemark string) string { + if r := strings.TrimSpace(publicRemark); r != "" { + return r + } + return name +} + // buildProbeNodeView renders one node for one audience. The public // shape is built by whitelist (hub node_view): address, hostname and // the private remark exist only in the admin (full) rendering. @@ -298,7 +311,7 @@ func buildProbeNodeView(src probeNodeSource, full bool) map[string]any { view := map[string]any{ "id": a.Id, - "name": src.name, + "name": probeDisplayName(src.name, m.PublicRemark), "country": m.CountryPin, "group": m.GroupName, "public_remark": m.PublicRemark, diff --git a/internal/web/service/probe_public_name_test.go b/internal/web/service/probe_public_name_test.go new file mode 100644 index 0000000..d36cc12 --- /dev/null +++ b/internal/web/service/probe_public_name_test.go @@ -0,0 +1,40 @@ +package service + +import ( + "testing" + + "github.com/mhsanaei/3x-ui/v3/internal/web/service/probe" +) + +// V2.2.1 ⑦: the public views' node name is the operator's public +// remark when one is set, and the node name otherwise — the panel's +// own list (a different surface) is untouched. + +func TestProbeDisplayNamePrefersPublicRemark(t *testing.T) { + if got := probeDisplayName("node-name", "东京 01"); got != "东京 01" { + t.Fatalf("with remark = %q, want the remark", got) + } + if got := probeDisplayName("node-name", ""); got != "node-name" { + t.Fatalf("empty remark = %q, want node name", got) + } + if got := probeDisplayName("node-name", " "); got != "node-name" { + t.Fatalf("whitespace-only remark = %q, want node name", got) + } +} + +func TestBuildProbeNodeViewNameSource(t *testing.T) { + withRemark := probeNodeSource{ + name: "internal-name", + meta: probe.Meta{PublicRemark: "公开名", Public: true}, + } + if got := buildProbeNodeView(withRemark, false)["name"]; got != "公开名" { + t.Fatalf("anonymous view name = %v, want the public remark", got) + } + if got := buildProbeNodeView(withRemark, true)["name"]; got != "公开名" { + t.Fatalf("admin view name = %v, want the public remark", got) + } + without := probeNodeSource{name: "internal-name", meta: probe.Meta{Public: true}} + if got := buildProbeNodeView(without, false)["name"]; got != "internal-name" { + t.Fatalf("view name without remark = %v, want the node name", got) + } +}