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
┌─ nix/pkgs/ookstats/default.nix ────────────────────────────────────────────┐
│ 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]; │
│ │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ nix/pkgs/ookstats/default.nix ──────┐
│ 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]; │
│ │
└──────────────────────────────────────────────┘
┌─ nix/pkgs/ookstats/src/cmd/analyze.go ─────────────────────────────────────┐
│ 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 │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...s/ookstats/src/cmd/analyze.go ───┐
│ 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 │
└──────────────────────────────────────────────┘
┌─ nix/pkgs/ookstats/src/cmd/build.go ───────────────────────────────────────┐
│ 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 │
│ } │
│ │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...kgs/ookstats/src/cmd/build.go ───┐
│ 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 │
│ } │
│ │
└──────────────────────────────────────────────┘
┌─ nix/pkgs/ookstats/src/cmd/fetch.go ───────────────────────────────────────┐
│ 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 │
│ }, │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...kgs/ookstats/src/cmd/fetch.go ───┐
│ 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 │
│ }, │
└──────────────────────────────────────────────┘
┌─ nix/pkgs/ookstats/src/cmd/root.go ────────────────────────────────────────┐
│ 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) │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...pkgs/ookstats/src/cmd/root.go ───┐
│ 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) │
│ } │
└──────────────────────────────────────────────┘
┌─ nix/pkgs/ookstats/src/go.mod ─────────────────────────────────────────────┐
│ 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 │
│ ) │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ nix/pkgs/ookstats/src/go.mod ───────┐
│ 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 │
│ ) │
└──────────────────────────────────────────────┘
┌─ nix/pkgs/ookstats/src/go.sum ─────────────────────────────────────────────┐
│ 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= │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ nix/pkgs/ookstats/src/go.sum ───────┐
│ 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= │
└──────────────────────────────────────────────┘
┌─ nix/pkgs/ookstats/src/internal/database/client.go ────────────────────────┐
│ 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) │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...c/internal/database/client.go ───┐
│ 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) │
└──────────────────────────────────────────────┘
┌─ nix/pkgs/ookstats/src/internal/generator/player_leaderboards.go ──────────┐
│ 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 │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...erator/player_leaderboards.go ───┐
│ 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 │
│ } │
└──────────────────────────────────────────────┘
┌─ nix/pkgs/ookstats/src/internal/pipeline/process.go ───────────────────────┐
│ 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 │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ .../internal/pipeline/process.go ───┐
│ 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