diff --git a/frontend-modern/src/utils/__tests__/tagColors.test.ts b/frontend-modern/src/utils/__tests__/tagColors.test.ts index 7e4e4103f..54a4584f5 100644 --- a/frontend-modern/src/utils/__tests__/tagColors.test.ts +++ b/frontend-modern/src/utils/__tests__/tagColors.test.ts @@ -48,6 +48,18 @@ describe('tagColors', () => { expect(colorA.bg).not.toBe(colorB.bg); }); + it('prefers proxmox-supplied colors over fallback palettes', () => { + const color = getTagColorWithSpecial('Production', false, { + production: '#112233', + }); + + expect(color).toEqual({ + bg: '#112233', + text: '#ffffff', + border: '#112233', + }); + }); + it('generates dark mode hash-based colors', () => { const color = getTagColorWithSpecial('custom', true); diff --git a/pkg/proxmox/client.go b/pkg/proxmox/client.go index 9c12ad978..813ec191b 100644 --- a/pkg/proxmox/client.go +++ b/pkg/proxmox/client.go @@ -1821,15 +1821,42 @@ func ParseTagColorMap(tagStyle string) map[string]string { continue } for _, pair := range strings.Split(strings.TrimPrefix(part, "color-map="), ";") { - kv := strings.SplitN(pair, ":", 2) - if len(kv) == 2 && len(kv[1]) == 6 { - colors[strings.ToLower(strings.TrimSpace(kv[0]))] = "#" + kv[1] + fields := strings.Split(pair, ":") + if len(fields) < 2 { + continue } + tag := strings.ToLower(strings.TrimSpace(fields[0])) + hex := strings.TrimSpace(fields[1]) + hex = strings.TrimPrefix(hex, "#") + if tag == "" || !isHexColorToken(hex) { + continue + } + colors[tag] = "#" + strings.ToLower(hex) } } return colors } +func isHexColorToken(value string) bool { + switch len(value) { + case 3, 6, 8: + default: + return false + } + + for _, r := range value { + switch { + case r >= '0' && r <= '9': + case r >= 'a' && r <= 'f': + case r >= 'A' && r <= 'F': + default: + return false + } + } + + return true +} + // ZFSPoolStatus represents the status of a ZFS pool (list endpoint) type ZFSPoolStatus struct { Name string `json:"name"` diff --git a/pkg/proxmox/client_test.go b/pkg/proxmox/client_test.go index 0f4b50f89..67fbe32a6 100644 --- a/pkg/proxmox/client_test.go +++ b/pkg/proxmox/client_test.go @@ -7,6 +7,7 @@ import ( "math" "net/http" "net/http/httptest" + "reflect" "strings" "testing" "time" @@ -146,6 +147,46 @@ func TestDiskUnmarshalRPM(t *testing.T) { } } +func TestParseTagColorMap(t *testing.T) { + tests := []struct { + name string + tagStyle string + expected map[string]string + }{ + { + name: "parses documented proxmox background and text color format", + tagStyle: "color-map=Production:000000:FFFFFF;staging:ffaa00:101010,ordering=config", + expected: map[string]string{ + "production": "#000000", + "staging": "#ffaa00", + }, + }, + { + name: "parses legacy single-color entries with leading hash", + tagStyle: "ordering=config,color-map=backup:#ABCDEF;ops:123456", + expected: map[string]string{ + "backup": "#abcdef", + "ops": "#123456", + }, + }, + { + name: "ignores invalid color tokens", + tagStyle: "color-map=good:00ff00;bad:zzzzzz;also-bad:12345", + expected: map[string]string{ + "good": "#00ff00", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if got := ParseTagColorMap(tc.tagStyle); !reflect.DeepEqual(got, tc.expected) { + t.Fatalf("ParseTagColorMap() = %#v, want %#v", got, tc.expected) + } + }) + } +} + func TestDiskUnmarshalJSON_InvalidJSON(t *testing.T) { var disk Disk err := json.Unmarshal([]byte(`{invalid json`), &disk)