OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
HASH      22cc45a47954
DATE      2026-05-02
SUBJECT   ookstats: dedupe player_equipment + fix profile staleness check
FILES     9 CHANGED
HASH      22cc45a47954
DATE      2026-05-02
SUBJECT   ookstats: dedupe player_equipment +
          fix profile staleness check
FILES     9 CHANGED
 

diff --git a/nix/pkgs/ookstats/src/cmd/build.go b/nix/pkgs/ookstats/src/cmd/build.
go
index 7986179..381476d 100644
--- a/nix/pkgs/ookstats/src/cmd/build.go
+++ b/nix/pkgs/ookstats/src/cmd/build.go
@@ -264,11 +264,7 @@ func fetchProfilesOnce(db *sql.DB, client *blizzard.Client) e
rror {
 		return nil
 	}
 
-	// batch in reasonable size
 	batchSize := 20
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
-	defer cancel()
-
 	totalProfiles := 0
 	totalItems := 0
 	processed := 0
@@ -286,7 +282,8 @@ func fetchProfilesOnce(db *sql.DB, client *blizzard.Client) er
ror {
 			"batch", batchNum,
 			"total_batches", totalBatches,
 			"players", len(batch))
-		results := client.FetchPlayerProfilesConcurrent(ctx, batch)
+		batchCtx, batchCancel := context.WithTimeout(context.Background(), 2*time.Minut
e)
+		results := client.FetchPlayerProfilesConcurrent(batchCtx, batch)
 		ts := time.Now().UnixMilli()
 		batchProfiles := 0
 		batchItems := 0
@@ -310,6 +307,7 @@ func fetchProfilesOnce(db *sql.DB, client *blizzard.Client) er
ror {
 			batchProfiles += profs
 			batchItems += items
 		}
+		batchCancel()
 		totalProfiles += batchProfiles
 		totalItems += batchItems
 		log.Info("batch complete",

diff --git a/nix/pkgs/ookstats/src/cmd/build
.go b/nix/pkgs/ookstats/src/cmd/build.go
index 7986179..381476d 100644
--- a/nix/pkgs/ookstats/src/cmd/build.go
+++ b/nix/pkgs/ookstats/src/cmd/build.go
@@ -264,11 +264,7 @@ func fetchProfilesOnce(
db *sql.DB, client *blizzard.Client) error {
 		return nil
 	}
 
-	// batch in reasonable size
 	batchSize := 20
-	ctx, cancel := context.WithTimeout(context
.Background(), 30*time.Minute)
-	defer cancel()
-
 	totalProfiles := 0
 	totalItems := 0
 	processed := 0
@@ -286,7 +282,8 @@ func fetchProfilesOnce(d
b *sql.DB, client *blizzard.Client) error {
 			"batch", batchNum,
 			"total_batches", totalBatches,
 			"players", len(batch))
-		results := client.FetchPlayerProfilesConc
urrent(ctx, batch)
+		batchCtx, batchCancel := context.WithTime
out(context.Background(), 2*time.Minute)
+		results := client.FetchPlayerProfilesConc
urrent(batchCtx, batch)
 		ts := time.Now().UnixMilli()
 		batchProfiles := 0
 		batchItems := 0
@@ -310,6 +307,7 @@ func fetchProfilesOnce(d
b *sql.DB, client *blizzard.Client) error {
 			batchProfiles += profs
 			batchItems += items
 		}
+		batchCancel()
 		totalProfiles += batchProfiles
 		totalItems += batchItems
 		log.Info("batch complete",
 

diff --git a/nix/pkgs/ookstats/src/cmd/populate.go b/nix/pkgs/ookstats/src/cmd/pop
ulate.go
index e07eb65..aedfd12 100644
--- a/nix/pkgs/ookstats/src/cmd/populate.go
+++ b/nix/pkgs/ookstats/src/cmd/populate.go
@@ -3,11 +3,15 @@ package cmd
 import (
 	"database/sql"
 	"encoding/json"
+	"errors"
 	"fmt"
+	"net/http"
 	"os"
 	"strings"
+	"time"
 
 	"github.com/spf13/cobra"
+	"ookstats/internal/blizzard"
 	"ookstats/internal/database"
 )
 
@@ -51,12 +55,13 @@ type WowSimsDatabase struct {
 }
 
 type WowSimsItem struct {
-	ID      int    `json:"id"`
-	Name    string `json:"name"`
-	Icon    string `json:"icon"`
-	Quality int    `json:"quality"`
-	Type    int    `json:"type"`
-	// items don't have stats array, they have other fields like weaponType, etc.
+	ID             int             `json:"id"`
+	Name           string          `json:"name"`
+	Icon           string          `json:"icon"`
+	Quality        int             `json:"quality"`
+	Type           int             `json:"type"`
+	ScalingOptions json.RawMessage `json:"scalingOptions,omitempty"`
+	ItemEffect     json.RawMessage `json:"itemEffect,omitempty"`
 }
 
 type WowSimsGem struct {
@@ -131,16 +136,26 @@ func populateItems(db *sql.DB, wowsimsDBPath string) error {
 	fmt.Println("Inserting items...")
 	insertCount := 0
 
-	// insert items (no stats array)
 	for _, item := range wowsimsDB.Items {
 		if item.ID == 0 {
 			continue
 		}
 
+		statsJSON := "{}"
+		if len(item.ScalingOptions) > 0 {
+			statsJSON = string(item.ScalingOptions)
+		}
+
+		var itemEffectJSON *string
+		if len(item.ItemEffect) > 0 {
+			s := string(item.ItemEffect)
+			itemEffectJSON = &s
+		}
+
 		_, err = tx.Exec(`
-			INSERT OR REPLACE INTO items (id, name, icon, quality, type, stats)
-			VALUES (?, ?, ?, ?, ?, ?)
-		`, item.ID, item.Name, item.Icon, item.Quality, item.Type, "{}")
+			INSERT OR REPLACE INTO items (id, name, icon, quality, type, stats, item_effec
t)
+			VALUES (?, ?, ?, ?, ?, ?, ?)
+		`, item.ID, item.Name, item.Icon, item.Quality, item.Type, statsJSON, itemEffec
tJSON)
 
 		if err != nil {
 			return fmt.Errorf("failed to insert item %d: %w", item.ID, err)
@@ -221,10 +236,149 @@ func populateItems(db *sql.DB, wowsimsDBPath string) error 
{
 	return nil
 }
 
+var populateItemsEnrichCmd = &cobra.Command{
+	Use:   "items-enrich",
+	Short: "Enrich items with Blizzard spell descriptions",
+	Long:  `Fetch spell descriptions from Blizzard API for items missing them.`,
+	RunE: func(cmd *cobra.Command, args []string) error {
+		fmt.Println("=== Item Enrichment ===")
+
+		region, _ := cmd.Flags().GetString("region")
+
+		db, err := database.Connect()
+		if err != nil {
+			return fmt.Errorf("failed to connect to database: %w", err)
+		}
+		defer db.Close()
+
+		client, err := blizzard.NewClient()
+		if err != nil {
+			return fmt.Errorf("failed to create blizzard client: %w", err)
+		}
+
+		if err := enrichItemsWithDescriptions(db, client, region); err != nil {
+			return fmt.Errorf("failed to enrich items: %w", err)
+		}
+
+		fmt.Printf("Item enrichment complete!\n")
+		return nil
+	},
+}
+
+func enrichItemsWithDescriptions(db *sql.DB, client *blizzard.Client, region stri
ng) error {
+	// get all item IDs that are missing spell_description (only trinkets type 12)
+	rows, err := db.Query(`
+		SELECT id FROM items
+		WHERE spell_description IS NULL
+		  AND type = 12
+		ORDER BY id
+	`)
+	if err != nil {
+		return fmt.Errorf("query items: %w", err)
+	}
+
+	var itemIDs []int
+	for rows.Next() {
+		var id int
+		if err := rows.Scan(&id); err != nil {
+			rows.Close()
+			return fmt.Errorf("scan item id: %w", err)
+		}
+		itemIDs = append(itemIDs, id)
+	}
+	rows.Close()
+
+	if len(itemIDs) == 0 {
+		fmt.Println("All items already have spell descriptions")
+		return nil
+	}
+
+	fmt.Printf("Found %d items missing spell descriptions\n", len(itemIDs))
+
+	// prepare update statement
+	updateStmt, err := db.Prepare(`UPDATE items SET spell_description = ? WHERE id =
 ?`)
+	if err != nil {
+		return fmt.Errorf("prepare update: %w", err)
+	}
+	defer updateStmt.Close()
+
+	fetched := 0
+	skipped := 0
+	notFound := 0
+	startTime := time.Now()
+
+	for i, itemID := range itemIDs {
+		// progress every 100 items
+		if (i+1)%100 == 0 {
+			elapsed := time.Since(startTime)
+			rate := float64(i+1) / elapsed.Seconds()
+			remaining := float64(len(itemIDs)-i-1) / rate
+			fmt.Printf("  Progress: %d/%d (%.1f/sec, ~%.0fs remaining)\n",
+				i+1, len(itemIDs), rate, remaining)
+		}
+
+		item, err := client.FetchItem(itemID, region)
+		if err != nil {
+			var apiErr *blizzard.APIError
+			if errors.As(err, &apiErr) && apiErr.Status == http.StatusNotFound {
+				notFound++
+				continue
+			}
+			fmt.Printf("  Warning: failed to fetch item %d: %v\n", itemID, err)
+			skipped++
+			continue
+		}
+
+		// extract spell descriptions
+		var descriptions []string
+		if item.PreviewItem != nil {
+			for _, spell := range item.PreviewItem.Spells {
+				if spell.Description != "" {
+					descriptions = append(descriptions, spell.Description)
+				}
+			}
+		}
+
+		if len(descriptions) == 0 {
+			continue
+		}
+
+		// debug: log first few items with descriptions
+		if fetched < 5 {
+			fmt.Printf("  DEBUG: Item %d (%s) has description: %.50s...\n", itemID, item.N
ame, descriptions[0])
+		}
+
+		// join multiple descriptions with newline
+		desc := strings.Join(descriptions, "\n")
+		result, err := updateStmt.Exec(desc, itemID)
+		if err != nil {
+			fmt.Printf("  Warning: failed to update item %d: %v\n", itemID, err)
+			skipped++
+			continue
+		}
+
+		rowsAffected, _ := result.RowsAffected()
+		if rowsAffected == 0 {
+			fmt.Printf("  Warning: item %d update affected 0 rows\n", itemID)
+		}
+
+		fetched++
+	}
+
+	elapsed := time.Since(startTime)
+	fmt.Printf("[OK] Enriched %d items with spell descriptions (%.1fs)\n", fetched, 
elapsed.Seconds())
+	fmt.Printf("     %d not found in Blizzard API, %d skipped due to errors\n", notF
ound, skipped)
+	return nil
+}
+
 func init() {
 	rootCmd.AddCommand(populateCmd)
 	populateCmd.AddCommand(populateItemsCmd)
+	populateCmd.AddCommand(populateItemsEnrichCmd)
 
 	// add flag for wowsims database path
 	populateItemsCmd.Flags().String("wowsims-db", "", "Path to WoW Sims database JSO
N file")
+
+	// add flag for region
+	populateItemsEnrichCmd.Flags().String("region", "us", "Blizzard API region (us, 
eu, etc)")
 }

diff --git a/nix/pkgs/ookstats/src/cmd/popul
ate.go b/nix/pkgs/ookstats/src/cmd/populate.
go
index e07eb65..aedfd12 100644
--- a/nix/pkgs/ookstats/src/cmd/populate.go
+++ b/nix/pkgs/ookstats/src/cmd/populate.go
@@ -3,11 +3,15 @@ package cmd
 import (
 	"database/sql"
 	"encoding/json"
+	"errors"
 	"fmt"
+	"net/http"
 	"os"
 	"strings"
+	"time"
 
 	"github.com/spf13/cobra"
+	"ookstats/internal/blizzard"
 	"ookstats/internal/database"
 )
 
@@ -51,12 +55,13 @@ type WowSimsDatabase str
uct {
 }
 
 type WowSimsItem struct {
-	ID      int    `json:"id"`
-	Name    string `json:"name"`
-	Icon    string `json:"icon"`
-	Quality int    `json:"quality"`
-	Type    int    `json:"type"`
-	// items don't have stats array, they have
 other fields like weaponType, etc.
+	ID             int             `json:"id"`
+	Name           string          `json:"name
"`
+	Icon           string          `json:"icon
"`
+	Quality        int             `json:"qual
ity"`
+	Type           int             `json:"type
"`
+	ScalingOptions json.RawMessage `json:"scal
ingOptions,omitempty"`
+	ItemEffect     json.RawMessage `json:"item
Effect,omitempty"`
 }
 
 type WowSimsGem struct {
@@ -131,16 +136,26 @@ func populateItems(db 
*sql.DB, wowsimsDBPath string) error {
 	fmt.Println("Inserting items...")
 	insertCount := 0
 
-	// insert items (no stats array)
 	for _, item := range wowsimsDB.Items {
 		if item.ID == 0 {
 			continue
 		}
 
+		statsJSON := "{}"
+		if len(item.ScalingOptions) > 0 {
+			statsJSON = string(item.ScalingOptions)
+		}
+
+		var itemEffectJSON *string
+		if len(item.ItemEffect) > 0 {
+			s := string(item.ItemEffect)
+			itemEffectJSON = &s
+		}
+
 		_, err = tx.Exec(`
-			INSERT OR REPLACE INTO items (id, name, 
icon, quality, type, stats)
-			VALUES (?, ?, ?, ?, ?, ?)
-		`, item.ID, item.Name, item.Icon, item.Qu
ality, item.Type, "{}")
+			INSERT OR REPLACE INTO items (id, name, 
icon, quality, type, stats, item_effect)
+			VALUES (?, ?, ?, ?, ?, ?, ?)
+		`, item.ID, item.Name, item.Icon, item.Qu
ality, item.Type, statsJSON, itemEffectJSON)
 
 		if err != nil {
 			return fmt.Errorf("failed to insert item
 %d: %w", item.ID, err)
@@ -221,10 +236,149 @@ func populateItems(db
 *sql.DB, wowsimsDBPath string) error {
 	return nil
 }
 
+var populateItemsEnrichCmd = &cobra.Command
{
+	Use:   "items-enrich",
+	Short: "Enrich items with Blizzard spell d
escriptions",
+	Long:  `Fetch spell descriptions from Bliz
zard API for items missing them.`,
+	RunE: func(cmd *cobra.Command, args []stri
ng) error {
+		fmt.Println("=== Item Enrichment ===")
+
+		region, _ := cmd.Flags().GetString("regio
n")
+
+		db, err := database.Connect()
+		if err != nil {
+			return fmt.Errorf("failed to connect to 
database: %w", err)
+		}
+		defer db.Close()
+
+		client, err := blizzard.NewClient()
+		if err != nil {
+			return fmt.Errorf("failed to create bliz
zard client: %w", err)
+		}
+
+		if err := enrichItemsWithDescriptions(db,
 client, region); err != nil {
+			return fmt.Errorf("failed to enrich item
s: %w", err)
+		}
+
+		fmt.Printf("Item enrichment complete!\n")
+		return nil
+	},
+}
+
+func enrichItemsWithDescriptions(db *sql.DB
, client *blizzard.Client, region string) er
ror {
+	// get all item IDs that are missing spell
_description (only trinkets type 12)
+	rows, err := db.Query(`
+		SELECT id FROM items
+		WHERE spell_description IS NULL
+		  AND type = 12
+		ORDER BY id
+	`)
+	if err != nil {
+		return fmt.Errorf("query items: %w", err)
+	}
+
+	var itemIDs []int
+	for rows.Next() {
+		var id int
+		if err := rows.Scan(&id); err != nil {
+			rows.Close()
+			return fmt.Errorf("scan item id: %w", er
r)
+		}
+		itemIDs = append(itemIDs, id)
+	}
+	rows.Close()
+
+	if len(itemIDs) == 0 {
+		fmt.Println("All items already have spell
 descriptions")
+		return nil
+	}
+
+	fmt.Printf("Found %d items missing spell d
escriptions\n", len(itemIDs))
+
+	// prepare update statement
+	updateStmt, err := db.Prepare(`UPDATE item
s SET spell_description = ? WHERE id = ?`)
+	if err != nil {
+		return fmt.Errorf("prepare update: %w", e
rr)
+	}
+	defer updateStmt.Close()
+
+	fetched := 0
+	skipped := 0
+	notFound := 0
+	startTime := time.Now()
+
+	for i, itemID := range itemIDs {
+		// progress every 100 items
+		if (i+1)%100 == 0 {
+			elapsed := time.Since(startTime)
+			rate := float64(i+1) / elapsed.Seconds()
+			remaining := float64(len(itemIDs)-i-1) /
 rate
+			fmt.Printf("  Progress: %d/%d (%.1f/sec,
 ~%.0fs remaining)\n",
+				i+1, len(itemIDs), rate, remaining)
+		}
+
+		item, err := client.FetchItem(itemID, reg
ion)
+		if err != nil {
+			var apiErr *blizzard.APIError
+			if errors.As(err, &apiErr) && apiErr.Sta
tus == http.StatusNotFound {
+				notFound++
+				continue
+			}
+			fmt.Printf("  Warning: failed to fetch i
tem %d: %v\n", itemID, err)
+			skipped++
+			continue
+		}
+
+		// extract spell descriptions
+		var descriptions []string
+		if item.PreviewItem != nil {
+			for _, spell := range item.PreviewItem.S
pells {
+				if spell.Description != "" {
+					descriptions = append(descriptions, sp
ell.Description)
+				}
+			}
+		}
+
+		if len(descriptions) == 0 {
+			continue
+		}
+
+		// debug: log first few items with descri
ptions
+		if fetched < 5 {
+			fmt.Printf("  DEBUG: Item %d (%s) has de
scription: %.50s...\n", itemID, item.Name, d
escriptions[0])
+		}
+
+		// join multiple descriptions with newlin
e
+		desc := strings.Join(descriptions, "\n")
+		result, err := updateStmt.Exec(desc, item
ID)
+		if err != nil {
+			fmt.Printf("  Warning: failed to update 
item %d: %v\n", itemID, err)
+			skipped++
+			continue
+		}
+
+		rowsAffected, _ := result.RowsAffected()
+		if rowsAffected == 0 {
+			fmt.Printf("  Warning: item %d update af
fected 0 rows\n", itemID)
+		}
+
+		fetched++
+	}
+
+	elapsed := time.Since(startTime)
+	fmt.Printf("[OK] Enriched %d items with sp
ell descriptions (%.1fs)\n", fetched, elapse
d.Seconds())
+	fmt.Printf("     %d not found in Blizzard 
API, %d skipped due to errors\n", notFound, 
skipped)
+	return nil
+}
+
 func init() {
 	rootCmd.AddCommand(populateCmd)
 	populateCmd.AddCommand(populateItemsCmd)
+	populateCmd.AddCommand(populateItemsEnrich
Cmd)
 
 	// add flag for wowsims database path
 	populateItemsCmd.Flags().String("wowsims-d
b", "", "Path to WoW Sims database JSON file
")
+
+	// add flag for region
+	populateItemsEnrichCmd.Flags().String("reg
ion", "us", "Blizzard API region (us, eu, et
c)")
 }
 

diff --git a/nix/pkgs/ookstats/src/internal/blizzard/profiles.go b/nix/pkgs/ooksta
ts/src/internal/blizzard/profiles.go
index 7bc92eb..33a60d6 100644
--- a/nix/pkgs/ookstats/src/internal/blizzard/profiles.go
+++ b/nix/pkgs/ookstats/src/internal/blizzard/profiles.go
@@ -87,6 +87,18 @@ func (c *Client) FetchCharacterAchievements(playerName, realmSl
ug, region string
 	return fetchPlayerProfileAPI[CharacterAchievementsResponse](c, url)
 }
 
+// FetchItem fetches item details from the static game data API
+func (c *Client) FetchItem(itemID int, region string) (*ItemDetailResponse, error
) {
+	// hardcoded namespace version - update when game patches
+	namespace := fmt.Sprintf("static-5.5.3_64530-classic-%s", region)
+	url := fmt.Sprintf(
+		"https://%s.api.blizzard.com/data/wow/item/%d?namespace=%s&locale=en_US",
+		region, itemID, namespace,
+	)
+
+	return fetchPlayerProfileAPI[ItemDetailResponse](c, url)
+}
+
 // fetchPlayerProfileAPI is a generic function for fetching player profile data
 func fetchPlayerProfileAPI[T any](c *Client, url string) (*T, error) {
 	const maxRetries = 3

diff --git a/nix/pkgs/ookstats/src/internal/
blizzard/profiles.go b/nix/pkgs/ookstats/src
/internal/blizzard/profiles.go
index 7bc92eb..33a60d6 100644
--- a/nix/pkgs/ookstats/src/internal/blizzar
d/profiles.go
+++ b/nix/pkgs/ookstats/src/internal/blizzar
d/profiles.go
@@ -87,6 +87,18 @@ func (c *Client) FetchCha
racterAchievements(playerName, realmSlug, re
gion string
 	return fetchPlayerProfileAPI[CharacterAchi
evementsResponse](c, url)
 }
 
+// FetchItem fetches item details from the 
static game data API
+func (c *Client) FetchItem(itemID int, regi
on string) (*ItemDetailResponse, error) {
+	// hardcoded namespace version - update wh
en game patches
+	namespace := fmt.Sprintf("static-5.5.3_645
30-classic-%s", region)
+	url := fmt.Sprintf(
+		"https://%s.api.blizzard.com/data/wow/ite
m/%d?namespace=%s&locale=en_US",
+		region, itemID, namespace,
+	)
+
+	return fetchPlayerProfileAPI[ItemDetailRes
ponse](c, url)
+}
+
 // fetchPlayerProfileAPI is a generic funct
ion for fetching player profile data
 func fetchPlayerProfileAPI[T any](c *Client
, url string) (*T, error) {
 	const maxRetries = 3
 

diff --git a/nix/pkgs/ookstats/src/internal/blizzard/types.go b/nix/pkgs/ookstats/
src/internal/blizzard/types.go
index d2ed1fb..6b5adcb 100644
--- a/nix/pkgs/ookstats/src/internal/blizzard/types.go
+++ b/nix/pkgs/ookstats/src/internal/blizzard/types.go
@@ -317,3 +317,24 @@ type SeasonDetailResponse struct {
 		ID int `json:"id"`
 	} `json:"periods"`
 }
+
+// ItemDetailResponse represents the response from the item detail API
+type ItemDetailResponse struct {
+	ID          int              `json:"id"`
+	Name        string           `json:"name"`
+	PreviewItem *ItemPreviewData `json:"preview_item,omitempty"`
+}
+
+// ItemPreviewData contains the preview_item section with spells
+type ItemPreviewData struct {
+	Spells []ItemSpellInfo `json:"spells,omitempty"`
+}
+
+// ItemSpellInfo contains spell information including description
+type ItemSpellInfo struct {
+	Spell struct {
+		ID   int    `json:"id"`
+		Name string `json:"name"`
+	} `json:"spell"`
+	Description string `json:"description"`
+}

diff --git a/nix/pkgs/ookstats/src/internal/
blizzard/types.go b/nix/pkgs/ookstats/src/in
ternal/blizzard/types.go
index d2ed1fb..6b5adcb 100644
--- a/nix/pkgs/ookstats/src/internal/blizzar
d/types.go
+++ b/nix/pkgs/ookstats/src/internal/blizzar
d/types.go
@@ -317,3 +317,24 @@ type SeasonDetailRespon
se struct {
 		ID int `json:"id"`
 	} `json:"periods"`
 }
+
+// ItemDetailResponse represents the respon
se from the item detail API
+type ItemDetailResponse struct {
+	ID          int              `json:"id"`
+	Name        string           `json:"name"`
+	PreviewItem *ItemPreviewData `json:"previe
w_item,omitempty"`
+}
+
+// ItemPreviewData contains the preview_ite
m section with spells
+type ItemPreviewData struct {
+	Spells []ItemSpellInfo `json:"spells,omite
mpty"`
+}
+
+// ItemSpellInfo contains spell information
 including description
+type ItemSpellInfo struct {
+	Spell struct {
+		ID   int    `json:"id"`
+		Name string `json:"name"`
+	} `json:"spell"`
+	Description string `json:"description"`
+}
 

diff --git a/nix/pkgs/ookstats/src/internal/database/migrations.go b/nix/pkgs/ooks
tats/src/internal/database/migrations.go
index 6072212..45d4c54 100644
--- a/nix/pkgs/ookstats/src/internal/database/migrations.go
+++ b/nix/pkgs/ookstats/src/internal/database/migrations.go
@@ -277,3 +277,156 @@ func migratePlayerRankingsPrimaryKey(db *sql.DB) error {
 	fmt.Printf("[OK] player_rankings table migrated - added PRIMARY KEY and removed 
duplicates\n")
 	return nil
 }
+
+// migrateEquipmentToUpsert adds UNIQUE constraint on (player_id, slot_type) and 
removes historical snapshots
+func migrateEquipmentToUpsert(db *sql.DB) error {
+	// Check if unique index already exists
+	var indexCount int
+	err := db.QueryRow(`
+		SELECT COUNT(*) FROM sqlite_master
+		WHERE type='index' AND name='idx_player_equipment_unique'
+	`).Scan(&indexCount)
+	if err != nil {
+		return fmt.Errorf("check index: %w", err)
+	}
+
+	if indexCount > 0 {
+		// Already migrated
+		return nil
+	}
+
+	fmt.Printf("Migrating player_equipment to single-row-per-slot model...\n")
+
+	// Count rows before
+	var beforeCount int
+	db.QueryRow(`SELECT COUNT(*) FROM player_equipment`).Scan(&beforeCount)
+
+	tx, err := db.Begin()
+	if err != nil {
+		return fmt.Errorf("begin transaction: %w", err)
+	}
+	defer tx.Rollback()
+
+	// Create new table with UNIQUE constraint
+	if _, err := tx.Exec(`
+		CREATE TABLE player_equipment_new (
+			id INTEGER PRIMARY KEY,
+			player_id INTEGER,
+			slot_type TEXT,
+			item_id INTEGER,
+			upgrade_id INTEGER,
+			quality TEXT,
+			item_name TEXT,
+			snapshot_timestamp INTEGER,
+			UNIQUE(player_id, slot_type)
+		)
+	`); err != nil {
+		return fmt.Errorf("create new table: %w", err)
+	}
+
+	// Copy only one row per (player_id, slot_type) - pick the one with highest ID
+	if _, err := tx.Exec(`
+		INSERT INTO player_equipment_new (id, player_id, slot_type, item_id, upgrade_id
, quality, item_name, snapshot_timestamp)
+		SELECT pe.id, pe.player_id, pe.slot_type, pe.item_id, pe.upgrade_id, pe.quality
, pe.item_name, pe.snapshot_timestamp
+		FROM player_equipment pe
+		INNER JOIN (
+			SELECT MAX(id) as max_id
+			FROM player_equipment
+			GROUP BY player_id, slot_type
+		) latest ON pe.id = latest.max_id
+	`); err != nil {
+		return fmt.Errorf("copy data: %w", err)
+	}
+
+	// Delete orphaned enchantments (those referencing equipment IDs not in the new 
table)
+	if _, err := tx.Exec(`
+		DELETE FROM player_equipment_enchantments
+		WHERE equipment_id NOT IN (SELECT id FROM player_equipment_new)
+	`); err != nil {
+		return fmt.Errorf("delete orphaned enchantments: %w", err)
+	}
+
+	// Drop old table and rename
+	if _, err := tx.Exec(`DROP TABLE player_equipment`); err != nil {
+		return fmt.Errorf("drop old table: %w", err)
+	}
+
+	if _, err := tx.Exec(`ALTER TABLE player_equipment_new RENAME TO player_equipmen
t`); err != nil {
+		return fmt.Errorf("rename table: %w", err)
+	}
+
+	// Create the unique index for the ON CONFLICT clause
+	if _, err := tx.Exec(`CREATE UNIQUE INDEX idx_player_equipment_unique ON player_
equipment(player_id, slot_type)`); err != nil {
+		return fmt.Errorf("create unique index: %w", err)
+	}
+
+	if err := tx.Commit(); err != nil {
+		return fmt.Errorf("commit: %w", err)
+	}
+
+	// Count rows after
+	var afterCount int
+	db.QueryRow(`SELECT COUNT(*) FROM player_equipment`).Scan(&afterCount)
+
+	fmt.Printf("[OK] player_equipment migrated: %d ? %d rows (removed %d historical 
snapshots)\n",
+		beforeCount, afterCount, beforeCount-afterCount)
+	return nil
+}
+
+// migrateItemsAddItemEffect adds item_effect column to items table if missing
+func migrateItemsAddItemEffect(db *sql.DB) error {
+	// Check if column already exists
+	rows, err := db.Query("PRAGMA table_info(items)")
+	if err != nil {
+		return nil // table doesn't exist yet
+	}
+	defer rows.Close()
+
+	hasItemEffect := false
+	for rows.Next() {
+		var cid int
+		var name, ctype string
+		var notnull, pk int
+		var dflt any
+		if err := rows.Scan(&cid, &name, &ctype, &notnull, &dflt, &pk); err != nil {
+			continue
+		}
+		if name == "item_effect" {
+			hasItemEffect = true
+			break
+		}
+	}
+
+	if hasItemEffect {
+		return nil
+	}
+
+	fmt.Printf("Adding item_effect column to items table...\n")
+	_, err = db.Exec(`ALTER TABLE items ADD COLUMN item_effect TEXT`)
+	if err != nil {
+		return fmt.Errorf("add item_effect column: %w", err)
+	}
+
+	fmt.Printf("[OK] Added item_effect column to items table\n")
+	return nil
+}
+
+// migrateItemsAddSpellDescription adds spell_description column to items table i
f missing
+func migrateItemsAddSpellDescription(db *sql.DB) error {
+	has, err := columnExists(db, "items", "spell_description")
+	if err != nil {
+		return nil // table doesn't exist yet
+	}
+	if has {
+		return nil
+	}
+
+	fmt.Printf("Adding spell_description column to items table...\n")
+	_, err = db.Exec(`ALTER TABLE items ADD COLUMN spell_description TEXT`)
+	if err != nil {
+		return fmt.Errorf("add spell_description column: %w", err)
+	}
+
+	fmt.Printf("[OK] Added spell_description column to items table\n")
+	return nil
+}

diff --git a/nix/pkgs/ookstats/src/internal/
database/migrations.go b/nix/pkgs/ookstats/s
rc/internal/database/migrations.go
index 6072212..45d4c54 100644
--- a/nix/pkgs/ookstats/src/internal/databas
e/migrations.go
+++ b/nix/pkgs/ookstats/src/internal/databas
e/migrations.go
@@ -277,3 +277,156 @@ func migratePlayerRank
ingsPrimaryKey(db *sql.DB) error {
 	fmt.Printf("[OK] player_rankings table mig
rated - added PRIMARY KEY and removed duplic
ates\n")
 	return nil
 }
+
+// migrateEquipmentToUpsert adds UNIQUE con
straint on (player_id, slot_type) and remove
s historical snapshots
+func migrateEquipmentToUpsert(db *sql.DB) e
rror {
+	// Check if unique index already exists
+	var indexCount int
+	err := db.QueryRow(`
+		SELECT COUNT(*) FROM sqlite_master
+		WHERE type='index' AND name='idx_player_e
quipment_unique'
+	`).Scan(&indexCount)
+	if err != nil {
+		return fmt.Errorf("check index: %w", err)
+	}
+
+	if indexCount > 0 {
+		// Already migrated
+		return nil
+	}
+
+	fmt.Printf("Migrating player_equipment to 
single-row-per-slot model...\n")
+
+	// Count rows before
+	var beforeCount int
+	db.QueryRow(`SELECT COUNT(*) FROM player_e
quipment`).Scan(&beforeCount)
+
+	tx, err := db.Begin()
+	if err != nil {
+		return fmt.Errorf("begin transaction: %w"
, err)
+	}
+	defer tx.Rollback()
+
+	// Create new table with UNIQUE constraint
+	if _, err := tx.Exec(`
+		CREATE TABLE player_equipment_new (
+			id INTEGER PRIMARY KEY,
+			player_id INTEGER,
+			slot_type TEXT,
+			item_id INTEGER,
+			upgrade_id INTEGER,
+			quality TEXT,
+			item_name TEXT,
+			snapshot_timestamp INTEGER,
+			UNIQUE(player_id, slot_type)
+		)
+	`); err != nil {
+		return fmt.Errorf("create new table: %w",
 err)
+	}
+
+	// Copy only one row per (player_id, slot_
type) - pick the one with highest ID
+	if _, err := tx.Exec(`
+		INSERT INTO player_equipment_new (id, pla
yer_id, slot_type, item_id, upgrade_id, qual
ity, item_name, snapshot_timestamp)
+		SELECT pe.id, pe.player_id, pe.slot_type,
 pe.item_id, pe.upgrade_id, pe.quality, pe.i
tem_name, pe.snapshot_timestamp
+		FROM player_equipment pe
+		INNER JOIN (
+			SELECT MAX(id) as max_id
+			FROM player_equipment
+			GROUP BY player_id, slot_type
+		) latest ON pe.id = latest.max_id
+	`); err != nil {
+		return fmt.Errorf("copy data: %w", err)
+	}
+
+	// Delete orphaned enchantments (those ref
erencing equipment IDs not in the new table)
+	if _, err := tx.Exec(`
+		DELETE FROM player_equipment_enchantments
+		WHERE equipment_id NOT IN (SELECT id FROM
 player_equipment_new)
+	`); err != nil {
+		return fmt.Errorf("delete orphaned enchan
tments: %w", err)
+	}
+
+	// Drop old table and rename
+	if _, err := tx.Exec(`DROP TABLE player_eq
uipment`); err != nil {
+		return fmt.Errorf("drop old table: %w", e
rr)
+	}
+
+	if _, err := tx.Exec(`ALTER TABLE player_e
quipment_new RENAME TO player_equipment`); e
rr != nil {
+		return fmt.Errorf("rename table: %w", err
)
+	}
+
+	// Create the unique index for the ON CONF
LICT clause
+	if _, err := tx.Exec(`CREATE UNIQUE INDEX 
idx_player_equipment_unique ON player_equipm
ent(player_id, slot_type)`); err != nil {
+		return fmt.Errorf("create unique index: %
w", err)
+	}
+
+	if err := tx.Commit(); err != nil {
+		return fmt.Errorf("commit: %w", err)
+	}
+
+	// Count rows after
+	var afterCount int
+	db.QueryRow(`SELECT COUNT(*) FROM player_e
quipment`).Scan(&afterCount)
+
+	fmt.Printf("[OK] player_equipment migrated
: %d ? %d rows (removed %d historical snapsh
ots)\n",
+		beforeCount, afterCount, beforeCount-afte
rCount)
+	return nil
+}
+
+// migrateItemsAddItemEffect adds item_effe
ct column to items table if missing
+func migrateItemsAddItemEffect(db *sql.DB) 
error {
+	// Check if column already exists
+	rows, err := db.Query("PRAGMA table_info(i
tems)")
+	if err != nil {
+		return nil // table doesn't exist yet
+	}
+	defer rows.Close()
+
+	hasItemEffect := false
+	for rows.Next() {
+		var cid int
+		var name, ctype string
+		var notnull, pk int
+		var dflt any
+		if err := rows.Scan(&cid, &name, &ctype, 
&notnull, &dflt, &pk); err != nil {
+			continue
+		}
+		if name == "item_effect" {
+			hasItemEffect = true
+			break
+		}
+	}
+
+	if hasItemEffect {
+		return nil
+	}
+
+	fmt.Printf("Adding item_effect column to i
tems table...\n")
+	_, err = db.Exec(`ALTER TABLE items ADD CO
LUMN item_effect TEXT`)
+	if err != nil {
+		return fmt.Errorf("add item_effect column
: %w", err)
+	}
+
+	fmt.Printf("[OK] Added item_effect column 
to items table\n")
+	return nil
+}
+
+// migrateItemsAddSpellDescription adds spe
ll_description column to items table if miss
ing
+func migrateItemsAddSpellDescription(db *sq
l.DB) error {
+	has, err := columnExists(db, "items", "spe
ll_description")
+	if err != nil {
+		return nil // table doesn't exist yet
+	}
+	if has {
+		return nil
+	}
+
+	fmt.Printf("Adding spell_description colum
n to items table...\n")
+	_, err = db.Exec(`ALTER TABLE items ADD CO
LUMN spell_description TEXT`)
+	if err != nil {
+		return fmt.Errorf("add spell_description 
column: %w", err)
+	}
+
+	fmt.Printf("[OK] Added spell_description c
olumn to items table\n")
+	return nil
+}
 

diff --git a/nix/pkgs/ookstats/src/internal/database/players.go b/nix/pkgs/ookstat
s/src/internal/database/players.go
index cac525a..0498fba 100644
--- a/nix/pkgs/ookstats/src/internal/database/players.go
+++ b/nix/pkgs/ookstats/src/internal/database/players.go
@@ -4,7 +4,6 @@ import (
 	"database/sql"
 	"fmt"
 	"ookstats/internal/blizzard"
-	"sort"
 	"strings"
 )
 
@@ -167,11 +166,20 @@ func (ds *DatabaseService) insertPlayerDetailsTx(tx *sql.Tx,
 playerID int, summa
 		avatarURL,
 		timestamp,
 	)
+	if err != nil {
+		return err
+	}
 
+	// always refresh last_updated to prevent re-fetching unchanged profiles
+	_, err = tx.Exec(
+		`UPDATE player_details SET last_updated = ? WHERE player_id = ?`,
+		timestamp, playerID,
+	)
 	return err
 }
 
-// insertPlayerEquipmentTx inserts player equipment data within a transaction
+// insertPlayerEquipmentTx upserts player equipment data within a transaction
+// Only stores current gear - one row per (player_id, slot_type)
 func (ds *DatabaseService) insertPlayerEquipmentTx(tx *sql.Tx, playerID int, equi
pment *blizzard.CharacterEquipmentResponse, timestamp int64) (int, error) {
 	if equipment == nil || len(equipment.EquippedItems) == 0 {
 		return 0, nil
@@ -180,102 +188,18 @@ func (ds *DatabaseService) insertPlayerEquipmentTx(tx *sql.
Tx, playerID int, equ
 	equipmentCount := 0
 
 	for _, item := range equipment.EquippedItems {
-		// check latest snapshot for this slot; skip writing if unchanged
-		var prevID sql.NullInt64
-		var prevItemID sql.NullInt64
-		var prevUpgradeID sql.NullInt64
-		var prevQuality, prevName sql.NullString
-		if err := tx.QueryRow(
-			`SELECT id, item_id, upgrade_id, quality, item_name
-             FROM player_equipment
-             WHERE player_id = ? AND slot_type = ?
-             ORDER BY snapshot_timestamp DESC
-             LIMIT 1`,
-			playerID, item.Slot.Type,
-		).Scan(&prevID, &prevItemID, &prevUpgradeID, &prevQuality, &prevName); err != n
il && err != sql.ErrNoRows {
-			return 0, fmt.Errorf("failed to query latest equipment: %w", err)
-		}
-
-		unchanged := false
-		if prevID.Valid {
-			prevUpg := 0
-			if prevUpgradeID.Valid {
-				prevUpg = int(prevUpgradeID.Int64)
-			}
-			curUpg := 0
-			if item.UpgradeID != nil {
-				curUpg = *item.UpgradeID
-			}
-			sameBasics := prevItemID.Valid && int(prevItemID.Int64) == item.Item.ID && pre
vQuality.Valid && prevQuality.String == item.Quality.Type && prevName.Valid && pre
vName.String == item.Name && prevUpg == curUpg
-
-			if sameBasics {
-				// compare enchantments as a canonical sorted signature
-				dbRows, qerr := tx.Query(
-					`SELECT
-                        COALESCE(enchantment_id, -1) as eid,
-                        COALESCE(source_item_id, -1) as sid,
-                        COALESCE(slot_id, -1) as slotId,
-                        COALESCE(slot_type, '') as slotType,
-                        COALESCE(spell_id, -1) as spellId,
-                        COALESCE(display_string, '') as disp
-                     FROM player_equipment_enchantments
-                     WHERE equipment_id = ?`, prevID.Int64)
-				if qerr != nil {
-					return 0, fmt.Errorf("failed to load existing enchantments: %w", qerr)
-				}
-				var dbSigs []string
-				for dbRows.Next() {
-					var eid, sid, slotId, spellId int
-					var slotType, disp string
-					if err := dbRows.Scan(&eid, &sid, &slotId, &slotType, &spellId, &disp); err 
!= nil {
-						dbRows.Close()
-						return 0, fmt.Errorf("failed to scan enchantment: %w", err)
-					}
-					dbSigs = append(dbSigs, fmt.Sprintf("%d|%d|%d|%s|%d|%s", eid, sid, slotId, s
lotType, spellId, disp))
-				}
-				dbRows.Close()
-				sort.Strings(dbSigs)
-
-				var curSigs []string
-				for _, ench := range item.Enchantments {
-					eid := -1
-					if ench.EnchantmentID != nil {
-						eid = *ench.EnchantmentID
-					}
-					sid := -1
-					if ench.SourceItem != nil {
-						sid = ench.SourceItem.ID
-					}
-					slotId := -1
-					var slotType string
-					if ench.EnchantmentSlot != nil {
-						slotId = ench.EnchantmentSlot.ID
-						slotType = ench.EnchantmentSlot.Type
-					}
-					spellId := -1
-					if ench.Spell != nil {
-						spellId = ench.Spell.Spell.ID
-					}
-					disp := ench.DisplayString
-					curSigs = append(curSigs, fmt.Sprintf("%d|%d|%d|%s|%d|%s", eid, sid, slotId,
 slotType, spellId, disp))
-				}
-				sort.Strings(curSigs)
-
-				if strings.Join(dbSigs, ";") == strings.Join(curSigs, ";") {
-					unchanged = true
-				}
-			}
-		}
-
-		if unchanged {
-			continue
-		}
-
+		// upsert equipment - replace if exists
 		result, err := tx.Exec(`
-            INSERT INTO player_equipment (
-                player_id, slot_type, item_id, upgrade_id, quality, item_name, sn
apshot_timestamp
-            ) VALUES (?, ?, ?, ?, ?, ?, ?)
-        `,
+			INSERT INTO player_equipment (
+				player_id, slot_type, item_id, upgrade_id, quality, item_name, snapshot_times
tamp
+			) VALUES (?, ?, ?, ?, ?, ?, ?)
+			ON CONFLICT(player_id, slot_type) DO UPDATE SET
+				item_id = excluded.item_id,
+				upgrade_id = excluded.upgrade_id,
+				quality = excluded.quality,
+				item_name = excluded.item_name,
+				snapshot_timestamp = excluded.snapshot_timestamp
+		`,
 			playerID,
 			item.Slot.Type,
 			item.Item.ID,
@@ -284,18 +208,32 @@ func (ds *DatabaseService) insertPlayerEquipmentTx(tx *sql.T
x, playerID int, equ
 			item.Name,
 			timestamp,
 		)
-
 		if err != nil {
-			return 0, fmt.Errorf("failed to insert equipment item: %w", err)
+			return 0, fmt.Errorf("failed to upsert equipment item: %w", err)
 		}
 
-		equipmentID, err := result.LastInsertId()
+		// get the equipment ID (works for both insert and update)
+		var equipmentID int64
+		err = tx.QueryRow(
+			`SELECT id FROM player_equipment WHERE player_id = ? AND slot_type = ?`,
+			playerID, item.Slot.Type,
+		).Scan(&equipmentID)
 		if err != nil {
 			return 0, fmt.Errorf("failed to get equipment ID: %w", err)
 		}
 
-		equipmentCount++
+		// delete old enchantments for this equipment slot
+		_, err = tx.Exec(`DELETE FROM player_equipment_enchantments WHERE equipment_id 
= ?`, equipmentID)
+		if err != nil {
+			return 0, fmt.Errorf("failed to delete old enchantments: %w", err)
+		}
 
+		rowsAffected, _ := result.RowsAffected()
+		if rowsAffected > 0 {
+			equipmentCount++
+		}
+
+		// insert new enchantments
 		for _, enchant := range item.Enchantments {
 			var sourceItemID *int
 			var sourceItemName *string
@@ -331,7 +269,6 @@ func (ds *DatabaseService) insertPlayerEquipmentTx(tx *sql.Tx,
 playerID int, equ
 				sourceItemName,
 				spellID,
 			)
-
 			if err != nil {
 				return 0, fmt.Errorf("failed to insert enchantment: %w", err)
 			}

diff --git a/nix/pkgs/ookstats/src/internal/
database/players.go b/nix/pkgs/ookstats/src/
internal/database/players.go
index cac525a..0498fba 100644
--- a/nix/pkgs/ookstats/src/internal/databas
e/players.go
+++ b/nix/pkgs/ookstats/src/internal/databas
e/players.go
@@ -4,7 +4,6 @@ import (
 	"database/sql"
 	"fmt"
 	"ookstats/internal/blizzard"
-	"sort"
 	"strings"
 )
 
@@ -167,11 +166,20 @@ func (ds *DatabaseServ
ice) insertPlayerDetailsTx(tx *sql.Tx, playe
rID int, summa
 		avatarURL,
 		timestamp,
 	)
+	if err != nil {
+		return err
+	}
 
+	// always refresh last_updated to prevent 
re-fetching unchanged profiles
+	_, err = tx.Exec(
+		`UPDATE player_details SET last_updated =
 ? WHERE player_id = ?`,
+		timestamp, playerID,
+	)
 	return err
 }
 
-// insertPlayerEquipmentTx inserts player e
quipment data within a transaction
+// insertPlayerEquipmentTx upserts player e
quipment data within a transaction
+// Only stores current gear - one row per (
player_id, slot_type)
 func (ds *DatabaseService) insertPlayerEqui
pmentTx(tx *sql.Tx, playerID int, equipment 
*blizzard.CharacterEquipmentResponse, timest
amp int64) (int, error) {
 	if equipment == nil || len(equipment.Equip
pedItems) == 0 {
 		return 0, nil
@@ -180,102 +188,18 @@ func (ds *DatabaseSer
vice) insertPlayerEquipmentTx(tx *sql.Tx, pl
ayerID int, equ
 	equipmentCount := 0
 
 	for _, item := range equipment.EquippedIte
ms {
-		// check latest snapshot for this slot; s
kip writing if unchanged
-		var prevID sql.NullInt64
-		var prevItemID sql.NullInt64
-		var prevUpgradeID sql.NullInt64
-		var prevQuality, prevName sql.NullString
-		if err := tx.QueryRow(
-			`SELECT id, item_id, upgrade_id, quality
, item_name
-             FROM player_equipment
-             WHERE player_id = ? AND slot_t
ype = ?
-             ORDER BY snapshot_timestamp DE
SC
-             LIMIT 1`,
-			playerID, item.Slot.Type,
-		).Scan(&prevID, &prevItemID, &prevUpgrade
ID, &prevQuality, &prevName); err != nil && 
err != sql.ErrNoRows {
-			return 0, fmt.Errorf("failed to query la
test equipment: %w", err)
-		}
-
-		unchanged := false
-		if prevID.Valid {
-			prevUpg := 0
-			if prevUpgradeID.Valid {
-				prevUpg = int(prevUpgradeID.Int64)
-			}
-			curUpg := 0
-			if item.UpgradeID != nil {
-				curUpg = *item.UpgradeID
-			}
-			sameBasics := prevItemID.Valid && int(pr
evItemID.Int64) == item.Item.ID && prevQuali
ty.Valid && prevQuality.String == item.Quali
ty.Type && prevName.Valid && prevName.String
 == item.Name && prevUpg == curUpg
-
-			if sameBasics {
-				// compare enchantments as a canonical 
sorted signature
-				dbRows, qerr := tx.Query(
-					`SELECT
-                        COALESCE(enchantmen
t_id, -1) as eid,
-                        COALESCE(source_ite
m_id, -1) as sid,
-                        COALESCE(slot_id, -
1) as slotId,
-                        COALESCE(slot_type,
 '') as slotType,
-                        COALESCE(spell_id, 
-1) as spellId,
-                        COALESCE(display_st
ring, '') as disp
-                     FROM player_equipment_
enchantments
-                     WHERE equipment_id = ?
`, prevID.Int64)
-				if qerr != nil {
-					return 0, fmt.Errorf("failed to load e
xisting enchantments: %w", qerr)
-				}
-				var dbSigs []string
-				for dbRows.Next() {
-					var eid, sid, slotId, spellId int
-					var slotType, disp string
-					if err := dbRows.Scan(&eid, &sid, &slo
tId, &slotType, &spellId, &disp); err != nil
 {
-						dbRows.Close()
-						return 0, fmt.Errorf("failed to scan 
enchantment: %w", err)
-					}
-					dbSigs = append(dbSigs, fmt.Sprintf("%
d|%d|%d|%s|%d|%s", eid, sid, slotId, slotTyp
e, spellId, disp))
-				}
-				dbRows.Close()
-				sort.Strings(dbSigs)
-
-				var curSigs []string
-				for _, ench := range item.Enchantments 
{
-					eid := -1
-					if ench.EnchantmentID != nil {
-						eid = *ench.EnchantmentID
-					}
-					sid := -1
-					if ench.SourceItem != nil {
-						sid = ench.SourceItem.ID
-					}
-					slotId := -1
-					var slotType string
-					if ench.EnchantmentSlot != nil {
-						slotId = ench.EnchantmentSlot.ID
-						slotType = ench.EnchantmentSlot.Type
-					}
-					spellId := -1
-					if ench.Spell != nil {
-						spellId = ench.Spell.Spell.ID
-					}
-					disp := ench.DisplayString
-					curSigs = append(curSigs, fmt.Sprintf(
"%d|%d|%d|%s|%d|%s", eid, sid, slotId, slotT
ype, spellId, disp))
-				}
-				sort.Strings(curSigs)
-
-				if strings.Join(dbSigs, ";") == strings
.Join(curSigs, ";") {
-					unchanged = true
-				}
-			}
-		}
-
-		if unchanged {
-			continue
-		}
-
+		// upsert equipment - replace if exists
 		result, err := tx.Exec(`
-            INSERT INTO player_equipment (
-                player_id, slot_type, item_
id, upgrade_id, quality, item_name, snapshot
_timestamp
-            ) VALUES (?, ?, ?, ?, ?, ?, ?)
-        `,
+			INSERT INTO player_equipment (
+				player_id, slot_type, item_id, upgrade_
id, quality, item_name, snapshot_timestamp
+			) VALUES (?, ?, ?, ?, ?, ?, ?)
+			ON CONFLICT(player_id, slot_type) DO UPD
ATE SET
+				item_id = excluded.item_id,
+				upgrade_id = excluded.upgrade_id,
+				quality = excluded.quality,
+				item_name = excluded.item_name,
+				snapshot_timestamp = excluded.snapshot_
timestamp
+		`,
 			playerID,
 			item.Slot.Type,
 			item.Item.ID,
@@ -284,18 +208,32 @@ func (ds *DatabaseServ
ice) insertPlayerEquipmentTx(tx *sql.Tx, pla
yerID int, equ
 			item.Name,
 			timestamp,
 		)
-
 		if err != nil {
-			return 0, fmt.Errorf("failed to insert e
quipment item: %w", err)
+			return 0, fmt.Errorf("failed to upsert e
quipment item: %w", err)
 		}
 
-		equipmentID, err := result.LastInsertId()
+		// get the equipment ID (works for both i
nsert and update)
+		var equipmentID int64
+		err = tx.QueryRow(
+			`SELECT id FROM player_equipment WHERE p
layer_id = ? AND slot_type = ?`,
+			playerID, item.Slot.Type,
+		).Scan(&equipmentID)
 		if err != nil {
 			return 0, fmt.Errorf("failed to get equi
pment ID: %w", err)
 		}
 
-		equipmentCount++
+		// delete old enchantments for this equip
ment slot
+		_, err = tx.Exec(`DELETE FROM player_equi
pment_enchantments WHERE equipment_id = ?`, 
equipmentID)
+		if err != nil {
+			return 0, fmt.Errorf("failed to delete o
ld enchantments: %w", err)
+		}
 
+		rowsAffected, _ := result.RowsAffected()
+		if rowsAffected > 0 {
+			equipmentCount++
+		}
+
+		// insert new enchantments
 		for _, enchant := range item.Enchantments
 {
 			var sourceItemID *int
 			var sourceItemName *string
@@ -331,7 +269,6 @@ func (ds *DatabaseServic
e) insertPlayerEquipmentTx(tx *sql.Tx, playe
rID int, equ
 				sourceItemName,
 				spellID,
 			)
-
 			if err != nil {
 				return 0, fmt.Errorf("failed to insert 
enchantment: %w", err)
 			}
 

diff --git a/nix/pkgs/ookstats/src/internal/database/schema.go b/nix/pkgs/ookstats
/src/internal/database/schema.go
index 0c809b7..ff93cb8 100644
--- a/nix/pkgs/ookstats/src/internal/database/schema.go
+++ b/nix/pkgs/ookstats/src/internal/database/schema.go
@@ -241,7 +241,9 @@ func EnsureCompleteSchema(db *sql.DB) error {
 			icon TEXT,
 			quality INTEGER,
 			type INTEGER,
-			stats TEXT
+			stats TEXT,
+			item_effect TEXT,
+			spell_description TEXT
 		)`,
 
 		// Season tables
@@ -292,6 +294,21 @@ func EnsureCompleteSchema(db *sql.DB) error {
 		return err
 	}
 
+	// Migrate equipment to single-row-per-slot model
+	if err := migrateEquipmentToUpsert(db); err != nil {
+		return err
+	}
+
+	// Add item_effect column to items table
+	if err := migrateItemsAddItemEffect(db); err != nil {
+		return err
+	}
+
+	// Add spell_description column to items table
+	if err := migrateItemsAddSpellDescription(db); err != nil {
+		return err
+	}
+
 	// Create indexes
 	return ensureRecommendedIndexes(db)
 }

diff --git a/nix/pkgs/ookstats/src/internal/
database/schema.go b/nix/pkgs/ookstats/src/i
nternal/database/schema.go
index 0c809b7..ff93cb8 100644
--- a/nix/pkgs/ookstats/src/internal/databas
e/schema.go
+++ b/nix/pkgs/ookstats/src/internal/databas
e/schema.go
@@ -241,7 +241,9 @@ func EnsureCompleteSchem
a(db *sql.DB) error {
 			icon TEXT,
 			quality INTEGER,
 			type INTEGER,
-			stats TEXT
+			stats TEXT,
+			item_effect TEXT,
+			spell_description TEXT
 		)`,
 
 		// Season tables
@@ -292,6 +294,21 @@ func EnsureCompleteSche
ma(db *sql.DB) error {
 		return err
 	}
 
+	// Migrate equipment to single-row-per-slo
t model
+	if err := migrateEquipmentToUpsert(db); er
r != nil {
+		return err
+	}
+
+	// Add item_effect column to items table
+	if err := migrateItemsAddItemEffect(db); e
rr != nil {
+		return err
+	}
+
+	// Add spell_description column to items t
able
+	if err := migrateItemsAddSpellDescription(
db); err != nil {
+		return err
+	}
+
 	// Create indexes
 	return ensureRecommendedIndexes(db)
 }
 

diff --git a/nix/pkgs/ookstats/src/internal/generator/players.go b/nix/pkgs/ooksta
ts/src/internal/generator/players.go
index e8ecc2d..95620a2 100644
--- a/nix/pkgs/ookstats/src/internal/generator/players.go
+++ b/nix/pkgs/ookstats/src/internal/generator/players.go
@@ -2,6 +2,7 @@ package generator
 
 import (
 	"database/sql"
+	"encoding/json"
 	"fmt"
 	"ookstats/internal/loader"
 	"ookstats/internal/utils"
@@ -135,7 +136,7 @@ func GeneratePlayers(db *sql.DB, out string, version string) e
rror {
 }
 
 // GeneratePlayerJSONs generates JSON files for all players concurrently
-func GeneratePlayerJSONs(players []loader.PlayerData, playerSeasonsMap map[int64]
[]loader.PlayerSeasonData, bestRunsMap map[int64][]loader.BestRunData, teamMembers
Map map[int64][]loader.TeamMemberData, equipmentMap map[int64]map[int64][]loader.E
quipmentData, enchantmentsMap map[int64][]loader.EnchantmentData, out, version str
ing) error {
+func GeneratePlayerJSONs(players []loader.PlayerData, playerSeasonsMap map[int64]
[]loader.PlayerSeasonData, bestRunsMap map[int64][]loader.BestRunData, teamMembers
Map map[int64][]loader.TeamMemberData, equipmentMap map[int64][]loader.EquipmentDa
ta, enchantmentsMap map[int64][]loader.EnchantmentData, out, version string) error
 {
 	startTime := time.Now()
 	const batchSize = 100
 	const numWorkers = 10
@@ -192,7 +193,7 @@ func GeneratePlayerJSONs(players []loader.PlayerData, playerSe
asonsMap map[int64
 }
 
 // generateSinglePlayerJSON generates a JSON file for a single player
-func generateSinglePlayerJSON(player loader.PlayerData, playerSeasonsMap map[int6
4][]loader.PlayerSeasonData, bestRunsMap map[int64][]loader.BestRunData, teamMembe
rsMap map[int64][]loader.TeamMemberData, equipmentMap map[int64]map[int64][]loader
.EquipmentData, enchantmentsMap map[int64][]loader.EnchantmentData, out, version s
tring) error {
+func generateSinglePlayerJSON(player loader.PlayerData, playerSeasonsMap map[int6
4][]loader.PlayerSeasonData, bestRunsMap map[int64][]loader.BestRunData, teamMembe
rsMap map[int64][]loader.TeamMemberData, equipmentMap map[int64][]loader.Equipment
Data, enchantmentsMap map[int64][]loader.EnchantmentData, out, version string) err
or {
 	// Build PlayerJSON with base info
 	pj := PlayerJSON{
 		ID:             player.ID,
@@ -319,66 +320,78 @@ func generateSinglePlayerJSON(player loader.PlayerData, play
erSeasonsMap map[int
 
 	// Build equipment
 	equipment := make(map[string]any)
-	for _, eqList := range equipmentMap[player.ID] {
-		for _, eq := range eqList {
-			eqData := map[string]any{
-				"id":                 eq.ID,
-				"slot_type":          eq.SlotType,
-				"item_id":            nil,
-				"upgrade_id":         nil,
-				"quality":            eq.Quality,
-				"item_name":          eq.ItemName,
-				"snapshot_timestamp": eq.SnapshotTs,
-				"item_icon_slug":     eq.ItemIcon.String,
-				"item_type":          eq.ItemType.String,
-				"enchantments":       []map[string]any{},
-			}
+	for _, eq := range equipmentMap[player.ID] {
+		eqData := map[string]any{
+			"id":                 eq.ID,
+			"slot_type":          eq.SlotType,
+			"item_id":            nil,
+			"upgrade_id":         nil,
+			"quality":            eq.Quality,
+			"item_name":          eq.ItemName,
+			"snapshot_timestamp": eq.SnapshotTs,
+			"item_icon_slug":     eq.ItemIcon.String,
+			"item_type":          eq.ItemType.String,
+			"enchantments":       []map[string]any{},
+		}
 
-			if eq.ItemID.Valid {
-				eqData["item_id"] = int(eq.ItemID.Int64)
+		if eq.ItemID.Valid {
+			eqData["item_id"] = int(eq.ItemID.Int64)
+		}
+		if eq.UpgradeID.Valid {
+			eqData["upgrade_id"] = int(eq.UpgradeID.Int64)
+		}
+		if eq.ItemStats.Valid && eq.ItemStats.String != "" && eq.ItemStats.String != "{
}" {
+			var scalingOpts any
+			if err := json.Unmarshal([]byte(eq.ItemStats.String), &scalingOpts); err == ni
l {
+				eqData["scaling_options"] = scalingOpts
 			}
-			if eq.UpgradeID.Valid {
-				eqData["upgrade_id"] = int(eq.UpgradeID.Int64)
+		}
+		if eq.ItemEffect.Valid && eq.ItemEffect.String != "" {
+			var itemEffect any
+			if err := json.Unmarshal([]byte(eq.ItemEffect.String), &itemEffect); err == ni
l {
+				eqData["item_effect"] = itemEffect
 			}
+		}
+		if eq.SpellDescription.Valid && eq.SpellDescription.String != "" {
+			eqData["spell_description"] = eq.SpellDescription.String
+		}
 
-			// Add enchantments
-			for _, ench := range enchantmentsMap[eq.ID] {
-				enchData := map[string]any{
-					"enchantment_id":   nil,
-					"slot_id":          nil,
-					"slot_type":        ench.SlotType.String,
-					"display_string":   ench.DisplayString.String,
-					"source_item_id":   nil,
-					"source_item_name": ench.SourceItemName.String,
-					"spell_id":         nil,
-				}
-
-				if ench.EnchantmentID.Valid {
-					enchData["enchantment_id"] = int(ench.EnchantmentID.Int64)
-				}
-				if ench.SlotID.Valid {
-					enchData["slot_id"] = int(ench.SlotID.Int64)
-				}
-				if ench.SourceItemID.Valid {
-					enchData["source_item_id"] = int(ench.SourceItemID.Int64)
-				}
-				if ench.SpellID.Valid {
-					enchData["spell_id"] = int(ench.SpellID.Int64)
-				}
-				if ench.GemIconSlug.Valid {
-					enchData["gem_icon_slug"] = ench.GemIconSlug.String
-				}
+		// Add enchantments
+		for _, ench := range enchantmentsMap[eq.ID] {
+			enchData := map[string]any{
+				"enchantment_id":   nil,
+				"slot_id":          nil,
+				"slot_type":        ench.SlotType.String,
+				"display_string":   ench.DisplayString.String,
+				"source_item_id":   nil,
+				"source_item_name": ench.SourceItemName.String,
+				"spell_id":         nil,
+			}
 
-				if arr, ok := eqData["enchantments"].([]map[string]any); ok {
-					eqData["enchantments"] = append(arr, enchData)
-				} else {
-					eqData["enchantments"] = []map[string]any{enchData}
-				}
+			if ench.EnchantmentID.Valid {
+				enchData["enchantment_id"] = int(ench.EnchantmentID.Int64)
+			}
+			if ench.SlotID.Valid {
+				enchData["slot_id"] = int(ench.SlotID.Int64)
+			}
+			if ench.SourceItemID.Valid {
+				enchData["source_item_id"] = int(ench.SourceItemID.Int64)
+			}
+			if ench.SpellID.Valid {
+				enchData["spell_id"] = int(ench.SpellID.Int64)
+			}
+			if ench.GemIconSlug.Valid {
+				enchData["gem_icon_slug"] = ench.GemIconSlug.String
 			}
 
-			equipment[eq.SlotType] = eqData
+			if arr, ok := eqData["enchantments"].([]map[string]any); ok {
+				eqData["enchantments"] = append(arr, enchData)
+			} else {
+				eqData["enchantments"] = []map[string]any{enchData}
+			}
 		}
-		break // Only process the latest timestamp
+
+		equipment[eq.SlotType] = eqData
 	}
 
 	// Create final JSON

diff --git a/nix/pkgs/ookstats/src/internal/
generator/players.go b/nix/pkgs/ookstats/src
/internal/generator/players.go
index e8ecc2d..95620a2 100644
--- a/nix/pkgs/ookstats/src/internal/generat
or/players.go
+++ b/nix/pkgs/ookstats/src/internal/generat
or/players.go
@@ -2,6 +2,7 @@ package generator
 
 import (
 	"database/sql"
+	"encoding/json"
 	"fmt"
 	"ookstats/internal/loader"
 	"ookstats/internal/utils"
@@ -135,7 +136,7 @@ func GeneratePlayers(db 
*sql.DB, out string, version string) error {
 }
 
 // GeneratePlayerJSONs generates JSON files
 for all players concurrently
-func GeneratePlayerJSONs(players []loader.P
layerData, playerSeasonsMap map[int64][]load
er.PlayerSeasonData, bestRunsMap map[int64][
]loader.BestRunData, teamMembersMap map[int6
4][]loader.TeamMemberData, equipmentMap map[
int64]map[int64][]loader.EquipmentData, ench
antmentsMap map[int64][]loader.EnchantmentDa
ta, out, version string) error {
+func GeneratePlayerJSONs(players []loader.P
layerData, playerSeasonsMap map[int64][]load
er.PlayerSeasonData, bestRunsMap map[int64][
]loader.BestRunData, teamMembersMap map[int6
4][]loader.TeamMemberData, equipmentMap map[
int64][]loader.EquipmentData, enchantmentsMa
p map[int64][]loader.EnchantmentData, out, v
ersion string) error {
 	startTime := time.Now()
 	const batchSize = 100
 	const numWorkers = 10
@@ -192,7 +193,7 @@ func GeneratePlayerJSONs
(players []loader.PlayerData, playerSeasonsM
ap map[int64
 }
 
 // generateSinglePlayerJSON generates a JSO
N file for a single player
-func generateSinglePlayerJSON(player loader
.PlayerData, playerSeasonsMap map[int64][]lo
ader.PlayerSeasonData, bestRunsMap map[int64
][]loader.BestRunData, teamMembersMap map[in
t64][]loader.TeamMemberData, equipmentMap ma
p[int64]map[int64][]loader.EquipmentData, en
chantmentsMap map[int64][]loader.Enchantment
Data, out, version string) error {
+func generateSinglePlayerJSON(player loader
.PlayerData, playerSeasonsMap map[int64][]lo
ader.PlayerSeasonData, bestRunsMap map[int64
][]loader.BestRunData, teamMembersMap map[in
t64][]loader.TeamMemberData, equipmentMap ma
p[int64][]loader.EquipmentData, enchantments
Map map[int64][]loader.EnchantmentData, out,
 version string) error {
 	// Build PlayerJSON with base info
 	pj := PlayerJSON{
 		ID:             player.ID,
@@ -319,66 +320,78 @@ func generateSinglePla
yerJSON(player loader.PlayerData, playerSeas
onsMap map[int
 
 	// Build equipment
 	equipment := make(map[string]any)
-	for _, eqList := range equipmentMap[player
.ID] {
-		for _, eq := range eqList {
-			eqData := map[string]any{
-				"id":                 eq.ID,
-				"slot_type":          eq.SlotType,
-				"item_id":            nil,
-				"upgrade_id":         nil,
-				"quality":            eq.Quality,
-				"item_name":          eq.ItemName,
-				"snapshot_timestamp": eq.SnapshotTs,
-				"item_icon_slug":     eq.ItemIcon.Strin
g,
-				"item_type":          eq.ItemType.Strin
g,
-				"enchantments":       []map[string]any{
},
-			}
+	for _, eq := range equipmentMap[player.ID]
 {
+		eqData := map[string]any{
+			"id":                 eq.ID,
+			"slot_type":          eq.SlotType,
+			"item_id":            nil,
+			"upgrade_id":         nil,
+			"quality":            eq.Quality,
+			"item_name":          eq.ItemName,
+			"snapshot_timestamp": eq.SnapshotTs,
+			"item_icon_slug":     eq.ItemIcon.String
,
+			"item_type":          eq.ItemType.String
,
+			"enchantments":       []map[string]any{}
,
+		}
 
-			if eq.ItemID.Valid {
-				eqData["item_id"] = int(eq.ItemID.Int64
)
+		if eq.ItemID.Valid {
+			eqData["item_id"] = int(eq.ItemID.Int64)
+		}
+		if eq.UpgradeID.Valid {
+			eqData["upgrade_id"] = int(eq.UpgradeID.
Int64)
+		}
+		if eq.ItemStats.Valid && eq.ItemStats.Str
ing != "" && eq.ItemStats.String != "{}" {
+			var scalingOpts any
+			if err := json.Unmarshal([]byte(eq.ItemS
tats.String), &scalingOpts); err == nil {
+				eqData["scaling_options"] = scalingOpts
 			}
-			if eq.UpgradeID.Valid {
-				eqData["upgrade_id"] = int(eq.UpgradeID
.Int64)
+		}
+		if eq.ItemEffect.Valid && eq.ItemEffect.S
tring != "" {
+			var itemEffect any
+			if err := json.Unmarshal([]byte(eq.ItemE
ffect.String), &itemEffect); err == nil {
+				eqData["item_effect"] = itemEffect
 			}
+		}
+		if eq.SpellDescription.Valid && eq.SpellD
escription.String != "" {
+			eqData["spell_description"] = eq.SpellDe
scription.String
+		}
 
-			// Add enchantments
-			for _, ench := range enchantmentsMap[eq.
ID] {
-				enchData := map[string]any{
-					"enchantment_id":   nil,
-					"slot_id":          nil,
-					"slot_type":        ench.SlotType.Stri
ng,
-					"display_string":   ench.DisplayString
.String,
-					"source_item_id":   nil,
-					"source_item_name": ench.SourceItemNam
e.String,
-					"spell_id":         nil,
-				}
-
-				if ench.EnchantmentID.Valid {
-					enchData["enchantment_id"] = int(ench.
EnchantmentID.Int64)
-				}
-				if ench.SlotID.Valid {
-					enchData["slot_id"] = int(ench.SlotID.
Int64)
-				}
-				if ench.SourceItemID.Valid {
-					enchData["source_item_id"] = int(ench.
SourceItemID.Int64)
-				}
-				if ench.SpellID.Valid {
-					enchData["spell_id"] = int(ench.SpellI
D.Int64)
-				}
-				if ench.GemIconSlug.Valid {
-					enchData["gem_icon_slug"] = ench.GemIc
onSlug.String
-				}
+		// Add enchantments
+		for _, ench := range enchantmentsMap[eq.I
D] {
+			enchData := map[string]any{
+				"enchantment_id":   nil,
+				"slot_id":          nil,
+				"slot_type":        ench.SlotType.Strin
g,
+				"display_string":   ench.DisplayString.
String,
+				"source_item_id":   nil,
+				"source_item_name": ench.SourceItemName
.String,
+				"spell_id":         nil,
+			}
 
-				if arr, ok := eqData["enchantments"].([
]map[string]any); ok {
-					eqData["enchantments"] = append(arr, e
nchData)
-				} else {
-					eqData["enchantments"] = []map[string]
any{enchData}
-				}
+			if ench.EnchantmentID.Valid {
+				enchData["enchantment_id"] = int(ench.E
nchantmentID.Int64)
+			}
+			if ench.SlotID.Valid {
+				enchData["slot_id"] = int(ench.SlotID.I
nt64)
+			}
+			if ench.SourceItemID.Valid {
+				enchData["source_item_id"] = int(ench.S
ourceItemID.Int64)
+			}
+			if ench.SpellID.Valid {
+				enchData["spell_id"] = int(ench.SpellID
.Int64)
+			}
+			if ench.GemIconSlug.Valid {
+				enchData["gem_icon_slug"] = ench.GemIco
nSlug.String
 			}
 
-			equipment[eq.SlotType] = eqData
+			if arr, ok := eqData["enchantments"].([]
map[string]any); ok {
+				eqData["enchantments"] = append(arr, en
chData)
+			} else {
+				eqData["enchantments"] = []map[string]a
ny{enchData}
+			}
 		}
-		break // Only process the latest timestam
p
+
+		equipment[eq.SlotType] = eqData
 	}
 
 	// Create final JSON
 

diff --git a/nix/pkgs/ookstats/src/internal/loader/equipment.go b/nix/pkgs/ookstat
s/src/internal/loader/equipment.go
index cecf405..042d51b 100644
--- a/nix/pkgs/ookstats/src/internal/loader/equipment.go
+++ b/nix/pkgs/ookstats/src/internal/loader/equipment.go
@@ -8,15 +8,18 @@ import (
 
 // EquipmentData represents a piece of equipment
 type EquipmentData struct {
-	ID         int64
-	SlotType   string
-	ItemID     sql.NullInt64
-	UpgradeID  sql.NullInt64
-	Quality    string
-	ItemName   string
-	SnapshotTs int64
-	ItemIcon   sql.NullString
-	ItemType   sql.NullString
+	ID               int64
+	SlotType         string
+	ItemID           sql.NullInt64
+	UpgradeID        sql.NullInt64
+	Quality          string
+	ItemName         string
+	SnapshotTs       int64
+	ItemIcon         sql.NullString
+	ItemType         sql.NullString
+	ItemStats        sql.NullString
+	ItemEffect       sql.NullString
+	SpellDescription sql.NullString
 }
 
 // EnchantmentData represents an enchantment or gem on equipment
@@ -33,10 +36,10 @@ type EnchantmentData struct {
 }
 
 // LoadAllEquipment loads equipment and enchantments for a set of players
-// Returns: map[playerID]map[timestamp][]EquipmentData, map[equipmentID][]Enchant
mentData, error
-func LoadAllEquipment(db *sql.DB, playerIDs []int64) (map[int64]map[int64][]Equip
mentData, map[int64][]EnchantmentData, error) {
+// Returns: map[playerID][]EquipmentData, map[equipmentID][]EnchantmentData, erro
r
+func LoadAllEquipment(db *sql.DB, playerIDs []int64) (map[int64][]EquipmentData, 
map[int64][]EnchantmentData, error) {
 	if len(playerIDs) == 0 {
-		return make(map[int64]map[int64][]EquipmentData), make(map[int64][]EnchantmentD
ata), nil
+		return make(map[int64][]EquipmentData), make(map[int64][]EnchantmentData), nil
 	}
 
 	// Build IN clause
@@ -47,62 +50,20 @@ func LoadAllEquipment(db *sql.DB, playerIDs []int64) (map[int6
4]map[int64][]Equi
 		args[i] = id
 	}
 
-	// Get latest timestamp per player first
-	latestQuery := fmt.Sprintf(`
-        SELECT player_id, MAX(snapshot_timestamp) 
-        FROM player_equipment 
-        WHERE player_id IN (%s) 
-        GROUP BY player_id
-    `, strings.Join(placeholders, ","))
-
-	latestRows, err := db.Query(latestQuery, args...)
-	if err != nil {
-		return nil, nil, err
-	}
-	defer latestRows.Close()
-
-	playerTimestamps := make(map[int64]int64)
-	for latestRows.Next() {
-		var playerID, timestamp int64
-		if err := latestRows.Scan(&playerID, &timestamp); err != nil {
-			return nil, nil, err
-		}
-		playerTimestamps[playerID] = timestamp
-	}
-
-	if len(playerTimestamps) == 0 {
-		return make(map[int64]map[int64][]EquipmentData), make(map[int64][]EnchantmentD
ata), nil
-	}
-
-	// Load equipment for latest timestamps in a single batched query
-	equipmentMap := make(map[int64]map[int64][]EquipmentData)
+	equipmentMap := make(map[int64][]EquipmentData)
 	var allEquipmentIDs []int64
 
-	// Build VALUES clause for all player/timestamp pairs
-	valuesBuilder := strings.Builder{}
-	eqArgs := make([]any, 0, len(playerTimestamps)*2)
-	first := true
-	for playerID, timestamp := range playerTimestamps {
-		if !first {
-			valuesBuilder.WriteString(",")
-		}
-		valuesBuilder.WriteString("(?,?)")
-		eqArgs = append(eqArgs, playerID, timestamp)
-		first = false
-	}
-
-	// Single query to load all equipment
+	// Simple query - one row per (player_id, slot_type)
 	eqQuery := fmt.Sprintf(`
-		WITH pairs(player_id, ts) AS (VALUES %s)
 		SELECT e.player_id, e.id, e.slot_type, e.item_id, e.upgrade_id, e.quality, e.it
em_name, e.snapshot_timestamp,
-		       i.icon AS item_icon_slug, i.type AS item_type
-		FROM pairs
-		JOIN player_equipment e ON e.player_id = pairs.player_id AND e.snapshot_timesta
mp = pairs.ts
+		       i.icon AS item_icon_slug, i.type AS item_type, i.stats AS item_stats, i.
item_effect, i.spell_description
+		FROM player_equipment e
 		LEFT JOIN items i ON e.item_id = i.id
+		WHERE e.player_id IN (%s)
 		ORDER BY e.player_id, e.slot_type
-	`, valuesBuilder.String())
+	`, strings.Join(placeholders, ","))
 
-	eqRows, err := db.Query(eqQuery, eqArgs...)
+	eqRows, err := db.Query(eqQuery, args...)
 	if err != nil {
 		return nil, nil, fmt.Errorf("batch equipment query: %w", err)
 	}
@@ -113,14 +74,11 @@ func LoadAllEquipment(db *sql.DB, playerIDs []int64) (map[int
64]map[int64][]Equi
 		var eq EquipmentData
 		if err := eqRows.Scan(
 			&playerID, &eq.ID, &eq.SlotType, &eq.ItemID, &eq.UpgradeID, &eq.Quality, &eq.I
temName, &eq.SnapshotTs,
-			&eq.ItemIcon, &eq.ItemType); err != nil {
+			&eq.ItemIcon, &eq.ItemType, &eq.ItemStats, &eq.ItemEffect, &eq.SpellDescriptio
n); err != nil {
 			return nil, nil, fmt.Errorf("scan equipment: %w", err)
 		}
 
-		if equipmentMap[playerID] == nil {
-			equipmentMap[playerID] = make(map[int64][]EquipmentData)
-		}
-		equipmentMap[playerID][eq.SnapshotTs] = append(equipmentMap[playerID][eq.Snapsh
otTs], eq)
+		equipmentMap[playerID] = append(equipmentMap[playerID], eq)
 		allEquipmentIDs = append(allEquipmentIDs, eq.ID)
 	}
 

diff --git a/nix/pkgs/ookstats/src/internal/
loader/equipment.go b/nix/pkgs/ookstats/src/
internal/loader/equipment.go
index cecf405..042d51b 100644
--- a/nix/pkgs/ookstats/src/internal/loader/
equipment.go
+++ b/nix/pkgs/ookstats/src/internal/loader/
equipment.go
@@ -8,15 +8,18 @@ import (
 
 // EquipmentData represents a piece of equi
pment
 type EquipmentData struct {
-	ID         int64
-	SlotType   string
-	ItemID     sql.NullInt64
-	UpgradeID  sql.NullInt64
-	Quality    string
-	ItemName   string
-	SnapshotTs int64
-	ItemIcon   sql.NullString
-	ItemType   sql.NullString
+	ID               int64
+	SlotType         string
+	ItemID           sql.NullInt64
+	UpgradeID        sql.NullInt64
+	Quality          string
+	ItemName         string
+	SnapshotTs       int64
+	ItemIcon         sql.NullString
+	ItemType         sql.NullString
+	ItemStats        sql.NullString
+	ItemEffect       sql.NullString
+	SpellDescription sql.NullString
 }
 
 // EnchantmentData represents an enchantmen
t or gem on equipment
@@ -33,10 +36,10 @@ type EnchantmentData str
uct {
 }
 
 // LoadAllEquipment loads equipment and enc
hantments for a set of players
-// Returns: map[playerID]map[timestamp][]Eq
uipmentData, map[equipmentID][]EnchantmentDa
ta, error
-func LoadAllEquipment(db *sql.DB, playerIDs
 []int64) (map[int64]map[int64][]EquipmentDa
ta, map[int64][]EnchantmentData, error) {
+// Returns: map[playerID][]EquipmentData, m
ap[equipmentID][]EnchantmentData, error
+func LoadAllEquipment(db *sql.DB, playerIDs
 []int64) (map[int64][]EquipmentData, map[in
t64][]EnchantmentData, error) {
 	if len(playerIDs) == 0 {
-		return make(map[int64]map[int64][]Equipme
ntData), make(map[int64][]EnchantmentData), 
nil
+		return make(map[int64][]EquipmentData), m
ake(map[int64][]EnchantmentData), nil
 	}
 
 	// Build IN clause
@@ -47,62 +50,20 @@ func LoadAllEquipment(db
 *sql.DB, playerIDs []int64) (map[int64]map[
int64][]Equi
 		args[i] = id
 	}
 
-	// Get latest timestamp per player first
-	latestQuery := fmt.Sprintf(`
-        SELECT player_id, MAX(snapshot_time
stamp) 
-        FROM player_equipment 
-        WHERE player_id IN (%s) 
-        GROUP BY player_id
-    `, strings.Join(placeholders, ","))
-
-	latestRows, err := db.Query(latestQuery, a
rgs...)
-	if err != nil {
-		return nil, nil, err
-	}
-	defer latestRows.Close()
-
-	playerTimestamps := make(map[int64]int64)
-	for latestRows.Next() {
-		var playerID, timestamp int64
-		if err := latestRows.Scan(&playerID, &tim
estamp); err != nil {
-			return nil, nil, err
-		}
-		playerTimestamps[playerID] = timestamp
-	}
-
-	if len(playerTimestamps) == 0 {
-		return make(map[int64]map[int64][]Equipme
ntData), make(map[int64][]EnchantmentData), 
nil
-	}
-
-	// Load equipment for latest timestamps in
 a single batched query
-	equipmentMap := make(map[int64]map[int64][
]EquipmentData)
+	equipmentMap := make(map[int64][]Equipment
Data)
 	var allEquipmentIDs []int64
 
-	// Build VALUES clause for all player/time
stamp pairs
-	valuesBuilder := strings.Builder{}
-	eqArgs := make([]any, 0, len(playerTimesta
mps)*2)
-	first := true
-	for playerID, timestamp := range playerTim
estamps {
-		if !first {
-			valuesBuilder.WriteString(",")
-		}
-		valuesBuilder.WriteString("(?,?)")
-		eqArgs = append(eqArgs, playerID, timesta
mp)
-		first = false
-	}
-
-	// Single query to load all equipment
+	// Simple query - one row per (player_id, 
slot_type)
 	eqQuery := fmt.Sprintf(`
-		WITH pairs(player_id, ts) AS (VALUES %s)
 		SELECT e.player_id, e.id, e.slot_type, e.
item_id, e.upgrade_id, e.quality, e.item_nam
e, e.snapshot_timestamp,
-		       i.icon AS item_icon_slug, i.type A
S item_type
-		FROM pairs
-		JOIN player_equipment e ON e.player_id = 
pairs.player_id AND e.snapshot_timestamp = p
airs.ts
+		       i.icon AS item_icon_slug, i.type A
S item_type, i.stats AS item_stats, i.item_e
ffect, i.spell_description
+		FROM player_equipment e
 		LEFT JOIN items i ON e.item_id = i.id
+		WHERE e.player_id IN (%s)
 		ORDER BY e.player_id, e.slot_type
-	`, valuesBuilder.String())
+	`, strings.Join(placeholders, ","))
 
-	eqRows, err := db.Query(eqQuery, eqArgs...
)
+	eqRows, err := db.Query(eqQuery, args...)
 	if err != nil {
 		return nil, nil, fmt.Errorf("batch equipm
ent query: %w", err)
 	}
@@ -113,14 +74,11 @@ func LoadAllEquipment(d
b *sql.DB, playerIDs []int64) (map[int64]map
[int64][]Equi
 		var eq EquipmentData
 		if err := eqRows.Scan(
 			&playerID, &eq.ID, &eq.SlotType, &eq.Ite
mID, &eq.UpgradeID, &eq.Quality, &eq.ItemNam
e, &eq.SnapshotTs,
-			&eq.ItemIcon, &eq.ItemType); err != nil 
{
+			&eq.ItemIcon, &eq.ItemType, &eq.ItemStat
s, &eq.ItemEffect, &eq.SpellDescription); er
r != nil {
 			return nil, nil, fmt.Errorf("scan equipm
ent: %w", err)
 		}
 
-		if equipmentMap[playerID] == nil {
-			equipmentMap[playerID] = make(map[int64]
[]EquipmentData)
-		}
-		equipmentMap[playerID][eq.SnapshotTs] = a
ppend(equipmentMap[playerID][eq.SnapshotTs],
 eq)
+		equipmentMap[playerID] = append(equipment
Map[playerID], eq)
 		allEquipmentIDs = append(allEquipmentIDs,
 eq.ID)
 	}
 
 
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET