OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
HASH      dbaf9e6de39c
DATE      2025-11-18
SUBJECT   nix: compute player class brackets
FILES     10 CHANGED
HASH      dbaf9e6de39c
DATE      2025-11-18
SUBJECT   nix: compute player class brackets
FILES     10 CHANGED
 

diff --git a/nix/pkgs/ookstats/default.nix b/nix/pkgs/ookstats/default.nix
index 2d018ea..06006b2 100644
--- a/nix/pkgs/ookstats/default.nix
+++ b/nix/pkgs/ookstats/default.nix
@@ -24,7 +24,7 @@ in
     version = "0.2.0";
     src = ./src;
 
-    vendorHash = "sha256-o4h4etGvTukPC/EixnCP+Ik++odm4ckozexkUB6Sv5g=";
+    vendorHash = "sha256-DHj4DTsXkFQZUimMrnqYc6RVTfsbukBmUcYt/dRwKUo=";
 
     nativeBuildInputs = [gcc installShellFiles makeWrapper];
 

diff --git a/nix/pkgs/ookstats/default.nix b
/nix/pkgs/ookstats/default.nix
index 2d018ea..06006b2 100644
--- a/nix/pkgs/ookstats/default.nix
+++ b/nix/pkgs/ookstats/default.nix
@@ -24,7 +24,7 @@ in
     version = "0.2.0";
     src = ./src;
 
-    vendorHash = "sha256-o4h4etGvTukPC/Eixn
CP+Ik++odm4ckozexkUB6Sv5g=";
+    vendorHash = "sha256-DHj4DTsXkFQZUimMrn
qYc6RVTfsbukBmUcYt/dRwKUo=";
 
     nativeBuildInputs = [gcc installShellFi
les makeWrapper];
 
 

diff --git a/nix/pkgs/ookstats/src/cmd/analyze.go b/nix/pkgs/ookstats/src/cmd/anal
yze.go
index 6a30341..9fe9a97 100644
--- a/nix/pkgs/ookstats/src/cmd/analyze.go
+++ b/nix/pkgs/ookstats/src/cmd/analyze.go
@@ -13,6 +13,7 @@ import (
     "strings"
     "time"
 
+    "github.com/charmbracelet/log"
     "github.com/spf13/cobra"
     "ookstats/internal/blizzard"
     "ookstats/internal/database"
@@ -96,7 +97,7 @@ func init() {
 
 func runAnalyze(db *sql.DB, client *blizzard.Client, realms map[string]blizzard.R
ealmInfo, dungeons []blizzard.DungeonInfo, periodsSpec string, outPath, statusDir 
string) error {
         dbService := database.NewDatabaseService(db)
-        fmt.Printf("Analyze: %d realms, %d dungeons\n", len(realms), len(dungeons
))
+        log.Info("analyze", "realms", len(realms), "dungeons", len(dungeons))
 
         type latest struct{
             Region        string `json:"region"`
@@ -148,7 +149,9 @@ func runAnalyze(db *sql.DB, client *blizzard.Client, realms ma
p[string]blizzard.
 
         // Process each region independently
         for region, regionRealms := range realmsByRegion {
-            fmt.Printf("\n========== Region: %s (%d realms) ==========\n", string
s.ToUpper(region), len(regionRealms))
+            log.Info("processing region",
+                "region", strings.ToUpper(region),
+                "realms", len(regionRealms))
 
             // Determine periods for this region
             var periods []string
@@ -158,16 +161,23 @@ func runAnalyze(db *sql.DB, client *blizzard.Client, realms 
map[string]blizzard.
                 // User specified periods (support ranges like "1020-1036")
                 periods, err = blizzard.ParsePeriods(periodsSpec)
                 if err != nil {
-                    fmt.Printf("Failed to parse periods for %s: %v - skipping reg
ion\n", strings.ToUpper(region), err)
+                    log.Error("failed to parse periods - skipping region",
+                        "region", strings.ToUpper(region),
+                        "error", err)
                     continue
                 }
-                fmt.Printf("Using user-specified periods: %v (%d periods)\n", per
iods, len(periods))
+                log.Info("using user-specified periods",
+                    "periods", periods,
+                    "count", len(periods))
             } else {
                 // Fetch periods from database (populated by season sync)
-                fmt.Printf("Fetching period list from database for %s...\n", stri
ngs.ToUpper(region))
+                log.Info("fetching period list from database",
+                    "region", strings.ToUpper(region))
                 periodInts, err := dbService.GetPeriodsForRegion(region)
                 if err != nil {
-                    fmt.Printf("Failed to fetch periods from database for %s: %v 
- skipping region\n", strings.ToUpper(region), err)
+                    log.Error("failed to fetch periods from database - skipping r
egion",
+                        "region", strings.ToUpper(region),
+                        "error", err)
                     continue
                 }
 
@@ -178,16 +188,21 @@ func runAnalyze(db *sql.DB, client *blizzard.Client, realms 
map[string]blizzard.
                 }
 
                 if len(periods) == 0 {
-                    fmt.Printf("No periods found in database for %s (run season s
ync first) - skipping region\n", strings.ToUpper(region))
+                    log.Warn("no periods found in database - skipping region",
+                        "region", strings.ToUpper(region))
                     continue
                 }
 
-                fmt.Printf("[OK] Fetched %d periods from database for %s (newest:
 %s, oldest: %s)\n",
-                    len(periods), strings.ToUpper(region), periods[0], periods[le
n(periods)-1])
+                log.Info("fetched periods from database",
+                    "count", len(periods),
+                    "region", strings.ToUpper(region),
+                    "newest", periods[0],
+                    "oldest", periods[len(periods)-1])
             }
 
             if len(periods) == 0 {
-                fmt.Printf("No periods to process for %s - skipping region\n", st
rings.ToUpper(region))
+                log.Warn("no periods to process - skipping region",
+                    "region", strings.ToUpper(region))
                 continue
             }
 
@@ -195,9 +210,14 @@ func runAnalyze(db *sql.DB, client *blizzard.Client, realms m
ap[string]blizzard.
 
             // Iterate periods for this region
             for _, period := range periods {
-                fmt.Printf("\n--- %s Period %s ---\n", strings.ToUpper(region), p
eriod)
+                log.Info("processing period",
+                    "region", strings.ToUpper(region),
+                    "period", period)
                 expected := len(regionRealms) * len(dungeons)
-                fmt.Printf("Expecting %d requests (%d realms ? %d dungeons)\n", e
xpected, len(regionRealms), len(dungeons))
+                log.Debug("expecting requests",
+                    "count", expected,
+                    "realms", len(regionRealms),
+                    "dungeons", len(dungeons))
                 processed := 0
 
                 ctx, cancel := context.WithTimeout(context.Background(), 20*time.
Minute)
@@ -207,10 +227,14 @@ func runAnalyze(db *sql.DB, client *blizzard.Client, realms 
map[string]blizzard.
                 total++
                 if res.Error != nil {
                     failed++
-                    // Print compact progress line every 50 items, or when a non-
404 error occurs
+                    // Log compact progress line every 50 items, or when a non-40
4 error occurs
                     if !strings.Contains(strings.ToLower(res.Error.Error()), "404
") || processed%50 == 0 {
-                        fmt.Printf("  [%4d/%4d] ERR %-3s %-18s %-24s: %v\n",
-                            processed, expected, strings.ToUpper(res.RealmInfo.Re
gion), res.RealmInfo.Slug, res.Dungeon.Slug, res.Error)
+                        log.Error("fetch error",
+                            "progress", fmt.Sprintf("%d/%d", processed, expected)
,
+                            "region", strings.ToUpper(res.RealmInfo.Region),
+                            "realm", res.RealmInfo.Slug,
+                            "dungeon", res.Dungeon.Slug,
+                            "error", res.Error)
                     }
                     continue
                 }
@@ -307,8 +331,13 @@ func runAnalyze(db *sql.DB, client *blizzard.Client, realms m
ap[string]blizzard.
                 }
                 if processed%50 == 0 {
                     elapsed := time.Since(start)
-                    fmt.Printf("  [%4d/%4d] OK  %-3s %-18s %-24s  ts=%d  (elapsed
 %s)\n",
-                        processed, expected, strings.ToUpper(res.RealmInfo.Region
), res.RealmInfo.Slug, res.Dungeon.Slug, maxTs, elapsed.Truncate(time.Second))
+                    log.Debug("fetch progress",
+                        "progress", fmt.Sprintf("%d/%d", processed, expected),
+                        "region", strings.ToUpper(res.RealmInfo.Region),
+                        "realm", res.RealmInfo.Slug,
+                        "dungeon", res.Dungeon.Slug,
+                        "ts", maxTs,
+                        "elapsed", elapsed.Truncate(time.Second))
                 }
 
                 // Record coverage
@@ -333,10 +362,23 @@ func runAnalyze(db *sql.DB, client *blizzard.Client, realms 
map[string]blizzard.
 
         // Print summary
         elapsed := time.Since(start)
-        fmt.Printf("\nAnalyze complete in %v: total=%d success=%d failed=%d realm
s_with_data=%d\n", elapsed, total, success, failed, len(items))
-        fmt.Println("\nLatest recorded run per realm:")
+        log.Info("analyze complete",
+            "elapsed", elapsed,
+            "total", total,
+            "success", success,
+            "failed", failed,
+            "realms_with_data", len(items))
+
+        log.Info("latest recorded run per realm")
         for _, e := range items {
-            fmt.Printf("  %-20s [%s] (%s) -> %s  | %s  period=%s  runs=%d\n", e.R
ealmName, strings.ToUpper(e.Region), e.RealmSlug, e.MostRecentISO, e.DungeonName, 
e.PeriodID, e.RunCount)
+            log.Debug("realm run",
+                "realm", e.RealmName,
+                "region", strings.ToUpper(e.Region),
+                "realm_slug", e.RealmSlug,
+                "timestamp", e.MostRecentISO,
+                "dungeon", e.DungeonName,
+                "period", e.PeriodID,
+                "runs", e.RunCount)
         }
 
         // Build realm_status structure for JSON
@@ -441,7 +483,7 @@ func runAnalyze(db *sql.DB, client *blizzard.Client, realms ma
p[string]blizzard.
             enc.SetIndent("", "  ")
             if err := enc.Encode(payload); err != nil { f.Close(); return fmt.Err
orf("encode json: %w", err) }
             if err := f.Close(); err != nil { return err }
-            fmt.Printf("\nWrote analysis JSON to %s\n", outPath)
+            log.Info("wrote analysis JSON", "path", outPath)
         }
 
         // Write per-realm status files if requested
@@ -523,7 +565,7 @@ func runAnalyze(db *sql.DB, client *blizzard.Client, realms ma
p[string]blizzard.
                 if err := enc.Encode(payload); err != nil { f.Close(); return fmt
.Errorf("encode status: %w", err) }
                 if err := f.Close(); err != nil { return err }
             }
-            fmt.Printf("Wrote per-realm status files to %s\n", statusDir)
+            log.Info("wrote per-realm status files", "dir", statusDir)
         }
 
         return nil

diff --git a/nix/pkgs/ookstats/src/cmd/analy
ze.go b/nix/pkgs/ookstats/src/cmd/analyze.go
index 6a30341..9fe9a97 100644
--- a/nix/pkgs/ookstats/src/cmd/analyze.go
+++ b/nix/pkgs/ookstats/src/cmd/analyze.go
@@ -13,6 +13,7 @@ import (
     "strings"
     "time"
 
+    "github.com/charmbracelet/log"
     "github.com/spf13/cobra"
     "ookstats/internal/blizzard"
     "ookstats/internal/database"
@@ -96,7 +97,7 @@ func init() {
 
 func runAnalyze(db *sql.DB, client *blizzar
d.Client, realms map[string]blizzard.RealmIn
fo, dungeons []blizzard.DungeonInfo, periods
Spec string, outPath, statusDir string) erro
r {
         dbService := database.NewDatabaseSe
rvice(db)
-        fmt.Printf("Analyze: %d realms, %d 
dungeons\n", len(realms), len(dungeons))
+        log.Info("analyze", "realms", len(r
ealms), "dungeons", len(dungeons))
 
         type latest struct{
             Region        string `json:"reg
ion"`
@@ -148,7 +149,9 @@ func runAnalyze(db *sql.
DB, client *blizzard.Client, realms map[stri
ng]blizzard.
 
         // Process each region independentl
y
         for region, regionRealms := range r
ealmsByRegion {
-            fmt.Printf("\n========== Region
: %s (%d realms) ==========\n", strings.ToUp
per(region), len(regionRealms))
+            log.Info("processing region",
+                "region", strings.ToUpper(r
egion),
+                "realms", len(regionRealms)
)
 
             // Determine periods for this r
egion
             var periods []string
@@ -158,16 +161,23 @@ func runAnalyze(db *sq
l.DB, client *blizzard.Client, realms map[st
ring]blizzard.
                 // User specified periods (
support ranges like "1020-1036")
                 periods, err = blizzard.Par
sePeriods(periodsSpec)
                 if err != nil {
-                    fmt.Printf("Failed to p
arse periods for %s: %v - skipping region\n"
, strings.ToUpper(region), err)
+                    log.Error("failed to pa
rse periods - skipping region",
+                        "region", strings.T
oUpper(region),
+                        "error", err)
                     continue
                 }
-                fmt.Printf("Using user-spec
ified periods: %v (%d periods)\n", periods, 
len(periods))
+                log.Info("using user-specif
ied periods",
+                    "periods", periods,
+                    "count", len(periods))
             } else {
                 // Fetch periods from datab
ase (populated by season sync)
-                fmt.Printf("Fetching period
 list from database for %s...\n", strings.To
Upper(region))
+                log.Info("fetching period l
ist from database",
+                    "region", strings.ToUpp
er(region))
                 periodInts, err := dbServic
e.GetPeriodsForRegion(region)
                 if err != nil {
-                    fmt.Printf("Failed to f
etch periods from database for %s: %v - skip
ping region\n", strings.ToUpper(region), err
)
+                    log.Error("failed to fe
tch periods from database - skipping region"
,
+                        "region", strings.T
oUpper(region),
+                        "error", err)
                     continue
                 }
 
@@ -178,16 +188,21 @@ func runAnalyze(db *sq
l.DB, client *blizzard.Client, realms map[st
ring]blizzard.
                 }
 
                 if len(periods) == 0 {
-                    fmt.Printf("No periods 
found in database for %s (run season sync fi
rst) - skipping region\n", strings.ToUpper(r
egion))
+                    log.Warn("no periods fo
und in database - skipping region",
+                        "region", strings.T
oUpper(region))
                     continue
                 }
 
-                fmt.Printf("[OK] Fetched %d
 periods from database for %s (newest: %s, o
ldest: %s)\n",
-                    len(periods), strings.T
oUpper(region), periods[0], periods[len(peri
ods)-1])
+                log.Info("fetched periods f
rom database",
+                    "count", len(periods),
+                    "region", strings.ToUpp
er(region),
+                    "newest", periods[0],
+                    "oldest", periods[len(p
eriods)-1])
             }
 
             if len(periods) == 0 {
-                fmt.Printf("No periods to p
rocess for %s - skipping region\n", strings.
ToUpper(region))
+                log.Warn("no periods to pro
cess - skipping region",
+                    "region", strings.ToUpp
er(region))
                 continue
             }
 
@@ -195,9 +210,14 @@ func runAnalyze(db *sql
.DB, client *blizzard.Client, realms map[str
ing]blizzard.
 
             // Iterate periods for this reg
ion
             for _, period := range periods 
{
-                fmt.Printf("\n--- %s Period
 %s ---\n", strings.ToUpper(region), period)
+                log.Info("processing period
",
+                    "region", strings.ToUpp
er(region),
+                    "period", period)
                 expected := len(regionRealm
s) * len(dungeons)
-                fmt.Printf("Expecting %d re
quests (%d realms ? %d dungeons)\n", expecte
d, len(regionRealms), len(dungeons))
+                log.Debug("expecting reques
ts",
+                    "count", expected,
+                    "realms", len(regionRea
lms),
+                    "dungeons", len(dungeon
s))
                 processed := 0
 
                 ctx, cancel := context.With
Timeout(context.Background(), 20*time.Minute
)
@@ -207,10 +227,14 @@ func runAnalyze(db *sq
l.DB, client *blizzard.Client, realms map[st
ring]blizzard.
                 total++
                 if res.Error != nil {
                     failed++
-                    // Print compact progre
ss line every 50 items, or when a non-404 er
ror occurs
+                    // Log compact progress
 line every 50 items, or when a non-404 erro
r occurs
                     if !strings.Contains(st
rings.ToLower(res.Error.Error()), "404") || 
processed%50 == 0 {
-                        fmt.Printf("  [%4d/
%4d] ERR %-3s %-18s %-24s: %v\n",
-                            processed, expe
cted, strings.ToUpper(res.RealmInfo.Region),
 res.RealmInfo.Slug, res.Dungeon.Slug, res.E
rror)
+                        log.Error("fetch er
ror",
+                            "progress", fmt
.Sprintf("%d/%d", processed, expected),
+                            "region", strin
gs.ToUpper(res.RealmInfo.Region),
+                            "realm", res.Re
almInfo.Slug,
+                            "dungeon", res.
Dungeon.Slug,
+                            "error", res.Er
ror)
                     }
                     continue
                 }
@@ -307,8 +331,13 @@ func runAnalyze(db *sql
.DB, client *blizzard.Client, realms map[str
ing]blizzard.
                 }
                 if processed%50 == 0 {
                     elapsed := time.Since(s
tart)
-                    fmt.Printf("  [%4d/%4d]
 OK  %-3s %-18s %-24s  ts=%d  (elapsed %s)\n
",
-                        processed, expected
, strings.ToUpper(res.RealmInfo.Region), res
.RealmInfo.Slug, res.Dungeon.Slug, maxTs, el
apsed.Truncate(time.Second))
+                    log.Debug("fetch progre
ss",
+                        "progress", fmt.Spr
intf("%d/%d", processed, expected),
+                        "region", strings.T
oUpper(res.RealmInfo.Region),
+                        "realm", res.RealmI
nfo.Slug,
+                        "dungeon", res.Dung
eon.Slug,
+                        "ts", maxTs,
+                        "elapsed", elapsed.
Truncate(time.Second))
                 }
 
                 // Record coverage
@@ -333,10 +362,23 @@ func runAnalyze(db *sq
l.DB, client *blizzard.Client, realms map[st
ring]blizzard.
 
         // Print summary
         elapsed := time.Since(start)
-        fmt.Printf("\nAnalyze complete in %
v: total=%d success=%d failed=%d realms_with
_data=%d\n", elapsed, total, success, failed
, len(items))
-        fmt.Println("\nLatest recorded run 
per realm:")
+        log.Info("analyze complete",
+            "elapsed", elapsed,
+            "total", total,
+            "success", success,
+            "failed", failed,
+            "realms_with_data", len(items))
+
+        log.Info("latest recorded run per r
ealm")
         for _, e := range items {
-            fmt.Printf("  %-20s [%s] (%s) -
> %s  | %s  period=%s  runs=%d\n", e.RealmNa
me, strings.ToUpper(e.Region), e.RealmSlug, 
e.MostRecentISO, e.DungeonName, e.PeriodID, 
e.RunCount)
+            log.Debug("realm run",
+                "realm", e.RealmName,
+                "region", strings.ToUpper(e
.Region),
+                "realm_slug", e.RealmSlug,
+                "timestamp", e.MostRecentIS
O,
+                "dungeon", e.DungeonName,
+                "period", e.PeriodID,
+                "runs", e.RunCount)
         }
 
         // Build realm_status structure for
 JSON
@@ -441,7 +483,7 @@ func runAnalyze(db *sql.
DB, client *blizzard.Client, realms map[stri
ng]blizzard.
             enc.SetIndent("", "  ")
             if err := enc.Encode(payload); 
err != nil { f.Close(); return fmt.Errorf("e
ncode json: %w", err) }
             if err := f.Close(); err != nil
 { return err }
-            fmt.Printf("\nWrote analysis JS
ON to %s\n", outPath)
+            log.Info("wrote analysis JSON",
 "path", outPath)
         }
 
         // Write per-realm status files if 
requested
@@ -523,7 +565,7 @@ func runAnalyze(db *sql.
DB, client *blizzard.Client, realms map[stri
ng]blizzard.
                 if err := enc.Encode(payloa
d); err != nil { f.Close(); return fmt.Error
f("encode status: %w", err) }
                 if err := f.Close(); err !=
 nil { return err }
             }
-            fmt.Printf("Wrote per-realm sta
tus files to %s\n", statusDir)
+            log.Info("wrote per-realm statu
s files", "dir", statusDir)
         }
 
         return nil
 

diff --git a/nix/pkgs/ookstats/src/cmd/build.go b/nix/pkgs/ookstats/src/cmd/build.
go
index eb19768..53d2689 100644
--- a/nix/pkgs/ookstats/src/cmd/build.go
+++ b/nix/pkgs/ookstats/src/cmd/build.go
@@ -10,6 +10,7 @@ import (
     "strings"
     "time"
 
+    "github.com/charmbracelet/log"
     "github.com/spf13/cobra"
     "ookstats/internal/blizzard"
     "ookstats/internal/database"
@@ -52,13 +53,13 @@ var buildCmd = &cobra.Command{
             // Only remove if it's a plain local file path
             if !strings.Contains(dbPath, "://") {
                 if _, err := os.Stat(dbPath); err == nil {
-                    fmt.Printf("Removing existing local DB: %s\n", dbPath)
+                    log.Info("removing existing database", "path", dbPath)
                     if rmErr := os.Remove(dbPath); rmErr != nil {
                         return fmt.Errorf("failed to remove db file: %w", rmErr)
                     }
                 }
             } else {
-                fmt.Printf("--from-scratch requested but DSN is not a local file 
(%s) - skipping deletion.\n", dbPath)
+                log.Warn("from-scratch requested but DSN is not a local file - sk
ipping deletion", "dsn", dbPath)
             }
         }
 
@@ -74,7 +75,12 @@ var buildCmd = &cobra.Command{
         }
 
         // 2) Populate items (embedded default; file can override)
-        fmt.Printf("\n=== Populating items (embedded or file override) ===\n")
+        log.Info("populating items", "source", func() string {
+            if wowsimsDB != "" {
+                return "file override"
+            }
+            return "embedded default"
+        }())
         if err := populateItems(db, wowsimsDB); err != nil {
             return fmt.Errorf("populate items: %w", err)
         }
@@ -90,13 +96,13 @@ var buildCmd = &cobra.Command{
         }
 
         // 4) Sync season metadata from API
-        fmt.Println("\n=== Syncing season metadata ===")
+        log.Info("syncing season metadata")
         if err := syncSeasons(db, client, regionsCSV); err != nil {
             return fmt.Errorf("sync seasons: %w", err)
         }
 
         // 5) Fetch CM runs using pipeline (includes child realm filtering)
-        fmt.Println("\n=== Fetching Challenge Mode leaderboards (global period sw
eep) ===")
+        log.Info("fetching challenge mode leaderboards", "sweep", "global period"
)
 
         dbService := database.NewDatabaseService(db)
 
@@ -139,39 +145,41 @@ var buildCmd = &cobra.Command{
             return fmt.Errorf("fetch challenge mode: %w", err)
         }
 
-        fmt.Printf("\n[OK] Fetch complete: %d runs, %d players in %v\n",
-            result.TotalRuns, result.TotalPlayers, result.Duration)
+        log.Info("fetch complete",
+            "runs", result.TotalRuns,
+            "players", result.TotalPlayers,
+            "duration", result.Duration)
 
         // 4) Process players (aggregations + rankings)
-        fmt.Println("\n=== Processing Players (aggregations + rankings) ===")
+        log.Info("processing players", "stage", "aggregations + rankings")
         if err := processPlayersOnce(db); err != nil {
             return err
         }
 
         // 5) Fetch detailed player profiles (optional)
         if !skipProfiles {
-            fmt.Println("\n=== Fetching detailed player profiles (9/9) ===")
+            log.Info("fetching detailed player profiles", "coverage", "9/9")
             if err := fetchProfilesOnce(db, client); err != nil {
                 return err
             }
         } else {
-            fmt.Println("Skipping player profile fetch per flag")
+            log.Info("skipping player profile fetch", "reason", "flag")
         }
 
         // 6) Process run rankings (global/regional)
-        fmt.Println("\n=== Processing Run Rankings (global + regional) ===")
+        log.Info("processing run rankings", "scopes", "global + regional")
         if err := processRunRankingsOnce(db); err != nil {
             return err
         }
 
         // 7) Generate static API
-        fmt.Println("\n=== Generating static API ===")
+        log.Info("generating static API")
         if err := generateAllAPI(db, normalizedOut, pageSize, shardSize, regionsC
SV); err != nil {
             return err
         }
 
         // 8) Generate status API via analyze
-        fmt.Println("\n=== Generating status API (analyze) ===")
+        log.Info("generating status API", "method", "analyze")
         statusDir := filepath.Join(normalizedOut, "api", "status")
         outPath := filepath.Join(statusDir, "latest-runs.json")
         // Get realms and dungeons for analyze
@@ -184,7 +192,7 @@ var buildCmd = &cobra.Command{
         // Print summary
         summarizeBuild(db, normalizedOut)
 
-        fmt.Printf("\nBuild complete. Static API at %s/api\n", normalizedOut)
+        log.Info("build complete", "api_location", normalizedOut+"/api")
         return nil
     },
 }
@@ -211,7 +219,10 @@ func fetchProfilesOnce(db *sql.DB, client *blizzard.Client) e
rror {
     dbService := database.NewDatabaseService(db)
     players, err := dbService.GetEligiblePlayersForProfileFetch()
     if err != nil { return fmt.Errorf("eligible players: %w", err) }
-    if len(players) == 0 { fmt.Println("No eligible players (9/9). Skipping profi
les."); return nil }
+    if len(players) == 0 {
+        log.Info("no eligible players with 9/9 coverage - skipping profiles")
+        return nil
+    }
 
     // batch in reasonable size
     batchSize := 20
@@ -227,25 +238,49 @@ func fetchProfilesOnce(db *sql.DB, client *blizzard.Client) 
error {
         end := i + batchSize
         if end > len(players) { end = len(players) }
         batch := players[i:end]
-        fmt.Printf("\nProfiles batch %d/%d (%d players)\n", (i/batchSize)+1, (len
(players)+batchSize-1)/batchSize, len(batch))
+        batchNum := (i/batchSize)+1
+        totalBatches := (len(players)+batchSize-1)/batchSize
+        log.Info("processing profiles batch",
+            "batch", batchNum,
+            "total_batches", totalBatches,
+            "players", len(batch))
         results := client.FetchPlayerProfilesConcurrent(ctx, batch)
         ts := time.Now().UnixMilli()
         batchProfiles := 0
         batchItems := 0
         for res := range results {
             processed++
-            if res.Error != nil { fmt.Printf("  [ERROR] %s (%s): %v\n", res.Playe
rName, res.Region, res.Error); continue }
+            if res.Error != nil {
+                log.Error("profile fetch failed",
+                    "player", res.PlayerName,
+                    "region", res.Region,
+                    "error", res.Error)
+                continue
+            }
             profs, items, err := dbService.InsertPlayerProfileData(res, ts)
-            if err != nil { fmt.Printf("  [ERROR] %s (%s): DB error - %v\n", res.
PlayerName, res.Region, err); continue }
+            if err != nil {
+                log.Error("profile insert failed",
+                    "player", res.PlayerName,
+                    "region", res.Region,
+                    "error", err)
+                continue
+            }
             batchProfiles += profs; batchItems += items
         }
         totalProfiles += batchProfiles
         totalItems += batchItems
-        fmt.Printf("  -> Batch complete: %d profiles, %d items (Total %d/%d)\n", 
batchProfiles, batchItems, processed, len(players))
+        log.Info("batch complete",
+            "profiles", batchProfiles,
+            "items", batchItems,
+            "total_processed", processed,
+            "total_players", len(players))
         if i+batchSize < len(players) { time.Sleep(1 * time.Second) }
     }
 
-    fmt.Printf("Profiles complete: %d profiles, %d items in %v\n", totalProfiles,
 totalItems, time.Since(start))
+    log.Info("profiles complete",
+        "profiles", totalProfiles,
+        "items", totalItems,
+        "duration", time.Since(start))
     return nil
 }
 
@@ -271,7 +306,7 @@ func generateAllAPI(db *sql.DB, outParent string, pageSize, sh
ardSize int, regio
     // search index
     if err := generator.GenerateSearchIndex(db, filepath.Join(base, "search"), sh
ardSize); err != nil { return err }
 
-    fmt.Println("[OK] Static API generated")
+    log.Info("static API generated")
     return nil
 }
 
@@ -283,13 +318,13 @@ func summarizeBuild(db *sql.DB, outParent string) {
     _ = db.QueryRow("SELECT COUNT(*) FROM player_profiles WHERE has_complete_cove
rage = 1").Scan(&completePlayers)
     _ = db.QueryRow("SELECT COUNT(*) FROM player_details").Scan(&detailsCount)
 
-    fmt.Printf("\n===== Build Summary =====\n")
-    fmt.Printf("Runs: %d\n", runCount)
-    fmt.Printf("Players: %d\n", playerCount)
-    fmt.Printf("Complete Coverage Players (9/9): %d\n", completePlayers)
-    fmt.Printf("Player Details Rows: %d\n", detailsCount)
+    log.Info("build summary",
+        "runs", runCount,
+        "players", playerCount,
+        "complete_coverage", completePlayers,
+        "player_details", detailsCount)
 
-    fmt.Printf("\nPer-Realm Period Coverage:\n")
+    log.Info("per-realm period coverage")
     rows, err := db.Query(`
         SELECT r.slug, GROUP_CONCAT(DISTINCT cr.period_id ORDER BY cr.period_id D
ESC)
         FROM challenge_runs cr
@@ -302,7 +337,7 @@ func summarizeBuild(db *sql.DB, outParent string) {
             var slug, periods string
             if err := rows.Scan(&slug, &periods); err == nil {
                 if periods == "" { periods = "-" }
-                fmt.Printf("  %s: [%s]\n", slug, periods)
+                log.Debug("realm coverage", "realm", slug, "periods", periods)
             }
         }
     }
@@ -327,42 +362,47 @@ func syncSeasons(db *sql.DB, client *blizzard.Client, region
sCSV string) error {
 		regions = []string{"us", "eu", "kr", "tw"}
 	}
 
-	fmt.Printf("Fetching season metadata from %d regions...\n", len(regions))
+	log.Info("fetching season metadata", "regions", len(regions))
 
 	// Process each region
 	for _, region := range regions {
-		fmt.Printf("\n=== Region: %s ===\n", strings.ToUpper(region))
+		log.Info("processing region", "region", strings.ToUpper(region))
 
 		// Fetch season index for this region
 		seasonIndex, err := client.FetchSeasonIndex(region)
 		if err != nil {
-			fmt.Printf("Failed to fetch season index for %s: %v - skipping region\n", stri
ngs.ToUpper(region), err)
+			log.Error("failed to fetch season index - skipping region",
+				"region", strings.ToUpper(region),
+				"error", err)
 			continue
 		}
 
 		if len(seasonIndex.Seasons) == 0 {
-			fmt.Printf("No seasons found for %s\n", strings.ToUpper(region))
+			log.Warn("no seasons found", "region", strings.ToUpper(region))
 			continue
 		}
 
-		fmt.Printf("Found %d seasons in %s\n", len(seasonIndex.Seasons), strings.ToUppe
r(region))
+		log.Info("found seasons", "count", len(seasonIndex.Seasons), "region", strings.
ToUpper(region))
 
 		// Process each season for this region
 		for _, seasonRef := range seasonIndex.Seasons {
 			seasonID := seasonRef.ID
-			fmt.Printf("  Season %d: ", seasonID)
 
 			// Fetch season details
 			seasonDetail, err := client.FetchSeasonDetail(region, seasonID)
 			if err != nil {
-				fmt.Printf("error fetching details - %v\n", err)
+				log.Error("error fetching season details",
+					"season", seasonID,
+					"error", err)
 				continue
 			}
 
 			// Upsert season with region
 			dbSeasonID, err := dbService.UpsertSeason(seasonDetail.ID, region, seasonDetai
l.SeasonName, seasonDetail.StartTimestamp)
 			if err != nil {
-				fmt.Printf("error upserting - %v\n", err)
+				log.Error("error upserting season",
+					"season", seasonID,
+					"error", err)
 				continue
 			}
 
@@ -374,22 +414,27 @@ func syncSeasons(db *sql.DB, client *blizzard.Client, region
sCSV string) error {
 				// Update period range
 				err = dbService.UpdateSeasonPeriodRange(dbSeasonID, firstPeriod, lastPeriod)
 				if err != nil {
-					fmt.Printf("error updating period range - %v\n", err)
+					log.Error("error updating period range", "error", err)
 				}
 
 				// Link each period
 				for _, periodRef := range seasonDetail.Periods {
 					err = dbService.LinkPeriodToSeason(periodRef.ID, dbSeasonID)
 					if err != nil {
-						fmt.Printf("error linking period %d - %v\n", periodRef.ID, err)
+						log.Error("error linking period",
+							"period", periodRef.ID,
+							"error", err)
 					}
 				}
 			}
-			fmt.Printf("%s (%d periods)\n", seasonDetail.SeasonName, len(seasonDetail.Peri
ods))
+			log.Info("season synced",
+				"season", seasonID,
+				"name", seasonDetail.SeasonName,
+				"periods", len(seasonDetail.Periods))
 		}
 	}
 
-	fmt.Println("\n[OK] Season metadata synced for all regions")
+	log.Info("season metadata synced for all regions")
 	return nil
 }
 

diff --git a/nix/pkgs/ookstats/src/cmd/build
.go b/nix/pkgs/ookstats/src/cmd/build.go
index eb19768..53d2689 100644
--- a/nix/pkgs/ookstats/src/cmd/build.go
+++ b/nix/pkgs/ookstats/src/cmd/build.go
@@ -10,6 +10,7 @@ import (
     "strings"
     "time"
 
+    "github.com/charmbracelet/log"
     "github.com/spf13/cobra"
     "ookstats/internal/blizzard"
     "ookstats/internal/database"
@@ -52,13 +53,13 @@ var buildCmd = &cobra.Co
mmand{
             // Only remove if it's a plain 
local file path
             if !strings.Contains(dbPath, ":
//") {
                 if _, err := os.Stat(dbPath
); err == nil {
-                    fmt.Printf("Removing ex
isting local DB: %s\n", dbPath)
+                    log.Info("removing exis
ting database", "path", dbPath)
                     if rmErr := os.Remove(d
bPath); rmErr != nil {
                         return fmt.Errorf("
failed to remove db file: %w", rmErr)
                     }
                 }
             } else {
-                fmt.Printf("--from-scratch 
requested but DSN is not a local file (%s) -
 skipping deletion.\n", dbPath)
+                log.Warn("from-scratch requ
ested but DSN is not a local file - skipping
 deletion", "dsn", dbPath)
             }
         }
 
@@ -74,7 +75,12 @@ var buildCmd = &cobra.Com
mand{
         }
 
         // 2) Populate items (embedded defa
ult; file can override)
-        fmt.Printf("\n=== Populating items 
(embedded or file override) ===\n")
+        log.Info("populating items", "sourc
e", func() string {
+            if wowsimsDB != "" {
+                return "file override"
+            }
+            return "embedded default"
+        }())
         if err := populateItems(db, wowsims
DB); err != nil {
             return fmt.Errorf("populate ite
ms: %w", err)
         }
@@ -90,13 +96,13 @@ var buildCmd = &cobra.Co
mmand{
         }
 
         // 4) Sync season metadata from API
-        fmt.Println("\n=== Syncing season m
etadata ===")
+        log.Info("syncing season metadata")
         if err := syncSeasons(db, client, r
egionsCSV); err != nil {
             return fmt.Errorf("sync seasons
: %w", err)
         }
 
         // 5) Fetch CM runs using pipeline 
(includes child realm filtering)
-        fmt.Println("\n=== Fetching Challen
ge Mode leaderboards (global period sweep) =
==")
+        log.Info("fetching challenge mode l
eaderboards", "sweep", "global period")
 
         dbService := database.NewDatabaseSe
rvice(db)
 
@@ -139,39 +145,41 @@ var buildCmd = &cobra.
Command{
             return fmt.Errorf("fetch challe
nge mode: %w", err)
         }
 
-        fmt.Printf("\n[OK] Fetch complete: 
%d runs, %d players in %v\n",
-            result.TotalRuns, result.TotalP
layers, result.Duration)
+        log.Info("fetch complete",
+            "runs", result.TotalRuns,
+            "players", result.TotalPlayers,
+            "duration", result.Duration)
 
         // 4) Process players (aggregations
 + rankings)
-        fmt.Println("\n=== Processing Playe
rs (aggregations + rankings) ===")
+        log.Info("processing players", "sta
ge", "aggregations + rankings")
         if err := processPlayersOnce(db); e
rr != nil {
             return err
         }
 
         // 5) Fetch detailed player profile
s (optional)
         if !skipProfiles {
-            fmt.Println("\n=== Fetching det
ailed player profiles (9/9) ===")
+            log.Info("fetching detailed pla
yer profiles", "coverage", "9/9")
             if err := fetchProfilesOnce(db,
 client); err != nil {
                 return err
             }
         } else {
-            fmt.Println("Skipping player pr
ofile fetch per flag")
+            log.Info("skipping player profi
le fetch", "reason", "flag")
         }
 
         // 6) Process run rankings (global/
regional)
-        fmt.Println("\n=== Processing Run R
ankings (global + regional) ===")
+        log.Info("processing run rankings",
 "scopes", "global + regional")
         if err := processRunRankingsOnce(db
); err != nil {
             return err
         }
 
         // 7) Generate static API
-        fmt.Println("\n=== Generating stati
c API ===")
+        log.Info("generating static API")
         if err := generateAllAPI(db, normal
izedOut, pageSize, shardSize, regionsCSV); e
rr != nil {
             return err
         }
 
         // 8) Generate status API via analy
ze
-        fmt.Println("\n=== Generating statu
s API (analyze) ===")
+        log.Info("generating status API", "
method", "analyze")
         statusDir := filepath.Join(normaliz
edOut, "api", "status")
         outPath := filepath.Join(statusDir,
 "latest-runs.json")
         // Get realms and dungeons for anal
yze
@@ -184,7 +192,7 @@ var buildCmd = &cobra.Co
mmand{
         // Print summary
         summarizeBuild(db, normalizedOut)
 
-        fmt.Printf("\nBuild complete. Stati
c API at %s/api\n", normalizedOut)
+        log.Info("build complete", "api_loc
ation", normalizedOut+"/api")
         return nil
     },
 }
@@ -211,7 +219,10 @@ func fetchProfilesOnce(
db *sql.DB, client *blizzard.Client) error {
     dbService := database.NewDatabaseServic
e(db)
     players, err := dbService.GetEligiblePl
ayersForProfileFetch()
     if err != nil { return fmt.Errorf("elig
ible players: %w", err) }
-    if len(players) == 0 { fmt.Println("No 
eligible players (9/9). Skipping profiles.")
; return nil }
+    if len(players) == 0 {
+        log.Info("no eligible players with 
9/9 coverage - skipping profiles")
+        return nil
+    }
 
     // batch in reasonable size
     batchSize := 20
@@ -227,25 +238,49 @@ func fetchProfilesOnce
(db *sql.DB, client *blizzard.Client) error 
{
         end := i + batchSize
         if end > len(players) { end = len(p
layers) }
         batch := players[i:end]
-        fmt.Printf("\nProfiles batch %d/%d 
(%d players)\n", (i/batchSize)+1, (len(playe
rs)+batchSize-1)/batchSize, len(batch))
+        batchNum := (i/batchSize)+1
+        totalBatches := (len(players)+batch
Size-1)/batchSize
+        log.Info("processing profiles batch
",
+            "batch", batchNum,
+            "total_batches", totalBatches,
+            "players", len(batch))
         results := client.FetchPlayerProfil
esConcurrent(ctx, batch)
         ts := time.Now().UnixMilli()
         batchProfiles := 0
         batchItems := 0
         for res := range results {
             processed++
-            if res.Error != nil { fmt.Print
f("  [ERROR] %s (%s): %v\n", res.PlayerName,
 res.Region, res.Error); continue }
+            if res.Error != nil {
+                log.Error("profile fetch fa
iled",
+                    "player", res.PlayerNam
e,
+                    "region", res.Region,
+                    "error", res.Error)
+                continue
+            }
             profs, items, err := dbService.
InsertPlayerProfileData(res, ts)
-            if err != nil { fmt.Printf("  [
ERROR] %s (%s): DB error - %v\n", res.Player
Name, res.Region, err); continue }
+            if err != nil {
+                log.Error("profile insert f
ailed",
+                    "player", res.PlayerNam
e,
+                    "region", res.Region,
+                    "error", err)
+                continue
+            }
             batchProfiles += profs; batchIt
ems += items
         }
         totalProfiles += batchProfiles
         totalItems += batchItems
-        fmt.Printf("  -> Batch complete: %d
 profiles, %d items (Total %d/%d)\n", batchP
rofiles, batchItems, processed, len(players)
)
+        log.Info("batch complete",
+            "profiles", batchProfiles,
+            "items", batchItems,
+            "total_processed", processed,
+            "total_players", len(players))
         if i+batchSize < len(players) { tim
e.Sleep(1 * time.Second) }
     }
 
-    fmt.Printf("Profiles complete: %d profi
les, %d items in %v\n", totalProfiles, total
Items, time.Since(start))
+    log.Info("profiles complete",
+        "profiles", totalProfiles,
+        "items", totalItems,
+        "duration", time.Since(start))
     return nil
 }
 
@@ -271,7 +306,7 @@ func generateAllAPI(db *
sql.DB, outParent string, pageSize, shardSiz
e int, regio
     // search index
     if err := generator.GenerateSearchIndex
(db, filepath.Join(base, "search"), shardSiz
e); err != nil { return err }
 
-    fmt.Println("[OK] Static API generated"
)
+    log.Info("static API generated")
     return nil
 }
 
@@ -283,13 +318,13 @@ func summarizeBuild(db
 *sql.DB, outParent string) {
     _ = db.QueryRow("SELECT COUNT(*) FROM p
layer_profiles WHERE has_complete_coverage =
 1").Scan(&completePlayers)
     _ = db.QueryRow("SELECT COUNT(*) FROM p
layer_details").Scan(&detailsCount)
 
-    fmt.Printf("\n===== Build Summary =====
\n")
-    fmt.Printf("Runs: %d\n", runCount)
-    fmt.Printf("Players: %d\n", playerCount
)
-    fmt.Printf("Complete Coverage Players (
9/9): %d\n", completePlayers)
-    fmt.Printf("Player Details Rows: %d\n",
 detailsCount)
+    log.Info("build summary",
+        "runs", runCount,
+        "players", playerCount,
+        "complete_coverage", completePlayer
s,
+        "player_details", detailsCount)
 
-    fmt.Printf("\nPer-Realm Period Coverage
:\n")
+    log.Info("per-realm period coverage")
     rows, err := db.Query(`
         SELECT r.slug, GROUP_CONCAT(DISTINC
T cr.period_id ORDER BY cr.period_id DESC)
         FROM challenge_runs cr
@@ -302,7 +337,7 @@ func summarizeBuild(db *
sql.DB, outParent string) {
             var slug, periods string
             if err := rows.Scan(&slug, &per
iods); err == nil {
                 if periods == "" { periods 
= "-" }
-                fmt.Printf("  %s: [%s]\n", 
slug, periods)
+                log.Debug("realm coverage",
 "realm", slug, "periods", periods)
             }
         }
     }
@@ -327,42 +362,47 @@ func syncSeasons(db *s
ql.DB, client *blizzard.Client, regionsCSV s
tring) error {
 		regions = []string{"us", "eu", "kr", "tw"
}
 	}
 
-	fmt.Printf("Fetching season metadata from 
%d regions...\n", len(regions))
+	log.Info("fetching season metadata", "regi
ons", len(regions))
 
 	// Process each region
 	for _, region := range regions {
-		fmt.Printf("\n=== Region: %s ===\n", stri
ngs.ToUpper(region))
+		log.Info("processing region", "region", s
trings.ToUpper(region))
 
 		// Fetch season index for this region
 		seasonIndex, err := client.FetchSeasonInd
ex(region)
 		if err != nil {
-			fmt.Printf("Failed to fetch season index
 for %s: %v - skipping region\n", strings.To
Upper(region), err)
+			log.Error("failed to fetch season index 
- skipping region",
+				"region", strings.ToUpper(region),
+				"error", err)
 			continue
 		}
 
 		if len(seasonIndex.Seasons) == 0 {
-			fmt.Printf("No seasons found for %s\n", 
strings.ToUpper(region))
+			log.Warn("no seasons found", "region", s
trings.ToUpper(region))
 			continue
 		}
 
-		fmt.Printf("Found %d seasons in %s\n", le
n(seasonIndex.Seasons), strings.ToUpper(regi
on))
+		log.Info("found seasons", "count", len(se
asonIndex.Seasons), "region", strings.ToUppe
r(region))
 
 		// Process each season for this region
 		for _, seasonRef := range seasonIndex.Sea
sons {
 			seasonID := seasonRef.ID
-			fmt.Printf("  Season %d: ", seasonID)
 
 			// Fetch season details
 			seasonDetail, err := client.FetchSeasonD
etail(region, seasonID)
 			if err != nil {
-				fmt.Printf("error fetching details - %v
\n", err)
+				log.Error("error fetching season detail
s",
+					"season", seasonID,
+					"error", err)
 				continue
 			}
 
 			// Upsert season with region
 			dbSeasonID, err := dbService.UpsertSeaso
n(seasonDetail.ID, region, seasonDetail.Seas
onName, seasonDetail.StartTimestamp)
 			if err != nil {
-				fmt.Printf("error upserting - %v\n", er
r)
+				log.Error("error upserting season",
+					"season", seasonID,
+					"error", err)
 				continue
 			}
 
@@ -374,22 +414,27 @@ func syncSeasons(db *s
ql.DB, client *blizzard.Client, regionsCSV s
tring) error {
 				// Update period range
 				err = dbService.UpdateSeasonPeriodRange
(dbSeasonID, firstPeriod, lastPeriod)
 				if err != nil {
-					fmt.Printf("error updating period rang
e - %v\n", err)
+					log.Error("error updating period range
", "error", err)
 				}
 
 				// Link each period
 				for _, periodRef := range seasonDetail.
Periods {
 					err = dbService.LinkPeriodToSeason(per
iodRef.ID, dbSeasonID)
 					if err != nil {
-						fmt.Printf("error linking period %d -
 %v\n", periodRef.ID, err)
+						log.Error("error linking period",
+							"period", periodRef.ID,
+							"error", err)
 					}
 				}
 			}
-			fmt.Printf("%s (%d periods)\n", seasonDe
tail.SeasonName, len(seasonDetail.Periods))
+			log.Info("season synced",
+				"season", seasonID,
+				"name", seasonDetail.SeasonName,
+				"periods", len(seasonDetail.Periods))
 		}
 	}
 
-	fmt.Println("\n[OK] Season metadata synced
 for all regions")
+	log.Info("season metadata synced for all r
egions")
 	return nil
 }
 
 

diff --git a/nix/pkgs/ookstats/src/cmd/fetch.go b/nix/pkgs/ookstats/src/cmd/fetch.
go
index 8e6daed..14ffd73 100644
--- a/nix/pkgs/ookstats/src/cmd/fetch.go
+++ b/nix/pkgs/ookstats/src/cmd/fetch.go
@@ -5,6 +5,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/charmbracelet/log"
 	"github.com/spf13/cobra"
 	"ookstats/internal/blizzard"
 	"ookstats/internal/database"
@@ -22,7 +23,7 @@ var fetchCMCmd = &cobra.Command{
 	Short: "Fetch challenge mode leaderboards",
 	Long:  `Fetch challenge mode leaderboard data for all realms and dungeons.`,
 	RunE: func(cmd *cobra.Command, args []string) error {
-		fmt.Println("Fetching challenge mode leaderboards...")
+		log.Info("fetching challenge mode leaderboards")
 
 		db, err := database.Connect()
 		if err != nil {
@@ -40,8 +41,8 @@ var fetchCMCmd = &cobra.Command{
 		client.Verbose = verbose
 		database.SetVerbose(verbose)
 
-		fmt.Println("Connected to local database")
-		fmt.Println("Blizzard API client initialized")
+		log.Info("connected to local database")
+		log.Info("blizzard API client initialized")
 
 		// Initialize database service
 		dbService := database.NewDatabaseService(db)
@@ -119,8 +120,10 @@ var fetchCMCmd = &cobra.Command{
 			return err
 		}
 
-		fmt.Printf("\nSuccessfully inserted %d runs and %d new players into local datab
ase\n", result.TotalRuns, result.TotalPlayers)
-		fmt.Printf("Database saved to: %s\n", database.DBFilePath())
+		log.Info("successfully inserted data into local database",
+			"runs", result.TotalRuns,
+			"players", result.TotalPlayers)
+		log.Info("database saved", "path", database.DBFilePath())
 
 		return nil
 	},
@@ -131,7 +134,7 @@ var fetchProfilesCmd = &cobra.Command{
 	Short: "Fetch detailed player profiles",
 	Long:  `Fetch detailed player profile data including equipment and character inf
ormation.`,
 	RunE: func(cmd *cobra.Command, args []string) error {
-		fmt.Println("=== Player Profile Fetcher ===")
+		log.Info("player profile fetcher")
 
 		db, err := database.Connect()
 		if err != nil {
@@ -148,8 +151,8 @@ var fetchProfilesCmd = &cobra.Command{
 		verbose, _ := cmd.InheritedFlags().GetBool("verbose")
 		client.Verbose = verbose
 
-		fmt.Println("Connected to local database")
-		fmt.Println("Blizzard API client initialized")
+		log.Info("connected to local database")
+		log.Info("blizzard API client initialized")
 
 		// Initialize database service
 		dbService := database.NewDatabaseService(db)
@@ -171,18 +174,18 @@ var fetchProfilesCmd = &cobra.Command{
 			return err
 		}
 
-		fmt.Printf("\n[OK] Player profile fetching complete!\n")
-		fmt.Printf("   Processed: %d players in %v\n", result.ProcessedCount, result.Du
ration)
-		fmt.Printf("   Updated: %d player profiles\n", result.TotalProfiles)
-		fmt.Printf("   Updated: %d equipment items\n", result.TotalEquipment)
+		log.Info("player profile fetching complete",
+			"processed", result.ProcessedCount,
+			"duration", result.Duration,
+			"profiles", result.TotalProfiles,
+			"equipment", result.TotalEquipment)
 
 		if result.ProcessedCount > 0 {
 			rate := float64(result.ProcessedCount) / result.Duration.Minutes()
-			fmt.Printf("   Rate: %.1f players/minute\n", rate)
+			log.Info("fetch rate", "players_per_minute", rate)
 		}
 
-		fmt.Println("\nNext steps:")
-		fmt.Println("  Run 'ookstats generate api' to rebuild the website with new play
er profile data")
+		log.Info("next step: run 'ookstats generate api' to rebuild the website")
 
 		return nil
 	},
@@ -193,7 +196,7 @@ var fetchSeasonsCmd = &cobra.Command{
 	Short: "Fetch and sync season metadata",
 	Long:  `Fetch season metadata from Blizzard API and populate the seasons and per
iod_seasons tables.`,
 	RunE: func(cmd *cobra.Command, args []string) error {
-		fmt.Println("=== Season Metadata Sync ===")
+		log.Info("season metadata sync")
 
 		db, err := database.Connect()
 		if err != nil {
@@ -209,8 +212,8 @@ var fetchSeasonsCmd = &cobra.Command{
 		verbose, _ := cmd.InheritedFlags().GetBool("verbose")
 		client.Verbose = verbose
 
-		fmt.Println("Connected to local database")
-		fmt.Println("Blizzard API client initialized")
+		log.Info("connected to local database")
+		log.Info("blizzard API client initialized")
 
 		dbService := database.NewDatabaseService(db)
 
@@ -229,46 +232,56 @@ var fetchSeasonsCmd = &cobra.Command{
 			regions = []string{"us", "eu", "kr", "tw"}
 		}
 
-		fmt.Printf("Syncing seasons for regions: %v\n\n", regions)
+		log.Info("syncing seasons", "regions", regions)
 
 		totalSeasons := 0
 		totalPeriods := 0
 
 		for _, region := range regions {
-			fmt.Printf("=== Region: %s ===\n", strings.ToUpper(region))
+			log.Info("processing region", "region", strings.ToUpper(region))
 
 			// Fetch season index
 			seasonIndex, err := client.FetchSeasonIndex(region)
 			if err != nil {
-				fmt.Printf("Error fetching season index for %s: %v\n", region, err)
+				log.Error("failed to fetch season index",
+					"region", region,
+					"error", err)
 				continue
 			}
 
-			fmt.Printf("Found %d seasons in %s\n", len(seasonIndex.Seasons), strings.ToUpp
er(region))
+			log.Info("found seasons",
+				"count", len(seasonIndex.Seasons),
+				"region", strings.ToUpper(region))
 
 			// Process each season
 			for _, seasonRef := range seasonIndex.Seasons {
 				seasonID := seasonRef.ID
-				fmt.Printf("\n--- Season %d ---\n", seasonID)
+				log.Info("processing season", "season_id", seasonID)
 
 				// Fetch season details
 				seasonDetail, err := client.FetchSeasonDetail(region, seasonID)
 				if err != nil {
-					fmt.Printf("Error fetching season %d details: %v\n", seasonID, err)
+					log.Error("failed to fetch season details",
+						"season_id", seasonID,
+						"error", err)
 					continue
 				}
 
 				// Upsert season
 				dbSeasonID, err := dbService.UpsertSeason(seasonDetail.ID, region, seasonDeta
il.SeasonName, seasonDetail.StartTimestamp)
 				if err != nil {
-					fmt.Printf("Error upserting season %d: %v\n", seasonID, err)
+					log.Error("failed to upsert season",
+						"season_id", seasonID,
+						"error", err)
 					continue
 				}
 				totalSeasons++
 
-				fmt.Printf("Season: %s (ID: %d)\n", seasonDetail.SeasonName, seasonDetail.ID)
-				fmt.Printf("Start: %d\n", seasonDetail.StartTimestamp)
-				fmt.Printf("Periods: %d\n", len(seasonDetail.Periods))
+				log.Info("season details",
+					"name", seasonDetail.SeasonName,
+					"id", seasonDetail.ID,
+					"start", seasonDetail.StartTimestamp,
+					"periods", len(seasonDetail.Periods))
 
 				// Link periods to season
 				if len(seasonDetail.Periods) > 0 {
@@ -278,28 +291,33 @@ var fetchSeasonsCmd = &cobra.Command{
 					// Update period range
 					err = dbService.UpdateSeasonPeriodRange(dbSeasonID, firstPeriod, lastPeriod)
 					if err != nil {
-						fmt.Printf("Error updating season period range: %v\n", err)
+						log.Error("failed to update season period range", "error", err)
 					}
 
 					// Link each period
 					for _, periodRef := range seasonDetail.Periods {
 						err = dbService.LinkPeriodToSeason(periodRef.ID, dbSeasonID)
 						if err != nil {
-							fmt.Printf("Error linking period %d to season %d: %v\n", periodRef.ID, sea
sonDetail.ID, err)
+							log.Error("failed to link period to season",
+								"period_id", periodRef.ID,
+								"season_id", seasonDetail.ID,
+								"error", err)
 						} else {
 							totalPeriods++
 						}
 					}
-					fmt.Printf("Linked %d periods to season %d\n", len(seasonDetail.Periods), se
asonDetail.ID)
+					log.Info("linked periods to season",
+						"periods", len(seasonDetail.Periods),
+						"season_id", seasonDetail.ID)
 				}
 			}
 
-			fmt.Printf("\n[OK] Synced seasons for %s\n", strings.ToUpper(region))
+			log.Info("synced seasons for region", "region", strings.ToUpper(region))
 		}
 
-		fmt.Printf("\n=== Sync Complete ===\n")
-		fmt.Printf("Total seasons synced: %d\n", totalSeasons)
-		fmt.Printf("Total period mappings created: %d\n", totalPeriods)
+		log.Info("sync complete",
+			"total_seasons", totalSeasons,
+			"total_periods", totalPeriods)
 
 		return nil
 	},

diff --git a/nix/pkgs/ookstats/src/cmd/fetch
.go b/nix/pkgs/ookstats/src/cmd/fetch.go
index 8e6daed..14ffd73 100644
--- a/nix/pkgs/ookstats/src/cmd/fetch.go
+++ b/nix/pkgs/ookstats/src/cmd/fetch.go
@@ -5,6 +5,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/charmbracelet/log"
 	"github.com/spf13/cobra"
 	"ookstats/internal/blizzard"
 	"ookstats/internal/database"
@@ -22,7 +23,7 @@ var fetchCMCmd = &cobra.Co
mmand{
 	Short: "Fetch challenge mode leaderboards"
,
 	Long:  `Fetch challenge mode leaderboard d
ata for all realms and dungeons.`,
 	RunE: func(cmd *cobra.Command, args []stri
ng) error {
-		fmt.Println("Fetching challenge mode lead
erboards...")
+		log.Info("fetching challenge mode leaderb
oards")
 
 		db, err := database.Connect()
 		if err != nil {
@@ -40,8 +41,8 @@ var fetchCMCmd = &cobra.Co
mmand{
 		client.Verbose = verbose
 		database.SetVerbose(verbose)
 
-		fmt.Println("Connected to local database"
)
-		fmt.Println("Blizzard API client initiali
zed")
+		log.Info("connected to local database")
+		log.Info("blizzard API client initialized
")
 
 		// Initialize database service
 		dbService := database.NewDatabaseService(
db)
@@ -119,8 +120,10 @@ var fetchCMCmd = &cobra
.Command{
 			return err
 		}
 
-		fmt.Printf("\nSuccessfully inserted %d ru
ns and %d new players into local database\n"
, result.TotalRuns, result.TotalPlayers)
-		fmt.Printf("Database saved to: %s\n", dat
abase.DBFilePath())
+		log.Info("successfully inserted data into
 local database",
+			"runs", result.TotalRuns,
+			"players", result.TotalPlayers)
+		log.Info("database saved", "path", databa
se.DBFilePath())
 
 		return nil
 	},
@@ -131,7 +134,7 @@ var fetchProfilesCmd = &
cobra.Command{
 	Short: "Fetch detailed player profiles",
 	Long:  `Fetch detailed player profile data
 including equipment and character informati
on.`,
 	RunE: func(cmd *cobra.Command, args []stri
ng) error {
-		fmt.Println("=== Player Profile Fetcher =
==")
+		log.Info("player profile fetcher")
 
 		db, err := database.Connect()
 		if err != nil {
@@ -148,8 +151,8 @@ var fetchProfilesCmd = &
cobra.Command{
 		verbose, _ := cmd.InheritedFlags().GetBoo
l("verbose")
 		client.Verbose = verbose
 
-		fmt.Println("Connected to local database"
)
-		fmt.Println("Blizzard API client initiali
zed")
+		log.Info("connected to local database")
+		log.Info("blizzard API client initialized
")
 
 		// Initialize database service
 		dbService := database.NewDatabaseService(
db)
@@ -171,18 +174,18 @@ var fetchProfilesCmd =
 &cobra.Command{
 			return err
 		}
 
-		fmt.Printf("\n[OK] Player profile fetchin
g complete!\n")
-		fmt.Printf("   Processed: %d players in %
v\n", result.ProcessedCount, result.Duration
)
-		fmt.Printf("   Updated: %d player profile
s\n", result.TotalProfiles)
-		fmt.Printf("   Updated: %d equipment item
s\n", result.TotalEquipment)
+		log.Info("player profile fetching complet
e",
+			"processed", result.ProcessedCount,
+			"duration", result.Duration,
+			"profiles", result.TotalProfiles,
+			"equipment", result.TotalEquipment)
 
 		if result.ProcessedCount > 0 {
 			rate := float64(result.ProcessedCount) /
 result.Duration.Minutes()
-			fmt.Printf("   Rate: %.1f players/minute
\n", rate)
+			log.Info("fetch rate", "players_per_minu
te", rate)
 		}
 
-		fmt.Println("\nNext steps:")
-		fmt.Println("  Run 'ookstats generate api
' to rebuild the website with new player pro
file data")
+		log.Info("next step: run 'ookstats genera
te api' to rebuild the website")
 
 		return nil
 	},
@@ -193,7 +196,7 @@ var fetchSeasonsCmd = &c
obra.Command{
 	Short: "Fetch and sync season metadata",
 	Long:  `Fetch season metadata from Blizzar
d API and populate the seasons and period_se
asons tables.`,
 	RunE: func(cmd *cobra.Command, args []stri
ng) error {
-		fmt.Println("=== Season Metadata Sync ===
")
+		log.Info("season metadata sync")
 
 		db, err := database.Connect()
 		if err != nil {
@@ -209,8 +212,8 @@ var fetchSeasonsCmd = &c
obra.Command{
 		verbose, _ := cmd.InheritedFlags().GetBoo
l("verbose")
 		client.Verbose = verbose
 
-		fmt.Println("Connected to local database"
)
-		fmt.Println("Blizzard API client initiali
zed")
+		log.Info("connected to local database")
+		log.Info("blizzard API client initialized
")
 
 		dbService := database.NewDatabaseService(
db)
 
@@ -229,46 +232,56 @@ var fetchSeasonsCmd = 
&cobra.Command{
 			regions = []string{"us", "eu", "kr", "tw
"}
 		}
 
-		fmt.Printf("Syncing seasons for regions: 
%v\n\n", regions)
+		log.Info("syncing seasons", "regions", re
gions)
 
 		totalSeasons := 0
 		totalPeriods := 0
 
 		for _, region := range regions {
-			fmt.Printf("=== Region: %s ===\n", strin
gs.ToUpper(region))
+			log.Info("processing region", "region", 
strings.ToUpper(region))
 
 			// Fetch season index
 			seasonIndex, err := client.FetchSeasonIn
dex(region)
 			if err != nil {
-				fmt.Printf("Error fetching season index
 for %s: %v\n", region, err)
+				log.Error("failed to fetch season index
",
+					"region", region,
+					"error", err)
 				continue
 			}
 
-			fmt.Printf("Found %d seasons in %s\n", l
en(seasonIndex.Seasons), strings.ToUpper(reg
ion))
+			log.Info("found seasons",
+				"count", len(seasonIndex.Seasons),
+				"region", strings.ToUpper(region))
 
 			// Process each season
 			for _, seasonRef := range seasonIndex.Se
asons {
 				seasonID := seasonRef.ID
-				fmt.Printf("\n--- Season %d ---\n", sea
sonID)
+				log.Info("processing season", "season_i
d", seasonID)
 
 				// Fetch season details
 				seasonDetail, err := client.FetchSeason
Detail(region, seasonID)
 				if err != nil {
-					fmt.Printf("Error fetching season %d d
etails: %v\n", seasonID, err)
+					log.Error("failed to fetch season deta
ils",
+						"season_id", seasonID,
+						"error", err)
 					continue
 				}
 
 				// Upsert season
 				dbSeasonID, err := dbService.UpsertSeas
on(seasonDetail.ID, region, seasonDetail.Sea
sonName, seasonDetail.StartTimestamp)
 				if err != nil {
-					fmt.Printf("Error upserting season %d:
 %v\n", seasonID, err)
+					log.Error("failed to upsert season",
+						"season_id", seasonID,
+						"error", err)
 					continue
 				}
 				totalSeasons++
 
-				fmt.Printf("Season: %s (ID: %d)\n", sea
sonDetail.SeasonName, seasonDetail.ID)
-				fmt.Printf("Start: %d\n", seasonDetail.
StartTimestamp)
-				fmt.Printf("Periods: %d\n", len(seasonD
etail.Periods))
+				log.Info("season details",
+					"name", seasonDetail.SeasonName,
+					"id", seasonDetail.ID,
+					"start", seasonDetail.StartTimestamp,
+					"periods", len(seasonDetail.Periods))
 
 				// Link periods to season
 				if len(seasonDetail.Periods) > 0 {
@@ -278,28 +291,33 @@ var fetchSeasonsCmd = 
&cobra.Command{
 					// Update period range
 					err = dbService.UpdateSeasonPeriodRang
e(dbSeasonID, firstPeriod, lastPeriod)
 					if err != nil {
-						fmt.Printf("Error updating season per
iod range: %v\n", err)
+						log.Error("failed to update season pe
riod range", "error", err)
 					}
 
 					// Link each period
 					for _, periodRef := range seasonDetail
.Periods {
 						err = dbService.LinkPeriodToSeason(pe
riodRef.ID, dbSeasonID)
 						if err != nil {
-							fmt.Printf("Error linking period %d 
to season %d: %v\n", periodRef.ID, seasonDet
ail.ID, err)
+							log.Error("failed to link period to 
season",
+								"period_id", periodRef.ID,
+								"season_id", seasonDetail.ID,
+								"error", err)
 						} else {
 							totalPeriods++
 						}
 					}
-					fmt.Printf("Linked %d periods to seaso
n %d\n", len(seasonDetail.Periods), seasonDe
tail.ID)
+					log.Info("linked periods to season",
+						"periods", len(seasonDetail.Periods),
+						"season_id", seasonDetail.ID)
 				}
 			}
 
-			fmt.Printf("\n[OK] Synced seasons for %s
\n", strings.ToUpper(region))
+			log.Info("synced seasons for region", "r
egion", strings.ToUpper(region))
 		}
 
-		fmt.Printf("\n=== Sync Complete ===\n")
-		fmt.Printf("Total seasons synced: %d\n", 
totalSeasons)
-		fmt.Printf("Total period mappings created
: %d\n", totalPeriods)
+		log.Info("sync complete",
+			"total_seasons", totalSeasons,
+			"total_periods", totalPeriods)
 
 		return nil
 	},
 

diff --git a/nix/pkgs/ookstats/src/cmd/root.go b/nix/pkgs/ookstats/src/cmd/root.go
index 23b8f1c..2d1e0ea 100644
--- a/nix/pkgs/ookstats/src/cmd/root.go
+++ b/nix/pkgs/ookstats/src/cmd/root.go
@@ -4,6 +4,7 @@ import (
     "fmt"
     "os"
 
+    "github.com/charmbracelet/log"
     "github.com/spf13/cobra"
     "ookstats/internal/database"
 )
@@ -17,6 +18,35 @@ Supports fetching data from Blizzard API, processing player ran
kings,
 and managing data in local SQLite database.`,
 }
 
+// logger is the global logger instance
+var logger *log.Logger
+
+// GetLogger returns the global logger instance
+func GetLogger() *log.Logger {
+	if logger == nil {
+		// fallback logger if not initialized
+		logger = log.Default()
+	}
+	return logger
+}
+
+// initLogger initializes the global logger with appropriate settings
+func initLogger(verbose bool) {
+	level := log.InfoLevel
+	if verbose {
+		level = log.DebugLevel
+	}
+
+	logger = log.NewWithOptions(os.Stderr, log.Options{
+		ReportTimestamp: true,
+		TimeFormat:      "15:04:05",
+		Level:           level,
+	})
+
+	// Set as default logger for the log package
+	log.SetDefault(logger)
+}
+
 // execute adds all child commands to the root command and sets flags appropriate
ly.
 // this is called by main.main(). It only needs to happen once to the rootCmd.
 func Execute() {
@@ -37,6 +67,11 @@ func init() {
 
     // Set override before running any subcommand
     rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
+        // Initialize logger with verbose setting
+        verbose, _ := cmd.Flags().GetBool("verbose")
+        initLogger(verbose)
+
+        // Set database path override
         if v, _ := cmd.Flags().GetString("db-file"); v != "" {
             database.SetDBPath(v)
         }

diff --git a/nix/pkgs/ookstats/src/cmd/root.
go b/nix/pkgs/ookstats/src/cmd/root.go
index 23b8f1c..2d1e0ea 100644
--- a/nix/pkgs/ookstats/src/cmd/root.go
+++ b/nix/pkgs/ookstats/src/cmd/root.go
@@ -4,6 +4,7 @@ import (
     "fmt"
     "os"
 
+    "github.com/charmbracelet/log"
     "github.com/spf13/cobra"
     "ookstats/internal/database"
 )
@@ -17,6 +18,35 @@ Supports fetching data fr
om Blizzard API, processing player rankings,
 and managing data in local SQLite database.
`,
 }
 
+// logger is the global logger instance
+var logger *log.Logger
+
+// GetLogger returns the global logger inst
ance
+func GetLogger() *log.Logger {
+	if logger == nil {
+		// fallback logger if not initialized
+		logger = log.Default()
+	}
+	return logger
+}
+
+// initLogger initializes the global logger
 with appropriate settings
+func initLogger(verbose bool) {
+	level := log.InfoLevel
+	if verbose {
+		level = log.DebugLevel
+	}
+
+	logger = log.NewWithOptions(os.Stderr, log
.Options{
+		ReportTimestamp: true,
+		TimeFormat:      "15:04:05",
+		Level:           level,
+	})
+
+	// Set as default logger for the log packa
ge
+	log.SetDefault(logger)
+}
+
 // execute adds all child commands to the r
oot command and sets flags appropriately.
 // this is called by main.main(). It only n
eeds to happen once to the rootCmd.
 func Execute() {
@@ -37,6 +67,11 @@ func init() {
 
     // Set override before running any subc
ommand
     rootCmd.PersistentPreRun = func(cmd *co
bra.Command, args []string) {
+        // Initialize logger with verbose s
etting
+        verbose, _ := cmd.Flags().GetBool("
verbose")
+        initLogger(verbose)
+
+        // Set database path override
         if v, _ := cmd.Flags().GetString("d
b-file"); v != "" {
             database.SetDBPath(v)
         }
 

diff --git a/nix/pkgs/ookstats/src/go.mod b/nix/pkgs/ookstats/src/go.mod
index 4c6503b..89f2250 100644
--- a/nix/pkgs/ookstats/src/go.mod
+++ b/nix/pkgs/ookstats/src/go.mod
@@ -3,14 +3,29 @@ module ookstats
 go 1.21
 
 require (
+	github.com/charmbracelet/log v0.4.2
 	github.com/spf13/cobra v1.10.1
 	github.com/tursodatabase/go-libsql v0.0.0-20250723062947-60e59c7150f4
 )
 
 require (
 	github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
+	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+	github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // in
direct
+	github.com/charmbracelet/lipgloss v1.1.0 // indirect
+	github.com/charmbracelet/x/ansi v0.8.0 // indirect
+	github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indi
rect
+	github.com/charmbracelet/x/term v0.2.1 // indirect
+	github.com/go-logfmt/logfmt v0.6.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 // ind
irect
+	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/mattn/go-runewidth v0.0.16 // indirect
+	github.com/muesli/termenv v0.16.0 // indirect
+	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/spf13/pflag v1.0.9 // indirect
+	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
 	golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect
+	golang.org/x/sys v0.30.0 // indirect
 )

diff --git a/nix/pkgs/ookstats/src/go.mod b/
nix/pkgs/ookstats/src/go.mod
index 4c6503b..89f2250 100644
--- a/nix/pkgs/ookstats/src/go.mod
+++ b/nix/pkgs/ookstats/src/go.mod
@@ -3,14 +3,29 @@ module ookstats
 go 1.21
 
 require (
+	github.com/charmbracelet/log v0.4.2
 	github.com/spf13/cobra v1.10.1
 	github.com/tursodatabase/go-libsql v0.0.0-
20250723062947-60e59c7150f4
 )
 
 require (
 	github.com/antlr4-go/antlr/v4 v4.13.0 // i
ndirect
+	github.com/aymanbagabas/go-osc52/v2 v2.0.1
 // indirect
+	github.com/charmbracelet/colorprofile v0.2
.3-0.20250311203215-f60798e515dc // indirect
+	github.com/charmbracelet/lipgloss v1.1.0 /
/ indirect
+	github.com/charmbracelet/x/ansi v0.8.0 // 
indirect
+	github.com/charmbracelet/x/cellbuf v0.0.13
-0.20250311204145-2c3ea96c31dd // indirect
+	github.com/charmbracelet/x/term v0.2.1 // 
indirect
+	github.com/go-logfmt/logfmt v0.6.0 // indi
rect
 	github.com/inconshreveable/mousetrap v1.1.
0 // indirect
 	github.com/libsql/sqlite-antlr4-parser v0.
0.0-20240327125255-dbf53b6cbf06 // indirect
+	github.com/lucasb-eyer/go-colorful v1.2.0 
// indirect
+	github.com/mattn/go-isatty v0.0.20 // indi
rect
+	github.com/mattn/go-runewidth v0.0.16 // i
ndirect
+	github.com/muesli/termenv v0.16.0 // indir
ect
+	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/spf13/pflag v1.0.9 // indirect
+	github.com/xo/terminfo v0.0.0-202209100020
29-abceb7e1c41e // indirect
 	golang.org/x/exp v0.0.0-20240325151524-a68
5a6edb6d8 // indirect
+	golang.org/x/sys v0.30.0 // indirect
 )
 

diff --git a/nix/pkgs/ookstats/src/go.sum b/nix/pkgs/ookstats/src/go.sum
index c4fd19d..ea0f955 100644
--- a/nix/pkgs/ookstats/src/go.sum
+++ b/nix/pkgs/ookstats/src/go.sum
@@ -1,26 +1,65 @@
 github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8T
VTI=
 github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9U
TnmpPaOR2g=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiEr
DT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2
ASbcXHWsFqH8hp8=
+github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZ
I35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
+github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod
 h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
+github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFq
R/noCY=
+github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0
binUUBwA0aL30=
+github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyi
g=
+github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf
2ysQu5nw=
+github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llX
n7xE=
+github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTk
owiI4blgS9Q=
+github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GV
L4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
+github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h
1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
+github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQa
GIAQ=
+github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEE
kHUMCmsxdUg=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NI
QQ7OS05n1F4g=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI
+c5H38=
+github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4
=
+github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEV
veWlfTs=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNG
IjoY=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s
0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfs
EA9PFc4w1p2J65bw=
 github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 h1:JLvn
7D+wXjH9g4Jsjo+VqmzTUpl/LX7vfr6VOfSWTdM=
 github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06/go.mod 
h1:FUkZ5OHjlGPjnM2UyGJz9TypXQFgYqw6AFNO1UiROTM=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69A
j6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7
ruzyGqttikkLy0=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY
=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7
dTCTo3Y=
+github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TU
LQc=
+github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh1
53qUoGf23w=
+github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
+github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3V
fY/Cnk=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl
0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbA
QM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNV
Y4sRDYZ/4=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJt
xc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx
88=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDY
RxCVz55jmeOWTM=
 github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
 github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4X
aB0=
 github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
 github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2
Bg=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJ
A=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8
C91i36aY=
 github.com/tursodatabase/go-libsql v0.0.0-20250723062947-60e59c7150f4 h1:UwxG3Vmt
rhYRF38SDa1M829udKBXGqYcbzcWd0EBImc=
 github.com/tursodatabase/go-libsql v0.0.0-20250723062947-60e59c7150f4/go.mod h1:T
jsB2miB8RW2Sse8sdxzVTdeGlx74GloD5zJYUC38d8=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF
/ppJZNG9ZpyihvCd0w101no=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuq
unuUZ/Dhy/avygyECGrLceyNeo4LiM=
 golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pS
yOt+UgdZw2BFZ+lEw=
 golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR
2hssIjF07kZFEiieALBM/ARQ=
 golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
 golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
+golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8h
uTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
 gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81H
FBacw=

diff --git a/nix/pkgs/ookstats/src/go.sum b/
nix/pkgs/ookstats/src/go.sum
index c4fd19d..ea0f955 100644
--- a/nix/pkgs/ookstats/src/go.sum
+++ b/nix/pkgs/ookstats/src/go.sum
@@ -1,26 +1,65 @@
 github.com/antlr4-go/antlr/v4 v4.13.0 h1:lx
Cg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
 github.com/antlr4-go/antlr/v4 v4.13.0/go.mo
d h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPa
OR2g=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 
h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ
2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/
go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXH
WsFqH8hp8=
+github.com/charmbracelet/colorprofile v0.2.
3-0.20250311203215-f60798e515dc h1:4pZI35227
imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
+github.com/charmbracelet/colorprofile v0.2.
3-0.20250311203215-f60798e515dc/go.mod h1:X4
/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
+github.com/charmbracelet/lipgloss v1.1.0 h1
:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY
=
+github.com/charmbracelet/lipgloss v1.1.0/go
.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUB
wA0aL30=
+github.com/charmbracelet/log v0.4.2 h1:hYt8
Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
+github.com/charmbracelet/log v0.4.2/go.mod 
h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5
nw=
+github.com/charmbracelet/x/ansi v0.8.0 h1:9
GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
+github.com/charmbracelet/x/ansi v0.8.0/go.m
od h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4b
lgS9Q=
+github.com/charmbracelet/x/cellbuf v0.0.13-
0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHE
wG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
+github.com/charmbracelet/x/cellbuf v0.0.13-
0.20250311204145-2c3ea96c31dd/go.mod h1:xe0n
KWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
+github.com/charmbracelet/x/term v0.2.1 h1:A
QeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
+github.com/charmbracelet/x/term v0.2.1/go.m
od h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCm
sxdUg=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.
mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS0
5n1F4g=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u
1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1
:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38
=
+github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu
3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
+github.com/go-logfmt/logfmt v0.6.0/go.mod h
1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfT
s=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg
4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:1
7dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/inconshreveable/mousetrap v1.1.0
 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9
tc8=
 github.com/inconshreveable/mousetrap v1.1.0
/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc
4w1p2J65bw=
 github.com/libsql/sqlite-antlr4-parser v0.0
.0-20240327125255-dbf53b6cbf06 h1:JLvn7D+wXj
H9g4Jsjo+VqmzTUpl/LX7vfr6VOfSWTdM=
 github.com/libsql/sqlite-antlr4-parser v0.0
.0-20240327125255-dbf53b6cbf06/go.mod h1:FUk
Z5OHjlGPjnM2UyGJz9TypXQFgYqw6AFNO1UiROTM=
+github.com/lucasb-eyer/go-colorful v1.2.0 h
1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nk
Y=
+github.com/lucasb-eyer/go-colorful v1.2.0/g
o.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGq
ttikkLy0=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0i
DuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h
1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3
Y=
+github.com/mattn/go-runewidth v0.0.16 h1:E5
ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
+github.com/mattn/go-runewidth v0.0.16/go.mo
d h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoG
f23w=
+github.com/muesli/termenv v0.16.0 h1:S5AlUN
9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
+github.com/muesli/termenv v0.16.0/go.mod h1
:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk
=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214
owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwaw
xfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DB
wDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod
 h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDY
Z/4=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6w
j4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEh
rYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3
SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 github.com/russross/blackfriday/v2 v2.1.0/g
o.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz5
5jmeOWTM=
 github.com/spf13/cobra v1.10.1 h1:lJeBwCfmr
nXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
 github.com/spf13/cobra v1.10.1/go.mod h1:7S
mJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
 github.com/spf13/pflag v1.0.9 h1:9exaQaMOCw
ffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
 github.com/spf13/pflag v1.0.9/go.mod h1:McX
fInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/testify v1.10.0 h1:Xv5e
rBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod 
h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36
aY=
 github.com/tursodatabase/go-libsql v0.0.0-2
0250723062947-60e59c7150f4 h1:UwxG3VmtrhYRF3
8SDa1M829udKBXGqYcbzcWd0EBImc=
 github.com/tursodatabase/go-libsql v0.0.0-2
0250723062947-60e59c7150f4/go.mod h1:TjsB2mi
B8RW2Sse8sdxzVTdeGlx74GloD5zJYUC38d8=
+github.com/xo/terminfo v0.0.0-2022091000202
9-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZN
G9ZpyihvCd0w101no=
+github.com/xo/terminfo v0.0.0-2022091000202
9-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/
Dhy/avygyECGrLceyNeo4LiM=
 golang.org/x/exp v0.0.0-20240325151524-a685
a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+Ug
dZw2BFZ+lEw=
 golang.org/x/exp v0.0.0-20240325151524-a685
a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIj
F07kZFEiieALBM/ARQ=
 golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr
1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
 golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1
gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJr
h7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFY
WkSue1YwXzLmsV5Gfq7Eiy72C1uc=
+golang.org/x/sys v0.30.0/go.mod h1:/VUhepia
JMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d
25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQ
CZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuv
ctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7B
CEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gotest.tools v2.2.0+incompatible h1:VsBPFP1
AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
 gotest.tools v2.2.0+incompatible/go.mod h1:
DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
 

diff --git a/nix/pkgs/ookstats/src/internal/database/client.go b/nix/pkgs/ookstats
/src/internal/database/client.go
index 29591ff..5360b99 100644
--- a/nix/pkgs/ookstats/src/internal/database/client.go
+++ b/nix/pkgs/ookstats/src/internal/database/client.go
@@ -191,6 +191,12 @@ func EnsureCompleteSchema(db *sql.DB) error {
 			global_ranking_bracket TEXT,
 			regional_ranking_bracket TEXT,
 			realm_ranking_bracket TEXT,
+			global_class_rank INTEGER,
+			region_class_rank INTEGER,
+			realm_class_rank INTEGER,
+			global_class_bracket TEXT,
+			region_class_bracket TEXT,
+			realm_class_bracket TEXT,
 			has_complete_coverage INTEGER DEFAULT 0,
 			last_updated INTEGER,
 			PRIMARY KEY (player_id, season_id)

diff --git a/nix/pkgs/ookstats/src/internal/
database/client.go b/nix/pkgs/ookstats/src/i
nternal/database/client.go
index 29591ff..5360b99 100644
--- a/nix/pkgs/ookstats/src/internal/databas
e/client.go
+++ b/nix/pkgs/ookstats/src/internal/databas
e/client.go
@@ -191,6 +191,12 @@ func EnsureCompleteSche
ma(db *sql.DB) error {
 			global_ranking_bracket TEXT,
 			regional_ranking_bracket TEXT,
 			realm_ranking_bracket TEXT,
+			global_class_rank INTEGER,
+			region_class_rank INTEGER,
+			realm_class_rank INTEGER,
+			global_class_bracket TEXT,
+			region_class_bracket TEXT,
+			realm_class_bracket TEXT,
 			has_complete_coverage INTEGER DEFAULT 0,
 			last_updated INTEGER,
 			PRIMARY KEY (player_id, season_id)
 

diff --git a/nix/pkgs/ookstats/src/internal/generator/player_leaderboards.go b/nix
/pkgs/ookstats/src/internal/generator/player_leaderboards.go
index ce0d26b..6d7811c 100644
--- a/nix/pkgs/ookstats/src/internal/generator/player_leaderboards.go
+++ b/nix/pkgs/ookstats/src/internal/generator/player_leaderboards.go
@@ -311,43 +311,41 @@ func generateClassScope(db *sql.DB, out, scope, region, real
mSlug, classKey stri
 	// Fetch ALL players for this season, then filter by class in Go (to handle fall
back logic)
 	var rows *sql.Rows
 	var err error
-	var bracketColumn string
 	if scope == "global" {
-		bracketColumn = "pp.global_ranking_bracket"
 		rows, err = db.Query(`
 			SELECT p.id, p.name, r.slug, r.name, r.region,
 				   COALESCE(pd.class_name,''), COALESCE(pd.active_spec_name,''), pp.main_spec
_id,
 				   pp.combined_best_time, pp.dungeons_completed, pp.total_runs,
-				   COALESCE(pp.global_ranking_bracket, '')
+				   COALESCE(pp.global_class_bracket, '')
 			FROM players p
 			JOIN realms r ON p.realm_id = r.id
 			JOIN player_profiles pp ON p.id = pp.player_id
 			LEFT JOIN player_details pd ON p.id = pd.player_id
 			WHERE pp.season_id = ? AND pp.has_complete_coverage = 1 AND pp.combined_best_t
ime IS NOT NULL
+				AND pp.class_name IS NOT NULL
 			ORDER BY pp.combined_best_time ASC, p.name ASC
 		`, seasonID)
 	} else if scope == "regional" {
-		bracketColumn = "pp.regional_ranking_bracket"
 		rows, err = db.Query(`
 			SELECT p.id, p.name, r.slug, r.name, r.region,
 				   COALESCE(pd.class_name,''), COALESCE(pd.active_spec_name,''), pp.main_spec
_id,
 				   pp.combined_best_time, pp.dungeons_completed, pp.total_runs,
-				   COALESCE(pp.regional_ranking_bracket, '')
+				   COALESCE(pp.region_class_bracket, '')
 			FROM players p
 			JOIN realms r ON p.realm_id = r.id
 			JOIN player_profiles pp ON p.id = pp.player_id
 			LEFT JOIN player_details pd ON p.id = pd.player_id
 			WHERE pp.season_id = ? AND r.region = ? AND pp.has_complete_coverage = 1 AND p
p.combined_best_time IS NOT NULL
+				AND pp.class_name IS NOT NULL
 			ORDER BY pp.combined_best_time ASC, p.name ASC
 		`, seasonID, region)
 	} else {
 		// Realm scope - include entire pool (parent + all children)
-		bracketColumn = "pp.realm_ranking_bracket"
 		rows, err = db.Query(`
 			SELECT p.id, p.name, r.slug, r.name, r.region,
 				   COALESCE(pd.class_name,''), COALESCE(pd.active_spec_name,''), pp.main_spec
_id,
 				   pp.combined_best_time, pp.dungeons_completed, pp.total_runs,
-				   COALESCE(pp.realm_ranking_bracket, '')
+				   COALESCE(pp.realm_class_bracket, '')
 			FROM players p
 			JOIN realms r ON p.realm_id = r.id
 			JOIN player_profiles pp ON p.id = pp.player_id
@@ -355,10 +353,10 @@ func generateClassScope(db *sql.DB, out, scope, region, real
mSlug, classKey stri
 			WHERE pp.season_id = ? AND r.region = ?
 				AND (r.slug = ? OR r.parent_realm_slug = ?)
 				AND pp.has_complete_coverage = 1 AND pp.combined_best_time IS NOT NULL
+				AND pp.class_name IS NOT NULL
 			ORDER BY pp.combined_best_time ASC, p.name ASC
 		`, seasonID, region, realmSlug, realmSlug)
 	}
-	_ = bracketColumn // unused for now, but documents intent
 	if err != nil {
 		return err
 	}

diff --git a/nix/pkgs/ookstats/src/internal/
generator/player_leaderboards.go b/nix/pkgs/
ookstats/src/internal/generator/player_leade
rboards.go
index ce0d26b..6d7811c 100644
--- a/nix/pkgs/ookstats/src/internal/generat
or/player_leaderboards.go
+++ b/nix/pkgs/ookstats/src/internal/generat
or/player_leaderboards.go
@@ -311,43 +311,41 @@ func generateClassScop
e(db *sql.DB, out, scope, region, realmSlug,
 classKey stri
 	// Fetch ALL players for this season, then
 filter by class in Go (to handle fallback l
ogic)
 	var rows *sql.Rows
 	var err error
-	var bracketColumn string
 	if scope == "global" {
-		bracketColumn = "pp.global_ranking_bracke
t"
 		rows, err = db.Query(`
 			SELECT p.id, p.name, r.slug, r.name, r.r
egion,
 				   COALESCE(pd.class_name,''), COALESCE
(pd.active_spec_name,''), pp.main_spec_id,
 				   pp.combined_best_time, pp.dungeons_c
ompleted, pp.total_runs,
-				   COALESCE(pp.global_ranking_bracket, 
'')
+				   COALESCE(pp.global_class_bracket, ''
)
 			FROM players p
 			JOIN realms r ON p.realm_id = r.id
 			JOIN player_profiles pp ON p.id = pp.pla
yer_id
 			LEFT JOIN player_details pd ON p.id = pd
.player_id
 			WHERE pp.season_id = ? AND pp.has_comple
te_coverage = 1 AND pp.combined_best_time IS
 NOT NULL
+				AND pp.class_name IS NOT NULL
 			ORDER BY pp.combined_best_time ASC, p.na
me ASC
 		`, seasonID)
 	} else if scope == "regional" {
-		bracketColumn = "pp.regional_ranking_brac
ket"
 		rows, err = db.Query(`
 			SELECT p.id, p.name, r.slug, r.name, r.r
egion,
 				   COALESCE(pd.class_name,''), COALESCE
(pd.active_spec_name,''), pp.main_spec_id,
 				   pp.combined_best_time, pp.dungeons_c
ompleted, pp.total_runs,
-				   COALESCE(pp.regional_ranking_bracket
, '')
+				   COALESCE(pp.region_class_bracket, ''
)
 			FROM players p
 			JOIN realms r ON p.realm_id = r.id
 			JOIN player_profiles pp ON p.id = pp.pla
yer_id
 			LEFT JOIN player_details pd ON p.id = pd
.player_id
 			WHERE pp.season_id = ? AND r.region = ? 
AND pp.has_complete_coverage = 1 AND pp.comb
ined_best_time IS NOT NULL
+				AND pp.class_name IS NOT NULL
 			ORDER BY pp.combined_best_time ASC, p.na
me ASC
 		`, seasonID, region)
 	} else {
 		// Realm scope - include entire pool (par
ent + all children)
-		bracketColumn = "pp.realm_ranking_bracket
"
 		rows, err = db.Query(`
 			SELECT p.id, p.name, r.slug, r.name, r.r
egion,
 				   COALESCE(pd.class_name,''), COALESCE
(pd.active_spec_name,''), pp.main_spec_id,
 				   pp.combined_best_time, pp.dungeons_c
ompleted, pp.total_runs,
-				   COALESCE(pp.realm_ranking_bracket, '
')
+				   COALESCE(pp.realm_class_bracket, '')
 			FROM players p
 			JOIN realms r ON p.realm_id = r.id
 			JOIN player_profiles pp ON p.id = pp.pla
yer_id
@@ -355,10 +353,10 @@ func generateClassScop
e(db *sql.DB, out, scope, region, realmSlug,
 classKey stri
 			WHERE pp.season_id = ? AND r.region = ?
 				AND (r.slug = ? OR r.parent_realm_slug 
= ?)
 				AND pp.has_complete_coverage = 1 AND pp
.combined_best_time IS NOT NULL
+				AND pp.class_name IS NOT NULL
 			ORDER BY pp.combined_best_time ASC, p.na
me ASC
 		`, seasonID, region, realmSlug, realmSlug
)
 	}
-	_ = bracketColumn // unused for now, but d
ocuments intent
 	if err != nil {
 		return err
 	}
 

diff --git a/nix/pkgs/ookstats/src/internal/pipeline/process.go b/nix/pkgs/ookstat
s/src/internal/pipeline/process.go
index f31333d..f9cd01f 100644
--- a/nix/pkgs/ookstats/src/internal/pipeline/process.go
+++ b/nix/pkgs/ookstats/src/internal/pipeline/process.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"time"
 
+	"github.com/charmbracelet/log"
 	"ookstats/internal/wow"
 )
 
@@ -15,14 +16,14 @@ type ProcessPlayersOptions struct {
 
 // ProcessPlayers processes player aggregations and rankings
 func ProcessPlayers(db *sql.DB, opts ProcessPlayersOptions) (profilesCreated int,
 qualifiedPlayers int, err error) {
-	fmt.Println("=== Player Aggregation ===")
+	log.Info("player aggregation")
 
 	// check if we have data
 	var runCount, playerCount int
 	db.QueryRow("SELECT COUNT(*) FROM challenge_runs").Scan(&runCount)
 	db.QueryRow("SELECT COUNT(*) FROM players").Scan(&playerCount)
 
-	fmt.Printf("Found %d runs and %d players in database\n", runCount, playerCount)
+	log.Info("found data in database", "runs", runCount, "players", playerCount)
 
 	if runCount == 0 {
 		return 0, 0, fmt.Errorf("no runs found in database - run 'fetch cm' first")
@@ -36,43 +37,49 @@ func ProcessPlayers(db *sql.DB, opts ProcessPlayersOptions) (p
rofilesCreated int
 	defer tx.Rollback()
 
 	// step 0: ensure seasons are properly configured
-	fmt.Println("\n0. Checking season configuration...")
+	log.Info("checking season configuration")
 	var seasonCount int
 	tx.QueryRow("SELECT COUNT(*) FROM seasons").Scan(&seasonCount)
 	if seasonCount == 0 {
-		fmt.Printf("Warning: No seasons found in database. Proceeding with legacy all-t
ime processing.\n")
+		log.Warn("no seasons found in database - proceeding with legacy all-time proces
sing")
 	} else {
-		fmt.Printf("Found %d seasons configured\n", seasonCount)
+		log.Info("found seasons configured", "count", seasonCount)
 	}
 
 	// step 1: create player aggregations (season-aware if seasons exist)
-	fmt.Println("\n1. Creating player aggregations...")
+	log.Info("creating player aggregations")
 	profilesCreated, err = createPlayerAggregations(tx)
 	if err != nil {
 		return 0, 0, fmt.Errorf("failed to create player aggregations: %w", err)
 	}
 
 	// step 2: compute player rankings (global, regional, realm) per season
-	fmt.Println("\n2. Computing player rankings...")
+	log.Info("computing player rankings")
 	qualifiedPlayers, err = computePlayerRankings(tx)
 	if err != nil {
 		return 0, 0, fmt.Errorf("failed to compute player rankings: %w", err)
 	}
 
+	// step 3: compute class-specific rankings per season
+	log.Info("computing class-specific rankings")
+	if err = computePlayerClassRankings(tx); err != nil {
+		return 0, 0, fmt.Errorf("failed to compute class rankings: %w", err)
+	}
+
 	// commit all changes
 	if err := tx.Commit(); err != nil {
 		return 0, 0, fmt.Errorf("failed to commit player aggregations: %w", err)
 	}
 
 	// optimize database
-	fmt.Println("\n3. Optimizing database...")
+	log.Info("optimizing database")
 	if _, err := db.Exec("VACUUM"); err != nil {
-		fmt.Printf("Warning: database optimization failed: %v\n", err)
+		log.Warn("database optimization failed", "error", err)
 	}
 
-	fmt.Printf("\nPlayer aggregation complete!\n")
-	fmt.Printf("   Created %d player profiles\n", profilesCreated)
-	fmt.Printf("   Computed rankings for %d qualified players\n", qualifiedPlayers)
+	log.Info("player aggregation complete",
+		"profiles", profilesCreated,
+		"qualified_players", qualifiedPlayers)
 
 	return profilesCreated, qualifiedPlayers, nil
 }
@@ -84,7 +91,7 @@ type ProcessRunRankingsOptions struct {
 
 // ProcessRunRankings computes global, regional, and realm rankings for all runs
 func ProcessRunRankings(db *sql.DB, opts ProcessRunRankingsOptions) error {
-	fmt.Println("=== Run Ranking Processor ===")
+	log.Info("run ranking processor")
 
 	// check if we have data
 	var runCount int
@@ -94,7 +101,7 @@ func ProcessRunRankings(db *sql.DB, opts ProcessRunRankingsOpti
ons) error {
 		return fmt.Errorf("no runs found in database - run 'fetch cm' first")
 	}
 
-	fmt.Printf("Found %d runs in database\n", runCount)
+	log.Info("found runs in database", "runs", runCount)
 
 	// begin transaction for all ranking operations
 	tx, err := db.Begin()
@@ -104,19 +111,19 @@ func ProcessRunRankings(db *sql.DB, opts ProcessRunRankingsO
ptions) error {
 	defer tx.Rollback()
 
 	// step 1: compute global run rankings
-	fmt.Println("\n1. Computing global run rankings...")
+	log.Info("computing global run rankings")
 	if err := computeGlobalRankings(tx); err != nil {
 		return fmt.Errorf("failed to compute global rankings: %w", err)
 	}
 
 	// step 2: compute regional run rankings
-	fmt.Println("\n2. Computing regional run rankings...")
+	log.Info("computing regional run rankings")
 	if err := computeRegionalRankings(tx); err != nil {
 		return fmt.Errorf("failed to compute regional rankings: %w", err)
 	}
 
 	// step 3: compute realm run rankings (pool-based for connected realms)
-	fmt.Println("\n3. Computing realm run rankings (pool-based)...")
+	log.Info("computing realm run rankings (pool-based)")
 	if err := computeRealmRankings(tx); err != nil {
 		return fmt.Errorf("failed to compute realm rankings: %w", err)
 	}
@@ -127,18 +134,18 @@ func ProcessRunRankings(db *sql.DB, opts ProcessRunRankingsO
ptions) error {
 	}
 
 	// optimize database
-	fmt.Println("\n4. Optimizing database...")
+	log.Info("optimizing database")
 	if _, err := db.Exec("VACUUM"); err != nil {
-		fmt.Printf("Warning: database optimization failed: %v\n", err)
+		log.Warn("database optimization failed", "error", err)
 	}
 
-	fmt.Printf("\nRun ranking computation complete!\n")
+	log.Info("run ranking computation complete")
 	return nil
 }
 
 // createPlayerAggregations creates player profiles and best runs
 func createPlayerAggregations(tx *sql.Tx) (int, error) {
-	fmt.Printf("Computing player aggregations...\n")
+	log.Info("computing player aggregations")
 
 	// clear existing aggregation data
 	if _, err := tx.Exec("DELETE FROM player_profiles"); err != nil {
@@ -147,12 +154,12 @@ func createPlayerAggregations(tx *sql.Tx) (int, error) {
 	if _, err := tx.Exec("DELETE FROM player_best_runs"); err != nil {
 		return 0, err
 	}
-	fmt.Printf("Cleared existing player aggregation data\n")
+	log.Info("cleared existing player aggregation data")
 
 	currentTime := time.Now().UnixMilli()
 
 	// step 1: find best run per player per dungeon per season WITH rankings in one 
efficient query
-	fmt.Printf("Step 1: Computing best runs per player per dungeon per season with r
ankings...\n")
+	log.Info("computing best runs per player per dungeon per season with rankings")
 	_, err := tx.Exec(`
 		INSERT INTO player_best_runs (
 			player_id, dungeon_id, run_id, duration, season_id, completed_timestamp,
@@ -217,9 +224,9 @@ func createPlayerAggregations(tx *sql.Tx) (int, error) {
 
 	var bestRunsCount int
 	tx.QueryRow("SELECT COUNT(*) FROM player_best_runs").Scan(&bestRunsCount)
-	fmt.Printf("[OK] Computed %d best runs with rankings in single query\n", bestRun
sCount)
+	log.Info("computed best runs with rankings", "count", bestRunsCount)
 
-	fmt.Printf("Step 3: Creating player profiles per season...\n")
+	log.Info("creating player profiles per season")
 	_, err = tx.Exec(`
 		INSERT INTO player_profiles (
 			player_id, season_id, name, realm_id, dungeons_completed, total_runs,
@@ -262,10 +269,10 @@ func createPlayerAggregations(tx *sql.Tx) (int, error) {
 
 	var profilesCount int
 	tx.QueryRow("SELECT COUNT(*) FROM player_profiles").Scan(&profilesCount)
-	fmt.Printf("[OK] Created %d player profiles\n", profilesCount)
+	log.Info("created player profiles", "count", profilesCount)
 
 	// step 4: determine main spec for each player per season based on best runs
-	fmt.Printf("Step 4: Computing main specs per season...\n")
+	log.Info("computing main specs per season")
 	_, err = tx.Exec(`
 		UPDATE player_profiles
 		SET main_spec_id = (
@@ -290,14 +297,14 @@ func createPlayerAggregations(tx *sql.Tx) (int, error) {
 	if err != nil {
 		return 0, err
 	}
-	fmt.Printf("[OK] Updated main specs\n")
+	log.Info("updated main specs")
 
 	// step 5: derive class from main spec
-	fmt.Printf("Step 5: Deriving class names from main specs...\n")
+	log.Info("deriving class names from main specs")
 	if err := deriveClassFromMainSpec(tx); err != nil {
 		return 0, fmt.Errorf("failed to derive class names: %w", err)
 	}
-	fmt.Printf("[OK] Updated class names\n")
+	log.Info("updated class names")
 
 	return profilesCount, nil
 }
@@ -351,13 +358,13 @@ func deriveClassFromMainSpec(tx *sql.Tx) error {
 		return fmt.Errorf("error iterating rows: %w", err)
 	}
 
-	fmt.Printf("  Derived class for %d player profiles\n", updatedCount)
+	log.Debug("derived class for player profiles", "count", updatedCount)
 	return nil
 }
 
 // computePlayerRankings computes rankings for players with complete coverage per
 season
 func computePlayerRankings(tx *sql.Tx) (int, error) {
-	fmt.Printf("Computing player rankings per season...\n")
+	log.Info("computing player rankings per season")
 
 	currentTime := time.Now().UnixMilli()
 
@@ -378,13 +385,13 @@ func computePlayerRankings(tx *sql.Tx) (int, error) {
 	}
 
 	if len(seasons) == 0 {
-		fmt.Printf("No seasons found in player profiles, skipping rankings\n")
+		log.Warn("no seasons found in player profiles - skipping rankings")
 		return 0, nil
 	}
 
 	totalQualified := 0
 	for _, seasonID := range seasons {
-		fmt.Printf("\n=== Processing Season %d ===\n", seasonID)
+		log.Info("processing season", "season_id", seasonID)
 
 		// get qualified players count for this season
 		var qualifiedCount int
@@ -393,17 +400,19 @@ func computePlayerRankings(tx *sql.Tx) (int, error) {
 			return 0, err
 		}
 
-		fmt.Printf("Found %d players with complete coverage in season %d\n", qualifiedC
ount, seasonID)
+		log.Info("found players with complete coverage",
+			"count", qualifiedCount,
+			"season_id", seasonID)
 
 		if qualifiedCount == 0 {
-			fmt.Printf("No qualified players found for season %d, skipping\n", seasonID)
+			log.Warn("no qualified players found - skipping season", "season_id", seasonID
)
 			continue
 		}
 
 		totalQualified += qualifiedCount
 
 		// step 1: global rankings for this season
-		fmt.Printf("Computing global rankings for season %d...\n", seasonID)
+		log.Info("computing global rankings", "season_id", seasonID)
 		_, err = tx.Exec(`
 			UPDATE player_profiles
 			SET global_ranking = (
@@ -434,7 +443,7 @@ func computePlayerRankings(tx *sql.Tx) (int, error) {
 		}
 
 		// update global ranking brackets for this season
-		fmt.Printf("Computing global ranking brackets for season %d...\n", seasonID)
+		log.Info("computing global ranking brackets", "season_id", seasonID)
 		_, err = tx.Exec(`
 			UPDATE player_profiles
 			SET global_ranking_bracket = (
@@ -458,7 +467,7 @@ func computePlayerRankings(tx *sql.Tx) (int, error) {
 		}
 
 		// step 2: regional rankings for this season
-		fmt.Printf("Computing regional rankings for season %d...\n", seasonID)
+		log.Info("computing regional rankings", "season_id", seasonID)
 		_, err = tx.Exec(`
 			UPDATE player_profiles
 			SET regional_ranking = (
@@ -491,7 +500,7 @@ func computePlayerRankings(tx *sql.Tx) (int, error) {
 		}
 
 		// update regional ranking brackets for this season
-		fmt.Printf("Computing regional ranking brackets for season %d...\n", seasonID)
+		log.Info("computing regional ranking brackets", "season_id", seasonID)
 		_, err = tx.Exec(`
 			UPDATE player_profiles
 			SET regional_ranking_bracket = (
@@ -529,7 +538,7 @@ func computePlayerRankings(tx *sql.Tx) (int, error) {
 		}
 
 		// step 3: realm rankings for this season (pool-based for connected realms)
-		fmt.Printf("Computing realm rankings for season %d (using realm pools)...\n", s
easonID)
+		log.Info("computing realm rankings (using realm pools)", "season_id", seasonID)
 		_, err = tx.Exec(`
 			UPDATE player_profiles
 			SET realm_ranking = (
@@ -565,7 +574,7 @@ func computePlayerRankings(tx *sql.Tx) (int, error) {
 		}
 
 		// update realm ranking brackets for this season (pool-based for connected real
ms)
-		fmt.Printf("Computing realm ranking brackets for season %d (using realm pools).
..\n", seasonID)
+		log.Info("computing realm ranking brackets (using realm pools)", "season_id", s
easonID)
 		_, err = tx.Exec(`
 			UPDATE player_profiles
 			SET realm_ranking_bracket = (
@@ -604,16 +613,232 @@ func computePlayerRankings(tx *sql.Tx) (int, error) {
 			return 0, err
 		}
 
-		fmt.Printf("[OK] Computed rankings for season %d with percentile brackets (%d q
ualified players)\n", seasonID, qualifiedCount)
+		log.Info("computed rankings for season with percentile brackets",
+			"season_id", seasonID,
+			"qualified_players", qualifiedCount)
 	}
 
-	fmt.Printf("\n[OK] Computed rankings for all seasons (total: %d qualified player
s across all seasons)\n", totalQualified)
+	log.Info("computed rankings for all seasons",
+		"total_qualified_players", totalQualified)
 	return totalQualified, nil
 }
 
+// computePlayerClassRankings computes class-specific rankings for players per se
ason
+func computePlayerClassRankings(tx *sql.Tx) error {
+	log.Info("computing class-specific player rankings per season")
+
+	// get all distinct season numbers
+	seasonRows, err := tx.Query("SELECT DISTINCT season_number FROM seasons ORDER BY
 season_number")
+	if err != nil {
+		return fmt.Errorf("failed to query seasons: %w", err)
+	}
+	defer seasonRows.Close()
+
+	var seasons []int
+	for seasonRows.Next() {
+		var seasonNumber int
+		if err := seasonRows.Scan(&seasonNumber); err != nil {
+			return err
+		}
+		seasons = append(seasons, seasonNumber)
+	}
+
+	if len(seasons) == 0 {
+		log.Warn("no seasons found, skipping class rankings")
+		return nil
+	}
+
+	for _, seasonID := range seasons {
+		log.Info("processing class rankings for season", "season_id", seasonID)
+
+		// step 1: global class rankings
+		_, err = tx.Exec(`
+			UPDATE player_profiles
+			SET global_class_rank = (
+				SELECT ranking FROM (
+					SELECT
+						player_id,
+						ROW_NUMBER() OVER (PARTITION BY class_name ORDER BY combined_best_time ASC)
 as ranking
+					FROM player_profiles
+					WHERE season_id = ? AND has_complete_coverage = 1 AND class_name IS NOT NULL
+				) class_ranks
+				WHERE class_ranks.player_id = player_profiles.player_id
+			)
+			WHERE season_id = ? AND has_complete_coverage = 1 AND class_name IS NOT NULL
+		`, seasonID, seasonID)
+		if err != nil {
+			return fmt.Errorf("failed to compute global class rankings: %w", err)
+		}
+
+		// step 2: global class ranking brackets
+		_, err = tx.Exec(`
+			UPDATE player_profiles
+			SET global_class_bracket = (
+				CASE
+					WHEN counts.combined_best_time = counts.class_min_time THEN 'artifact'
+					ELSE
+						CASE
+							WHEN (CAST(counts.global_class_rank AS REAL) / CAST(counts.class_total AS 
REAL) * 100) <= 1.0 THEN 'excellent'
+							WHEN (CAST(counts.global_class_rank AS REAL) / CAST(counts.class_total AS 
REAL) * 100) <= 5.0 THEN 'legendary'
+							WHEN (CAST(counts.global_class_rank AS REAL) / CAST(counts.class_total AS 
REAL) * 100) <= 20.0 THEN 'epic'
+							WHEN (CAST(counts.global_class_rank AS REAL) / CAST(counts.class_total AS 
REAL) * 100) <= 40.0 THEN 'rare'
+							WHEN (CAST(counts.global_class_rank AS REAL) / CAST(counts.class_total AS 
REAL) * 100) <= 60.0 THEN 'uncommon'
+							ELSE 'common'
+						END
+				END
+			)
+			FROM (
+				SELECT
+					pp.player_id,
+					pp.global_class_rank,
+					pp.combined_best_time,
+					MIN(pp.combined_best_time) OVER (PARTITION BY pp.class_name) as class_min_ti
me,
+					COUNT(*) OVER (PARTITION BY pp.class_name) as class_total
+				FROM player_profiles pp
+				WHERE pp.season_id = ? AND pp.has_complete_coverage = 1
+					AND pp.global_class_rank IS NOT NULL AND pp.class_name IS NOT NULL
+			) counts
+			WHERE player_profiles.player_id = counts.player_id
+				AND player_profiles.season_id = ?
+				AND player_profiles.has_complete_coverage = 1
+				AND player_profiles.global_class_rank IS NOT NULL
+		`, seasonID, seasonID)
+		if err != nil {
+			return fmt.Errorf("failed to compute global class brackets: %w", err)
+		}
+
+		// step 3: regional class rankings
+		_, err = tx.Exec(`
+			UPDATE player_profiles
+			SET region_class_rank = (
+				SELECT ranking FROM (
+					SELECT
+						pp.player_id,
+						ROW_NUMBER() OVER (PARTITION BY r.region, pp.class_name ORDER BY pp.combine
d_best_time ASC) as ranking
+					FROM player_profiles pp
+					INNER JOIN realms r ON pp.realm_id = r.id
+					WHERE pp.season_id = ? AND pp.has_complete_coverage = 1 AND pp.class_name IS
 NOT NULL
+				) class_ranks
+				WHERE class_ranks.player_id = player_profiles.player_id
+			)
+			WHERE season_id = ? AND has_complete_coverage = 1 AND class_name IS NOT NULL
+		`, seasonID, seasonID)
+		if err != nil {
+			return fmt.Errorf("failed to compute regional class rankings: %w", err)
+		}
+
+		// step 4: regional class ranking brackets
+		_, err = tx.Exec(`
+			UPDATE player_profiles
+			SET region_class_bracket = (
+				CASE
+					WHEN counts.combined_best_time = counts.class_regional_min_time THEN 'artifa
ct'
+					ELSE
+						CASE
+							WHEN (CAST(counts.region_class_rank AS REAL) / CAST(counts.class_regional_
total AS REAL) * 100) <= 1.0 THEN 'excellent'
+							WHEN (CAST(counts.region_class_rank AS REAL) / CAST(counts.class_regional_
total AS REAL) * 100) <= 5.0 THEN 'legendary'
+							WHEN (CAST(counts.region_class_rank AS REAL) / CAST(counts.class_regional_
total AS REAL) * 100) <= 20.0 THEN 'epic'
+							WHEN (CAST(counts.region_class_rank AS REAL) / CAST(counts.class_regional_
total AS REAL) * 100) <= 40.0 THEN 'rare'
+							WHEN (CAST(counts.region_class_rank AS REAL) / CAST(counts.class_regional_
total AS REAL) * 100) <= 60.0 THEN 'uncommon'
+							ELSE 'common'
+						END
+				END
+			)
+			FROM (
+				SELECT
+					pp.player_id,
+					pp.region_class_rank,
+					pp.combined_best_time,
+					MIN(pp.combined_best_time) OVER (PARTITION BY r.region, pp.class_name) as cl
ass_regional_min_time,
+					COUNT(*) OVER (PARTITION BY r.region, pp.class_name) as class_regional_total
+				FROM player_profiles pp
+				INNER JOIN realms r ON pp.realm_id = r.id
+				WHERE pp.season_id = ? AND pp.has_complete_coverage = 1
+					AND pp.region_class_rank IS NOT NULL AND pp.class_name IS NOT NULL
+			) counts
+			WHERE player_profiles.player_id = counts.player_id
+				AND player_profiles.season_id = ?
+				AND player_profiles.has_complete_coverage = 1
+				AND player_profiles.region_class_rank IS NOT NULL
+		`, seasonID, seasonID)
+		if err != nil {
+			return fmt.Errorf("failed to compute regional class brackets: %w", err)
+		}
+
+		// step 5: realm class rankings (pool-based for connected realms)
+		_, err = tx.Exec(`
+			UPDATE player_profiles
+			SET realm_class_rank = (
+				SELECT ranking FROM (
+					SELECT
+						pp.player_id,
+						ROW_NUMBER() OVER (
+							PARTITION BY COALESCE(parent_r.id, r.id), pp.class_name
+							ORDER BY pp.combined_best_time ASC
+						) as ranking
+					FROM player_profiles pp
+					JOIN realms r ON pp.realm_id = r.id
+					LEFT JOIN realms parent_r ON r.parent_realm_slug = parent_r.slug AND r.regio
n = parent_r.region
+					WHERE pp.season_id = ? AND pp.has_complete_coverage = 1 AND pp.class_name IS
 NOT NULL
+				) class_ranks
+				WHERE class_ranks.player_id = player_profiles.player_id
+			)
+			WHERE season_id = ? AND has_complete_coverage = 1 AND class_name IS NOT NULL
+		`, seasonID, seasonID)
+		if err != nil {
+			return fmt.Errorf("failed to compute realm class rankings: %w", err)
+		}
+
+		// step 6: realm class ranking brackets (pool-based)
+		_, err = tx.Exec(`
+			UPDATE player_profiles
+			SET realm_class_bracket = (
+				CASE
+					WHEN counts.combined_best_time = counts.class_pool_min_time THEN 'artifact'
+					ELSE
+						CASE
+							WHEN (CAST(counts.realm_class_rank AS REAL) / CAST(counts.class_pool_total
 AS REAL) * 100) <= 1.0 THEN 'excellent'
+							WHEN (CAST(counts.realm_class_rank AS REAL) / CAST(counts.class_pool_total
 AS REAL) * 100) <= 5.0 THEN 'legendary'
+							WHEN (CAST(counts.realm_class_rank AS REAL) / CAST(counts.class_pool_total
 AS REAL) * 100) <= 20.0 THEN 'epic'
+							WHEN (CAST(counts.realm_class_rank AS REAL) / CAST(counts.class_pool_total
 AS REAL) * 100) <= 40.0 THEN 'rare'
+							WHEN (CAST(counts.realm_class_rank AS REAL) / CAST(counts.class_pool_total
 AS REAL) * 100) <= 60.0 THEN 'uncommon'
+							ELSE 'common'
+						END
+				END
+			)
+			FROM (
+				SELECT
+					pp.player_id,
+					pp.realm_class_rank,
+					pp.combined_best_time,
+					COALESCE(parent_r.id, r.id) as pool_id,
+					MIN(pp.combined_best_time) OVER (PARTITION BY COALESCE(parent_r.id, r.id), p
p.class_name) as class_pool_min_time,
+					COUNT(*) OVER (PARTITION BY COALESCE(parent_r.id, r.id), pp.class_name) as c
lass_pool_total
+				FROM player_profiles pp
+				JOIN realms r ON pp.realm_id = r.id
+				LEFT JOIN realms parent_r ON r.parent_realm_slug = parent_r.slug AND r.region
 = parent_r.region
+				WHERE pp.season_id = ? AND pp.has_complete_coverage = 1
+					AND pp.realm_class_rank IS NOT NULL AND pp.class_name IS NOT NULL
+			) counts
+			WHERE player_profiles.player_id = counts.player_id
+				AND player_profiles.season_id = ?
+				AND player_profiles.has_complete_coverage = 1
+				AND player_profiles.realm_class_rank IS NOT NULL
+		`, seasonID, seasonID)
+		if err != nil {
+			return fmt.Errorf("failed to compute realm class brackets: %w", err)
+		}
+
+		log.Info("computed class rankings for season", "season_id", seasonID)
+	}
+
+	log.Info("computed class rankings for all seasons")
+	return nil
+}
+
 // computeGlobalRankings computes global rankings for all runs (per season)
 func computeGlobalRankings(tx *sql.Tx) error {
-	fmt.Printf("Computing global rankings per season...\n")
+	log.Info("computing global rankings per season")
 
 	currentTime := time.Now().UnixMilli()
 
@@ -646,7 +871,7 @@ func computeGlobalRankings(tx *sql.Tx) error {
 	}
 
 	// update percentile brackets for unfiltered global rankings using efficient SQL
 (per season)
-	fmt.Printf("Computing global ranking brackets per season...\n")
+	log.Info("computing global ranking brackets per season")
 	_, err = tx.Exec(`
 		UPDATE run_rankings
 		SET percentile_bracket = (
@@ -784,7 +1009,7 @@ func computeGlobalRankings(tx *sql.Tx) error {
 	}
 
 	// update percentile brackets for filtered global rankings using efficient SQL (
per season)
-	fmt.Printf("Computing filtered global ranking brackets per season...\n")
+	log.Info("computing filtered global ranking brackets per season")
 	_, err = tx.Exec(`
 		UPDATE run_rankings
 		SET percentile_bracket = (
@@ -824,13 +1049,13 @@ func computeGlobalRankings(tx *sql.Tx) error {
 		return err
 	}
 
-	fmt.Printf("[OK] Computed global rankings with percentile brackets (all and filt
ered)\n")
+	log.Info("computed global rankings with percentile brackets (all and filtered)")
 	return nil
 }
 
 // computeRegionalRankings computes regional rankings for all runs
 func computeRegionalRankings(tx *sql.Tx) error {
-	fmt.Printf("Computing regional rankings...\n")
+	log.Info("computing regional rankings")
 
 	currentTime := time.Now().UnixMilli()
 
@@ -883,7 +1108,7 @@ func computeRegionalRankings(tx *sql.Tx) error {
 		}
 
 		// update percentile brackets for unfiltered regional rankings using efficient 
SQL (per season)
-		fmt.Printf("Computing unfiltered regional ranking brackets for %s per season...
\n", region)
+		log.Info("computing unfiltered regional ranking brackets per season", "region",
 region)
 		_, err = tx.Exec(`
 			UPDATE run_rankings
 			SET percentile_bracket = (
@@ -1027,7 +1252,7 @@ func computeRegionalRankings(tx *sql.Tx) error {
 
 		// update percentile brackets for filtered regional rankings using efficient SQ
L (per season)
 		filteredScope := region + "_filtered"
-		fmt.Printf("Computing filtered regional ranking brackets for %s per season...\n
", region)
+		log.Info("computing filtered regional ranking brackets per season", "region", r
egion)
 		_, err = tx.Exec(`
 			UPDATE run_rankings
 			SET percentile_bracket = (
@@ -1068,13 +1293,13 @@ func computeRegionalRankings(tx *sql.Tx) error {
 		}
 	}
 
-	fmt.Printf("[OK] Computed regional rankings with percentile brackets for %d regi
ons\n", len(regions))
+	log.Info("computed regional rankings with percentile brackets", "regions", len(r
egions))
 	return nil
 }
 
 // computeRealmRankings computes realm rankings per realm pool (connected realms 
grouped together)
 func computeRealmRankings(tx *sql.Tx) error {
-	fmt.Printf("Computing realm rankings (pool-based for connected realms)...\n")
+	log.Info("computing realm rankings (pool-based for connected realms)")
 
 	currentTime := time.Now().UnixMilli()
 
@@ -1111,7 +1336,7 @@ func computeRealmRankings(tx *sql.Tx) error {
 		pools = append(pools, pool)
 	}
 
-	fmt.Printf("Found %d realm pools to process\n", len(pools))
+	log.Info("found realm pools to process", "pools", len(pools))
 
 	for _, pool := range pools {
 		// unfiltered realm rankings (per season) using pool-based partitioning
@@ -1341,6 +1566,6 @@ func computeRealmRankings(tx *sql.Tx) error {
 		}
 	}
 
-	fmt.Printf("[OK] Computed realm rankings with percentile brackets for %d realm p
ools\n", len(pools))
+	log.Info("computed realm rankings with percentile brackets", "pools", len(pools)
)
 	return nil
 }

diff --git a/nix/pkgs/ookstats/src/internal/
pipeline/process.go b/nix/pkgs/ookstats/src/
internal/pipeline/process.go
index f31333d..f9cd01f 100644
--- a/nix/pkgs/ookstats/src/internal/pipelin
e/process.go
+++ b/nix/pkgs/ookstats/src/internal/pipelin
e/process.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"time"
 
+	"github.com/charmbracelet/log"
 	"ookstats/internal/wow"
 )
 
@@ -15,14 +16,14 @@ type ProcessPlayersOptio
ns struct {
 
 // ProcessPlayers processes player aggregat
ions and rankings
 func ProcessPlayers(db *sql.DB, opts Proces
sPlayersOptions) (profilesCreated int, quali
fiedPlayers int, err error) {
-	fmt.Println("=== Player Aggregation ===")
+	log.Info("player aggregation")
 
 	// check if we have data
 	var runCount, playerCount int
 	db.QueryRow("SELECT COUNT(*) FROM challeng
e_runs").Scan(&runCount)
 	db.QueryRow("SELECT COUNT(*) FROM players"
).Scan(&playerCount)
 
-	fmt.Printf("Found %d runs and %d players i
n database\n", runCount, playerCount)
+	log.Info("found data in database", "runs",
 runCount, "players", playerCount)
 
 	if runCount == 0 {
 		return 0, 0, fmt.Errorf("no runs found in
 database - run 'fetch cm' first")
@@ -36,43 +37,49 @@ func ProcessPlayers(db *
sql.DB, opts ProcessPlayersOptions) (profile
sCreated int
 	defer tx.Rollback()
 
 	// step 0: ensure seasons are properly con
figured
-	fmt.Println("\n0. Checking season configur
ation...")
+	log.Info("checking season configuration")
 	var seasonCount int
 	tx.QueryRow("SELECT COUNT(*) FROM seasons"
).Scan(&seasonCount)
 	if seasonCount == 0 {
-		fmt.Printf("Warning: No seasons found in 
database. Proceeding with legacy all-time pr
ocessing.\n")
+		log.Warn("no seasons found in database - 
proceeding with legacy all-time processing")
 	} else {
-		fmt.Printf("Found %d seasons configured\n
", seasonCount)
+		log.Info("found seasons configured", "cou
nt", seasonCount)
 	}
 
 	// step 1: create player aggregations (sea
son-aware if seasons exist)
-	fmt.Println("\n1. Creating player aggregat
ions...")
+	log.Info("creating player aggregations")
 	profilesCreated, err = createPlayerAggrega
tions(tx)
 	if err != nil {
 		return 0, 0, fmt.Errorf("failed to create
 player aggregations: %w", err)
 	}
 
 	// step 2: compute player rankings (global
, regional, realm) per season
-	fmt.Println("\n2. Computing player ranking
s...")
+	log.Info("computing player rankings")
 	qualifiedPlayers, err = computePlayerRanki
ngs(tx)
 	if err != nil {
 		return 0, 0, fmt.Errorf("failed to comput
e player rankings: %w", err)
 	}
 
+	// step 3: compute class-specific rankings
 per season
+	log.Info("computing class-specific ranking
s")
+	if err = computePlayerClassRankings(tx); e
rr != nil {
+		return 0, 0, fmt.Errorf("failed to comput
e class rankings: %w", err)
+	}
+
 	// commit all changes
 	if err := tx.Commit(); err != nil {
 		return 0, 0, fmt.Errorf("failed to commit
 player aggregations: %w", err)
 	}
 
 	// optimize database
-	fmt.Println("\n3. Optimizing database...")
+	log.Info("optimizing database")
 	if _, err := db.Exec("VACUUM"); err != nil
 {
-		fmt.Printf("Warning: database optimizatio
n failed: %v\n", err)
+		log.Warn("database optimization failed", 
"error", err)
 	}
 
-	fmt.Printf("\nPlayer aggregation complete!
\n")
-	fmt.Printf("   Created %d player profiles\
n", profilesCreated)
-	fmt.Printf("   Computed rankings for %d qu
alified players\n", qualifiedPlayers)
+	log.Info("player aggregation complete",
+		"profiles", profilesCreated,
+		"qualified_players", qualifiedPlayers)
 
 	return profilesCreated, qualifiedPlayers, 
nil
 }
@@ -84,7 +91,7 @@ type ProcessRunRankingsOpt
ions struct {
 
 // ProcessRunRankings computes global, regi
onal, and realm rankings for all runs
 func ProcessRunRankings(db *sql.DB, opts Pr
ocessRunRankingsOptions) error {
-	fmt.Println("=== Run Ranking Processor ===
")
+	log.Info("run ranking processor")
 
 	// check if we have data
 	var runCount int
@@ -94,7 +101,7 @@ func ProcessRunRankings(d
b *sql.DB, opts ProcessRunRankingsOptions) e
rror {
 		return fmt.Errorf("no runs found in datab
ase - run 'fetch cm' first")
 	}
 
-	fmt.Printf("Found %d runs in database\n", 
runCount)
+	log.Info("found runs in database", "runs",
 runCount)
 
 	// begin transaction for all ranking opera
tions
 	tx, err := db.Begin()
@@ -104,19 +111,19 @@ func ProcessRunRanking
s(db *sql.DB, opts ProcessRunRankingsOptions
) error {
 	defer tx.Rollback()
 
 	// step 1: compute global run rankings
-	fmt.Println("\n1. Computing global run ran
kings...")
+	log.Info("computing global run rankings")
 	if err := computeGlobalRankings(tx); err !
= nil {
 		return fmt.Errorf("failed to compute glob
al rankings: %w", err)
 	}
 
 	// step 2: compute regional run rankings
-	fmt.Println("\n2. Computing regional run r
ankings...")
+	log.Info("computing regional run rankings"
)
 	if err := computeRegionalRankings(tx); err
 != nil {
 		return fmt.Errorf("failed to compute regi
onal rankings: %w", err)
 	}
 
 	// step 3: compute realm run rankings (poo
l-based for connected realms)
-	fmt.Println("\n3. Computing realm run rank
ings (pool-based)...")
+	log.Info("computing realm run rankings (po
ol-based)")
 	if err := computeRealmRankings(tx); err !=
 nil {
 		return fmt.Errorf("failed to compute real
m rankings: %w", err)
 	}
@@ -127,18 +134,18 @@ func ProcessRunRanking
s(db *sql.DB, opts ProcessRunRankingsOptions
) error {
 	}
 
 	// optimize database
-	fmt.Println("\n4. Optimizing database...")
+	log.Info("optimizing database")
 	if _, err := db.Exec("VACUUM"); err != nil
 {
-		fmt.Printf("Warning: database optimizatio
n failed: %v\n", err)
+		log.Warn("database optimization failed", 
"error", err)
 	}
 
-	fmt.Printf("\nRun ranking computation comp
lete!\n")
+	log.Info("run ranking computation complete
")
 	return nil
 }
 
 // createPlayerAggregations creates player 
profiles and best runs
 func createPlayerAggregations(tx *sql.Tx) (
int, error) {
-	fmt.Printf("Computing player aggregations.
..\n")
+	log.Info("computing player aggregations")
 
 	// clear existing aggregation data
 	if _, err := tx.Exec("DELETE FROM player_p
rofiles"); err != nil {
@@ -147,12 +154,12 @@ func createPlayerAggre
gations(tx *sql.Tx) (int, error) {
 	if _, err := tx.Exec("DELETE FROM player_b
est_runs"); err != nil {
 		return 0, err
 	}
-	fmt.Printf("Cleared existing player aggreg
ation data\n")
+	log.Info("cleared existing player aggregat
ion data")
 
 	currentTime := time.Now().UnixMilli()
 
 	// step 1: find best run per player per du
ngeon per season WITH rankings in one effici
ent query
-	fmt.Printf("Step 1: Computing best runs pe
r player per dungeon per season with ranking
s...\n")
+	log.Info("computing best runs per player p
er dungeon per season with rankings")
 	_, err := tx.Exec(`
 		INSERT INTO player_best_runs (
 			player_id, dungeon_id, run_id, duration,
 season_id, completed_timestamp,
@@ -217,9 +224,9 @@ func createPlayerAggrega
tions(tx *sql.Tx) (int, error) {
 
 	var bestRunsCount int
 	tx.QueryRow("SELECT COUNT(*) FROM player_b
est_runs").Scan(&bestRunsCount)
-	fmt.Printf("[OK] Computed %d best runs wit
h rankings in single query\n", bestRunsCount
)
+	log.Info("computed best runs with rankings
", "count", bestRunsCount)
 
-	fmt.Printf("Step 3: Creating player profil
es per season...\n")
+	log.Info("creating player profiles per sea
son")
 	_, err = tx.Exec(`
 		INSERT INTO player_profiles (
 			player_id, season_id, name, realm_id, du
ngeons_completed, total_runs,
@@ -262,10 +269,10 @@ func createPlayerAggre
gations(tx *sql.Tx) (int, error) {
 
 	var profilesCount int
 	tx.QueryRow("SELECT COUNT(*) FROM player_p
rofiles").Scan(&profilesCount)
-	fmt.Printf("[OK] Created %d player profile
s\n", profilesCount)
+	log.Info("created player profiles", "count
", profilesCount)
 
 	// step 4: determine main spec for each pl
ayer per season based on best runs
-	fmt.Printf("Step 4: Computing main specs p
er season...\n")
+	log.Info("computing main specs per season"
)
 	_, err = tx.Exec(`
 		UPDATE player_profiles
 		SET main_spec_id = (
@@ -290,14 +297,14 @@ func createPlayerAggre
gations(tx *sql.Tx) (int, error) {
 	if err != nil {
 		return 0, err
 	}
-	fmt.Printf("[OK] Updated main specs\n")
+	log.Info("updated main specs")
 
 	// step 5: derive class from main spec
-	fmt.Printf("Step 5: Deriving class names f
rom main specs...\n")
+	log.Info("deriving class names from main s
pecs")
 	if err := deriveClassFromMainSpec(tx); err
 != nil {
 		return 0, fmt.Errorf("failed to derive cl
ass names: %w", err)
 	}
-	fmt.Printf("[OK] Updated class names\n")
+	log.Info("updated class names")
 
 	return profilesCount, nil
 }
@@ -351,13 +358,13 @@ func deriveClassFromMa
inSpec(tx *sql.Tx) error {
 		return fmt.Errorf("error iterating rows: 
%w", err)
 	}
 
-	fmt.Printf("  Derived class for %d player 
profiles\n", updatedCount)
+	log.Debug("derived class for player profil
es", "count", updatedCount)
 	return nil
 }
 
 // computePlayerRankings computes rankings 
for players with complete coverage per seaso
n
 func computePlayerRankings(tx *sql.Tx) (int
, error) {
-	fmt.Printf("Computing player rankings per 
season...\n")
+	log.Info("computing player rankings per se
ason")
 
 	currentTime := time.Now().UnixMilli()
 
@@ -378,13 +385,13 @@ func computePlayerRank
ings(tx *sql.Tx) (int, error) {
 	}
 
 	if len(seasons) == 0 {
-		fmt.Printf("No seasons found in player pr
ofiles, skipping rankings\n")
+		log.Warn("no seasons found in player prof
iles - skipping rankings")
 		return 0, nil
 	}
 
 	totalQualified := 0
 	for _, seasonID := range seasons {
-		fmt.Printf("\n=== Processing Season %d ==
=\n", seasonID)
+		log.Info("processing season", "season_id"
, seasonID)
 
 		// get qualified players count for this s
eason
 		var qualifiedCount int
@@ -393,17 +400,19 @@ func computePlayerRank
ings(tx *sql.Tx) (int, error) {
 			return 0, err
 		}
 
-		fmt.Printf("Found %d players with complet
e coverage in season %d\n", qualifiedCount, 
seasonID)
+		log.Info("found players with complete cov
erage",
+			"count", qualifiedCount,
+			"season_id", seasonID)
 
 		if qualifiedCount == 0 {
-			fmt.Printf("No qualified players found f
or season %d, skipping\n", seasonID)
+			log.Warn("no qualified players found - s
kipping season", "season_id", seasonID)
 			continue
 		}
 
 		totalQualified += qualifiedCount
 
 		// step 1: global rankings for this seaso
n
-		fmt.Printf("Computing global rankings for
 season %d...\n", seasonID)
+		log.Info("computing global rankings", "se
ason_id", seasonID)
 		_, err = tx.Exec(`
 			UPDATE player_profiles
 			SET global_ranking = (
@@ -434,7 +443,7 @@ func computePlayerRankin
gs(tx *sql.Tx) (int, error) {
 		}
 
 		// update global ranking brackets for thi
s season
-		fmt.Printf("Computing global ranking brac
kets for season %d...\n", seasonID)
+		log.Info("computing global ranking bracke
ts", "season_id", seasonID)
 		_, err = tx.Exec(`
 			UPDATE player_profiles
 			SET global_ranking_bracket = (
@@ -458,7 +467,7 @@ func computePlayerRankin
gs(tx *sql.Tx) (int, error) {
 		}
 
 		// step 2: regional rankings for this sea
son
-		fmt.Printf("Computing regional rankings f
or season %d...\n", seasonID)
+		log.Info("computing regional rankings", "
season_id", seasonID)
 		_, err = tx.Exec(`
 			UPDATE player_profiles
 			SET regional_ranking = (
@@ -491,7 +500,7 @@ func computePlayerRankin
gs(tx *sql.Tx) (int, error) {
 		}
 
 		// update regional ranking brackets for t
his season
-		fmt.Printf("Computing regional ranking br
ackets for season %d...\n", seasonID)
+		log.Info("computing regional ranking brac
kets", "season_id", seasonID)
 		_, err = tx.Exec(`
 			UPDATE player_profiles
 			SET regional_ranking_bracket = (
@@ -529,7 +538,7 @@ func computePlayerRankin
gs(tx *sql.Tx) (int, error) {
 		}
 
 		// step 3: realm rankings for this season
 (pool-based for connected realms)
-		fmt.Printf("Computing realm rankings for 
season %d (using realm pools)...\n", seasonI
D)
+		log.Info("computing realm rankings (using
 realm pools)", "season_id", seasonID)
 		_, err = tx.Exec(`
 			UPDATE player_profiles
 			SET realm_ranking = (
@@ -565,7 +574,7 @@ func computePlayerRankin
gs(tx *sql.Tx) (int, error) {
 		}
 
 		// update realm ranking brackets for this
 season (pool-based for connected realms)
-		fmt.Printf("Computing realm ranking brack
ets for season %d (using realm pools)...\n",
 seasonID)
+		log.Info("computing realm ranking bracket
s (using realm pools)", "season_id", seasonI
D)
 		_, err = tx.Exec(`
 			UPDATE player_profiles
 			SET realm_ranking_bracket = (
@@ -604,16 +613,232 @@ func computePlayerRan
kings(tx *sql.Tx) (int, error) {
 			return 0, err
 		}
 
-		fmt.Printf("[OK] Computed rankings for se
ason %d with percentile brackets (%d qualifi
ed players)\n", seasonID, qualifiedCount)
+		log.Info("computed rankings for season wi
th percentile brackets",
+			"season_id", seasonID,
+			"qualified_players", qualifiedCount)
 	}
 
-	fmt.Printf("\n[OK] Computed rankings for a
ll seasons (total: %d qualified players acro
ss all seasons)\n", totalQualified)
+	log.Info("computed rankings for all season
s",
+		"total_qualified_players", totalQualified
)
 	return totalQualified, nil
 }
 
+// computePlayerClassRankings computes clas
s-specific rankings for players per season
+func computePlayerClassRankings(tx *sql.Tx)
 error {
+	log.Info("computing class-specific player 
rankings per season")
+
+	// get all distinct season numbers
+	seasonRows, err := tx.Query("SELECT DISTIN
CT season_number FROM seasons ORDER BY seaso
n_number")
+	if err != nil {
+		return fmt.Errorf("failed to query season
s: %w", err)
+	}
+	defer seasonRows.Close()
+
+	var seasons []int
+	for seasonRows.Next() {
+		var seasonNumber int
+		if err := seasonRows.Scan(&seasonNumber);
 err != nil {
+			return err
+		}
+		seasons = append(seasons, seasonNumber)
+	}
+
+	if len(seasons) == 0 {
+		log.Warn("no seasons found, skipping clas
s rankings")
+		return nil
+	}
+
+	for _, seasonID := range seasons {
+		log.Info("processing class rankings for s
eason", "season_id", seasonID)
+
+		// step 1: global class rankings
+		_, err = tx.Exec(`
+			UPDATE player_profiles
+			SET global_class_rank = (
+				SELECT ranking FROM (
+					SELECT
+						player_id,
+						ROW_NUMBER() OVER (PARTITION BY class
_name ORDER BY combined_best_time ASC) as ra
nking
+					FROM player_profiles
+					WHERE season_id = ? AND has_complete_c
overage = 1 AND class_name IS NOT NULL
+				) class_ranks
+				WHERE class_ranks.player_id = player_pr
ofiles.player_id
+			)
+			WHERE season_id = ? AND has_complete_cov
erage = 1 AND class_name IS NOT NULL
+		`, seasonID, seasonID)
+		if err != nil {
+			return fmt.Errorf("failed to compute glo
bal class rankings: %w", err)
+		}
+
+		// step 2: global class ranking brackets
+		_, err = tx.Exec(`
+			UPDATE player_profiles
+			SET global_class_bracket = (
+				CASE
+					WHEN counts.combined_best_time = count
s.class_min_time THEN 'artifact'
+					ELSE
+						CASE
+							WHEN (CAST(counts.global_class_rank 
AS REAL) / CAST(counts.class_total AS REAL) 
* 100) <= 1.0 THEN 'excellent'
+							WHEN (CAST(counts.global_class_rank 
AS REAL) / CAST(counts.class_total AS REAL) 
* 100) <= 5.0 THEN 'legendary'
+							WHEN (CAST(counts.global_class_rank 
AS REAL) / CAST(counts.class_total AS REAL) 
* 100) <= 20.0 THEN 'epic'
+							WHEN (CAST(counts.global_class_rank 
AS REAL) / CAST(counts.class_total AS REAL) 
* 100) <= 40.0 THEN 'rare'
+							WHEN (CAST(counts.global_class_rank 
AS REAL) / CAST(counts.class_total AS REAL) 
* 100) <= 60.0 THEN 'uncommon'
+							ELSE 'common'
+						END
+				END
+			)
+			FROM (
+				SELECT
+					pp.player_id,
+					pp.global_class_rank,
+					pp.combined_best_time,
+					MIN(pp.combined_best_time) OVER (PARTI
TION BY pp.class_name) as class_min_time,
+					COUNT(*) OVER (PARTITION BY pp.class_n
ame) as class_total
+				FROM player_profiles pp
+				WHERE pp.season_id = ? AND pp.has_compl
ete_coverage = 1
+					AND pp.global_class_rank IS NOT NULL A
ND pp.class_name IS NOT NULL
+			) counts
+			WHERE player_profiles.player_id = counts
.player_id
+				AND player_profiles.season_id = ?
+				AND player_profiles.has_complete_covera
ge = 1
+				AND player_profiles.global_class_rank I
S NOT NULL
+		`, seasonID, seasonID)
+		if err != nil {
+			return fmt.Errorf("failed to compute glo
bal class brackets: %w", err)
+		}
+
+		// step 3: regional class rankings
+		_, err = tx.Exec(`
+			UPDATE player_profiles
+			SET region_class_rank = (
+				SELECT ranking FROM (
+					SELECT
+						pp.player_id,
+						ROW_NUMBER() OVER (PARTITION BY r.reg
ion, pp.class_name ORDER BY pp.combined_best
_time ASC) as ranking
+					FROM player_profiles pp
+					INNER JOIN realms r ON pp.realm_id = r
.id
+					WHERE pp.season_id = ? AND pp.has_comp
lete_coverage = 1 AND pp.class_name IS NOT N
ULL
+				) class_ranks
+				WHERE class_ranks.player_id = player_pr
ofiles.player_id
+			)
+			WHERE season_id = ? AND has_complete_cov
erage = 1 AND class_name IS NOT NULL
+		`, seasonID, seasonID)
+		if err != nil {
+			return fmt.Errorf("failed to compute reg
ional class rankings: %w", err)
+		}
+
+		// step 4: regional class ranking bracket
s
+		_, err = tx.Exec(`
+			UPDATE player_profiles
+			SET region_class_bracket = (
+				CASE
+					WHEN counts.combined_best_time = count
s.class_regional_min_time THEN 'artifact'
+					ELSE
+						CASE
+							WHEN (CAST(counts.region_class_rank 
AS REAL) / CAST(counts.class_regional_total 
AS REAL) * 100) <= 1.0 THEN 'excellent'
+							WHEN (CAST(counts.region_class_rank 
AS REAL) / CAST(counts.class_regional_total 
AS REAL) * 100) <= 5.0 THEN 'legendary'
+							WHEN (CAST(counts.region_class_rank 
AS REAL) / CAST(counts.class_regional_total 
AS REAL) * 100) <= 20.0 THEN 'epic'
+							WHEN (CAST(counts.region_class_rank 
AS REAL) / CAST(counts.class_regional_total 
AS REAL) * 100) <= 40.0 THEN 'rare'
+							WHEN (CAST(counts.region_class_rank 
AS REAL) / CAST(counts.class_regional_total 
AS REAL) * 100) <= 60.0 THEN 'uncommon'
+							ELSE 'common'
+						END
+				END
+			)
+			FROM (
+				SELECT
+					pp.player_id,
+					pp.region_class_rank,
+					pp.combined_best_time,
+					MIN(pp.combined_best_time) OVER (PARTI
TION BY r.region, pp.class_name) as class_re
gional_min_time,
+					COUNT(*) OVER (PARTITION BY r.region, 
pp.class_name) as class_regional_total
+				FROM player_profiles pp
+				INNER JOIN realms r ON pp.realm_id = r.
id
+				WHERE pp.season_id = ? AND pp.has_compl
ete_coverage = 1
+					AND pp.region_class_rank IS NOT NULL A
ND pp.class_name IS NOT NULL
+			) counts
+			WHERE player_profiles.player_id = counts
.player_id
+				AND player_profiles.season_id = ?
+				AND player_profiles.has_complete_covera
ge = 1
+				AND player_profiles.region_class_rank I
S NOT NULL
+		`, seasonID, seasonID)
+		if err != nil {
+			return fmt.Errorf("failed to compute reg
ional class brackets: %w", err)
+		}
+
+		// step 5: realm class rankings (pool-bas
ed for connected realms)
+		_, err = tx.Exec(`
+			UPDATE player_profiles
+			SET realm_class_rank = (
+				SELECT ranking FROM (
+					SELECT
+						pp.player_id,
+						ROW_NUMBER() OVER (
+							PARTITION BY COALESCE(parent_r.id, r
.id), pp.class_name
+							ORDER BY pp.combined_best_time ASC
+						) as ranking
+					FROM player_profiles pp
+					JOIN realms r ON pp.realm_id = r.id
+					LEFT JOIN realms parent_r ON r.parent_
realm_slug = parent_r.slug AND r.region = pa
rent_r.region
+					WHERE pp.season_id = ? AND pp.has_comp
lete_coverage = 1 AND pp.class_name IS NOT N
ULL
+				) class_ranks
+				WHERE class_ranks.player_id = player_pr
ofiles.player_id
+			)
+			WHERE season_id = ? AND has_complete_cov
erage = 1 AND class_name IS NOT NULL
+		`, seasonID, seasonID)
+		if err != nil {
+			return fmt.Errorf("failed to compute rea
lm class rankings: %w", err)
+		}
+
+		// step 6: realm class ranking brackets (
pool-based)
+		_, err = tx.Exec(`
+			UPDATE player_profiles
+			SET realm_class_bracket = (
+				CASE
+					WHEN counts.combined_best_time = count
s.class_pool_min_time THEN 'artifact'
+					ELSE
+						CASE
+							WHEN (CAST(counts.realm_class_rank A
S REAL) / CAST(counts.class_pool_total AS RE
AL) * 100) <= 1.0 THEN 'excellent'
+							WHEN (CAST(counts.realm_class_rank A
S REAL) / CAST(counts.class_pool_total AS RE
AL) * 100) <= 5.0 THEN 'legendary'
+							WHEN (CAST(counts.realm_class_rank A
S REAL) / CAST(counts.class_pool_total AS RE
AL) * 100) <= 20.0 THEN 'epic'
+							WHEN (CAST(counts.realm_class_rank A
S REAL) / CAST(counts.class_pool_total AS RE
AL) * 100) <= 40.0 THEN 'rare'
+							WHEN (CAST(counts.realm_class_rank A
S REAL) / CAST(counts.class_pool_total AS RE
AL) * 100) <= 60.0 THEN 'uncommon'
+							ELSE 'common'
+						END
+				END
+			)
+			FROM (
+				SELECT
+					pp.player_id,
+					pp.realm_class_rank,
+					pp.combined_best_time,
+					COALESCE(parent_r.id, r.id) as pool_id
,
+					MIN(pp.combined_best_time) OVER (PARTI
TION BY COALESCE(parent_r.id, r.id), pp.clas
s_name) as class_pool_min_time,
+					COUNT(*) OVER (PARTITION BY COALESCE(p
arent_r.id, r.id), pp.class_name) as class_p
ool_total
+				FROM player_profiles pp
+				JOIN realms r ON pp.realm_id = r.id
+				LEFT JOIN realms parent_r ON r.parent_r
ealm_slug = parent_r.slug AND r.region = par
ent_r.region
+				WHERE pp.season_id = ? AND pp.has_compl
ete_coverage = 1
+					AND pp.realm_class_rank IS NOT NULL AN
D pp.class_name IS NOT NULL
+			) counts
+			WHERE player_profiles.player_id = counts
.player_id
+				AND player_profiles.season_id = ?
+				AND player_profiles.has_complete_covera
ge = 1
+				AND player_profiles.realm_class_rank IS
 NOT NULL
+		`, seasonID, seasonID)
+		if err != nil {
+			return fmt.Errorf("failed to compute rea
lm class brackets: %w", err)
+		}
+
+		log.Info("computed class rankings for sea
son", "season_id", seasonID)
+	}
+
+	log.Info("computed class rankings for all 
seasons")
+	return nil
+}
+
 // computeGlobalRankings computes global ra
nkings for all runs (per season)
 func computeGlobalRankings(tx *sql.Tx) erro
r {
-	fmt.Printf("Computing global rankings per 
season...\n")
+	log.Info("computing global rankings per se
ason")
 
 	currentTime := time.Now().UnixMilli()
 
@@ -646,7 +871,7 @@ func computeGlobalRankin
gs(tx *sql.Tx) error {
 	}
 
 	// update percentile brackets for unfilter
ed global rankings using efficient SQL (per 
season)
-	fmt.Printf("Computing global ranking brack
ets per season...\n")
+	log.Info("computing global ranking bracket
s per season")
 	_, err = tx.Exec(`
 		UPDATE run_rankings
 		SET percentile_bracket = (
@@ -784,7 +1009,7 @@ func computeGlobalRanki
ngs(tx *sql.Tx) error {
 	}
 
 	// update percentile brackets for filtered
 global rankings using efficient SQL (per se
ason)
-	fmt.Printf("Computing filtered global rank
ing brackets per season...\n")
+	log.Info("computing filtered global rankin
g brackets per season")
 	_, err = tx.Exec(`
 		UPDATE run_rankings
 		SET percentile_bracket = (
@@ -824,13 +1049,13 @@ func computeGlobalRan
kings(tx *sql.Tx) error {
 		return err
 	}
 
-	fmt.Printf("[OK] Computed global rankings 
with percentile brackets (all and filtered)\
n")
+	log.Info("computed global rankings with pe
rcentile brackets (all and filtered)")
 	return nil
 }
 
 // computeRegionalRankings computes regiona
l rankings for all runs
 func computeRegionalRankings(tx *sql.Tx) er
ror {
-	fmt.Printf("Computing regional rankings...
\n")
+	log.Info("computing regional rankings")
 
 	currentTime := time.Now().UnixMilli()
 
@@ -883,7 +1108,7 @@ func computeRegionalRan
kings(tx *sql.Tx) error {
 		}
 
 		// update percentile brackets for unfilte
red regional rankings using efficient SQL (p
er season)
-		fmt.Printf("Computing unfiltered regional
 ranking brackets for %s per season...\n", r
egion)
+		log.Info("computing unfiltered regional r
anking brackets per season", "region", regio
n)
 		_, err = tx.Exec(`
 			UPDATE run_rankings
 			SET percentile_bracket = (
@@ -1027,7 +1252,7 @@ func computeRegionalRa
nkings(tx *sql.Tx) error {
 
 		// update percentile brackets for filtere
d regional rankings using efficient SQL (per
 season)
 		filteredScope := region + "_filtered"
-		fmt.Printf("Computing filtered regional r
anking brackets for %s per season...\n", reg
ion)
+		log.Info("computing filtered regional ran
king brackets per season", "region", region)
 		_, err = tx.Exec(`
 			UPDATE run_rankings
 			SET percentile_bracket = (
@@ -1068,13 +1293,13 @@ func computeRegional
Rankings(tx *sql.Tx) error {
 		}
 	}
 
-	fmt.Printf("[OK] Computed regional ranking
s with percentile brackets for %d regions\n"
, len(regions))
+	log.Info("computed regional rankings with 
percentile brackets", "regions", len(regions
))
 	return nil
 }
 
 // computeRealmRankings computes realm rank
ings per realm pool (connected realms groupe
d together)
 func computeRealmRankings(tx *sql.Tx) error
 {
-	fmt.Printf("Computing realm rankings (pool
-based for connected realms)...\n")
+	log.Info("computing realm rankings (pool-b
ased for connected realms)")
 
 	currentTime := time.Now().UnixMilli()
 
@@ -1111,7 +1336,7 @@ func computeRealmRanki
ngs(tx *sql.Tx) error {
 		pools = append(pools, pool)
 	}
 
-	fmt.Printf("Found %d realm pools to proces
s\n", len(pools))
+	log.Info("found realm pools to process", "
pools", len(pools))
 
 	for _, pool := range pools {
 		// unfiltered realm rankings (per season)
 using pool-based partitioning
@@ -1341,6 +1566,6 @@ func computeRealmRanki
ngs(tx *sql.Tx) error {
 		}
 	}
 
-	fmt.Printf("[OK] Computed realm rankings w
ith percentile brackets for %d realm pools\n
", len(pools))
+	log.Info("computed realm rankings with per
centile brackets", "pools", len(pools))
 	return nil
 }
 
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET