diff --git a/src/internal/mgr/group.go b/src/internal/mgr/group.go index 6b63a3e..d5c36cf 100644 --- a/src/internal/mgr/group.go +++ b/src/internal/mgr/group.go @@ -3,6 +3,7 @@ package mgr import ( "errors" "regexp" + "strconv" "strings" "vpsmgr/internal/db" @@ -19,6 +20,15 @@ func UserGroupLabel(name string) string { return name[len(g.Parent)+1:] } +// MachineSpecs renders a compact quota tag for the container switcher: +// "cgg", e.g. "4c8g40g". CPU reuses FormatCPU (tenths of a +// core, so 0.5 stays fractional); memory is MiB rendered as GiB with a trimmed +// decimal (1024 -> "1", 512 -> "0.5"); disk is already GiB. +func MachineSpecs(cpuTenths, memMB, diskGB int) string { + mem := strconv.FormatFloat(float64(memMB)/1024, 'f', -1, 64) + return FormatCPU(cpuTenths) + "c" + mem + "g" + strconv.Itoa(diskGB) + "g" +} + type UserGroup struct { Name string Parent string diff --git a/src/internal/mgr/group_test.go b/src/internal/mgr/group_test.go index 58f4d55..e187937 100644 --- a/src/internal/mgr/group_test.go +++ b/src/internal/mgr/group_test.go @@ -42,6 +42,22 @@ func TestUserGroupLabel(t *testing.T) { } } +func TestMachineSpecs(t *testing.T) { + for _, test := range []struct { + cpu, mem, disk int + want string + }{ + {10, 1024, 10, "1c1g10g"}, + {40, 8192, 40, "4c8g40g"}, + {5, 512, 10, "0.5c0.5g10g"}, + {20, 1536, 20, "2c1.5g20g"}, + } { + if got := MachineSpecs(test.cpu, test.mem, test.disk); got != test.want { + t.Errorf("MachineSpecs(%d, %d, %d) = %q, want %q", test.cpu, test.mem, test.disk, got, test.want) + } + } +} + func TestUserGroupName(t *testing.T) { if got := UserGroupName("alice-1"); got != "alice" { t.Fatalf("UserGroupName(alice-1) = %q, want alice", got) diff --git a/src/internal/panel/panel.go b/src/internal/panel/panel.go index 4dbb470..7798946 100644 --- a/src/internal/panel/panel.go +++ b/src/internal/panel/panel.go @@ -6,6 +6,7 @@ import ( "net" "net/http" "sort" + "strconv" "strings" "sync" "time" @@ -127,10 +128,18 @@ type sshKeyRow struct { Active bool `json:"active"` } +// groupMemberRow is one container in the user's group switcher. Label and Specs +// are precomputed because the template cannot call mgr helpers or convert MiB. +type groupMemberRow struct { + Name string // account name, used as the ", ">" + child + ""} { + if !strings.Contains(body, want) { + t.Errorf("overview switcher missing %q", want) + } + } + if i, j := strings.Index(body, base), strings.Index(body, child); i < 0 || j < 0 || i > j { + t.Errorf("switcher order: base should precede child (base at %d, child at %d)", i, j) + } + if !strings.Contains(body, "Machine A") { + t.Error("notes card should tag the current machine on a multi-container group") + } + + // A lone container is not a group: no switcher, no machine tag. + if _, err := d.CreateUser("bob", hash, "10.42.0.4", 3, 30003, 10200, 10, 1024, 10); err != nil { + t.Fatal(err) + } + cookie = loginAndCookie(t, h, prefix, "bob", "pw") + body = doReq(t, h, http.MethodGet, prefix, nil, cookie).Body.String() + if strings.Contains(body, "Machine A") { + t.Error("single-container user should not show a machine tag") + } + if strings.Contains(body, "switch-user") { + t.Error("single-container user should not render the switcher") + } +} + // TestStatsEndpoint verifies the /stats JSON endpoint: it requires auth, // returns per-minute points for the current user, and computes per-minute // bandwidth as the delta of the cumulative rx/tx counters between samples. diff --git a/src/internal/panel/templates/overview.html b/src/internal/panel/templates/overview.html index 9ae1dda..7bd8e0f 100644 --- a/src/internal/panel/templates/overview.html +++ b/src/internal/panel/templates/overview.html @@ -234,7 +234,7 @@ footer a{color:var(--muted);text-decoration:underline} {{if .Err}}
{{.Err}}
{{end}} {{if gt .GroupCount 1}} -
⚠ {{if eq .Lang "zh"}}您的用户组内有多台机器,当前机器序号为{{else}}Your user group has multiple containers. Current container number:{{end}} {{.GroupIndex}}。{{if eq .Lang "zh"}}请确认您操作的机器无误。{{else}}Confirm this is the intended container before operating.{{end}}{{if gt (len .GroupUsers) 1}}
{{end}}
+
⚠ {{if eq .Lang "zh"}}您的用户组内有多台机器,当前机器序号为{{else}}Your user group has multiple containers. Current container number:{{end}} {{.GroupIndex}}。{{if eq .Lang "zh"}}请确认您操作的机器无误。{{else}}Confirm this is the intended container before operating.{{end}}{{if gt (len .GroupUsers) 1}}
{{end}}
{{end}} {{if .Impersonated}}
@@ -319,7 +319,7 @@ footer a{color:var(--muted);text-decoration:underline}
-

{{if eq .Lang "zh"}}便利贴{{else}}Sticky notes{{end}}

+

{{if eq .Lang "zh"}}便利贴{{else}}Sticky notes{{end}}{{if gt .GroupCount 1}} · {{if eq .Lang "zh"}}机器{{else}}Machine{{end}} {{.GroupIndex}}{{end}}