From db534ea3890439d00afd7b44768f9e8bd4f8e622 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 25 Jun 2026 21:44:16 +0200 Subject: [PATCH] fix(inventory-go): restore GET /inventory/{name} (live Inv window was empty) The Go cutover omitted get_character_inventory; the React InventoryWindow fetches GET /inventory/{name} and got 404 -> empty. Port the endpoint: per-character items with placement (current_wielded_location/container_id/ items_capacity), mana (current_mana/max_mana from original_json IntValues), icon overlays, and join-table combat/req/enh/rating stats; material-prefixed name. Returns {character_name,item_count,items}. Co-Authored-By: Claude Opus 4.8 --- go-services/inventory-go/inventory_char.go | 92 +++++++++++++++++++ .../inventory-go/inventory_char_test.go | 35 +++++++ go-services/inventory-go/main.go | 17 ++-- 3 files changed, 136 insertions(+), 8 deletions(-) create mode 100644 go-services/inventory-go/inventory_char.go create mode 100644 go-services/inventory-go/inventory_char_test.go diff --git a/go-services/inventory-go/inventory_char.go b/go-services/inventory-go/inventory_char.go new file mode 100644 index 00000000..f7a8bc57 --- /dev/null +++ b/go-services/inventory-go/inventory_char.go @@ -0,0 +1,92 @@ +package main + +import ( + "net/http" + "strings" +) + +// GET /inventory/{character_name} — full per-character inventory for the React +// Inventory window. Port of inventory-service get_character_inventory + +// enrich_db_item (main.py:2622 / 2338). The Go cutover omitted this endpoint +// (it was assumed unused), but the React InventoryWindow fetches it, so its +// absence (404) made the live inventory render empty. +// +// Returns {character_name, item_count, items:[...]} with the snake_case fields +// the frontend normalizeItem consumes: placement via current_wielded_location / +// container_id / items_capacity, the mana panel via current_mana / max_mana, +// icon overlays, plus tooltip combat/requirement/enhancement/rating stats. Mana +// and icon overlays are pulled straight from original_json IntValues (same keys +// the plugin/search path use); the rest come from the normalized join tables. +func (s *Server) handleCharacterInventory(w http.ResponseWriter, r *http.Request) { + name := r.PathValue("character_name") + limit := clampInt(qIntDefault(r.URL.Query(), "limit", 1000), 1, 5000) + offset := qIntDefault(r.URL.Query(), "offset", 0) + if offset < 0 { + offset = 0 + } + + const q = ` + SELECT + i.item_id, i.name, i.icon, i.object_class, i.value, i.burden, + i.current_wielded_location, i.container_id, i.items_capacity, i.stack_size, + cs.max_damage, cs.armor_level, cs.damage_bonus, cs.attack_bonus, + cs.melee_defense_bonus, cs.magic_defense_bonus, + r.wield_level, r.skill_level, r.equip_skill, r.lore_requirement, + e.material, e.imbue, e.item_set, e.tinks, e.workmanship, + rt.damage_rating, rt.crit_rating, rt.crit_damage_rating, rt.heal_boost_rating, + NULLIF((rd.original_json->'IntValues'->>'218103815')::int, 0) AS current_mana, + NULLIF((rd.original_json->'IntValues'->>'218103814')::int, 0) AS max_mana, + NULLIF((rd.original_json->'IntValues'->>'218103849')::int, 0) AS icon_overlay_id, + NULLIF((rd.original_json->'IntValues'->>'218103850')::int, 0) AS icon_underlay_id + FROM items i + LEFT JOIN item_combat_stats cs ON i.id = cs.item_id + LEFT JOIN item_requirements r ON i.id = r.item_id + LEFT JOIN item_enhancements e ON i.id = e.item_id + LEFT JOIN item_ratings rt ON i.id = rt.item_id + LEFT JOIN item_raw_data rd ON i.id = rd.item_id + WHERE i.character_name = $1 + ORDER BY i.name + LIMIT $2 OFFSET $3` + + rows, err := queryRowsAsMaps(r.Context(), s.pool, q, name, limit, offset) + if err != nil { + s.dbErr(w, "inventory/"+name, err) + return + } + + items := make([]map[string]any, 0, len(rows)) + for _, row := range rows { + items = append(items, enrichInventoryRow(row)) + } + + // Unlike the Python endpoint (404 on no rows), always return 200 with a + // possibly-empty list — the window treats both as empty, and 200 avoids the + // frontend's catch-all error path. + writeJSON(w, http.StatusOK, map[string]any{ + "character_name": name, + "item_count": len(items), + "items": items, + }) +} + +// enrichInventoryRow flattens a joined inventory row into the frontend item +// shape: drops NULL columns and applies the material-name prefix to the item +// name (enrich_db_item parity, e.g. "Pyreal" + "Chiran Helm" -> "Pyreal Chiran +// Helm"), preserving the un-prefixed name in original_name. +func enrichInventoryRow(row map[string]any) map[string]any { + out := make(map[string]any, len(row)+2) + for k, v := range row { + if v != nil { + out[k] = v + } + } + if mat, ok := out["material"].(string); ok && mat != "" { + out["material_name"] = mat + if name, ok := out["name"].(string); ok && name != "" && + !strings.HasPrefix(strings.ToLower(name), strings.ToLower(mat)) { + out["name"] = mat + " " + name + out["original_name"] = name + } + } + return out +} diff --git a/go-services/inventory-go/inventory_char_test.go b/go-services/inventory-go/inventory_char_test.go new file mode 100644 index 00000000..bc90d41c --- /dev/null +++ b/go-services/inventory-go/inventory_char_test.go @@ -0,0 +1,35 @@ +package main + +import "testing" + +func TestEnrichInventoryRow(t *testing.T) { + // NULL columns are dropped. + out := enrichInventoryRow(map[string]any{"name": "Helm", "armor_level": nil, "value": 100}) + if _, ok := out["armor_level"]; ok { + t.Errorf("nil column armor_level should be dropped, got %v", out["armor_level"]) + } + if out["value"] != 100 { + t.Errorf("value = %v, want 100", out["value"]) + } + + // Material prefix is applied and original_name preserved. + out = enrichInventoryRow(map[string]any{"name": "Chiran Helm", "material": "Pyreal"}) + if out["name"] != "Pyreal Chiran Helm" { + t.Errorf("name = %v, want %q", out["name"], "Pyreal Chiran Helm") + } + if out["original_name"] != "Chiran Helm" { + t.Errorf("original_name = %v, want %q", out["original_name"], "Chiran Helm") + } + if out["material_name"] != "Pyreal" { + t.Errorf("material_name = %v, want Pyreal", out["material_name"]) + } + + // Already-prefixed name is not doubled. + out = enrichInventoryRow(map[string]any{"name": "Pyreal Helm", "material": "Pyreal"}) + if out["name"] != "Pyreal Helm" { + t.Errorf("name = %v, want no double prefix", out["name"]) + } + if _, ok := out["original_name"]; ok { + t.Errorf("original_name should be unset when no prefix applied") + } +} diff --git a/go-services/inventory-go/main.go b/go-services/inventory-go/main.go index ffe56787..51611dcc 100644 --- a/go-services/inventory-go/main.go +++ b/go-services/inventory-go/main.go @@ -28,14 +28,14 @@ import ( var buildVersion = "dev" type Server struct { - pool *pgxpool.Pool - attributeSets map[string]string // AttributeSetInfo: set-id -> set name - objectClasses map[int]string // ObjectClass: id -> name - materials map[int]string // MaterialType: id -> name - spells map[int]map[string]any // SpellTable: spell-id -> raw spell value object - equipMaskMap map[int]string // EquipMask: mask -> technical name (exact lookup) - equipMaskOrdered []equipMaskEntry // EquipMask in ascending-mask order (bit-flag decode) - log *slog.Logger + pool *pgxpool.Pool + attributeSets map[string]string // AttributeSetInfo: set-id -> set name + objectClasses map[int]string // ObjectClass: id -> name + materials map[int]string // MaterialType: id -> name + spells map[int]map[string]any // SpellTable: spell-id -> raw spell value object + equipMaskMap map[int]string // EquipMask: mask -> technical name (exact lookup) + equipMaskOrdered []equipMaskEntry // EquipMask in ascending-mask order (bit-flag decode) + log *slog.Logger } func main() { @@ -93,6 +93,7 @@ func main() { mux.HandleFunc("GET /sets/list", srv.handleSetsList) mux.HandleFunc("GET /characters/list", srv.handleCharactersList) mux.HandleFunc("GET /search/items", srv.handleSearchItems) + mux.HandleFunc("GET /inventory/{character_name}", srv.handleCharacterInventory) mux.HandleFunc("POST /debug/process", srv.handleDebugProcess) // Ingestion (works in read-write mode; on the read-only prod instance these // fail the read-only transaction, which is the intended guard).