aboutsummaryrefslogtreecommitdiff
path: root/internal/admin/api_colors.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/admin/api_colors.go')
-rw-r--r--internal/admin/api_colors.go373
1 files changed, 289 insertions, 84 deletions
diff --git a/internal/admin/api_colors.go b/internal/admin/api_colors.go
index 882a096..00caa98 100644
--- a/internal/admin/api_colors.go
+++ b/internal/admin/api_colors.go
@@ -10,6 +10,7 @@ import (
"thehouseoficarus/internal/color"
"thehouseoficarus/internal/config"
+ "thehouseoficarus/internal/game"
)
// colorCategory describes one editable color category: its config key, a
@@ -38,77 +39,132 @@ type colorSection struct {
Lines []colorLine `json:"lines"`
}
-// colorCategories is the curated, grouped, ordered master list mirroring the
-// keys produced by config.DefaultColors(). Editing here is the single source
-// of truth for the admin color themer's left-side list.
-var colorCategories = []colorCategory{
- // Room / look
- {Name: "room_name", Label: "Room Name", Group: "Room"},
- {Name: "room_number", Label: "Room Number", Group: "Room"},
- {Name: "room_desc", Label: "Room Description", Group: "Room"},
- {Name: "exit_direction", Label: "Exit Direction", Group: "Room"},
- {Name: "exit_name", Label: "Exit Name", Group: "Room"},
- {Name: "mob", Label: "Mob", Group: "Room"},
- {Name: "protected_mob", Label: "Protected Mob", Group: "Room"},
- {Name: "player_name", Label: "Player Name", Group: "Room"},
- {Name: "item", Label: "Item", Group: "Room"},
- {Name: "dim", Label: "Dim / Brackets", Group: "Room"},
-
- // Movement
- {Name: "direction", Label: "Move Direction", Group: "Movement"},
-
- // Combat
- {Name: "damage_dealt", Label: "Damage Dealt", Group: "Combat"},
- {Name: "damage_taken", Label: "Damage Taken", Group: "Combat"},
- {Name: "miss", Label: "Miss", Group: "Combat"},
- {Name: "death", Label: "Death", Group: "Combat"},
- {Name: "victory", Label: "Victory", Group: "Combat"},
- {Name: "warning", Label: "Warning", Group: "Combat"},
- {Name: "error", Label: "Error", Group: "Combat"},
- {Name: "science_mod", Label: "Science Mod", Group: "Combat"},
-
- // XP / level
- {Name: "xp", Label: "XP Drop", Group: "XP / Level"},
- {Name: "level_up", Label: "Level Up", Group: "XP / Level"},
-
- // Pickups / economy
- {Name: "currency_pickup", Label: "Currency Pickup", Group: "Pickups / Economy"},
-
- // Chat / dialog
- {Name: "say", Label: "Say", Group: "Chat / Dialog"},
- {Name: "global", Label: "Global", Group: "Chat / Dialog"},
- {Name: "broadcast", Label: "Broadcast", Group: "Chat / Dialog"},
- {Name: "dialog", Label: "Dialog", Group: "Chat / Dialog"},
- {Name: "dialog_options", Label: "Dialog Options", Group: "Chat / Dialog"},
-
- // Drops / consume / fire
- {Name: "drop_message", Label: "Drop Message", Group: "Drops / Consume / Fire"},
- {Name: "eat_food", Label: "Eat Food", Group: "Drops / Consume / Fire"},
- {Name: "fire", Label: "Fire", Group: "Drops / Consume / Fire"},
-
- // Tick / status
- {Name: "visual_tick", Label: "Visual Tick", Group: "Tick / Status"},
- {Name: "visual_first_tick", Label: "Visual First Tick", Group: "Tick / Status"},
-
- // Tech / farm
- {Name: "battery", Label: "Battery", Group: "Tech / Farm"},
- {Name: "tech_depleted", Label: "Tech Depleted", Group: "Tech / Farm"},
- {Name: "farm_grow", Label: "Farm Grow", Group: "Tech / Farm"},
- {Name: "farm_disease", Label: "Farm Disease", Group: "Tech / Farm"},
-
- // Assassin / map / safespot
- {Name: "assassin_task", Label: "Assassin Task", Group: "Assassin / Map / Safespot"},
- {Name: "map_at", Label: "Map At (you)", Group: "Assassin / Map / Safespot"},
- {Name: "map_blocked", Label: "Map Blocked", Group: "Assassin / Map / Safespot"},
- {Name: "map_agility", Label: "Map Agility", Group: "Assassin / Map / Safespot"},
- {Name: "safespot_alert", Label: "Safespot Alert", Group: "Assassin / Map / Safespot"},
-
- // System
- {Name: "sequence", Label: "Sequence (trigger)", Group: "System"},
+// playerColorLabels maps each player-overridable color category name to the
+// human-friendly label shown in the admin GUI. Order comes from the shared
+// game.PlayerColorCategoryOrder() so the admin editor always mirrors the
+// in-game `colors` command output exactly.
+var playerColorLabels = map[string]string{
+ "room_name": "Room Name",
+ "room_number": "Room Number",
+ "room_desc": "Room Description",
+ "exit_direction": "Exit Direction",
+ "exit_name": "Exit Name",
+ "dim": "Dim / Brackets",
+ "mob": "Mob",
+ "protected_mob": "Protected Mob",
+ "player_name": "Player Name",
+ "item": "Item",
+ "direction": "Move Direction",
+ "damage_dealt": "Damage Dealt",
+ "damage_taken": "Damage Taken",
+ "miss": "Miss",
+ "death": "Death",
+ "victory": "Victory",
+ "warning": "Warning",
+ "error": "Error",
+ "science_mod": "Science Mod",
+ "xp": "XP Drop",
+ "level_up": "Level Up",
+ "currency_pickup": "Currency Pickup",
+ "say": "Say",
+ "global": "Global",
+ "broadcast": "Broadcast",
+ "dialog": "Dialog",
+ "dialog_options": "Dialog Options",
+ "drop_message": "Drop Message",
+ "eat_food": "Eat Food",
+ "fire": "Fire",
+ "visual_tick": "Visual Tick",
+ "visual_first_tick": "Visual First Tick",
+ "battery": "Battery",
+ "tech_depleted": "Tech Depleted",
+ "farm_grow": "Farm Grow",
+ "farm_disease": "Farm Disease",
+ "assassin_task": "Assassin Task",
+ "map_at": "Map At (you)",
+ "map_blocked": "Map Blocked",
+ "map_agility": "Map Agility",
+ "safespot_alert": "Safespot Alert",
+ "sequence": "Sequence (trigger)",
}
-// knownColor returns true if name is one of the editable categories.
-func knownColor(name string) bool {
+// colorCategories is the ordered list of player-overridable color categories
+// shown in the admin GUI. Built from game.PlayerColorCategoryOrder() so it
+// stays in lock-step with the in-game `colors` command output. Group is left
+// empty: the editor renders one flat list under the "Player Colors" heading.
+var colorCategories = buildPlayerCategories()
+
+func buildPlayerCategories() []colorCategory {
+ order := game.PlayerColorCategoryOrder()
+ out := make([]colorCategory, 0, len(order))
+ for _, name := range order {
+ label := playerColorLabels[name]
+ if label == "" {
+ label = name
+ }
+ out = append(out, colorCategory{Name: name, Label: label})
+ }
+ return out
+}
+
+// constantCategories is the ordered list of constant (player-invisible) color
+// categories exposed in the admin GUI under the "Constants" heading. Order is
+// family-grouped for skimmability; Group is currently unused by the client.
+var constantCategories = []colorCategory{
+ // HP / status bars
+ {Name: "hp_bar_high", Label: "HP Bar — High (>=70%)", Group: "Bars"},
+ {Name: "hp_bar_med", Label: "HP Bar — Medium (>=40%)", Group: "Bars"},
+ {Name: "hp_bar_low", Label: "HP Bar — Low (<40%)", Group: "Bars"},
+ {Name: "hp_bar_empty", Label: "HP Bar — Empty Cells", Group: "Bars"},
+ {Name: "task_bar_high", Label: "Task Bar — High (>=70%)", Group: "Bars"},
+ {Name: "task_bar_med", Label: "Task Bar — Medium (>=40%)", Group: "Bars"},
+ {Name: "task_bar_low", Label: "Task Bar — Low (<40%)", Group: "Bars"},
+ {Name: "battery_bar", Label: "Battery Bar Fill", Group: "Bars"},
+ {Name: "run_energy_bar", Label: "Run Energy Bar Fill", Group: "Bars"},
+
+ // Skill-level tier color on the score panel
+ {Name: "skill_lvl_max", Label: "Skill Level Color — 99", Group: "Skills"},
+ {Name: "skill_lvl_high", Label: "Skill Level Color — 80+", Group: "Skills"},
+ {Name: "skill_lvl_mid", Label: "Skill Level Color — 60+", Group: "Skills"},
+ {Name: "skill_lvl_low", Label: "Skill Level Color — 40+", Group: "Skills"},
+ {Name: "skill_lvl_base", Label: "Skill Level Color — base", Group: "Skills"},
+
+ // Level-difference bands
+ {Name: "level_diff_equal", Label: "Level Diff — Equal", Group: "Level Diff"},
+ {Name: "level_diff_up_small", Label: "Level Diff — Slightly Higher (+1..+4)", Group: "Level Diff"},
+ {Name: "level_diff_up_big", Label: "Level Diff — Much Higher (>=+5)", Group: "Level Diff"},
+ {Name: "level_diff_down_small", Label: "Level Diff — Slightly Lower (-1..-4)", Group: "Level Diff"},
+ {Name: "level_diff_down_big", Label: "Level Diff — Much Lower (<=-5)", Group: "Level Diff"},
+
+ // Tech list status / labels
+ {Name: "tech_status_on", Label: "Tech Status — ON", Group: "Tech"},
+ {Name: "tech_status_off", Label: "Tech Status — off", Group: "Tech"},
+ {Name: "tech_status_locked", Label: "Tech Status — locked", Group: "Tech"},
+ {Name: "tech_name_unlocked", Label: "Tech Name — unlocked", Group: "Tech"},
+ {Name: "tech_name_locked", Label: "Tech Name — locked", Group: "Tech"},
+ {Name: "active_tech_on", Label: "Active Tech [ON] tag", Group: "Tech"},
+ {Name: "active_tech_status", Label: "Active Buff [BUFF] tag", Group: "Tech"},
+ {Name: "active_tech_stat", Label: "Tech / Buff Stat Name", Group: "Tech"},
+ {Name: "score_max_value", Label: "Score Panel Max Value", Group: "Tech"},
+
+ // Shared table-heading chrome
+ {Name: "table_header_label", Label: "Table Header — Label", Group: "Tables"},
+ {Name: "table_header_value", Label: "Table Header — Value", Group: "Tables"},
+ {Name: "table_header_detail", Label: "Table Header — Detail", Group: "Tables"},
+ {Name: "table_header_xp", Label: "Table Header — XP / Cost", Group: "Tables"},
+ {Name: "table_header_desc", Label: "Table Header — Description", Group: "Tables"},
+ {Name: "production_locked", Label: "Production Menu — Locked Row", Group: "Tables"},
+
+ // APS / look inline accents
+ {Name: "aps_here", Label: "APS Datapad — \"here\"", Group: "Accents"},
+ {Name: "mob_hp_inline", Label: "Mob Inline HP", Group: "Accents"},
+ {Name: "protected_label", Label: "Mob \"(protected)\" tag", Group: "Accents"},
+ {Name: "god_tag_label", Label: "God-mode [LOCAL]/[GLOBAL]/(HIDDEN)", Group: "Accents"},
+}
+
+// knownPlayerColor returns true if name is one of the player-overridable
+// categories.
+func knownPlayerColor(name string) bool {
for _, c := range colorCategories {
if c.Name == name {
return true
@@ -117,6 +173,22 @@ func knownColor(name string) bool {
return false
}
+// knownConstantColor returns true if name is one of the constant
+// (admin-only) color categories.
+func knownConstantColor(name string) bool {
+ for _, c := range constantCategories {
+ if c.Name == name {
+ return true
+ }
+ }
+ return false
+}
+
+// knownColor returns true if name is editable in either section.
+func knownColor(name string) bool {
+ return knownPlayerColor(name) || knownConstantColor(name)
+}
+
// seg is a small constructor helper for the inline preview builder.
func seg(t, cat string) colorSegment {
return colorSegment{T: t, C: cat}
@@ -157,12 +229,12 @@ func buildPreview() []colorSection {
seg("║", "dim"), seg("■", ""), seg("-", ""), seg("@", "map_at"), seg("-", ""), seg("■", ""), seg("║", "dim")),
ln(seg("Crassius Rex", "protected_mob"), seg(" watches the gate. ", ""),
seg("║", "dim"), seg(" ", ""), seg("│", "map_agility"), seg(" ", ""), seg("║", "dim")),
- ln(seg("A man ", "mob"), seg("(level 5)", "victory"), seg(" scribbles something in a small notebook. ", ""),
+ ln(seg("A man ", "mob"), seg("(level 5)", "level_diff_down_big"), seg(" scribbles something in a small notebook. ", ""),
seg("║", "dim"), seg(" ", ""), seg("■", ""), seg(" ", ""), seg("║", "dim")),
ln(seg(" ", ""),
seg("╚═════╝", "dim")),
txt(""),
- ln(seg("Dalinar", "player_name"), seg(" (level 123)", "error"), seg(" is here.", "")),
+ ln(seg("Dalinar", "player_name"), seg(" (level 123)", "level_diff_equal"), seg(" is here.", "")),
txt(""),
txt("On the ground:"),
ln(seg(" bones ", "item"), seg("(reserved for Dalinar 65t)", "dim")),
@@ -191,19 +263,19 @@ func buildPreview() []colorSection {
ln(seg("You attack ", ""), seg("the man", "mob"), seg("! Style: accurate (+3 atk)", "")),
ln(seg("You hit ", ""), seg("the man", "mob"), seg(" for ", ""),
seg("\U0001FB38 1 \U0001FB34 damage. ", "damage_dealt"),
- seg("[█████████░]", "victory"), seg(" [ 6/7]", ""),
+ seg("[", ""), seg("█████████", "hp_bar_high"), seg("░", "hp_bar_empty"), seg("]", ""), seg(" [ 6/7]", ""),
seg(" (+18xp acc, +6xp hp)", "xp")),
ln(seg("The man misses you.", "miss")),
ln(seg("You hit ", ""), seg("the man", "mob"), seg(" for ", ""),
seg("\U0001FB38 6 \U0001FB34 damage. ", "damage_dealt"),
- seg("[░░░░░░░░░░]", "error"), seg(" [ 0/7]", ""),
+ seg("[", ""), seg("░░░░░░░░░░", "hp_bar_empty"), seg("]", ""), seg(" [ 0/7]", ""),
seg(" (+18xp acc, +6xp hp)", "xp")),
ln(seg("You have defeated ", ""), seg("the man", "mob"), seg("!", "victory")),
ln(seg("*** You are now level 12 accuracy! ***", "level_up")),
txt(""),
ln(seg("The man", "mob"), seg(" hits you for ", ""),
seg("\U0001FB38 5 \U0001FB34 damage. ", "damage_taken"),
- seg("[██████░░░░]", "warning"), seg(" [22/40]", "")),
+ seg("[", ""), seg("██████", "hp_bar_med"), seg("░░░░", "hp_bar_empty"), seg("]", ""), seg(" [22/40]", "")),
ln(seg("Oh dear, you are dead!", "death")),
txt(""),
txt(""),
@@ -268,6 +340,87 @@ func buildPreview() []colorSection {
},
})
+ // --- Constants (player-invisible colors) ----------------------------
+ sections = append(sections, colorSection{
+ Title: "Constants (admin-only)",
+ Lines: []colorLine{
+ txt(""),
+ ln(seg("HP bar high ", ""), seg("[", ""), seg("██████", "hp_bar_high"), seg("░░░░", "hp_bar_empty"), seg("]", "")),
+ ln(seg("HP bar med ", ""), seg("[", ""), seg("████", "hp_bar_med"), seg("░░░░░░", "hp_bar_empty"), seg("]", "")),
+ ln(seg("HP bar low ", ""), seg("[", ""), seg("██", "hp_bar_low"), seg("░░░░░░░░", "hp_bar_empty"), seg("]", "")),
+ ln(seg("HP bar empty", ""), seg("[", ""), seg("░░░░░░░░░░", "hp_bar_empty"), seg("]", "")),
+ ln(seg("Task bar high", ""), seg("[", ""), seg("██████", "task_bar_high"), seg("░░░░", "hp_bar_empty"), seg("]", "")),
+ ln(seg("Task bar med ", ""), seg("[", ""), seg("████", "task_bar_med"), seg("░░░░░░", "hp_bar_empty"), seg("]", "")),
+ ln(seg("Task bar low ", ""), seg("[", ""), seg("██", "task_bar_low"), seg("░░░░░░░░", "hp_bar_empty"), seg("]", "")),
+ ln(seg("BAT ", ""), seg("[", ""), seg("██████", "battery_bar"), seg("░░░░░░░░░░░░░░░░░░░░", ""), seg("] corbat", "")),
+ ln(seg("ENG ", ""), seg("[", ""), seg("█████████████", "run_energy_bar"), seg("░░░░░░░░░░░░", "hp_bar_empty"), seg("]", "")),
+ txt(""),
+ // Constants composed with player colors, as they appear in real
+ // combat output. Lets the admin see a hp_bar_med / hp_bar_low
+ // change reflected immediately in a realistic damage line.
+ ln(seg("You hit ", ""), seg("the man", "mob"), seg(" for ", ""),
+ seg("\U0001FB38 5 \U0001FB34 damage. ", "damage_dealt"),
+ seg("[", ""), seg("█████", "hp_bar_med"), seg("░░░░░", "hp_bar_empty"), seg("]", ""), seg(" [ 5/7]", "")),
+ ln(seg("The man", "mob"), seg(" hits you for ", ""),
+ seg("\U0001FB38 8 \U0001FB34 damage. ", "damage_taken"),
+ seg("[", ""), seg("██", "hp_bar_low"), seg("░░░░░░░░", "hp_bar_empty"), seg("]", ""), seg(" [ 3/40]", "")),
+ ln(seg("Task advance: ", ""),
+ seg("You advance the work on ", ""),
+ seg("the ore seam", "mob"), seg(". ", ""),
+ seg("[", ""), seg("██████", "task_bar_high"), seg("░░░░", "hp_bar_empty"), seg("]", ""), seg(" 60%", "")),
+ txt(""),
+ ln(seg("Skill levels: ", ""),
+ seg("99", "skill_lvl_max"), seg(" ", ""),
+ seg("85", "skill_lvl_high"), seg(" ", ""),
+ seg("63", "skill_lvl_mid"), seg(" ", ""),
+ seg("42", "skill_lvl_low"), seg(" ", ""),
+ seg("12", "skill_lvl_base")),
+ txt(""),
+ ln(seg("Level diff: ", ""),
+ seg("(level 10)", "level_diff_equal"), seg(" vs you 10", ""),
+ seg(" (level 13)", "level_diff_up_small"), seg(" vs you 10", ""),
+ seg(" (level 20)", "level_diff_up_big")),
+ ln(seg(" ", ""),
+ seg("(level 7)", "level_diff_down_small"), seg(" vs you 10", ""),
+ seg(" (level 3)", "level_diff_down_big"), seg(" vs you 10", "")),
+ txt(""),
+ ln(seg("Technology: ", ""),
+ seg("Buff Helper ", "tech_name_unlocked"),
+ seg("ON", "tech_status_on"), seg(" ", ""),
+ seg("Empty Tech ", "tech_name_locked"),
+ seg("locked", "tech_status_locked")),
+ ln(seg(" ", ""),
+ seg("Off Tech ", "tech_name_unlocked"),
+ seg("off", "tech_status_off")),
+ ln(seg("Active: ", ""),
+ seg("[ON] ", "active_tech_on"),
+ seg("BullsEye ", "active_tech_stat"), seg("[BUFF] ", "active_tech_status"), seg("Strength", "active_tech_stat")),
+ ln(seg("Max values: ", ""),
+ seg("Combat Lvl 99 / HP 40 / BAT 100 / ENG 100", "score_max_value")),
+ txt(""),
+ ln(seg("Skill table", ""), seg(" (", "table_header_label"), seg("table_header", "table_header_label"), seg(" ", ""), seg("header", "table_header_label"), seg(") ", ""),
+ seg("Skill ", "table_header_label"), seg("Abbr ", "table_header_detail"), seg("Level ", "table_header_value"), seg("XP ", "table_header_xp"), seg("XP to Next", "table_header_xp")),
+ ln(seg("Buff table ", ""),
+ seg("Option ", "table_header_label"), seg("Value ", "table_header_value"), seg("Valid ", "table_header_detail"), seg("Description", "table_header_desc")),
+ ln(seg("Production: ", ""),
+ seg("bronze axe ", "production_locked"), seg("Lv 3 ", "production_locked"), seg("(missing materials)", "production_locked")),
+ txt(""),
+ ln(seg("Mob inline HP: ", ""),
+ seg("a scout", "mob"), seg(" [", ""), seg("3", "mob_hp_inline"), seg("/7hp]", "")),
+ ln(seg("Protected mob: ", ""),
+ seg("Dragon", "protected_mob"), seg(" ", ""), seg("(protected)", "protected_label")),
+ ln(seg("God-mode tags: ", ""),
+ seg("[LOCAL]", "god_tag_label"), seg(" ", ""),
+ seg("[GLOBAL]", "god_tag_label"), seg(" ", ""),
+ seg("(HIDDEN)", "god_tag_label")),
+ ln(seg("APS Datapad: ", ""),
+ seg("#2001 ", "table_header_detail"),
+ seg("Admin Tower ", "table_header_label"),
+ seg("here", "aps_here")),
+ txt(""),
+ },
+ })
+
return sections
}
@@ -300,31 +453,64 @@ func (s *AdminServer) getColors(w http.ResponseWriter, _ *http.Request) {
def[c.Name] = ""
}
}
+
+ liveConst := make(map[string]string, len(constantCategories))
+ for _, c := range constantCategories {
+ if v, ok := s.cfg.ConstantColors[c.Name]; ok {
+ liveConst[c.Name] = v
+ } else {
+ liveConst[c.Name] = ""
+ }
+ }
+ constDefaults := config.DefaultConstantColors()
+ defConst := make(map[string]string, len(constantCategories))
+ for _, c := range constantCategories {
+ if v, ok := constDefaults[c.Name]; ok {
+ defConst[c.Name] = v
+ } else {
+ defConst[c.Name] = ""
+ }
+ }
+
writeJSON(w, map[string]any{
- "colors": live,
- "default_colors": def,
- "categories": colorCategories,
- "preview": buildPreview(),
+ "colors": live,
+ "default_colors": def,
+ "categories": colorCategories,
+ "constant_colors": liveConst,
+ "default_constant_colors": defConst,
+ "constant_categories": constantCategories,
+ "preview": buildPreview(),
})
}
func (s *AdminServer) saveColors(w http.ResponseWriter, r *http.Request) {
var body struct {
- Colors map[string]string `json:"colors"`
+ Colors map[string]string `json:"colors"`
+ ConstantColors map[string]string `json:"constant_colors"`
}
if err := readJSON(r, &body); err != nil {
writeJSONError(w, "invalid request body", http.StatusBadRequest)
return
}
- // Validate every supplied key/spec against the known config grammar.
+ // Validate every supplied player-color key/spec.
for name, val := range body.Colors {
- if !knownColor(name) {
+ if !knownPlayerColor(name) {
writeJSONError(w, "unknown color category: "+name, http.StatusBadRequest)
return
}
- // "off" or empty is explicitly allowed (NoColor). Anything else must
- // parse to a non-empty ColorSpec whose only unknown we tolerate is none.
+ spec := color.Parse(val)
+ if strings.TrimSpace(val) != "" && val != "off" && spec.Empty() {
+ writeJSONError(w, "invalid color spec for "+name+": "+val, http.StatusBadRequest)
+ return
+ }
+ }
+ // Validate every supplied constant-color key/spec.
+ for name, val := range body.ConstantColors {
+ if !knownConstantColor(name) {
+ writeJSONError(w, "unknown constant color category: "+name, http.StatusBadRequest)
+ return
+ }
spec := color.Parse(val)
if strings.TrimSpace(val) != "" && val != "off" && spec.Empty() {
writeJSONError(w, "invalid color spec for "+name+": "+val, http.StatusBadRequest)
@@ -342,6 +528,16 @@ func (s *AdminServer) saveColors(w http.ResponseWriter, r *http.Request) {
}
s.cfg.DefaultColors = next
+ curConst := s.cfg.ConstantColors
+ nextConst := make(map[string]string, len(curConst)+len(body.ConstantColors))
+ for k, v := range curConst {
+ nextConst[k] = v
+ }
+ for k, v := range body.ConstantColors {
+ nextConst[k] = v
+ }
+ s.cfg.ConstantColors = nextConst
+
if err := s.writeConfigFile(); err != nil {
writeJSONError(w, "save failed: "+err.Error(), http.StatusInternalServerError)
return
@@ -355,6 +551,7 @@ func (s *AdminServer) handleColorsReset(w http.ResponseWriter, r *http.Request)
return
}
s.cfg.DefaultColors = config.DefaultColors()
+ s.cfg.ConstantColors = config.DefaultConstantColors()
if err := s.writeConfigFile(); err != nil {
writeJSONError(w, "save failed: "+err.Error(), http.StatusInternalServerError)
return
@@ -368,7 +565,15 @@ func (s *AdminServer) handleColorsReset(w http.ResponseWriter, r *http.Request)
live[c.Name] = ""
}
}
- writeJSON(w, map[string]any{"ok": true, "colors": live})
+ liveConst := make(map[string]string, len(constantCategories))
+ for _, c := range constantCategories {
+ if v, ok := s.cfg.ConstantColors[c.Name]; ok {
+ liveConst[c.Name] = v
+ } else {
+ liveConst[c.Name] = ""
+ }
+ }
+ writeJSON(w, map[string]any{"ok": true, "colors": live, "constant_colors": liveConst})
}
// writeConfigFile marshals the entire live *config.Config back to config.yaml,