OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
HASH      71864949ae99
DATE      2026-05-03
SUBJECT   ookstats: add stats json generator
FILES     3 CHANGED
HASH      71864949ae99
DATE      2026-05-03
SUBJECT   ookstats: add stats json generator
FILES     3 CHANGED
 

diff --git a/nix/pkgs/ookstats/src/cmd/build.go b/nix/pkgs/ookstats/src/cmd/build.
go
index f00f009..300b137 100644
--- a/nix/pkgs/ookstats/src/cmd/build.go
+++ b/nix/pkgs/ookstats/src/cmd/build.go
@@ -200,6 +200,12 @@ var buildCmd = &cobra.Command{
 			return err
 		}
 
+		// 9b) Generate stats page JSON
+		log.Info("generating stats page JSON")
+		if err := generator.GenerateStats(db, normalizedOut); err != nil {
+			return err
+		}
+
 		// 10) Generate status API via analyze
 		log.Info("generating status API", "method", "analyze")
 		statusDir := filepath.Join(normalizedOut, "api", "status")

diff --git a/nix/pkgs/ookstats/src/cmd/build
.go b/nix/pkgs/ookstats/src/cmd/build.go
index f00f009..300b137 100644
--- a/nix/pkgs/ookstats/src/cmd/build.go
+++ b/nix/pkgs/ookstats/src/cmd/build.go
@@ -200,6 +200,12 @@ var buildCmd = &cobra.C
ommand{
 			return err
 		}
 
+		// 9b) Generate stats page JSON
+		log.Info("generating stats page JSON")
+		if err := generator.GenerateStats(db, nor
malizedOut); err != nil {
+			return err
+		}
+
 		// 10) Generate status API via analyze
 		log.Info("generating status API", "method
", "analyze")
 		statusDir := filepath.Join(normalizedOut,
 "api", "status")
 

diff --git a/nix/pkgs/ookstats/src/cmd/generate.go b/nix/pkgs/ookstats/src/cmd/gen
erate.go
index d35f6c5..d430b6a 100644
--- a/nix/pkgs/ookstats/src/cmd/generate.go
+++ b/nix/pkgs/ookstats/src/cmd/generate.go
@@ -116,6 +116,32 @@ var generateHomeCmd = &cobra.Command{
 	},
 }
 
+var generateStatsCmd = &cobra.Command{
+	Use:   "stats",
+	Short: "Generate stats page JSON (totals, completion tiers, spec counts, top run
ners, weekly activity)",
+	RunE: func(cmd *cobra.Command, args []string) error {
+		outDir, _ := cmd.Flags().GetString("out")
+		if strings.TrimSpace(outDir) == "" {
+			return errors.New("--out is required")
+		}
+		db, err := database.Connect()
+		if err != nil {
+			return fmt.Errorf("failed to connect to db: %w", err)
+		}
+		defer db.Close()
+
+		if err := os.MkdirAll(filepath.Join(outDir, "api"), 0o755); err != nil {
+			return fmt.Errorf("mkdir base: %w", err)
+		}
+
+		if err := generator.GenerateStats(db, outDir); err != nil {
+			return err
+		}
+		fmt.Printf("\nStats JSON generated at %s/api/stats.json\n", outDir)
+		return nil
+	},
+}
+
 func init() {
 	rootCmd.AddCommand(generateCmd)
 	generateCmd.AddCommand(generateAPICmd)
@@ -131,4 +157,7 @@ func init() {
 
 	generateCmd.AddCommand(generateHomeCmd)
 	generateHomeCmd.Flags().String("out", "web/public", "Output directory (api/home.
json will be written under it)")
+
+	generateCmd.AddCommand(generateStatsCmd)
+	generateStatsCmd.Flags().String("out", "web/public", "Output directory (api/stat
s.json will be written under it)")
 }

diff --git a/nix/pkgs/ookstats/src/cmd/gener
ate.go b/nix/pkgs/ookstats/src/cmd/generate.
go
index d35f6c5..d430b6a 100644
--- a/nix/pkgs/ookstats/src/cmd/generate.go
+++ b/nix/pkgs/ookstats/src/cmd/generate.go
@@ -116,6 +116,32 @@ var generateHomeCmd = &
cobra.Command{
 	},
 }
 
+var generateStatsCmd = &cobra.Command{
+	Use:   "stats",
+	Short: "Generate stats page JSON (totals, 
completion tiers, spec counts, top runners, 
weekly activity)",
+	RunE: func(cmd *cobra.Command, args []stri
ng) error {
+		outDir, _ := cmd.Flags().GetString("out")
+		if strings.TrimSpace(outDir) == "" {
+			return errors.New("--out is required")
+		}
+		db, err := database.Connect()
+		if err != nil {
+			return fmt.Errorf("failed to connect to 
db: %w", err)
+		}
+		defer db.Close()
+
+		if err := os.MkdirAll(filepath.Join(outDi
r, "api"), 0o755); err != nil {
+			return fmt.Errorf("mkdir base: %w", err)
+		}
+
+		if err := generator.GenerateStats(db, out
Dir); err != nil {
+			return err
+		}
+		fmt.Printf("\nStats JSON generated at %s/
api/stats.json\n", outDir)
+		return nil
+	},
+}
+
 func init() {
 	rootCmd.AddCommand(generateCmd)
 	generateCmd.AddCommand(generateAPICmd)
@@ -131,4 +157,7 @@ func init() {
 
 	generateCmd.AddCommand(generateHomeCmd)
 	generateHomeCmd.Flags().String("out", "web
/public", "Output directory (api/home.json w
ill be written under it)")
+
+	generateCmd.AddCommand(generateStatsCmd)
+	generateStatsCmd.Flags().String("out", "we
b/public", "Output directory (api/stats.json
 will be written under it)")
 }
 

diff --git a/nix/pkgs/ookstats/src/internal/generator/stats.go b/nix/pkgs/ookstats
/src/internal/generator/stats.go
new file mode 100644
index 0000000..a60d41b
--- /dev/null
+++ b/nix/pkgs/ookstats/src/internal/generator/stats.go
@@ -0,0 +1,627 @@
+package generator
+
+import (
+	"database/sql"
+	"fmt"
+	"path/filepath"
+	"time"
+
+	"ookstats/internal/wow"
+	"ookstats/internal/writer"
+)
+
+// StatsJSON is the top-level shape for web/public/api/stats.json.
+// Three scopes are emitted: cross-season "all_time" plus one entry per season nu
mber.
+type StatsJSON struct {
+	GeneratedAt int64                            `json:"generated_at"`
+	// Scopes is keyed by region (global/us/eu/kr/tw), then by season key
+	// (all_time/season_1/season_2). Region "global" includes all regions.
+	Scopes map[string]map[string]StatsScope `json:"scopes"`
+}
+
+// StatsScope holds aggregated stats for a single scope (all-time or one season).
+type StatsScope struct {
+	TotalRuns         int64                       `json:"total_runs"`
+	TotalPlayers      int64                       `json:"total_players"`
+	NineOfNinePlayers int64                       `json:"nine_of_nine_players"`
+	CompletionTiers   CompletionTiers             `json:"completion_tiers"`
+	SpecCounts        map[string][]SpecCountEntry `json:"spec_counts"`
+	WeeklyActivity    []WeeklyActivityEntry       `json:"weekly_activity"`
+}
+
+// CompletionTiers groups the 9-of-9 medal-tier counts.
+type CompletionTiers struct {
+	NineOfNineGold     CompletionTier `json:"9_of_9_gold"`
+	NineOfNinePlatinum CompletionTier `json:"9_of_9_platinum"`
+	NineOfNineTitle    CompletionTier `json:"9_of_9_title"`
+}
+
+// CompletionTier - count plus three percentile denominators.
+// `percentile_of_all_players` = count / scope.total_players (rarity vs scoped po
ol)
+// `percentile_of_completed_players` = count / players_with_9_dungeon_bests (rari
ty among finishers)
+// `percentile_of_all_time_players` = count / all_time.total_players (stable cros
s-scope denominator)
+type CompletionTier struct {
+	Count                        int64   `json:"count"`
+	PercentileOfAllPlayers       float64 `json:"percentile_of_all_players"`
+	PercentileOfCompletedPlayers float64 `json:"percentile_of_completed_players"`
+	PercentileOfAllTimePlayers   float64 `json:"percentile_of_all_time_players"`
+}
+
+// SpecCountEntry - one row of the spec-distribution charts.
+type SpecCountEntry struct {
+	SpecID    int    `json:"spec_id"`
+	ClassName string `json:"class_name"`
+	SpecName  string `json:"spec_name"`
+	Count     int64  `json:"count"`
+}
+
+// WeeklyActivityEntry - one bucket on the activity timeline (Monday-start week).
+type WeeklyActivityEntry struct {
+	WeekStart string `json:"week_start"` // YYYY-MM-DD
+	RunCount  int64  `json:"run_count"`
+}
+
+// dungeonThresholds - gold / platinum / title timer thresholds in milliseconds.
+// Gold = the achievement par-time (the "beat the timer" goal).
+// Platinum / title are tighter community-defined cutoffs (see /docs or original 
source).
+type dungeonThresholds struct {
+	GoldMs     int64
+	PlatinumMs int64
+	TitleMs    int64
+}
+
+var dungeonTimerThresholds = map[int]dungeonThresholds{
+	2:  {GoldMs: 900000, PlatinumMs: 615000, TitleMs: 510000},   // Temple of the Ja
de Serpent (15:00 / 10:15 / 8:30)
+	56: {GoldMs: 720000, PlatinumMs: 495000, TitleMs: 390000},   // Stormstout Brewe
ry        (12:00 / 8:15 / 6:30)
+	57: {GoldMs: 780000, PlatinumMs: 480000, TitleMs: 330000},   // Gate of the Sett
ing Sun   (13:00 / 8:00 / 5:30)
+	58: {GoldMs: 1260000, PlatinumMs: 840000, TitleMs: 630000},  // Shado-Pan Monast
ery       (21:00 / 14:00 / 10:30)
+	59: {GoldMs: 1050000, PlatinumMs: 735000, TitleMs: 615000},  // Siege of Niuzao 
Temple    (17:30 / 12:15 / 10:15)
+	60: {GoldMs: 720000, PlatinumMs: 495000, TitleMs: 405000},   // Mogu'shan Palace
          (12:00 / 8:15 / 6:45)
+	76: {GoldMs: 1140000, PlatinumMs: 615000, TitleMs: 435000},  // Scholomance     
          (19:00 / 10:15 / 7:15)
+	77: {GoldMs: 780000, PlatinumMs: 480000, TitleMs: 255000},   // Scarlet Halls   
          (13:00 / 8:00 / 4:15)
+	78: {GoldMs: 780000, PlatinumMs: 540000, TitleMs: 330000},   // Scarlet Monaster
y         (13:00 / 9:00 / 5:30)
+}
+
+// totalDungeons in the season - a player must clear all of them to qualify for 9
/9.
+var totalDungeons = len(dungeonTimerThresholds)
+
+// In-game cutoffs are lenient: the displayed cutoff is the first second that
+// still earns the medal. e.g. a 5:30.800 run beats a "5:30" title cutoff,
+// because the timer reads "5:30" until it ticks over to 5:31. So the actual
+// pass condition is `duration < threshold + leniencyMs`.
+const leniencyMs int64 = 1000
+
+// beatsTier reports whether duration earns at least the given threshold.
+func beatsTier(duration, thresholdMs int64) bool {
+	return duration < thresholdMs+leniencyMs
+}
+
+// Scopes emitted in the output. Order matters for JSON readability.
+type statsScopeDef struct {
+	Key       string
+	SeasonNum int // 0 = all-time
+}
+
+var statsScopes = []statsScopeDef{
+	{Key: "all_time", SeasonNum: 0},
+	{Key: "season_1", SeasonNum: 1},
+	{Key: "season_2", SeasonNum: 2},
+}
+
+// statsRegions are the region scopes emitted; "global" means no region filter.
+var statsRegions = []string{"global", "us", "eu", "kr", "tw"}
+
+// GenerateStats writes outDir/api/stats.json with aggregated stats per (region, 
season) scope.
+func GenerateStats(db *sql.DB, outDir string) error {
+	out := StatsJSON{
+		GeneratedAt: time.Now().UnixMilli(),
+		Scopes:      make(map[string]map[string]StatsScope, len(statsRegions)),
+	}
+
+	for _, region := range statsRegions {
+		regionScopes := make(map[string]StatsScope, len(statsScopes))
+		// Compute all-time first per region so its total_players is the stable
+		// denominator for per-season scopes within that region.
+		var allTimeTotalPlayers int64
+		for _, sc := range statsScopes {
+			ss, err := buildStatsScope(db, sc.SeasonNum, region, allTimeTotalPlayers)
+			if err != nil {
+				return fmt.Errorf("scope %s/%s: %w", region, sc.Key, err)
+			}
+			if sc.SeasonNum == 0 {
+				allTimeTotalPlayers = ss.TotalPlayers
+			}
+			regionScopes[sc.Key] = ss
+		}
+		out.Scopes[region] = regionScopes
+	}
+
+	outPath := filepath.Join(outDir, "api", "stats.json")
+	if err := writer.WriteJSONFileCompact(outPath, out); err != nil {
+		return fmt.Errorf("write stats.json: %w", err)
+	}
+	return nil
+}
+
+// buildStatsScope assembles a single StatsScope.
+// seasonNum == 0 means cross-season (all-time). region == "global" means no regi
on filter.
+// For all-time, pass allTimeTotalPlayers=0; for per-season builds, pass the all-
time scope's
+// TotalPlayers (within the same region) so the `percentile_of_all_time_players` 
field
+// uses a stable denominator.
+func buildStatsScope(db *sql.DB, seasonNum int, region string, allTimeTotalPlayer
s int64) (StatsScope, error) {
+	var s StatsScope
+	var err error
+
+	if s.TotalRuns, err = countTotalRuns(db, seasonNum, region); err != nil {
+		return s, fmt.Errorf("total runs: %w", err)
+	}
+
+	if s.TotalPlayers, err = countTotalPlayers(db, seasonNum, region); err != nil {
+		return s, fmt.Errorf("total players: %w", err)
+	}
+
+	// On all-time, the stable denominator is just this scope's own TotalPlayers.
+	allTimeDen := allTimeTotalPlayers
+	if seasonNum == 0 {
+		allTimeDen = s.TotalPlayers
+	}
+
+	tiers, completedCount, err := computeCompletionTiers(db, seasonNum, region)
+	if err != nil {
+		return s, fmt.Errorf("completion tiers: %w", err)
+	}
+	s.CompletionTiers = tiers
+	s.NineOfNinePlayers = completedCount
+	// fill percentile fields now that we know all three denominators
+	s.CompletionTiers.NineOfNineGold = withPercentiles(s.CompletionTiers.NineOfNineG
old, s.TotalPlayers, completedCount, allTimeDen)
+	s.CompletionTiers.NineOfNinePlatinum = withPercentiles(s.CompletionTiers.NineOfN
inePlatinum, s.TotalPlayers, completedCount, allTimeDen)
+	s.CompletionTiers.NineOfNineTitle = withPercentiles(s.CompletionTiers.NineOfNine
Title, s.TotalPlayers, completedCount, allTimeDen)
+
+	if s.SpecCounts, err = computeSpecCounts(db, seasonNum, region); err != nil {
+		return s, fmt.Errorf("spec counts: %w", err)
+	}
+
+	if s.WeeklyActivity, err = queryWeeklyActivity(db, seasonNum, region); err != ni
l {
+		return s, fmt.Errorf("weekly activity: %w", err)
+	}
+
+	return s, nil
+}
+
+// seasonClauseAndArg returns the SQL fragment and arg slice for filtering by cha
llenge_runs.season_id.
+// Empty string for all-time (seasonNum == 0).
+func seasonClauseAndArg(seasonNum int, alias string) (string, []any) {
+	if seasonNum == 0 {
+		return "", nil
+	}
+	if alias == "" {
+		alias = "challenge_runs"
+	}
+	return fmt.Sprintf(" AND %s.season_id = ?", alias), []any{seasonNum}
+}
+
+// regionClauseAndArg filters by realm region. region == "global" returns empty (
no filter).
+// `realmAlias` is the SQL alias of the realms table reference in the FROM/JOIN c
lause.
+func regionClauseAndArg(region, realmAlias string) (string, []any) {
+	if region == "" || region == "global" {
+		return "", nil
+	}
+	if realmAlias == "" {
+		realmAlias = "r"
+	}
+	return fmt.Sprintf(" AND %s.region = ?", realmAlias), []any{region}
+}
+
+func countTotalRuns(db *sql.DB, seasonNum int, region string) (int64, error) {
+	// Region filter requires joining realms; only join when needed to keep the glob
al
+	// case as a fast plain COUNT(*) on challenge_runs.
+	if region == "" || region == "global" {
+		q := "SELECT COUNT(*) FROM challenge_runs cr WHERE 1=1"
+		clause, args := seasonClauseAndArg(seasonNum, "cr")
+		var n int64
+		if err := db.QueryRow(q+clause, args...).Scan(&n); err != nil {
+			return 0, err
+		}
+		return n, nil
+	}
+	q := "SELECT COUNT(*) FROM challenge_runs cr JOIN realms r ON r.id = cr.realm_id
 WHERE 1=1"
+	sClause, sArgs := seasonClauseAndArg(seasonNum, "cr")
+	rClause, rArgs := regionClauseAndArg(region, "r")
+	args := append(sArgs, rArgs...)
+	var n int64
+	if err := db.QueryRow(q+sClause+rClause, args...).Scan(&n); err != nil {
+		return 0, err
+	}
+	return n, nil
+}
+
+// countTotalPlayers counts distinct players who appear in run_members within sco
pe.
+// "Within scope" = any player who participated in a run during the given season+
region.
+func countTotalPlayers(db *sql.DB, seasonNum int, region string) (int64, error) {
+	// Always join through challenge_runs so we can filter by season and/or region.
+	q := `SELECT COUNT(DISTINCT rm.player_id)
+	      FROM run_members rm
+	      JOIN challenge_runs cr ON rm.run_id = cr.id`
+	if region != "" && region != "global" {
+		q += " JOIN realms r ON r.id = cr.realm_id"
+	}
+	q += " WHERE 1=1"
+	sClause, sArgs := seasonClauseAndArg(seasonNum, "cr")
+	rClause, rArgs := regionClauseAndArg(region, "r")
+	args := append(sArgs, rArgs...)
+	var n int64
+	if err := db.QueryRow(q+sClause+rClause, args...).Scan(&n); err != nil {
+		return 0, err
+	}
+	return n, nil
+}
+
+// computeCompletionTiers loads each player's best time per (season, dungeon) and
+// classifies them against the gold / platinum / title thresholds.
+// Tiers are achievements earned *within a single season*, so for the all-time sc
ope
+// we take the union of per-season qualifiers - a player counts if they hit 9/9 o
f a
+// tier in any single season, not by combining best times across seasons.
+// Returns the tier counts and the count of players who completed all 9 dungeons 
in
+// at least one season (the "completed" denominator).
+func computeCompletionTiers(db *sql.DB, seasonNum int, region string) (Completion
Tiers, int64, error) {
+	q := `
+		SELECT rm.player_id, cr.season_id, cr.dungeon_id, MIN(cr.duration) AS best_dura
tion
+		FROM run_members rm
+		JOIN challenge_runs cr ON rm.run_id = cr.id
+		%s
+		WHERE 1=1 %s %s
+		GROUP BY rm.player_id, cr.season_id, cr.dungeon_id
+	`
+	regionJoin := ""
+	if region != "" && region != "global" {
+		regionJoin = "JOIN realms r ON r.id = cr.realm_id"
+	}
+	sClause, sArgs := seasonClauseAndArg(seasonNum, "cr")
+	rClause, rArgs := regionClauseAndArg(region, "r")
+	args := append(sArgs, rArgs...)
+	rows, err := db.Query(fmt.Sprintf(q, regionJoin, sClause, rClause), args...)
+	if err != nil {
+		return CompletionTiers{}, 0, err
+	}
+	defer rows.Close()
+
+	// (player_id, season_id) -> per-tier dungeon-cleared count
+	type key struct {
+		playerID int64
+		seasonID int
+	}
+	type counts struct{ gold, plat, title, anyDungeon int }
+	per := make(map[key]*counts)
+	for rows.Next() {
+		var playerID int64
+		var seasonID, dungeonID int
+		var bestDuration int64
+		if err := rows.Scan(&playerID, &seasonID, &dungeonID, &bestDuration); err != ni
l {
+			return CompletionTiers{}, 0, err
+		}
+		threshold, ok := dungeonTimerThresholds[dungeonID]
+		if !ok {
+			continue // unknown dungeon - skip silently
+		}
+		k := key{playerID, seasonID}
+		c := per[k]
+		if c == nil {
+			c = &counts{}
+			per[k] = c
+		}
+		c.anyDungeon++
+		if beatsTier(bestDuration, threshold.GoldMs) {
+			c.gold++
+		}
+		if beatsTier(bestDuration, threshold.PlatinumMs) {
+			c.plat++
+		}
+		if beatsTier(bestDuration, threshold.TitleMs) {
+			c.title++
+		}
+	}
+	if err := rows.Err(); err != nil {
+		return CompletionTiers{}, 0, err
+	}
+
+	// Union per player: did they qualify in *any* season?
+	type qualifiers struct{ gold, plat, title, completed bool }
+	playerQual := make(map[int64]*qualifiers)
+	for k, c := range per {
+		q := playerQual[k.playerID]
+		if q == nil {
+			q = &qualifiers{}
+			playerQual[k.playerID] = q
+		}
+		if c.anyDungeon == totalDungeons {
+			q.completed = true
+		}
+		if c.gold == totalDungeons {
+			q.gold = true
+		}
+		if c.plat == totalDungeons {
+			q.plat = true
+		}
+		if c.title == totalDungeons {
+			q.title = true
+		}
+	}
+
+	var tiers CompletionTiers
+	var completedCount int64
+	for _, q := range playerQual {
+		if q.completed {
+			completedCount++
+		}
+		if q.gold {
+			tiers.NineOfNineGold.Count++
+		}
+		if q.plat {
+			tiers.NineOfNinePlatinum.Count++
+		}
+		if q.title {
+			tiers.NineOfNineTitle.Count++
+		}
+	}
+	return tiers, completedCount, nil
+}
+
+func withPercentiles(t CompletionTier, totalPlayers, completedPlayers, allTimePla
yers int64) CompletionTier {
+	if totalPlayers > 0 {
+		t.PercentileOfAllPlayers = round2(float64(t.Count) * 100.0 / float64(totalPlaye
rs))
+	}
+	if completedPlayers > 0 {
+		t.PercentileOfCompletedPlayers = round2(float64(t.Count) * 100.0 / float64(comp
letedPlayers))
+	}
+	if allTimePlayers > 0 {
+		t.PercentileOfAllTimePlayers = round2(float64(t.Count) * 100.0 / float64(allTim
ePlayers))
+	}
+	return t
+}
+
+// computeSpecCounts returns the spec-distribution charts:
+//   all_runs:      every run_member row in scope
+//   gold_runs:     run_members where the run beat the gold threshold for its dun
geon
+//   platinum_runs: same for platinum
+//   title_runs:    same for title
+//   top_50_runs:   run_members where the run was top-50 in scope (globally team-
filtered for
+//                  region=="global", regionally team-filtered for a specific reg
ion)
+func computeSpecCounts(db *sql.DB, seasonNum int, region string) (map[string][]Sp
ecCountEntry, error) {
+	out := map[string][]SpecCountEntry{}
+
+	allRuns, err := querySpecCountsForRuns(db, seasonNum, region)
+	if err != nil {
+		return nil, fmt.Errorf("all_runs: %w", err)
+	}
+	out["all_runs"] = allRuns
+
+	for _, tier := range []struct {
+		key        string
+		thresholdF func(dungeonThresholds) int64
+	}{
+		{"gold_runs", func(t dungeonThresholds) int64 { return t.GoldMs }},
+		{"platinum_runs", func(t dungeonThresholds) int64 { return t.PlatinumMs }},
+		{"title_runs", func(t dungeonThresholds) int64 { return t.TitleMs }},
+	} {
+		tierCounts, err := querySpecCountsForTier(db, seasonNum, region, tier.threshold
F)
+		if err != nil {
+			return nil, fmt.Errorf("%s: %w", tier.key, err)
+		}
+		out[tier.key] = tierCounts
+	}
+
+	top50, err := querySpecCountsForTop50(db, seasonNum, region)
+	if err != nil {
+		return nil, fmt.Errorf("top_50_runs: %w", err)
+	}
+	out["top_50_runs"] = top50
+
+	return out, nil
+}
+
+// querySpecCountsForRuns counts run_member spec_ids across all runs in scope.
+func querySpecCountsForRuns(db *sql.DB, seasonNum int, region string) ([]SpecCoun
tEntry, error) {
+	regionJoin := ""
+	if region != "" && region != "global" {
+		regionJoin = "JOIN realms r ON r.id = cr.realm_id"
+	}
+	q := `
+		SELECT rm.spec_id, COUNT(*)
+		FROM run_members rm
+		JOIN challenge_runs cr ON rm.run_id = cr.id
+		%s
+		WHERE rm.spec_id IS NOT NULL %s %s
+		GROUP BY rm.spec_id
+		ORDER BY COUNT(*) DESC
+	`
+	sClause, sArgs := seasonClauseAndArg(seasonNum, "cr")
+	rClause, rArgs := regionClauseAndArg(region, "r")
+	args := append(sArgs, rArgs...)
+	rows, err := db.Query(fmt.Sprintf(q, regionJoin, sClause, rClause), args...)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	return scanSpecCounts(rows)
+}
+
+// querySpecCountsForTier counts run_member spec_ids across runs that beat a per-
dungeon threshold.
+// We can't easily express the per-dungeon threshold in a single SQL query, so in
stead we load
+// (run_id, dungeon_id, duration) rows in scope, decide qualification in Go, then
 count specs by
+// the qualifying run_id set.
+func querySpecCountsForTier(db *sql.DB, seasonNum int, region string, thresholdF 
func(dungeonThresholds) int64) ([]SpecCountEntry, error) {
+	regionJoin := ""
+	if region != "" && region != "global" {
+		regionJoin = "JOIN realms r ON r.id = cr.realm_id"
+	}
+	sClause, sArgs := seasonClauseAndArg(seasonNum, "cr")
+	rClause, rArgs := regionClauseAndArg(region, "r")
+	args := append(sArgs, rArgs...)
+	rows, err := db.Query(fmt.Sprintf(`
+		SELECT cr.id, cr.dungeon_id, cr.duration
+		FROM challenge_runs cr
+		%s
+		WHERE 1=1 %s %s
+	`, regionJoin, sClause, rClause), args...)
+	if err != nil {
+		return nil, err
+	}
+	qualifying := make(map[int64]struct{})
+	for rows.Next() {
+		var id int64
+		var dungeonID int
+		var duration int64
+		if err := rows.Scan(&id, &dungeonID, &duration); err != nil {
+			rows.Close()
+			return nil, err
+		}
+		threshold, ok := dungeonTimerThresholds[dungeonID]
+		if !ok {
+			continue
+		}
+		if beatsTier(duration, thresholdF(threshold)) {
+			qualifying[id] = struct{}{}
+		}
+	}
+	rows.Close()
+	if len(qualifying) == 0 {
+		return []SpecCountEntry{}, nil
+	}
+
+	// Count spec_ids in run_members for qualifying runs
+	specCounts := make(map[int]int64)
+	memberRows, err := db.Query(`
+		SELECT run_id, spec_id
+		FROM run_members
+		WHERE spec_id IS NOT NULL
+	`)
+	if err != nil {
+		return nil, err
+	}
+	defer memberRows.Close()
+	for memberRows.Next() {
+		var runID int64
+		var specID int
+		if err := memberRows.Scan(&runID, &specID); err != nil {
+			return nil, err
+		}
+		if _, ok := qualifying[runID]; !ok {
+			continue
+		}
+		specCounts[specID]++
+	}
+	return specCountsToSlice(specCounts), nil
+}
+
+// querySpecCountsForTop50 counts spec_ids in top-50 runs.
+// region "global" ? top 50 globally (filtered); region "us"/"eu"/etc ? top 50 wi
thin that region.
+func querySpecCountsForTop50(db *sql.DB, seasonNum int, region string) ([]SpecCou
ntEntry, error) {
+	rankingType := "global"
+	rankingScope := "filtered"
+	if region != "" && region != "global" {
+		rankingType = "regional"
+		rankingScope = region + "_filtered"
+	}
+	q := `
+		SELECT rm.spec_id, COUNT(*)
+		FROM run_members rm
+		JOIN run_rankings rr ON rm.run_id = rr.run_id
+		WHERE rm.spec_id IS NOT NULL
+		  AND rr.ranking_type = ?
+		  AND rr.ranking_scope = ?
+		  AND rr.ranking <= 50
+	`
+	args := []any{rankingType, rankingScope}
+	if seasonNum != 0 {
+		q += " AND rr.season_id = ?"
+		args = append(args, seasonNum)
+	}
+	q += " GROUP BY rm.spec_id ORDER BY COUNT(*) DESC"
+
+	rows, err := db.Query(q, args...)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	return scanSpecCounts(rows)
+}
+
+func scanSpecCounts(rows *sql.Rows) ([]SpecCountEntry, error) {
+	var out []SpecCountEntry
+	for rows.Next() {
+		var specID int
+		var count int64
+		if err := rows.Scan(&specID, &count); err != nil {
+			return nil, err
+		}
+		entry := SpecCountEntry{SpecID: specID, Count: count}
+		if cls, spec, ok := wow.GetClassAndSpec(specID); ok {
+			entry.ClassName = cls
+			entry.SpecName = spec
+		}
+		out = append(out, entry)
+	}
+	return out, rows.Err()
+}
+
+// specCountsToSlice converts a map to a sorted SpecCountEntry slice (desc by cou
nt).
+func specCountsToSlice(m map[int]int64) []SpecCountEntry {
+	out := make([]SpecCountEntry, 0, len(m))
+	for specID, count := range m {
+		entry := SpecCountEntry{SpecID: specID, Count: count}
+		if cls, spec, ok := wow.GetClassAndSpec(specID); ok {
+			entry.ClassName = cls
+			entry.SpecName = spec
+		}
+		out = append(out, entry)
+	}
+	// simple insertion sort by count desc (small N: ~33 specs)
+	for i := 1; i < len(out); i++ {
+		for j := i; j > 0 && out[j].Count > out[j-1].Count; j-- {
+			out[j], out[j-1] = out[j-1], out[j]
+		}
+	}
+	return out
+}
+
+// queryWeeklyActivity returns runs grouped by Monday-start week.
+// Timestamps are stored in milliseconds; SQLite date functions need seconds.
+func queryWeeklyActivity(db *sql.DB, seasonNum int, region string) ([]WeeklyActiv
ityEntry, error) {
+	regionJoin := ""
+	if region != "" && region != "global" {
+		regionJoin = "JOIN realms r ON r.id = cr.realm_id"
+	}
+	sClause, sArgs := seasonClauseAndArg(seasonNum, "cr")
+	rClause, rArgs := regionClauseAndArg(region, "r")
+	args := append(sArgs, rArgs...)
+	// 'weekday 1' advances to next Monday; '-7 days' rolls back to start of current
 week.
+	q := fmt.Sprintf(`
+		SELECT
+			date(cr.completed_timestamp / 1000, 'unixepoch', 'weekday 1', '-7 days') AS we
ek_start,
+			COUNT(*) AS run_count
+		FROM challenge_runs cr
+		%s
+		WHERE cr.completed_timestamp IS NOT NULL %s %s
+		GROUP BY week_start
+		ORDER BY week_start
+	`, regionJoin, sClause, rClause)
+	rows, err := db.Query(q, args...)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+
+	var out []WeeklyActivityEntry
+	for rows.Next() {
+		var e WeeklyActivityEntry
+		if err := rows.Scan(&e.WeekStart, &e.RunCount); err != nil {
+			return nil, err
+		}
+		out = append(out, e)
+	}
+	return out, rows.Err()
+}
+
+func round2(f float64) float64 {
+	return float64(int64(f*100+0.5)) / 100
+}

diff --git a/nix/pkgs/ookstats/src/internal/
generator/stats.go b/nix/pkgs/ookstats/src/i
nternal/generator/stats.go
new file mode 100644
index 0000000..a60d41b
--- /dev/null
+++ b/nix/pkgs/ookstats/src/internal/generat
or/stats.go
@@ -0,0 +1,627 @@
+package generator
+
+import (
+	"database/sql"
+	"fmt"
+	"path/filepath"
+	"time"
+
+	"ookstats/internal/wow"
+	"ookstats/internal/writer"
+)
+
+// StatsJSON is the top-level shape for web
/public/api/stats.json.
+// Three scopes are emitted: cross-season "
all_time" plus one entry per season number.
+type StatsJSON struct {
+	GeneratedAt int64                         
   `json:"generated_at"`
+	// Scopes is keyed by region (global/us/eu
/kr/tw), then by season key
+	// (all_time/season_1/season_2). Region "g
lobal" includes all regions.
+	Scopes map[string]map[string]StatsScope `j
son:"scopes"`
+}
+
+// StatsScope holds aggregated stats for a 
single scope (all-time or one season).
+type StatsScope struct {
+	TotalRuns         int64                   
    `json:"total_runs"`
+	TotalPlayers      int64                   
    `json:"total_players"`
+	NineOfNinePlayers int64                   
    `json:"nine_of_nine_players"`
+	CompletionTiers   CompletionTiers         
    `json:"completion_tiers"`
+	SpecCounts        map[string][]SpecCountEn
try `json:"spec_counts"`
+	WeeklyActivity    []WeeklyActivityEntry   
    `json:"weekly_activity"`
+}
+
+// CompletionTiers groups the 9-of-9 medal-
tier counts.
+type CompletionTiers struct {
+	NineOfNineGold     CompletionTier `json:"9
_of_9_gold"`
+	NineOfNinePlatinum CompletionTier `json:"9
_of_9_platinum"`
+	NineOfNineTitle    CompletionTier `json:"9
_of_9_title"`
+}
+
+// CompletionTier - count plus three percen
tile denominators.
+// `percentile_of_all_players` = count / sc
ope.total_players (rarity vs scoped pool)
+// `percentile_of_completed_players` = coun
t / players_with_9_dungeon_bests (rarity amo
ng finishers)
+// `percentile_of_all_time_players` = count
 / all_time.total_players (stable cross-scop
e denominator)
+type CompletionTier struct {
+	Count                        int64   `json
:"count"`
+	PercentileOfAllPlayers       float64 `json
:"percentile_of_all_players"`
+	PercentileOfCompletedPlayers float64 `json
:"percentile_of_completed_players"`
+	PercentileOfAllTimePlayers   float64 `json
:"percentile_of_all_time_players"`
+}
+
+// SpecCountEntry - one row of the spec-dis
tribution charts.
+type SpecCountEntry struct {
+	SpecID    int    `json:"spec_id"`
+	ClassName string `json:"class_name"`
+	SpecName  string `json:"spec_name"`
+	Count     int64  `json:"count"`
+}
+
+// WeeklyActivityEntry - one bucket on the 
activity timeline (Monday-start week).
+type WeeklyActivityEntry struct {
+	WeekStart string `json:"week_start"` // YY
YY-MM-DD
+	RunCount  int64  `json:"run_count"`
+}
+
+// dungeonThresholds - gold / platinum / ti
tle timer thresholds in milliseconds.
+// Gold = the achievement par-time (the "be
at the timer" goal).
+// Platinum / title are tighter community-d
efined cutoffs (see /docs or original source
).
+type dungeonThresholds struct {
+	GoldMs     int64
+	PlatinumMs int64
+	TitleMs    int64
+}
+
+var dungeonTimerThresholds = map[int]dungeo
nThresholds{
+	2:  {GoldMs: 900000, PlatinumMs: 615000, T
itleMs: 510000},   // Temple of the Jade Ser
pent (15:00 / 10:15 / 8:30)
+	56: {GoldMs: 720000, PlatinumMs: 495000, T
itleMs: 390000},   // Stormstout Brewery    
    (12:00 / 8:15 / 6:30)
+	57: {GoldMs: 780000, PlatinumMs: 480000, T
itleMs: 330000},   // Gate of the Setting Su
n   (13:00 / 8:00 / 5:30)
+	58: {GoldMs: 1260000, PlatinumMs: 840000, 
TitleMs: 630000},  // Shado-Pan Monastery   
    (21:00 / 14:00 / 10:30)
+	59: {GoldMs: 1050000, PlatinumMs: 735000, 
TitleMs: 615000},  // Siege of Niuzao Temple
    (17:30 / 12:15 / 10:15)
+	60: {GoldMs: 720000, PlatinumMs: 495000, T
itleMs: 405000},   // Mogu'shan Palace      
    (12:00 / 8:15 / 6:45)
+	76: {GoldMs: 1140000, PlatinumMs: 615000, 
TitleMs: 435000},  // Scholomance           
    (19:00 / 10:15 / 7:15)
+	77: {GoldMs: 780000, PlatinumMs: 480000, T
itleMs: 255000},   // Scarlet Halls         
    (13:00 / 8:00 / 4:15)
+	78: {GoldMs: 780000, PlatinumMs: 540000, T
itleMs: 330000},   // Scarlet Monastery     
    (13:00 / 9:00 / 5:30)
+}
+
+// totalDungeons in the season - a player m
ust clear all of them to qualify for 9/9.
+var totalDungeons = len(dungeonTimerThresho
lds)
+
+// In-game cutoffs are lenient: the display
ed cutoff is the first second that
+// still earns the medal. e.g. a 5:30.800 r
un beats a "5:30" title cutoff,
+// because the timer reads "5:30" until it 
ticks over to 5:31. So the actual
+// pass condition is `duration < threshold 
+ leniencyMs`.
+const leniencyMs int64 = 1000
+
+// beatsTier reports whether duration earns
 at least the given threshold.
+func beatsTier(duration, thresholdMs int64)
 bool {
+	return duration < thresholdMs+leniencyMs
+}
+
+// Scopes emitted in the output. Order matt
ers for JSON readability.
+type statsScopeDef struct {
+	Key       string
+	SeasonNum int // 0 = all-time
+}
+
+var statsScopes = []statsScopeDef{
+	{Key: "all_time", SeasonNum: 0},
+	{Key: "season_1", SeasonNum: 1},
+	{Key: "season_2", SeasonNum: 2},
+}
+
+// statsRegions are the region scopes emitt
ed; "global" means no region filter.
+var statsRegions = []string{"global", "us",
 "eu", "kr", "tw"}
+
+// GenerateStats writes outDir/api/stats.js
on with aggregated stats per (region, season
) scope.
+func GenerateStats(db *sql.DB, outDir strin
g) error {
+	out := StatsJSON{
+		GeneratedAt: time.Now().UnixMilli(),
+		Scopes:      make(map[string]map[string]S
tatsScope, len(statsRegions)),
+	}
+
+	for _, region := range statsRegions {
+		regionScopes := make(map[string]StatsScop
e, len(statsScopes))
+		// Compute all-time first per region so i
ts total_players is the stable
+		// denominator for per-season scopes with
in that region.
+		var allTimeTotalPlayers int64
+		for _, sc := range statsScopes {
+			ss, err := buildStatsScope(db, sc.Season
Num, region, allTimeTotalPlayers)
+			if err != nil {
+				return fmt.Errorf("scope %s/%s: %w", re
gion, sc.Key, err)
+			}
+			if sc.SeasonNum == 0 {
+				allTimeTotalPlayers = ss.TotalPlayers
+			}
+			regionScopes[sc.Key] = ss
+		}
+		out.Scopes[region] = regionScopes
+	}
+
+	outPath := filepath.Join(outDir, "api", "s
tats.json")
+	if err := writer.WriteJSONFileCompact(outP
ath, out); err != nil {
+		return fmt.Errorf("write stats.json: %w",
 err)
+	}
+	return nil
+}
+
+// buildStatsScope assembles a single Stats
Scope.
+// seasonNum == 0 means cross-season (all-t
ime). region == "global" means no region fil
ter.
+// For all-time, pass allTimeTotalPlayers=0
; for per-season builds, pass the all-time s
cope's
+// TotalPlayers (within the same region) so
 the `percentile_of_all_time_players` field
+// uses a stable denominator.
+func buildStatsScope(db *sql.DB, seasonNum 
int, region string, allTimeTotalPlayers int6
4) (StatsScope, error) {
+	var s StatsScope
+	var err error
+
+	if s.TotalRuns, err = countTotalRuns(db, s
easonNum, region); err != nil {
+		return s, fmt.Errorf("total runs: %w", er
r)
+	}
+
+	if s.TotalPlayers, err = countTotalPlayers
(db, seasonNum, region); err != nil {
+		return s, fmt.Errorf("total players: %w",
 err)
+	}
+
+	// On all-time, the stable denominator is 
just this scope's own TotalPlayers.
+	allTimeDen := allTimeTotalPlayers
+	if seasonNum == 0 {
+		allTimeDen = s.TotalPlayers
+	}
+
+	tiers, completedCount, err := computeCompl
etionTiers(db, seasonNum, region)
+	if err != nil {
+		return s, fmt.Errorf("completion tiers: %
w", err)
+	}
+	s.CompletionTiers = tiers
+	s.NineOfNinePlayers = completedCount
+	// fill percentile fields now that we know
 all three denominators
+	s.CompletionTiers.NineOfNineGold = withPer
centiles(s.CompletionTiers.NineOfNineGold, s
.TotalPlayers, completedCount, allTimeDen)
+	s.CompletionTiers.NineOfNinePlatinum = wit
hPercentiles(s.CompletionTiers.NineOfNinePla
tinum, s.TotalPlayers, completedCount, allTi
meDen)
+	s.CompletionTiers.NineOfNineTitle = withPe
rcentiles(s.CompletionTiers.NineOfNineTitle,
 s.TotalPlayers, completedCount, allTimeDen)
+
+	if s.SpecCounts, err = computeSpecCounts(d
b, seasonNum, region); err != nil {
+		return s, fmt.Errorf("spec counts: %w", e
rr)
+	}
+
+	if s.WeeklyActivity, err = queryWeeklyActi
vity(db, seasonNum, region); err != nil {
+		return s, fmt.Errorf("weekly activity: %w
", err)
+	}
+
+	return s, nil
+}
+
+// seasonClauseAndArg returns the SQL fragm
ent and arg slice for filtering by challenge
_runs.season_id.
+// Empty string for all-time (seasonNum == 
0).
+func seasonClauseAndArg(seasonNum int, alia
s string) (string, []any) {
+	if seasonNum == 0 {
+		return "", nil
+	}
+	if alias == "" {
+		alias = "challenge_runs"
+	}
+	return fmt.Sprintf(" AND %s.season_id = ?"
, alias), []any{seasonNum}
+}
+
+// regionClauseAndArg filters by realm regi
on. region == "global" returns empty (no fil
ter).
+// `realmAlias` is the SQL alias of the rea
lms table reference in the FROM/JOIN clause.
+func regionClauseAndArg(region, realmAlias 
string) (string, []any) {
+	if region == "" || region == "global" {
+		return "", nil
+	}
+	if realmAlias == "" {
+		realmAlias = "r"
+	}
+	return fmt.Sprintf(" AND %s.region = ?", r
ealmAlias), []any{region}
+}
+
+func countTotalRuns(db *sql.DB, seasonNum i
nt, region string) (int64, error) {
+	// Region filter requires joining realms; 
only join when needed to keep the global
+	// case as a fast plain COUNT(*) on challe
nge_runs.
+	if region == "" || region == "global" {
+		q := "SELECT COUNT(*) FROM challenge_runs
 cr WHERE 1=1"
+		clause, args := seasonClauseAndArg(season
Num, "cr")
+		var n int64
+		if err := db.QueryRow(q+clause, args...).
Scan(&n); err != nil {
+			return 0, err
+		}
+		return n, nil
+	}
+	q := "SELECT COUNT(*) FROM challenge_runs 
cr JOIN realms r ON r.id = cr.realm_id WHERE
 1=1"
+	sClause, sArgs := seasonClauseAndArg(seaso
nNum, "cr")
+	rClause, rArgs := regionClauseAndArg(regio
n, "r")
+	args := append(sArgs, rArgs...)
+	var n int64
+	if err := db.QueryRow(q+sClause+rClause, a
rgs...).Scan(&n); err != nil {
+		return 0, err
+	}
+	return n, nil
+}
+
+// countTotalPlayers counts distinct player
s who appear in run_members within scope.
+// "Within scope" = any player who particip
ated in a run during the given season+region
.
+func countTotalPlayers(db *sql.DB, seasonNu
m int, region string) (int64, error) {
+	// Always join through challenge_runs so w
e can filter by season and/or region.
+	q := `SELECT COUNT(DISTINCT rm.player_id)
+	      FROM run_members rm
+	      JOIN challenge_runs cr ON rm.run_id 
= cr.id`
+	if region != "" && region != "global" {
+		q += " JOIN realms r ON r.id = cr.realm_i
d"
+	}
+	q += " WHERE 1=1"
+	sClause, sArgs := seasonClauseAndArg(seaso
nNum, "cr")
+	rClause, rArgs := regionClauseAndArg(regio
n, "r")
+	args := append(sArgs, rArgs...)
+	var n int64
+	if err := db.QueryRow(q+sClause+rClause, a
rgs...).Scan(&n); err != nil {
+		return 0, err
+	}
+	return n, nil
+}
+
+// computeCompletionTiers loads each player
's best time per (season, dungeon) and
+// classifies them against the gold / plati
num / title thresholds.
+// Tiers are achievements earned *within a 
single season*, so for the all-time scope
+// we take the union of per-season qualifie
rs - a player counts if they hit 9/9 of a
+// tier in any single season, not by combin
ing best times across seasons.
+// Returns the tier counts and the count of
 players who completed all 9 dungeons in
+// at least one season (the "completed" den
ominator).
+func computeCompletionTiers(db *sql.DB, sea
sonNum int, region string) (CompletionTiers,
 int64, error) {
+	q := `
+		SELECT rm.player_id, cr.season_id, cr.dun
geon_id, MIN(cr.duration) AS best_duration
+		FROM run_members rm
+		JOIN challenge_runs cr ON rm.run_id = cr.
id
+		%s
+		WHERE 1=1 %s %s
+		GROUP BY rm.player_id, cr.season_id, cr.d
ungeon_id
+	`
+	regionJoin := ""
+	if region != "" && region != "global" {
+		regionJoin = "JOIN realms r ON r.id = cr.
realm_id"
+	}
+	sClause, sArgs := seasonClauseAndArg(seaso
nNum, "cr")
+	rClause, rArgs := regionClauseAndArg(regio
n, "r")
+	args := append(sArgs, rArgs...)
+	rows, err := db.Query(fmt.Sprintf(q, regio
nJoin, sClause, rClause), args...)
+	if err != nil {
+		return CompletionTiers{}, 0, err
+	}
+	defer rows.Close()
+
+	// (player_id, season_id) -> per-tier dung
eon-cleared count
+	type key struct {
+		playerID int64
+		seasonID int
+	}
+	type counts struct{ gold, plat, title, any
Dungeon int }
+	per := make(map[key]*counts)
+	for rows.Next() {
+		var playerID int64
+		var seasonID, dungeonID int
+		var bestDuration int64
+		if err := rows.Scan(&playerID, &seasonID,
 &dungeonID, &bestDuration); err != nil {
+			return CompletionTiers{}, 0, err
+		}
+		threshold, ok := dungeonTimerThresholds[d
ungeonID]
+		if !ok {
+			continue // unknown dungeon - skip silen
tly
+		}
+		k := key{playerID, seasonID}
+		c := per[k]
+		if c == nil {
+			c = &counts{}
+			per[k] = c
+		}
+		c.anyDungeon++
+		if beatsTier(bestDuration, threshold.Gold
Ms) {
+			c.gold++
+		}
+		if beatsTier(bestDuration, threshold.Plat
inumMs) {
+			c.plat++
+		}
+		if beatsTier(bestDuration, threshold.Titl
eMs) {
+			c.title++
+		}
+	}
+	if err := rows.Err(); err != nil {
+		return CompletionTiers{}, 0, err
+	}
+
+	// Union per player: did they qualify in *
any* season?
+	type qualifiers struct{ gold, plat, title,
 completed bool }
+	playerQual := make(map[int64]*qualifiers)
+	for k, c := range per {
+		q := playerQual[k.playerID]
+		if q == nil {
+			q = &qualifiers{}
+			playerQual[k.playerID] = q
+		}
+		if c.anyDungeon == totalDungeons {
+			q.completed = true
+		}
+		if c.gold == totalDungeons {
+			q.gold = true
+		}
+		if c.plat == totalDungeons {
+			q.plat = true
+		}
+		if c.title == totalDungeons {
+			q.title = true
+		}
+	}
+
+	var tiers CompletionTiers
+	var completedCount int64
+	for _, q := range playerQual {
+		if q.completed {
+			completedCount++
+		}
+		if q.gold {
+			tiers.NineOfNineGold.Count++
+		}
+		if q.plat {
+			tiers.NineOfNinePlatinum.Count++
+		}
+		if q.title {
+			tiers.NineOfNineTitle.Count++
+		}
+	}
+	return tiers, completedCount, nil
+}
+
+func withPercentiles(t CompletionTier, tota
lPlayers, completedPlayers, allTimePlayers i
nt64) CompletionTier {
+	if totalPlayers > 0 {
+		t.PercentileOfAllPlayers = round2(float64
(t.Count) * 100.0 / float64(totalPlayers))
+	}
+	if completedPlayers > 0 {
+		t.PercentileOfCompletedPlayers = round2(f
loat64(t.Count) * 100.0 / float64(completedP
layers))
+	}
+	if allTimePlayers > 0 {
+		t.PercentileOfAllTimePlayers = round2(flo
at64(t.Count) * 100.0 / float64(allTimePlaye
rs))
+	}
+	return t
+}
+
+// computeSpecCounts returns the spec-distr
ibution charts:
+//   all_runs:      every run_member row in
 scope
+//   gold_runs:     run_members where the r
un beat the gold threshold for its dungeon
+//   platinum_runs: same for platinum
+//   title_runs:    same for title
+//   top_50_runs:   run_members where the r
un was top-50 in scope (globally team-filter
ed for
+//                  region=="global", regio
nally team-filtered for a specific region)
+func computeSpecCounts(db *sql.DB, seasonNu
m int, region string) (map[string][]SpecCoun
tEntry, error) {
+	out := map[string][]SpecCountEntry{}
+
+	allRuns, err := querySpecCountsForRuns(db,
 seasonNum, region)
+	if err != nil {
+		return nil, fmt.Errorf("all_runs: %w", er
r)
+	}
+	out["all_runs"] = allRuns
+
+	for _, tier := range []struct {
+		key        string
+		thresholdF func(dungeonThresholds) int64
+	}{
+		{"gold_runs", func(t dungeonThresholds) i
nt64 { return t.GoldMs }},
+		{"platinum_runs", func(t dungeonThreshold
s) int64 { return t.PlatinumMs }},
+		{"title_runs", func(t dungeonThresholds) 
int64 { return t.TitleMs }},
+	} {
+		tierCounts, err := querySpecCountsForTier
(db, seasonNum, region, tier.thresholdF)
+		if err != nil {
+			return nil, fmt.Errorf("%s: %w", tier.ke
y, err)
+		}
+		out[tier.key] = tierCounts
+	}
+
+	top50, err := querySpecCountsForTop50(db, 
seasonNum, region)
+	if err != nil {
+		return nil, fmt.Errorf("top_50_runs: %w",
 err)
+	}
+	out["top_50_runs"] = top50
+
+	return out, nil
+}
+
+// querySpecCountsForRuns counts run_member
 spec_ids across all runs in scope.
+func querySpecCountsForRuns(db *sql.DB, sea
sonNum int, region string) ([]SpecCountEntry
, error) {
+	regionJoin := ""
+	if region != "" && region != "global" {
+		regionJoin = "JOIN realms r ON r.id = cr.
realm_id"
+	}
+	q := `
+		SELECT rm.spec_id, COUNT(*)
+		FROM run_members rm
+		JOIN challenge_runs cr ON rm.run_id = cr.
id
+		%s
+		WHERE rm.spec_id IS NOT NULL %s %s
+		GROUP BY rm.spec_id
+		ORDER BY COUNT(*) DESC
+	`
+	sClause, sArgs := seasonClauseAndArg(seaso
nNum, "cr")
+	rClause, rArgs := regionClauseAndArg(regio
n, "r")
+	args := append(sArgs, rArgs...)
+	rows, err := db.Query(fmt.Sprintf(q, regio
nJoin, sClause, rClause), args...)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	return scanSpecCounts(rows)
+}
+
+// querySpecCountsForTier counts run_member
 spec_ids across runs that beat a per-dungeo
n threshold.
+// We can't easily express the per-dungeon 
threshold in a single SQL query, so instead 
we load
+// (run_id, dungeon_id, duration) rows in s
cope, decide qualification in Go, then count
 specs by
+// the qualifying run_id set.
+func querySpecCountsForTier(db *sql.DB, sea
sonNum int, region string, thresholdF func(d
ungeonThresholds) int64) ([]SpecCountEntry, 
error) {
+	regionJoin := ""
+	if region != "" && region != "global" {
+		regionJoin = "JOIN realms r ON r.id = cr.
realm_id"
+	}
+	sClause, sArgs := seasonClauseAndArg(seaso
nNum, "cr")
+	rClause, rArgs := regionClauseAndArg(regio
n, "r")
+	args := append(sArgs, rArgs...)
+	rows, err := db.Query(fmt.Sprintf(`
+		SELECT cr.id, cr.dungeon_id, cr.duration
+		FROM challenge_runs cr
+		%s
+		WHERE 1=1 %s %s
+	`, regionJoin, sClause, rClause), args...)
+	if err != nil {
+		return nil, err
+	}
+	qualifying := make(map[int64]struct{})
+	for rows.Next() {
+		var id int64
+		var dungeonID int
+		var duration int64
+		if err := rows.Scan(&id, &dungeonID, &dur
ation); err != nil {
+			rows.Close()
+			return nil, err
+		}
+		threshold, ok := dungeonTimerThresholds[d
ungeonID]
+		if !ok {
+			continue
+		}
+		if beatsTier(duration, thresholdF(thresho
ld)) {
+			qualifying[id] = struct{}{}
+		}
+	}
+	rows.Close()
+	if len(qualifying) == 0 {
+		return []SpecCountEntry{}, nil
+	}
+
+	// Count spec_ids in run_members for quali
fying runs
+	specCounts := make(map[int]int64)
+	memberRows, err := db.Query(`
+		SELECT run_id, spec_id
+		FROM run_members
+		WHERE spec_id IS NOT NULL
+	`)
+	if err != nil {
+		return nil, err
+	}
+	defer memberRows.Close()
+	for memberRows.Next() {
+		var runID int64
+		var specID int
+		if err := memberRows.Scan(&runID, &specID
); err != nil {
+			return nil, err
+		}
+		if _, ok := qualifying[runID]; !ok {
+			continue
+		}
+		specCounts[specID]++
+	}
+	return specCountsToSlice(specCounts), nil
+}
+
+// querySpecCountsForTop50 counts spec_ids 
in top-50 runs.
+// region "global" ? top 50 globally (filte
red); region "us"/"eu"/etc ? top 50 within t
hat region.
+func querySpecCountsForTop50(db *sql.DB, se
asonNum int, region string) ([]SpecCountEntr
y, error) {
+	rankingType := "global"
+	rankingScope := "filtered"
+	if region != "" && region != "global" {
+		rankingType = "regional"
+		rankingScope = region + "_filtered"
+	}
+	q := `
+		SELECT rm.spec_id, COUNT(*)
+		FROM run_members rm
+		JOIN run_rankings rr ON rm.run_id = rr.ru
n_id
+		WHERE rm.spec_id IS NOT NULL
+		  AND rr.ranking_type = ?
+		  AND rr.ranking_scope = ?
+		  AND rr.ranking <= 50
+	`
+	args := []any{rankingType, rankingScope}
+	if seasonNum != 0 {
+		q += " AND rr.season_id = ?"
+		args = append(args, seasonNum)
+	}
+	q += " GROUP BY rm.spec_id ORDER BY COUNT(
*) DESC"
+
+	rows, err := db.Query(q, args...)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	return scanSpecCounts(rows)
+}
+
+func scanSpecCounts(rows *sql.Rows) ([]Spec
CountEntry, error) {
+	var out []SpecCountEntry
+	for rows.Next() {
+		var specID int
+		var count int64
+		if err := rows.Scan(&specID, &count); err
 != nil {
+			return nil, err
+		}
+		entry := SpecCountEntry{SpecID: specID, C
ount: count}
+		if cls, spec, ok := wow.GetClassAndSpec(s
pecID); ok {
+			entry.ClassName = cls
+			entry.SpecName = spec
+		}
+		out = append(out, entry)
+	}
+	return out, rows.Err()
+}
+
+// specCountsToSlice converts a map to a so
rted SpecCountEntry slice (desc by count).
+func specCountsToSlice(m map[int]int64) []S
pecCountEntry {
+	out := make([]SpecCountEntry, 0, len(m))
+	for specID, count := range m {
+		entry := SpecCountEntry{SpecID: specID, C
ount: count}
+		if cls, spec, ok := wow.GetClassAndSpec(s
pecID); ok {
+			entry.ClassName = cls
+			entry.SpecName = spec
+		}
+		out = append(out, entry)
+	}
+	// simple insertion sort by count desc (sm
all N: ~33 specs)
+	for i := 1; i < len(out); i++ {
+		for j := i; j > 0 && out[j].Count > out[j
-1].Count; j-- {
+			out[j], out[j-1] = out[j-1], out[j]
+		}
+	}
+	return out
+}
+
+// queryWeeklyActivity returns runs grouped
 by Monday-start week.
+// Timestamps are stored in milliseconds; S
QLite date functions need seconds.
+func queryWeeklyActivity(db *sql.DB, season
Num int, region string) ([]WeeklyActivityEnt
ry, error) {
+	regionJoin := ""
+	if region != "" && region != "global" {
+		regionJoin = "JOIN realms r ON r.id = cr.
realm_id"
+	}
+	sClause, sArgs := seasonClauseAndArg(seaso
nNum, "cr")
+	rClause, rArgs := regionClauseAndArg(regio
n, "r")
+	args := append(sArgs, rArgs...)
+	// 'weekday 1' advances to next Monday; '-
7 days' rolls back to start of current week.
+	q := fmt.Sprintf(`
+		SELECT
+			date(cr.completed_timestamp / 1000, 'uni
xepoch', 'weekday 1', '-7 days') AS week_sta
rt,
+			COUNT(*) AS run_count
+		FROM challenge_runs cr
+		%s
+		WHERE cr.completed_timestamp IS NOT NULL 
%s %s
+		GROUP BY week_start
+		ORDER BY week_start
+	`, regionJoin, sClause, rClause)
+	rows, err := db.Query(q, args...)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+
+	var out []WeeklyActivityEntry
+	for rows.Next() {
+		var e WeeklyActivityEntry
+		if err := rows.Scan(&e.WeekStart, &e.RunC
ount); err != nil {
+			return nil, err
+		}
+		out = append(out, e)
+	}
+	return out, rows.Err()
+}
+
+func round2(f float64) float64 {
+	return float64(int64(f*100+0.5)) / 100
+}
 
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET