OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
HASH      f9bab020c966
DATE      2025-12-11
SUBJECT   ookstats: throw away period based season, we use timestamps now
FILES     10 CHANGED
HASH      f9bab020c966
DATE      2025-12-11
SUBJECT   ookstats: throw away period based
          season, we use timestamps now
FILES     10 CHANGED
 

diff --git a/nix/pkgs/ookstats/src/cmd/backfill.go b/nix/pkgs/ookstats/src/cmd/bac
kfill.go
new file mode 100644
index 0000000..a56bd5a
--- /dev/null
+++ b/nix/pkgs/ookstats/src/cmd/backfill.go
@@ -0,0 +1,78 @@
+package cmd
+
+import (
+	"fmt"
+	"ookstats/internal/database"
+
+	"github.com/spf13/cobra"
+)
+
+var backfillCmd = &cobra.Command{
+	Use:   "backfill",
+	Short: "Backfill missing data",
+	Long:  `Backfill and update historical data with new fields.`,
+}
+
+var backfillSeasonsCmd = &cobra.Command{
+	Use:   "seasons",
+	Short: "Assign season_id to all challenge runs",
+	Long: `Assigns season_id to all existing challenge runs based on their completed
_timestamp
+and the season start/end timestamps for their region.`,
+	RunE: func(cmd *cobra.Command, args []string) error {
+		fmt.Println("=== Backfill Season Assignments ===")
+
+		db, err := database.Connect()
+		if err != nil {
+			return fmt.Errorf("failed to connect to database: %w", err)
+		}
+		defer db.Close()
+
+		fmt.Printf("Connected to database: %s\n", database.DBFilePath())
+
+		dbService := database.NewDatabaseService(db)
+
+		fmt.Println("\nAssigning seasons to challenge runs based on timestamps...")
+		err = dbService.AssignRunsToSeasons()
+		if err != nil {
+			return fmt.Errorf("failed to assign seasons: %w", err)
+		}
+
+		// show stats
+		fmt.Println("\n Season Assignment Statistics")
+		rows, err := db.Query(`
+			SELECT
+				s.region,
+				COALESCE(s.season_name, 'Season ' || s.season_number) as season_display,
+				COUNT(cr.id) as run_count
+			FROM seasons s
+			LEFT JOIN challenge_runs cr ON cr.season_id = s.season_number AND cr.realm_id 
IN (
+				SELECT id FROM realms WHERE region = s.region
+			)
+			GROUP BY s.id, s.region, s.season_name, s.season_number
+			ORDER BY s.region, s.season_number
+		`)
+		if err != nil {
+			return fmt.Errorf("failed to query stats: %w", err)
+		}
+		defer rows.Close()
+
+		fmt.Printf("\n%-8s %-20s %10s\n", "Region", "Season", "Runs")
+		fmt.Println("----------------------------------------")
+		for rows.Next() {
+			var region, seasonName string
+			var runCount int
+			if err := rows.Scan(&region, &seasonName, &runCount); err != nil {
+				return fmt.Errorf("failed to scan row: %w", err)
+			}
+			fmt.Printf("%-8s %-20s %10d\n", region, seasonName, runCount)
+		}
+
+		fmt.Printf("\n[OK] Season backfill complete!\n")
+		return nil
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(backfillCmd)
+	backfillCmd.AddCommand(backfillSeasonsCmd)
+}

diff --git a/nix/pkgs/ookstats/src/cmd/backf
ill.go b/nix/pkgs/ookstats/src/cmd/backfill.
go
new file mode 100644
index 0000000..a56bd5a
--- /dev/null
+++ b/nix/pkgs/ookstats/src/cmd/backfill.go
@@ -0,0 +1,78 @@
+package cmd
+
+import (
+	"fmt"
+	"ookstats/internal/database"
+
+	"github.com/spf13/cobra"
+)
+
+var backfillCmd = &cobra.Command{
+	Use:   "backfill",
+	Short: "Backfill missing data",
+	Long:  `Backfill and update historical dat
a with new fields.`,
+}
+
+var backfillSeasonsCmd = &cobra.Command{
+	Use:   "seasons",
+	Short: "Assign season_id to all challenge 
runs",
+	Long: `Assigns season_id to all existing c
hallenge runs based on their completed_times
tamp
+and the season start/end timestamps for the
ir region.`,
+	RunE: func(cmd *cobra.Command, args []stri
ng) error {
+		fmt.Println("=== Backfill Season Assignme
nts ===")
+
+		db, err := database.Connect()
+		if err != nil {
+			return fmt.Errorf("failed to connect to 
database: %w", err)
+		}
+		defer db.Close()
+
+		fmt.Printf("Connected to database: %s\n",
 database.DBFilePath())
+
+		dbService := database.NewDatabaseService(
db)
+
+		fmt.Println("\nAssigning seasons to chall
enge runs based on timestamps...")
+		err = dbService.AssignRunsToSeasons()
+		if err != nil {
+			return fmt.Errorf("failed to assign seas
ons: %w", err)
+		}
+
+		// show stats
+		fmt.Println("\n Season Assignment Statist
ics")
+		rows, err := db.Query(`
+			SELECT
+				s.region,
+				COALESCE(s.season_name, 'Season ' || s.
season_number) as season_display,
+				COUNT(cr.id) as run_count
+			FROM seasons s
+			LEFT JOIN challenge_runs cr ON cr.season
_id = s.season_number AND cr.realm_id IN (
+				SELECT id FROM realms WHERE region = s.
region
+			)
+			GROUP BY s.id, s.region, s.season_name, 
s.season_number
+			ORDER BY s.region, s.season_number
+		`)
+		if err != nil {
+			return fmt.Errorf("failed to query stats
: %w", err)
+		}
+		defer rows.Close()
+
+		fmt.Printf("\n%-8s %-20s %10s\n", "Region
", "Season", "Runs")
+		fmt.Println("----------------------------
------------")
+		for rows.Next() {
+			var region, seasonName string
+			var runCount int
+			if err := rows.Scan(&region, &seasonName
, &runCount); err != nil {
+				return fmt.Errorf("failed to scan row: 
%w", err)
+			}
+			fmt.Printf("%-8s %-20s %10d\n", region, 
seasonName, runCount)
+		}
+
+		fmt.Printf("\n[OK] Season backfill comple
te!\n")
+		return nil
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(backfillCmd)
+	backfillCmd.AddCommand(backfillSeasonsCmd)
+}
 

diff --git a/nix/pkgs/ookstats/src/cmd/build.go b/nix/pkgs/ookstats/src/cmd/build.
go
index dd53a82..c532b2c 100644
--- a/nix/pkgs/ookstats/src/cmd/build.go
+++ b/nix/pkgs/ookstats/src/cmd/build.go
@@ -151,19 +151,25 @@ var buildCmd = &cobra.Command{
 			"players", result.TotalPlayers,
 			"duration", result.Duration)
 
-		// 4) Fingerprint players (merge duplicates)
+		// 4) Assign seasons to runs based on timestamps
+		log.Info("assigning seasons to runs", "method", "timestamp-based")
+		if err := dbService.AssignRunsToSeasons(); err != nil {
+			return fmt.Errorf("assign seasons: %w", err)
+		}
+
+		// 5) Fingerprint players (merge duplicates)
 		log.Info("fingerprinting players", "stage", "identity detection + merge")
 		if err := fingerprintPlayersOnce(db, client); err != nil {
 			return err
 		}
 
-		// 5) Process players (aggregations + rankings)
+		// 6) Process players (aggregations + rankings)
 		log.Info("processing players", "stage", "aggregations + rankings")
 		if err := processPlayersOnce(db); err != nil {
 			return err
 		}
 
-		// 6) Fetch detailed player profiles (optional)
+		// 7) Fetch detailed player profiles (optional)
 		if !skipProfiles {
 			log.Info("fetching detailed player profiles", "coverage", "9/9")
 			if err := fetchProfilesOnce(db, client); err != nil {
@@ -173,19 +179,19 @@ var buildCmd = &cobra.Command{
 			log.Info("skipping player profile fetch", "reason", "flag")
 		}
 
-		// 7) Process run rankings (global/regional)
+		// 8) Process run rankings (global/regional)
 		log.Info("processing run rankings", "scopes", "global + regional")
 		if err := processRunRankingsOnce(db); err != nil {
 			return err
 		}
 
-		// 8) Generate static API
+		// 9) Generate static API
 		log.Info("generating static API")
 		if err := generateAllAPI(db, normalizedOut, pageSize, shardSize, regionsCSV); e
rr != nil {
 			return err
 		}
 
-		// 9) Generate status API via analyze
+		// 10) Generate status API via analyze
 		log.Info("generating status API", "method", "analyze")
 		statusDir := filepath.Join(normalizedOut, "api", "status")
 		outPath := filepath.Join(statusDir, "latest-runs.json")

diff --git a/nix/pkgs/ookstats/src/cmd/build
.go b/nix/pkgs/ookstats/src/cmd/build.go
index dd53a82..c532b2c 100644
--- a/nix/pkgs/ookstats/src/cmd/build.go
+++ b/nix/pkgs/ookstats/src/cmd/build.go
@@ -151,19 +151,25 @@ var buildCmd = &cobra.
Command{
 			"players", result.TotalPlayers,
 			"duration", result.Duration)
 
-		// 4) Fingerprint players (merge duplicat
es)
+		// 4) Assign seasons to runs based on tim
estamps
+		log.Info("assigning seasons to runs", "me
thod", "timestamp-based")
+		if err := dbService.AssignRunsToSeasons()
; err != nil {
+			return fmt.Errorf("assign seasons: %w", 
err)
+		}
+
+		// 5) Fingerprint players (merge duplicat
es)
 		log.Info("fingerprinting players", "stage
", "identity detection + merge")
 		if err := fingerprintPlayersOnce(db, clie
nt); err != nil {
 			return err
 		}
 
-		// 5) Process players (aggregations + ran
kings)
+		// 6) Process players (aggregations + ran
kings)
 		log.Info("processing players", "stage", "
aggregations + rankings")
 		if err := processPlayersOnce(db); err != 
nil {
 			return err
 		}
 
-		// 6) Fetch detailed player profiles (opt
ional)
+		// 7) Fetch detailed player profiles (opt
ional)
 		if !skipProfiles {
 			log.Info("fetching detailed player profi
les", "coverage", "9/9")
 			if err := fetchProfilesOnce(db, client);
 err != nil {
@@ -173,19 +179,19 @@ var buildCmd = &cobra.
Command{
 			log.Info("skipping player profile fetch"
, "reason", "flag")
 		}
 
-		// 7) Process run rankings (global/region
al)
+		// 8) Process run rankings (global/region
al)
 		log.Info("processing run rankings", "scop
es", "global + regional")
 		if err := processRunRankingsOnce(db); err
 != nil {
 			return err
 		}
 
-		// 8) Generate static API
+		// 9) Generate static API
 		log.Info("generating static API")
 		if err := generateAllAPI(db, normalizedOu
t, pageSize, shardSize, regionsCSV); err != 
nil {
 			return err
 		}
 
-		// 9) Generate status API via analyze
+		// 10) Generate status API via analyze
 		log.Info("generating status API", "method
", "analyze")
 		statusDir := filepath.Join(normalizedOut,
 "api", "status")
 		outPath := filepath.Join(statusDir, "late
st-runs.json")
 

diff --git a/nix/pkgs/ookstats/src/cmd/schema.go b/nix/pkgs/ookstats/src/cmd/schem
a.go
index d5fb0cd..57c1531 100644
--- a/nix/pkgs/ookstats/src/cmd/schema.go
+++ b/nix/pkgs/ookstats/src/cmd/schema.go
@@ -41,7 +41,56 @@ var schemaInitCmd = &cobra.Command{
 	},
 }
 
+var schemaMigrateCmd = &cobra.Command{
+	Use:   "migrate",
+	Short: "Migrate database schema to add season_id column",
+	Long:  `Adds the season_id column to challenge_runs table if it doesn't exist.`,
+	RunE: func(cmd *cobra.Command, args []string) error {
+		fmt.Println("=== Database Schema Migration ===")
+
+		db, err := database.Connect()
+		if err != nil {
+			return fmt.Errorf("failed to connect to database: %w", err)
+		}
+		defer db.Close()
+
+		fmt.Printf("Connected to database: %s\n", database.DBFilePath())
+
+		// check if season_id column already exists
+		var columnExists int
+		err = db.QueryRow(`
+			SELECT COUNT(*)
+			FROM pragma_table_info('challenge_runs')
+			WHERE name = 'season_id'
+		`).Scan(&columnExists)
+		if err != nil {
+			return fmt.Errorf("failed to check column existence: %w", err)
+		}
+
+		if columnExists > 0 {
+			fmt.Println("[SKIP] season_id column already exists")
+			return nil
+		}
+
+		fmt.Println("Adding season_id column to challenge_runs table...")
+		_, err = db.Exec("ALTER TABLE challenge_runs ADD COLUMN season_id INTEGER")
+		if err != nil {
+			return fmt.Errorf("failed to add season_id column: %w", err)
+		}
+
+		fmt.Println("Creating index on season_id...")
+		_, err = db.Exec("CREATE INDEX IF NOT EXISTS idx_challenge_runs_season ON chall
enge_runs(season_id, dungeon_id, completed_timestamp)")
+		if err != nil {
+			return fmt.Errorf("failed to create index: %w", err)
+		}
+
+		fmt.Printf("\n[OK] Schema migration complete!\n")
+		return nil
+	},
+}
+
 func init() {
 	rootCmd.AddCommand(schemaCmd)
 	schemaCmd.AddCommand(schemaInitCmd)
+	schemaCmd.AddCommand(schemaMigrateCmd)
 }

diff --git a/nix/pkgs/ookstats/src/cmd/schem
a.go b/nix/pkgs/ookstats/src/cmd/schema.go
index d5fb0cd..57c1531 100644
--- a/nix/pkgs/ookstats/src/cmd/schema.go
+++ b/nix/pkgs/ookstats/src/cmd/schema.go
@@ -41,7 +41,56 @@ var schemaInitCmd = &cobr
a.Command{
 	},
 }
 
+var schemaMigrateCmd = &cobra.Command{
+	Use:   "migrate",
+	Short: "Migrate database schema to add sea
son_id column",
+	Long:  `Adds the season_id column to chall
enge_runs table if it doesn't exist.`,
+	RunE: func(cmd *cobra.Command, args []stri
ng) error {
+		fmt.Println("=== Database Schema Migratio
n ===")
+
+		db, err := database.Connect()
+		if err != nil {
+			return fmt.Errorf("failed to connect to 
database: %w", err)
+		}
+		defer db.Close()
+
+		fmt.Printf("Connected to database: %s\n",
 database.DBFilePath())
+
+		// check if season_id column already exis
ts
+		var columnExists int
+		err = db.QueryRow(`
+			SELECT COUNT(*)
+			FROM pragma_table_info('challenge_runs')
+			WHERE name = 'season_id'
+		`).Scan(&columnExists)
+		if err != nil {
+			return fmt.Errorf("failed to check colum
n existence: %w", err)
+		}
+
+		if columnExists > 0 {
+			fmt.Println("[SKIP] season_id column alr
eady exists")
+			return nil
+		}
+
+		fmt.Println("Adding season_id column to c
hallenge_runs table...")
+		_, err = db.Exec("ALTER TABLE challenge_r
uns ADD COLUMN season_id INTEGER")
+		if err != nil {
+			return fmt.Errorf("failed to add season_
id column: %w", err)
+		}
+
+		fmt.Println("Creating index on season_id.
..")
+		_, err = db.Exec("CREATE INDEX IF NOT EXI
STS idx_challenge_runs_season ON challenge_r
uns(season_id, dungeon_id, completed_timesta
mp)")
+		if err != nil {
+			return fmt.Errorf("failed to create inde
x: %w", err)
+		}
+
+		fmt.Printf("\n[OK] Schema migration compl
ete!\n")
+		return nil
+	},
+}
+
 func init() {
 	rootCmd.AddCommand(schemaCmd)
 	schemaCmd.AddCommand(schemaInitCmd)
+	schemaCmd.AddCommand(schemaMigrateCmd)
 }
 

diff --git a/nix/pkgs/ookstats/src/internal/database/client.go b/nix/pkgs/ookstats
/src/internal/database/client.go
index 64ecdc6..34821d0 100644
--- a/nix/pkgs/ookstats/src/internal/database/client.go
+++ b/nix/pkgs/ookstats/src/internal/database/client.go
@@ -157,7 +157,8 @@ func EnsureCompleteSchema(db *sql.DB) error {
 			period_id INTEGER,
 			period_start_timestamp INTEGER,
 			period_end_timestamp INTEGER,
-			team_signature TEXT
+			team_signature TEXT,
+			season_id INTEGER
 		)`,
 
 		`CREATE TABLE IF NOT EXISTS players (
@@ -429,6 +430,7 @@ func ensureRecommendedIndexes(db *sql.DB) error {
 		"CREATE INDEX IF NOT EXISTS idx_run_members_player_id ON run_members(player_id)
",
 		"CREATE INDEX IF NOT EXISTS idx_challenge_runs_dungeon_duration ON challenge_ru
ns(dungeon_id, duration)",
 		// Season-related indexes
+		"CREATE INDEX IF NOT EXISTS idx_challenge_runs_season ON challenge_runs(season_
id, dungeon_id, completed_timestamp)",
 		"CREATE INDEX IF NOT EXISTS idx_run_rankings_season ON run_rankings(season_id, 
ranking_type, ranking_scope, dungeon_id)",
 		"CREATE INDEX IF NOT EXISTS idx_player_best_runs_season ON player_best_runs(sea
son_id, player_id)",
 		"CREATE INDEX IF NOT EXISTS idx_player_profiles_season ON player_profiles(seaso
n_id, global_ranking)",

diff --git a/nix/pkgs/ookstats/src/internal/
database/client.go b/nix/pkgs/ookstats/src/i
nternal/database/client.go
index 64ecdc6..34821d0 100644
--- a/nix/pkgs/ookstats/src/internal/databas
e/client.go
+++ b/nix/pkgs/ookstats/src/internal/databas
e/client.go
@@ -157,7 +157,8 @@ func EnsureCompleteSchem
a(db *sql.DB) error {
 			period_id INTEGER,
 			period_start_timestamp INTEGER,
 			period_end_timestamp INTEGER,
-			team_signature TEXT
+			team_signature TEXT,
+			season_id INTEGER
 		)`,
 
 		`CREATE TABLE IF NOT EXISTS players (
@@ -429,6 +430,7 @@ func ensureRecommendedIn
dexes(db *sql.DB) error {
 		"CREATE INDEX IF NOT EXISTS idx_run_membe
rs_player_id ON run_members(player_id)",
 		"CREATE INDEX IF NOT EXISTS idx_challenge
_runs_dungeon_duration ON challenge_runs(dun
geon_id, duration)",
 		// Season-related indexes
+		"CREATE INDEX IF NOT EXISTS idx_challenge
_runs_season ON challenge_runs(season_id, du
ngeon_id, completed_timestamp)",
 		"CREATE INDEX IF NOT EXISTS idx_run_ranki
ngs_season ON run_rankings(season_id, rankin
g_type, ranking_scope, dungeon_id)",
 		"CREATE INDEX IF NOT EXISTS idx_player_be
st_runs_season ON player_best_runs(season_id
, player_id)",
 		"CREATE INDEX IF NOT EXISTS idx_player_pr
ofiles_season ON player_profiles(season_id, 
global_ranking)",
 

diff --git a/nix/pkgs/ookstats/src/internal/database/operations.go b/nix/pkgs/ooks
tats/src/internal/database/operations.go
index 08fe0a3..51f547e 100644
--- a/nix/pkgs/ookstats/src/internal/database/operations.go
+++ b/nix/pkgs/ookstats/src/internal/database/operations.go
@@ -148,6 +148,18 @@ func (ds *DatabaseService) InsertLeaderboardData(leaderboard 
*blizzard.Leaderboa
 	playersInserted := 0
 	allRealms := blizzard.GetAllRealms()
 
+	// determine season for runs based on completed timestamp
+	var seasonID *int
+	if leaderboard.LeadingGroups[0].CompletedTimestamp > 0 {
+		sid, err := ds.DetermineSeasonForRun(realmInfo.Region, leaderboard.LeadingGroup
s[0].CompletedTimestamp)
+		if err != nil {
+			return 0, 0, fmt.Errorf("failed to determine season: %w", err)
+		}
+		if sid > 0 {
+			seasonID = &sid
+		}
+	}
+
 	// begin transaction
 	tx, err := ds.db.Begin()
 	if err != nil {
@@ -170,11 +182,25 @@ func (ds *DatabaseService) InsertLeaderboardData(leaderboard
 *blizzard.Leaderboa
 
 		teamSignature := utils.ComputeTeamSignature(playerIDs)
 
+		// determine season for this specific run if different from first
+		runSeasonID := seasonID
+		if run.CompletedTimestamp != leaderboard.LeadingGroups[0].CompletedTimestamp {
+			sid, err := ds.DetermineSeasonForRun(realmInfo.Region, run.CompletedTimestamp)
+			if err != nil {
+				return 0, 0, fmt.Errorf("failed to determine season for run: %w", err)
+			}
+			if sid > 0 {
+				runSeasonID = &sid
+			} else {
+				runSeasonID = nil
+			}
+		}
+
 		// insert run with team signature to prevent duplicates
 		runQuery := `
 			INSERT OR IGNORE INTO challenge_runs
-			(duration, completed_timestamp, keystone_level, dungeon_id, realm_id, period_i
d, period_start_timestamp, period_end_timestamp, team_signature)
-			VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
+			(duration, completed_timestamp, keystone_level, dungeon_id, realm_id, period_i
d, period_start_timestamp, period_end_timestamp, team_signature, season_id)
+			VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
 		`
 
 		result, err := tx.Exec(runQuery,
@@ -187,6 +213,7 @@ func (ds *DatabaseService) InsertLeaderboardData(leaderboard *
blizzard.Leaderboa
 			leaderboard.PeriodStartTimestamp,
 			leaderboard.PeriodEndTimestamp,
 			teamSignature,
+			runSeasonID,
 		)
 		if err != nil {
 			return 0, 0, fmt.Errorf("failed to insert run: %w", err)
@@ -718,11 +745,23 @@ func (ds *DatabaseService) insertLeaderboardDataTx(tx *sql.T
x, leaderboard *bliz
 
 		teamSignature := utils.ComputeTeamSignature(playerIDs)
 
+		// determine season for this run
+		var runSeasonID *int
+		if run.CompletedTimestamp > 0 {
+			sid, err := ds.DetermineSeasonForRun(realmInfo.Region, run.CompletedTimestamp)
+			if err != nil {
+				return 0, 0, fmt.Errorf("failed to determine season for run: %w", err)
+			}
+			if sid > 0 {
+				runSeasonID = &sid
+			}
+		}
+
 		// insert run
 		runQuery := `
 			INSERT OR IGNORE INTO challenge_runs
-			(duration, completed_timestamp, keystone_level, dungeon_id, realm_id, period_i
d, period_start_timestamp, period_end_timestamp, team_signature)
-			VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
+			(duration, completed_timestamp, keystone_level, dungeon_id, realm_id, period_i
d, period_start_timestamp, period_end_timestamp, team_signature, season_id)
+			VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
 		`
 
 		result, err := tx.Exec(runQuery,
@@ -735,6 +774,7 @@ func (ds *DatabaseService) insertLeaderboardDataTx(tx *sql.Tx,
 leaderboard *bliz
 			leaderboard.PeriodStartTimestamp,
 			leaderboard.PeriodEndTimestamp,
 			teamSignature,
+			runSeasonID,
 		)
 		if err != nil {
 			return 0, 0, fmt.Errorf("failed to insert run: %w", err)
@@ -1517,6 +1557,80 @@ func (ds *DatabaseService) GetPeriodsForRegion(region strin
g) ([]int, error) {
 	return periods, rows.Err()
 }
 
+// DetermineSeasonForRun determines which season a run belongs to based on
+// completed_timestamp and region. Returns season_number (not id) for the matchin
g season.
+func (ds *DatabaseService) DetermineSeasonForRun(region string, completedTimestam
p int64) (int, error) {
+	query := `
+		SELECT season_number
+		FROM seasons
+		WHERE region = ?
+		  AND start_timestamp <= ?
+		  AND (end_timestamp IS NULL OR end_timestamp > ?)
+		ORDER BY start_timestamp DESC
+		LIMIT 1
+	`
+	var seasonNumber int
+	err := ds.db.QueryRow(query, region, completedTimestamp, completedTimestamp).Sca
n(&seasonNumber)
+	if err == sql.ErrNoRows {
+		return 0, nil
+	}
+	return seasonNumber, err
+}
+
+// AssignRunsToSeasons assigns season_id to all challenge_runs based on completed
_timestamp
+// and realm region. This is used to backfill existing data.
+func (ds *DatabaseService) AssignRunsToSeasons() error {
+	// get all regions
+	regions := []string{"us", "eu", "kr", "tw"}
+
+	for _, region := range regions {
+		fmt.Printf("Assigning runs to seasons for region: %s\n", region)
+
+		// update runs for this region
+		query := `
+			UPDATE challenge_runs
+			SET season_id = (
+				SELECT s.season_number
+				FROM seasons s
+				JOIN realms r ON r.region = s.region
+				WHERE r.id = challenge_runs.realm_id
+				  AND s.start_timestamp <= challenge_runs.completed_timestamp
+				  AND (s.end_timestamp IS NULL OR s.end_timestamp > challenge_runs.completed_
timestamp)
+				ORDER BY s.start_timestamp DESC
+				LIMIT 1
+			)
+			WHERE realm_id IN (
+				SELECT id FROM realms WHERE region = ?
+			)
+		`
+
+		result, err := ds.db.Exec(query, region)
+		if err != nil {
+			return fmt.Errorf("failed to assign seasons for region %s: %w", region, err)
+		}
+
+		rowsAffected, err := result.RowsAffected()
+		if err != nil {
+			return fmt.Errorf("failed to get rows affected for region %s: %w", region, err
)
+		}
+
+		fmt.Printf("  Updated %d runs for region %s\n", rowsAffected, region)
+	}
+
+	// count runs without season assignment
+	var orphanedRuns int
+	err := ds.db.QueryRow("SELECT COUNT(*) FROM challenge_runs WHERE season_id IS NU
LL").Scan(&orphanedRuns)
+	if err != nil {
+		return fmt.Errorf("failed to count orphaned runs: %w", err)
+	}
+
+	if orphanedRuns > 0 {
+		fmt.Printf("Warning: %d runs could not be assigned to any season\n", orphanedRu
ns)
+	}
+
+	return nil
+}
+
 // GetRealmPoolIDs returns all realm IDs in a realm pool (parent + all children)
 // For a child realm, it returns the parent and all siblings
 // For a parent realm, it returns itself and all children

diff --git a/nix/pkgs/ookstats/src/internal/
database/operations.go b/nix/pkgs/ookstats/s
rc/internal/database/operations.go
index 08fe0a3..51f547e 100644
--- a/nix/pkgs/ookstats/src/internal/databas
e/operations.go
+++ b/nix/pkgs/ookstats/src/internal/databas
e/operations.go
@@ -148,6 +148,18 @@ func (ds *DatabaseServi
ce) InsertLeaderboardData(leaderboard *blizz
ard.Leaderboa
 	playersInserted := 0
 	allRealms := blizzard.GetAllRealms()
 
+	// determine season for runs based on comp
leted timestamp
+	var seasonID *int
+	if leaderboard.LeadingGroups[0].CompletedT
imestamp > 0 {
+		sid, err := ds.DetermineSeasonForRun(real
mInfo.Region, leaderboard.LeadingGroups[0].C
ompletedTimestamp)
+		if err != nil {
+			return 0, 0, fmt.Errorf("failed to deter
mine season: %w", err)
+		}
+		if sid > 0 {
+			seasonID = &sid
+		}
+	}
+
 	// begin transaction
 	tx, err := ds.db.Begin()
 	if err != nil {
@@ -170,11 +182,25 @@ func (ds *DatabaseServ
ice) InsertLeaderboardData(leaderboard *bliz
zard.Leaderboa
 
 		teamSignature := utils.ComputeTeamSignatu
re(playerIDs)
 
+		// determine season for this specific run
 if different from first
+		runSeasonID := seasonID
+		if run.CompletedTimestamp != leaderboard.
LeadingGroups[0].CompletedTimestamp {
+			sid, err := ds.DetermineSeasonForRun(rea
lmInfo.Region, run.CompletedTimestamp)
+			if err != nil {
+				return 0, 0, fmt.Errorf("failed to dete
rmine season for run: %w", err)
+			}
+			if sid > 0 {
+				runSeasonID = &sid
+			} else {
+				runSeasonID = nil
+			}
+		}
+
 		// insert run with team signature to prev
ent duplicates
 		runQuery := `
 			INSERT OR IGNORE INTO challenge_runs
-			(duration, completed_timestamp, keystone
_level, dungeon_id, realm_id, period_id, per
iod_start_timestamp, period_end_timestamp, t
eam_signature)
-			VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
+			(duration, completed_timestamp, keystone
_level, dungeon_id, realm_id, period_id, per
iod_start_timestamp, period_end_timestamp, t
eam_signature, season_id)
+			VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
 		`
 
 		result, err := tx.Exec(runQuery,
@@ -187,6 +213,7 @@ func (ds *DatabaseServic
e) InsertLeaderboardData(leaderboard *blizza
rd.Leaderboa
 			leaderboard.PeriodStartTimestamp,
 			leaderboard.PeriodEndTimestamp,
 			teamSignature,
+			runSeasonID,
 		)
 		if err != nil {
 			return 0, 0, fmt.Errorf("failed to inser
t run: %w", err)
@@ -718,11 +745,23 @@ func (ds *DatabaseServ
ice) insertLeaderboardDataTx(tx *sql.Tx, lea
derboard *bliz
 
 		teamSignature := utils.ComputeTeamSignatu
re(playerIDs)
 
+		// determine season for this run
+		var runSeasonID *int
+		if run.CompletedTimestamp > 0 {
+			sid, err := ds.DetermineSeasonForRun(rea
lmInfo.Region, run.CompletedTimestamp)
+			if err != nil {
+				return 0, 0, fmt.Errorf("failed to dete
rmine season for run: %w", err)
+			}
+			if sid > 0 {
+				runSeasonID = &sid
+			}
+		}
+
 		// insert run
 		runQuery := `
 			INSERT OR IGNORE INTO challenge_runs
-			(duration, completed_timestamp, keystone
_level, dungeon_id, realm_id, period_id, per
iod_start_timestamp, period_end_timestamp, t
eam_signature)
-			VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
+			(duration, completed_timestamp, keystone
_level, dungeon_id, realm_id, period_id, per
iod_start_timestamp, period_end_timestamp, t
eam_signature, season_id)
+			VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
 		`
 
 		result, err := tx.Exec(runQuery,
@@ -735,6 +774,7 @@ func (ds *DatabaseServic
e) insertLeaderboardDataTx(tx *sql.Tx, leade
rboard *bliz
 			leaderboard.PeriodStartTimestamp,
 			leaderboard.PeriodEndTimestamp,
 			teamSignature,
+			runSeasonID,
 		)
 		if err != nil {
 			return 0, 0, fmt.Errorf("failed to inser
t run: %w", err)
@@ -1517,6 +1557,80 @@ func (ds *DatabaseSer
vice) GetPeriodsForRegion(region string) ([]
int, error) {
 	return periods, rows.Err()
 }
 
+// DetermineSeasonForRun determines which s
eason a run belongs to based on
+// completed_timestamp and region. Returns 
season_number (not id) for the matching seas
on.
+func (ds *DatabaseService) DetermineSeasonF
orRun(region string, completedTimestamp int6
4) (int, error) {
+	query := `
+		SELECT season_number
+		FROM seasons
+		WHERE region = ?
+		  AND start_timestamp <= ?
+		  AND (end_timestamp IS NULL OR end_times
tamp > ?)
+		ORDER BY start_timestamp DESC
+		LIMIT 1
+	`
+	var seasonNumber int
+	err := ds.db.QueryRow(query, region, compl
etedTimestamp, completedTimestamp).Scan(&sea
sonNumber)
+	if err == sql.ErrNoRows {
+		return 0, nil
+	}
+	return seasonNumber, err
+}
+
+// AssignRunsToSeasons assigns season_id to
 all challenge_runs based on completed_times
tamp
+// and realm region. This is used to backfi
ll existing data.
+func (ds *DatabaseService) AssignRunsToSeas
ons() error {
+	// get all regions
+	regions := []string{"us", "eu", "kr", "tw"
}
+
+	for _, region := range regions {
+		fmt.Printf("Assigning runs to seasons for
 region: %s\n", region)
+
+		// update runs for this region
+		query := `
+			UPDATE challenge_runs
+			SET season_id = (
+				SELECT s.season_number
+				FROM seasons s
+				JOIN realms r ON r.region = s.region
+				WHERE r.id = challenge_runs.realm_id
+				  AND s.start_timestamp <= challenge_ru
ns.completed_timestamp
+				  AND (s.end_timestamp IS NULL OR s.end
_timestamp > challenge_runs.completed_timest
amp)
+				ORDER BY s.start_timestamp DESC
+				LIMIT 1
+			)
+			WHERE realm_id IN (
+				SELECT id FROM realms WHERE region = ?
+			)
+		`
+
+		result, err := ds.db.Exec(query, region)
+		if err != nil {
+			return fmt.Errorf("failed to assign seas
ons for region %s: %w", region, err)
+		}
+
+		rowsAffected, err := result.RowsAffected(
)
+		if err != nil {
+			return fmt.Errorf("failed to get rows af
fected for region %s: %w", region, err)
+		}
+
+		fmt.Printf("  Updated %d runs for region 
%s\n", rowsAffected, region)
+	}
+
+	// count runs without season assignment
+	var orphanedRuns int
+	err := ds.db.QueryRow("SELECT COUNT(*) FRO
M challenge_runs WHERE season_id IS NULL").S
can(&orphanedRuns)
+	if err != nil {
+		return fmt.Errorf("failed to count orphan
ed runs: %w", err)
+	}
+
+	if orphanedRuns > 0 {
+		fmt.Printf("Warning: %d runs could not be
 assigned to any season\n", orphanedRuns)
+	}
+
+	return nil
+}
+
 // GetRealmPoolIDs returns all realm IDs in
 a realm pool (parent + all children)
 // For a child realm, it returns the parent
 and all siblings
 // For a parent realm, it returns itself an
d all children
 

diff --git a/nix/pkgs/ookstats/src/internal/generator/leaderboards.go b/nix/pkgs/o
okstats/src/internal/generator/leaderboards.go
index 274bd09..70dd0a6 100644
--- a/nix/pkgs/ookstats/src/internal/generator/leaderboards.go
+++ b/nix/pkgs/ookstats/src/internal/generator/leaderboards.go
@@ -210,8 +210,8 @@ func generateGlobalLeaderboard(db *sql.DB, out string, d dunge
onInfo, seasonID,
 	err := db.QueryRow(`
 		SELECT COUNT(DISTINCT team_signature)
 		FROM challenge_runs cr
-		LEFT JOIN period_seasons ps ON cr.period_id = ps.period_id
-		WHERE cr.dungeon_id = ? AND COALESCE(ps.season_id, 1) = ?
+		
+		WHERE cr.dungeon_id = ? AND cr.season_id = ?
 	`, d.ID, seasonID).Scan(&total)
 	if err != nil {
 		return fmt.Errorf("global count: %w", err)
@@ -245,8 +245,8 @@ func generateRegionalLeaderboard(db *sql.DB, out, region strin
g, d dungeonInfo,
 			SELECT team_signature
 			FROM challenge_runs cr
 			JOIN realms r ON cr.realm_id = r.id
-			LEFT JOIN period_seasons ps ON cr.period_id = ps.period_id
-			WHERE cr.dungeon_id = ? AND r.region = ? AND COALESCE(ps.season_id, 1) = ?
+			
+			WHERE cr.dungeon_id = ? AND r.region = ? AND cr.season_id = ?
 			GROUP BY team_signature
 		) x
 	`, d.ID, region, seasonID).Scan(&total)
@@ -288,8 +288,8 @@ func generateRealmLeaderboard(db *sql.DB, out, region, realmSl
ug string, d dunge
 			SELECT team_signature
 			FROM challenge_runs cr
 			JOIN realms rr ON cr.realm_id = rr.id
-			LEFT JOIN period_seasons ps ON cr.period_id = ps.period_id
-			WHERE cr.dungeon_id = ? AND rr.region = ? AND rr.slug = ? AND COALESCE(ps.seas
on_id, 1) = ?
+			
+			WHERE cr.dungeon_id = ? AND rr.region = ? AND rr.slug = ? AND cr.season_id = ?
 			GROUP BY team_signature
 		) x
 	`, d.ID, region, realmSlug, seasonID).Scan(&total); err != nil {

diff --git a/nix/pkgs/ookstats/src/internal/
generator/leaderboards.go b/nix/pkgs/ookstat
s/src/internal/generator/leaderboards.go
index 274bd09..70dd0a6 100644
--- a/nix/pkgs/ookstats/src/internal/generat
or/leaderboards.go
+++ b/nix/pkgs/ookstats/src/internal/generat
or/leaderboards.go
@@ -210,8 +210,8 @@ func generateGlobalLeade
rboard(db *sql.DB, out string, d dungeonInfo
, seasonID,
 	err := db.QueryRow(`
 		SELECT COUNT(DISTINCT team_signature)
 		FROM challenge_runs cr
-		LEFT JOIN period_seasons ps ON cr.period_
id = ps.period_id
-		WHERE cr.dungeon_id = ? AND COALESCE(ps.s
eason_id, 1) = ?
+		
+		WHERE cr.dungeon_id = ? AND cr.season_id 
= ?
 	`, d.ID, seasonID).Scan(&total)
 	if err != nil {
 		return fmt.Errorf("global count: %w", err
)
@@ -245,8 +245,8 @@ func generateRegionalLea
derboard(db *sql.DB, out, region string, d d
ungeonInfo,
 			SELECT team_signature
 			FROM challenge_runs cr
 			JOIN realms r ON cr.realm_id = r.id
-			LEFT JOIN period_seasons ps ON cr.period
_id = ps.period_id
-			WHERE cr.dungeon_id = ? AND r.region = ?
 AND COALESCE(ps.season_id, 1) = ?
+			
+			WHERE cr.dungeon_id = ? AND r.region = ?
 AND cr.season_id = ?
 			GROUP BY team_signature
 		) x
 	`, d.ID, region, seasonID).Scan(&total)
@@ -288,8 +288,8 @@ func generateRealmLeader
board(db *sql.DB, out, region, realmSlug str
ing, d dunge
 			SELECT team_signature
 			FROM challenge_runs cr
 			JOIN realms rr ON cr.realm_id = rr.id
-			LEFT JOIN period_seasons ps ON cr.period
_id = ps.period_id
-			WHERE cr.dungeon_id = ? AND rr.region = 
? AND rr.slug = ? AND COALESCE(ps.season_id,
 1) = ?
+			
+			WHERE cr.dungeon_id = ? AND rr.region = 
? AND rr.slug = ? AND cr.season_id = ?
 			GROUP BY team_signature
 		) x
 	`, d.ID, region, realmSlug, seasonID).Scan
(&total); err != nil {
 

diff --git a/nix/pkgs/ookstats/src/internal/loader/runs.go b/nix/pkgs/ookstats/src
/internal/loader/runs.go
index a745fc8..c13376d 100644
--- a/nix/pkgs/ookstats/src/internal/loader/runs.go
+++ b/nix/pkgs/ookstats/src/internal/loader/runs.go
@@ -211,7 +211,7 @@ func LoadCanonicalRuns(db *sql.DB, dungeonID int, region strin
g, realmSlug strin
 		args = append(args, realmSlug)
 	}
 	// Filter by season
-	where += " AND COALESCE(ps.season_id, 1) = ?"
+	where += " AND cr.season_id = ?"
 	args = append(args, seasonID)
 
 	q := fmt.Sprintf(`
@@ -220,7 +220,6 @@ func LoadCanonicalRuns(db *sql.DB, dungeonID int, region strin
g, realmSlug strin
                ROW_NUMBER() OVER (PARTITION BY cr.team_signature ORDER BY cr.dura
tion ASC, cr.completed_timestamp ASC, cr.id ASC) AS rn
         FROM challenge_runs cr
         JOIN realms r ON cr.realm_id = r.id
-        LEFT JOIN period_seasons ps ON cr.period_id = ps.period_id
         %s
       )
       SELECT id FROM ranked WHERE rn = 1
@@ -282,8 +281,7 @@ func LoadCanonicalRuns(db *sql.DB, dungeonID int, region strin
g, realmSlug strin
                ROW_NUMBER() OVER (PARTITION BY cr.dungeon_id, cr.realm_id ORDER B
Y cr.duration ASC, cr.completed_timestamp ASC, cr.id ASC) as realm_ranking,
                COUNT(*) OVER (PARTITION BY cr.dungeon_id, cr.realm_id) as total_i
n_realm_dungeon
         FROM challenge_runs cr
-        LEFT JOIN period_seasons ps ON cr.period_id = ps.period_id
-        WHERE COALESCE(ps.season_id, 1) = ?
+        WHERE cr.season_id = ?
       )
       SELECT cr.id, cr.duration, cr.completed_timestamp, cr.keystone_level,
              d.name, rr.name, rr.region,

diff --git a/nix/pkgs/ookstats/src/internal/
loader/runs.go b/nix/pkgs/ookstats/src/inter
nal/loader/runs.go
index a745fc8..c13376d 100644
--- a/nix/pkgs/ookstats/src/internal/loader/
runs.go
+++ b/nix/pkgs/ookstats/src/internal/loader/
runs.go
@@ -211,7 +211,7 @@ func LoadCanonicalRuns(d
b *sql.DB, dungeonID int, region string, rea
lmSlug strin
 		args = append(args, realmSlug)
 	}
 	// Filter by season
-	where += " AND COALESCE(ps.season_id, 1) =
 ?"
+	where += " AND cr.season_id = ?"
 	args = append(args, seasonID)
 
 	q := fmt.Sprintf(`
@@ -220,7 +220,6 @@ func LoadCanonicalRuns(d
b *sql.DB, dungeonID int, region string, rea
lmSlug strin
                ROW_NUMBER() OVER (PARTITION
 BY cr.team_signature ORDER BY cr.duration A
SC, cr.completed_timestamp ASC, cr.id ASC) A
S rn
         FROM challenge_runs cr
         JOIN realms r ON cr.realm_id = r.id
-        LEFT JOIN period_seasons ps ON cr.p
eriod_id = ps.period_id
         %s
       )
       SELECT id FROM ranked WHERE rn = 1
@@ -282,8 +281,7 @@ func LoadCanonicalRuns(d
b *sql.DB, dungeonID int, region string, rea
lmSlug strin
                ROW_NUMBER() OVER (PARTITION
 BY cr.dungeon_id, cr.realm_id ORDER BY cr.d
uration ASC, cr.completed_timestamp ASC, cr.
id ASC) as realm_ranking,
                COUNT(*) OVER (PARTITION BY 
cr.dungeon_id, cr.realm_id) as total_in_real
m_dungeon
         FROM challenge_runs cr
-        LEFT JOIN period_seasons ps ON cr.p
eriod_id = ps.period_id
-        WHERE COALESCE(ps.season_id, 1) = ?
+        WHERE cr.season_id = ?
       )
       SELECT cr.id, cr.duration, cr.complet
ed_timestamp, cr.keystone_level,
              d.name, rr.name, rr.region,
 

diff --git a/nix/pkgs/ookstats/src/internal/pipeline/process.go b/nix/pkgs/ookstat
s/src/internal/pipeline/process.go
index f9cd01f..89a7455 100644
--- a/nix/pkgs/ookstats/src/internal/pipeline/process.go
+++ b/nix/pkgs/ookstats/src/internal/pipeline/process.go
@@ -1,5 +1,8 @@
 package pipeline
 
+// season assignment has been migrated to use cr.season_id directly instead of pe
riod_seasons lookups.
+// all queries now use timestamp-based season assignment from the challenge_runs 
table.
+
 import (
 	"database/sql"
 	"fmt"
@@ -171,7 +174,7 @@ func createPlayerAggregations(tx *sql.Tx) (int, error) {
 			cr.dungeon_id,
 			cr.id as run_id,
 			cr.duration,
-			COALESCE(s_agg.season_number, 1) as season_id,
+			cr.season_id,
 			cr.completed_timestamp,
 			rr_gf.ranking as global_ranking_filtered,
 			rr_rf.ranking as regional_ranking_filtered,
@@ -181,41 +184,29 @@ func createPlayerAggregations(tx *sql.Tx) (int, error) {
 			rr_lf.percentile_bracket as realm_percentile_bracket
 		FROM run_members rm
 		INNER JOIN challenge_runs cr ON rm.run_id = cr.id
-		LEFT JOIN (
-			SELECT ps.period_id, MIN(s.season_number) as season_number
-			FROM period_seasons ps
-			JOIN seasons s ON ps.season_id = s.id
-			GROUP BY ps.period_id
-		) s_agg ON cr.period_id = s_agg.period_id
 		INNER JOIN (
 			SELECT
 				rm2.player_id,
 				cr2.dungeon_id,
-				COALESCE(s2_agg.season_number, 1) as season_id,
+				cr2.season_id,
 				MIN(cr2.duration) as best_duration
 			FROM run_members rm2
 			INNER JOIN challenge_runs cr2 ON rm2.run_id = cr2.id
-			LEFT JOIN (
-				SELECT ps.period_id, MIN(s.season_number) as season_number
-				FROM period_seasons ps
-				JOIN seasons s ON ps.season_id = s.id
-				GROUP BY ps.period_id
-			) s2_agg ON cr2.period_id = s2_agg.period_id
-			GROUP BY rm2.player_id, cr2.dungeon_id, COALESCE(s2_agg.season_number, 1)
+			GROUP BY rm2.player_id, cr2.dungeon_id, cr2.season_id
 		) best_times ON rm.player_id = best_times.player_id
 					 AND cr.dungeon_id = best_times.dungeon_id
-					 AND COALESCE(s_agg.season_number, 1) = best_times.season_id
+					 AND cr.season_id = best_times.season_id
 					 AND cr.duration = best_times.best_duration
 		LEFT JOIN run_rankings rr_gf ON cr.id = rr_gf.run_id
 			AND rr_gf.ranking_type = 'global' AND rr_gf.ranking_scope = 'filtered'
-			AND rr_gf.season_id = COALESCE(s_agg.season_number, 1)
+			AND rr_gf.season_id = cr.season_id
 		LEFT JOIN run_rankings rr_rf ON cr.id = rr_rf.run_id
 			AND rr_rf.ranking_type = 'regional' AND rr_rf.ranking_scope = 'filtered'
-			AND rr_rf.season_id = COALESCE(s_agg.season_number, 1)
+			AND rr_rf.season_id = cr.season_id
 		LEFT JOIN run_rankings rr_lf ON cr.id = rr_lf.run_id
 			AND rr_lf.ranking_type = 'realm' AND rr_lf.ranking_scope = 'filtered'
-			AND rr_lf.season_id = COALESCE(s_agg.season_number, 1)
-		GROUP BY rm.player_id, cr.dungeon_id, COALESCE(s_agg.season_number, 1)
+			AND rr_lf.season_id = cr.season_id
+		GROUP BY rm.player_id, cr.dungeon_id, cr.season_id
 		HAVING cr.id = MIN(cr.id)
 	`)
 	if err != nil {
@@ -250,16 +241,10 @@ func createPlayerAggregations(tx *sql.Tx) (int, error) {
 		FROM players p
 		INNER JOIN player_best_runs pbr ON p.id = pbr.player_id
 		INNER JOIN (
-			SELECT rm.player_id, COALESCE(s_agg.season_number, 1) as season_id, COUNT(*) a
s run_count
+			SELECT rm.player_id, cr.season_id, COUNT(*) as run_count
 			FROM run_members rm
 			INNER JOIN challenge_runs cr ON rm.run_id = cr.id
-			LEFT JOIN (
-				SELECT ps.period_id, MIN(s.season_number) as season_number
-				FROM period_seasons ps
-				JOIN seasons s ON ps.season_id = s.id
-				GROUP BY ps.period_id
-			) s_agg ON cr.period_id = s_agg.period_id
-			GROUP BY rm.player_id, COALESCE(s_agg.season_number, 1)
+			GROUP BY rm.player_id, cr.season_id
 		) season_runs ON p.id = season_runs.player_id AND pbr.season_id = season_runs.s
eason_id
 		GROUP BY p.id, pbr.season_id, p.name, p.realm_id, season_runs.run_count
 	`, currentTime)
@@ -855,16 +840,10 @@ func computeGlobalRankings(tx *sql.Tx) error {
 			cr.dungeon_id,
 			'global' as ranking_type,
 			'all' as ranking_scope,
-			ROW_NUMBER() OVER (PARTITION BY cr.dungeon_id, COALESCE(s_agg.season_number, 1
) ORDER BY cr.duration ASC, cr.completed_timestamp ASC) as ranking,
-			COALESCE(s_agg.season_number, 1) as season_id,
+			ROW_NUMBER() OVER (PARTITION BY cr.dungeon_id, cr.season_id ORDER BY cr.durati
on ASC, cr.completed_timestamp ASC) as ranking,
+			cr.season_id as season_id,
 			? as computed_at
 		FROM challenge_runs cr
-		LEFT JOIN (
-			SELECT ps.period_id, MIN(s.season_number) as season_number
-			FROM period_seasons ps
-			JOIN seasons s ON ps.season_id = s.id
-			GROUP BY ps.period_id
-		) s_agg ON cr.period_id = s_agg.period_id
 	`, currentTime)
 	if err != nil {
 		return err
@@ -930,14 +909,8 @@ func computeGlobalRankings(tx *sql.Tx) error {
 
 	// Get all seasons (including fallback season 1 for unmapped periods)
 	seasonRows, err := tx.Query(`
-		SELECT DISTINCT COALESCE(s_agg.season_number, 1) as season_id
+		SELECT DISTINCT cr.season_id as season_id
 		FROM challenge_runs cr
-		LEFT JOIN (
-			SELECT ps.period_id, MIN(s.season_number) as season_number
-			FROM period_seasons ps
-			JOIN seasons s ON ps.season_id = s.id
-			GROUP BY ps.period_id
-		) s_agg ON cr.period_id = s_agg.period_id
 	`)
 	if err != nil {
 		return err
@@ -962,13 +935,7 @@ func computeGlobalRankings(tx *sql.Tx) error {
 						cr.team_signature,
 						MIN(cr.duration) as best_duration
 					FROM challenge_runs cr
-					LEFT JOIN (
-						SELECT ps.period_id, MIN(s.season_number) as season_number
-						FROM period_seasons ps
-						JOIN seasons s ON ps.season_id = s.id
-						GROUP BY ps.period_id
-					) s_agg ON cr.period_id = s_agg.period_id
-					WHERE cr.dungeon_id = ? AND COALESCE(s_agg.season_number, 1) = ?
+					WHERE cr.dungeon_id = ? AND cr.season_id = ?
 					GROUP BY cr.team_signature
 				),
 				filtered_runs AS (
@@ -978,15 +945,9 @@ func computeGlobalRankings(tx *sql.Tx) error {
 						cr.completed_timestamp,
 						ROW_NUMBER() OVER (ORDER BY cr.duration ASC, cr.completed_timestamp ASC) as
 filtered_rank
 					FROM challenge_runs cr
-					LEFT JOIN (
-						SELECT ps.period_id, MIN(s.season_number) as season_number
-						FROM period_seasons ps
-						JOIN seasons s ON ps.season_id = s.id
-						GROUP BY ps.period_id
-					) s_agg ON cr.period_id = s_agg.period_id
 					INNER JOIN best_team_runs btr ON cr.team_signature = btr.team_signature
 												 AND cr.duration = btr.best_duration
-					WHERE cr.dungeon_id = ? AND COALESCE(s_agg.season_number, 1) = ?
+					WHERE cr.dungeon_id = ? AND cr.season_id = ?
 					GROUP BY cr.team_signature
 					HAVING cr.id = MIN(cr.id)
 				)
@@ -1089,17 +1050,11 @@ func computeRegionalRankings(tx *sql.Tx) error {
 				cr.dungeon_id,
 				'regional' as ranking_type,
 				? as ranking_scope,
-				ROW_NUMBER() OVER (PARTITION BY cr.dungeon_id, COALESCE(s_agg.season_number, 
1) ORDER BY cr.duration ASC, cr.completed_timestamp ASC) as ranking,
-				COALESCE(s_agg.season_number, 1) as season_id,
+				ROW_NUMBER() OVER (PARTITION BY cr.dungeon_id, cr.season_id ORDER BY cr.durat
ion ASC, cr.completed_timestamp ASC) as ranking,
+				cr.season_id as season_id,
 				? as computed_at
 			FROM challenge_runs cr
 			INNER JOIN realms r ON cr.realm_id = r.id
-			LEFT JOIN (
-				SELECT ps.period_id, MIN(s.season_number) as season_number
-				FROM period_seasons ps
-				JOIN seasons s ON ps.season_id = s.id
-				GROUP BY ps.period_id
-			) s_agg ON cr.period_id = s_agg.period_id
 			WHERE r.region = ?
 		`, region, currentTime, region)
 
@@ -1167,15 +1122,9 @@ func computeRegionalRankings(tx *sql.Tx) error {
 
 		// Get all seasons for this region
 		seasonRows, err := tx.Query(`
-			SELECT DISTINCT COALESCE(s_agg.season_number, 1) as season_id
+			SELECT DISTINCT cr.season_id as season_id
 			FROM challenge_runs cr
 			INNER JOIN realms r ON cr.realm_id = r.id
-			LEFT JOIN (
-				SELECT ps.period_id, MIN(s.season_number) as season_number
-				FROM period_seasons ps
-				JOIN seasons s ON ps.season_id = s.id
-				GROUP BY ps.period_id
-			) s_agg ON cr.period_id = s_agg.period_id
 			WHERE r.region = ?
 		`, region)
 		if err != nil {
@@ -1203,13 +1152,7 @@ func computeRegionalRankings(tx *sql.Tx) error {
 							MIN(cr.duration) as best_duration
 						FROM challenge_runs cr
 						INNER JOIN realms r ON cr.realm_id = r.id
-						LEFT JOIN (
-							SELECT ps.period_id, MIN(s.season_number) as season_number
-							FROM period_seasons ps
-							JOIN seasons s ON ps.season_id = s.id
-							GROUP BY ps.period_id
-						) s_agg ON cr.period_id = s_agg.period_id
-						WHERE cr.dungeon_id = ? AND r.region = ? AND COALESCE(s_agg.season_number, 
1) = ?
+						WHERE cr.dungeon_id = ? AND r.region = ? AND cr.season_id = ?
 						GROUP BY cr.team_signature
 					),
 					filtered_runs AS (
@@ -1220,15 +1163,9 @@ func computeRegionalRankings(tx *sql.Tx) error {
 							ROW_NUMBER() OVER (ORDER BY cr.duration ASC, cr.completed_timestamp ASC) a
s filtered_rank
 						FROM challenge_runs cr
 						INNER JOIN realms r ON cr.realm_id = r.id
-						LEFT JOIN (
-							SELECT ps.period_id, MIN(s.season_number) as season_number
-							FROM period_seasons ps
-							JOIN seasons s ON ps.season_id = s.id
-							GROUP BY ps.period_id
-						) s_agg ON cr.period_id = s_agg.period_id
 						INNER JOIN best_team_runs btr ON cr.team_signature = btr.team_signature
 														AND cr.duration = btr.best_duration
-						WHERE cr.dungeon_id = ? AND r.region = ? AND COALESCE(s_agg.season_number, 
1) = ?
+						WHERE cr.dungeon_id = ? AND r.region = ? AND cr.season_id = ?
 						GROUP BY cr.team_signature
 						HAVING cr.id = MIN(cr.id)
 					)
@@ -1348,20 +1285,14 @@ func computeRealmRankings(tx *sql.Tx) error {
 				'realm' as ranking_type,
 				? as ranking_scope,
 				ROW_NUMBER() OVER (
-					PARTITION BY cr.dungeon_id, COALESCE(s_agg.season_number, 1)
+					PARTITION BY cr.dungeon_id, cr.season_id
 					ORDER BY cr.duration ASC, cr.completed_timestamp ASC
 				) as ranking,
-				COALESCE(s_agg.season_number, 1) as season_id,
+				cr.season_id as season_id,
 				? as computed_at
 			FROM challenge_runs cr
 			INNER JOIN realms r ON cr.realm_id = r.id
 			LEFT JOIN realms parent_r ON r.parent_realm_slug = parent_r.slug AND r.region 
= parent_r.region
-			LEFT JOIN (
-				SELECT ps.period_id, MIN(s.season_number) as season_number
-				FROM period_seasons ps
-				JOIN seasons s ON ps.season_id = s.id
-				GROUP BY ps.period_id
-			) s_agg ON cr.period_id = s_agg.period_id
 			WHERE r.region = ? AND COALESCE(parent_r.slug, r.slug) = ?
 		`, pool.PoolSlug, currentTime, pool.Region, pool.PoolSlug)
 
@@ -1430,16 +1361,10 @@ func computeRealmRankings(tx *sql.Tx) error {
 	for _, pool := range pools {
 		// Get seasons for this pool
 		seasonRows, err := tx.Query(`
-			SELECT DISTINCT COALESCE(s_agg.season_number, 1) as season_id
+			SELECT DISTINCT cr.season_id as season_id
 			FROM challenge_runs cr
 			INNER JOIN realms r ON cr.realm_id = r.id
 			LEFT JOIN realms parent_r ON r.parent_realm_slug = parent_r.slug AND r.region 
= parent_r.region
-			LEFT JOIN (
-				SELECT ps.period_id, MIN(s.season_number) as season_number
-				FROM period_seasons ps
-				JOIN seasons s ON ps.season_id = s.id
-				GROUP BY ps.period_id
-			) s_agg ON cr.period_id = s_agg.period_id
 			WHERE r.region = ? AND COALESCE(parent_r.slug, r.slug) = ?
 		`, pool.Region, pool.PoolSlug)
 		if err != nil {
@@ -1468,16 +1393,10 @@ func computeRealmRankings(tx *sql.Tx) error {
 						FROM challenge_runs cr
 						INNER JOIN realms r ON cr.realm_id = r.id
 						LEFT JOIN realms parent_r ON r.parent_realm_slug = parent_r.slug AND r.regi
on = parent_r.region
-						LEFT JOIN (
-							SELECT ps.period_id, MIN(s.season_number) as season_number
-							FROM period_seasons ps
-							JOIN seasons s ON ps.season_id = s.id
-							GROUP BY ps.period_id
-						) s_agg ON cr.period_id = s_agg.period_id
 						WHERE cr.dungeon_id = ?
 							AND r.region = ?
 							AND COALESCE(parent_r.slug, r.slug) = ?
-							AND COALESCE(s_agg.season_number, 1) = ?
+							AND cr.season_id = ?
 						GROUP BY cr.team_signature
 					),
 					filtered_runs AS (
@@ -1489,18 +1408,12 @@ func computeRealmRankings(tx *sql.Tx) error {
 						FROM challenge_runs cr
 						INNER JOIN realms r ON cr.realm_id = r.id
 						LEFT JOIN realms parent_r ON r.parent_realm_slug = parent_r.slug AND r.regi
on = parent_r.region
-						LEFT JOIN (
-							SELECT ps.period_id, MIN(s.season_number) as season_number
-							FROM period_seasons ps
-							JOIN seasons s ON ps.season_id = s.id
-							GROUP BY ps.period_id
-						) s_agg ON cr.period_id = s_agg.period_id
 						INNER JOIN best_team_runs btr ON cr.team_signature = btr.team_signature
 														AND cr.duration = btr.best_duration
 						WHERE cr.dungeon_id = ?
 							AND r.region = ?
 							AND COALESCE(parent_r.slug, r.slug) = ?
-							AND COALESCE(s_agg.season_number, 1) = ?
+							AND cr.season_id = ?
 						GROUP BY cr.team_signature
 						HAVING cr.id = MIN(cr.id)
 					)

diff --git a/nix/pkgs/ookstats/src/internal/
pipeline/process.go b/nix/pkgs/ookstats/src/
internal/pipeline/process.go
index f9cd01f..89a7455 100644
--- a/nix/pkgs/ookstats/src/internal/pipelin
e/process.go
+++ b/nix/pkgs/ookstats/src/internal/pipelin
e/process.go
@@ -1,5 +1,8 @@
 package pipeline
 
+// season assignment has been migrated to u
se cr.season_id directly instead of period_s
easons lookups.
+// all queries now use timestamp-based seas
on assignment from the challenge_runs table.
+
 import (
 	"database/sql"
 	"fmt"
@@ -171,7 +174,7 @@ func createPlayerAggrega
tions(tx *sql.Tx) (int, error) {
 			cr.dungeon_id,
 			cr.id as run_id,
 			cr.duration,
-			COALESCE(s_agg.season_number, 1) as seas
on_id,
+			cr.season_id,
 			cr.completed_timestamp,
 			rr_gf.ranking as global_ranking_filtered
,
 			rr_rf.ranking as regional_ranking_filter
ed,
@@ -181,41 +184,29 @@ func createPlayerAggre
gations(tx *sql.Tx) (int, error) {
 			rr_lf.percentile_bracket as realm_percen
tile_bracket
 		FROM run_members rm
 		INNER JOIN challenge_runs cr ON rm.run_id
 = cr.id
-		LEFT JOIN (
-			SELECT ps.period_id, MIN(s.season_number
) as season_number
-			FROM period_seasons ps
-			JOIN seasons s ON ps.season_id = s.id
-			GROUP BY ps.period_id
-		) s_agg ON cr.period_id = s_agg.period_id
 		INNER JOIN (
 			SELECT
 				rm2.player_id,
 				cr2.dungeon_id,
-				COALESCE(s2_agg.season_number, 1) as se
ason_id,
+				cr2.season_id,
 				MIN(cr2.duration) as best_duration
 			FROM run_members rm2
 			INNER JOIN challenge_runs cr2 ON rm2.run
_id = cr2.id
-			LEFT JOIN (
-				SELECT ps.period_id, MIN(s.season_numbe
r) as season_number
-				FROM period_seasons ps
-				JOIN seasons s ON ps.season_id = s.id
-				GROUP BY ps.period_id
-			) s2_agg ON cr2.period_id = s2_agg.perio
d_id
-			GROUP BY rm2.player_id, cr2.dungeon_id, 
COALESCE(s2_agg.season_number, 1)
+			GROUP BY rm2.player_id, cr2.dungeon_id, 
cr2.season_id
 		) best_times ON rm.player_id = best_times
.player_id
 					 AND cr.dungeon_id = best_times.dungeo
n_id
-					 AND COALESCE(s_agg.season_number, 1) 
= best_times.season_id
+					 AND cr.season_id = best_times.season_
id
 					 AND cr.duration = best_times.best_dur
ation
 		LEFT JOIN run_rankings rr_gf ON cr.id = r
r_gf.run_id
 			AND rr_gf.ranking_type = 'global' AND rr
_gf.ranking_scope = 'filtered'
-			AND rr_gf.season_id = COALESCE(s_agg.sea
son_number, 1)
+			AND rr_gf.season_id = cr.season_id
 		LEFT JOIN run_rankings rr_rf ON cr.id = r
r_rf.run_id
 			AND rr_rf.ranking_type = 'regional' AND 
rr_rf.ranking_scope = 'filtered'
-			AND rr_rf.season_id = COALESCE(s_agg.sea
son_number, 1)
+			AND rr_rf.season_id = cr.season_id
 		LEFT JOIN run_rankings rr_lf ON cr.id = r
r_lf.run_id
 			AND rr_lf.ranking_type = 'realm' AND rr_
lf.ranking_scope = 'filtered'
-			AND rr_lf.season_id = COALESCE(s_agg.sea
son_number, 1)
-		GROUP BY rm.player_id, cr.dungeon_id, COA
LESCE(s_agg.season_number, 1)
+			AND rr_lf.season_id = cr.season_id
+		GROUP BY rm.player_id, cr.dungeon_id, cr.
season_id
 		HAVING cr.id = MIN(cr.id)
 	`)
 	if err != nil {
@@ -250,16 +241,10 @@ func createPlayerAggre
gations(tx *sql.Tx) (int, error) {
 		FROM players p
 		INNER JOIN player_best_runs pbr ON p.id =
 pbr.player_id
 		INNER JOIN (
-			SELECT rm.player_id, COALESCE(s_agg.seas
on_number, 1) as season_id, COUNT(*) as run_
count
+			SELECT rm.player_id, cr.season_id, COUNT
(*) as run_count
 			FROM run_members rm
 			INNER JOIN challenge_runs cr ON rm.run_i
d = cr.id
-			LEFT JOIN (
-				SELECT ps.period_id, MIN(s.season_numbe
r) as season_number
-				FROM period_seasons ps
-				JOIN seasons s ON ps.season_id = s.id
-				GROUP BY ps.period_id
-			) s_agg ON cr.period_id = s_agg.period_i
d
-			GROUP BY rm.player_id, COALESCE(s_agg.se
ason_number, 1)
+			GROUP BY rm.player_id, cr.season_id
 		) season_runs ON p.id = season_runs.playe
r_id AND pbr.season_id = season_runs.season_
id
 		GROUP BY p.id, pbr.season_id, p.name, p.r
ealm_id, season_runs.run_count
 	`, currentTime)
@@ -855,16 +840,10 @@ func computeGlobalRank
ings(tx *sql.Tx) error {
 			cr.dungeon_id,
 			'global' as ranking_type,
 			'all' as ranking_scope,
-			ROW_NUMBER() OVER (PARTITION BY cr.dunge
on_id, COALESCE(s_agg.season_number, 1) ORDE
R BY cr.duration ASC, cr.completed_timestamp
 ASC) as ranking,
-			COALESCE(s_agg.season_number, 1) as seas
on_id,
+			ROW_NUMBER() OVER (PARTITION BY cr.dunge
on_id, cr.season_id ORDER BY cr.duration ASC
, cr.completed_timestamp ASC) as ranking,
+			cr.season_id as season_id,
 			? as computed_at
 		FROM challenge_runs cr
-		LEFT JOIN (
-			SELECT ps.period_id, MIN(s.season_number
) as season_number
-			FROM period_seasons ps
-			JOIN seasons s ON ps.season_id = s.id
-			GROUP BY ps.period_id
-		) s_agg ON cr.period_id = s_agg.period_id
 	`, currentTime)
 	if err != nil {
 		return err
@@ -930,14 +909,8 @@ func computeGlobalRanki
ngs(tx *sql.Tx) error {
 
 	// Get all seasons (including fallback sea
son 1 for unmapped periods)
 	seasonRows, err := tx.Query(`
-		SELECT DISTINCT COALESCE(s_agg.season_num
ber, 1) as season_id
+		SELECT DISTINCT cr.season_id as season_id
 		FROM challenge_runs cr
-		LEFT JOIN (
-			SELECT ps.period_id, MIN(s.season_number
) as season_number
-			FROM period_seasons ps
-			JOIN seasons s ON ps.season_id = s.id
-			GROUP BY ps.period_id
-		) s_agg ON cr.period_id = s_agg.period_id
 	`)
 	if err != nil {
 		return err
@@ -962,13 +935,7 @@ func computeGlobalRanki
ngs(tx *sql.Tx) error {
 						cr.team_signature,
 						MIN(cr.duration) as best_duration
 					FROM challenge_runs cr
-					LEFT JOIN (
-						SELECT ps.period_id, MIN(s.season_num
ber) as season_number
-						FROM period_seasons ps
-						JOIN seasons s ON ps.season_id = s.id
-						GROUP BY ps.period_id
-					) s_agg ON cr.period_id = s_agg.period
_id
-					WHERE cr.dungeon_id = ? AND COALESCE(s
_agg.season_number, 1) = ?
+					WHERE cr.dungeon_id = ? AND cr.season_
id = ?
 					GROUP BY cr.team_signature
 				),
 				filtered_runs AS (
@@ -978,15 +945,9 @@ func computeGlobalRanki
ngs(tx *sql.Tx) error {
 						cr.completed_timestamp,
 						ROW_NUMBER() OVER (ORDER BY cr.durati
on ASC, cr.completed_timestamp ASC) as filte
red_rank
 					FROM challenge_runs cr
-					LEFT JOIN (
-						SELECT ps.period_id, MIN(s.season_num
ber) as season_number
-						FROM period_seasons ps
-						JOIN seasons s ON ps.season_id = s.id
-						GROUP BY ps.period_id
-					) s_agg ON cr.period_id = s_agg.period
_id
 					INNER JOIN best_team_runs btr ON cr.te
am_signature = btr.team_signature
 												 AND cr.duration = btr.best_dur
ation
-					WHERE cr.dungeon_id = ? AND COALESCE(s
_agg.season_number, 1) = ?
+					WHERE cr.dungeon_id = ? AND cr.season_
id = ?
 					GROUP BY cr.team_signature
 					HAVING cr.id = MIN(cr.id)
 				)
@@ -1089,17 +1050,11 @@ func computeRegional
Rankings(tx *sql.Tx) error {
 				cr.dungeon_id,
 				'regional' as ranking_type,
 				? as ranking_scope,
-				ROW_NUMBER() OVER (PARTITION BY cr.dung
eon_id, COALESCE(s_agg.season_number, 1) ORD
ER BY cr.duration ASC, cr.completed_timestam
p ASC) as ranking,
-				COALESCE(s_agg.season_number, 1) as sea
son_id,
+				ROW_NUMBER() OVER (PARTITION BY cr.dung
eon_id, cr.season_id ORDER BY cr.duration AS
C, cr.completed_timestamp ASC) as ranking,
+				cr.season_id as season_id,
 				? as computed_at
 			FROM challenge_runs cr
 			INNER JOIN realms r ON cr.realm_id = r.i
d
-			LEFT JOIN (
-				SELECT ps.period_id, MIN(s.season_numbe
r) as season_number
-				FROM period_seasons ps
-				JOIN seasons s ON ps.season_id = s.id
-				GROUP BY ps.period_id
-			) s_agg ON cr.period_id = s_agg.period_i
d
 			WHERE r.region = ?
 		`, region, currentTime, region)
 
@@ -1167,15 +1122,9 @@ func computeRegionalR
ankings(tx *sql.Tx) error {
 
 		// Get all seasons for this region
 		seasonRows, err := tx.Query(`
-			SELECT DISTINCT COALESCE(s_agg.season_nu
mber, 1) as season_id
+			SELECT DISTINCT cr.season_id as season_i
d
 			FROM challenge_runs cr
 			INNER JOIN realms r ON cr.realm_id = r.i
d
-			LEFT JOIN (
-				SELECT ps.period_id, MIN(s.season_numbe
r) as season_number
-				FROM period_seasons ps
-				JOIN seasons s ON ps.season_id = s.id
-				GROUP BY ps.period_id
-			) s_agg ON cr.period_id = s_agg.period_i
d
 			WHERE r.region = ?
 		`, region)
 		if err != nil {
@@ -1203,13 +1152,7 @@ func computeRegionalR
ankings(tx *sql.Tx) error {
 							MIN(cr.duration) as best_duration
 						FROM challenge_runs cr
 						INNER JOIN realms r ON cr.realm_id = 
r.id
-						LEFT JOIN (
-							SELECT ps.period_id, MIN(s.season_nu
mber) as season_number
-							FROM period_seasons ps
-							JOIN seasons s ON ps.season_id = s.i
d
-							GROUP BY ps.period_id
-						) s_agg ON cr.period_id = s_agg.perio
d_id
-						WHERE cr.dungeon_id = ? AND r.region 
= ? AND COALESCE(s_agg.season_number, 1) = ?
+						WHERE cr.dungeon_id = ? AND r.region 
= ? AND cr.season_id = ?
 						GROUP BY cr.team_signature
 					),
 					filtered_runs AS (
@@ -1220,15 +1163,9 @@ func computeRegionalR
ankings(tx *sql.Tx) error {
 							ROW_NUMBER() OVER (ORDER BY cr.durat
ion ASC, cr.completed_timestamp ASC) as filt
ered_rank
 						FROM challenge_runs cr
 						INNER JOIN realms r ON cr.realm_id = 
r.id
-						LEFT JOIN (
-							SELECT ps.period_id, MIN(s.season_nu
mber) as season_number
-							FROM period_seasons ps
-							JOIN seasons s ON ps.season_id = s.i
d
-							GROUP BY ps.period_id
-						) s_agg ON cr.period_id = s_agg.perio
d_id
 						INNER JOIN best_team_runs btr ON cr.t
eam_signature = btr.team_signature
 														AND cr.duration = btr.best_du
ration
-						WHERE cr.dungeon_id = ? AND r.region 
= ? AND COALESCE(s_agg.season_number, 1) = ?
+						WHERE cr.dungeon_id = ? AND r.region 
= ? AND cr.season_id = ?
 						GROUP BY cr.team_signature
 						HAVING cr.id = MIN(cr.id)
 					)
@@ -1348,20 +1285,14 @@ func computeRealmRan
kings(tx *sql.Tx) error {
 				'realm' as ranking_type,
 				? as ranking_scope,
 				ROW_NUMBER() OVER (
-					PARTITION BY cr.dungeon_id, COALESCE(s
_agg.season_number, 1)
+					PARTITION BY cr.dungeon_id, cr.season_
id
 					ORDER BY cr.duration ASC, cr.completed
_timestamp ASC
 				) as ranking,
-				COALESCE(s_agg.season_number, 1) as sea
son_id,
+				cr.season_id as season_id,
 				? as computed_at
 			FROM challenge_runs cr
 			INNER JOIN realms r ON cr.realm_id = r.i
d
 			LEFT JOIN realms parent_r ON r.parent_re
alm_slug = parent_r.slug AND r.region = pare
nt_r.region
-			LEFT JOIN (
-				SELECT ps.period_id, MIN(s.season_numbe
r) as season_number
-				FROM period_seasons ps
-				JOIN seasons s ON ps.season_id = s.id
-				GROUP BY ps.period_id
-			) s_agg ON cr.period_id = s_agg.period_i
d
 			WHERE r.region = ? AND COALESCE(parent_r
.slug, r.slug) = ?
 		`, pool.PoolSlug, currentTime, pool.Regio
n, pool.PoolSlug)
 
@@ -1430,16 +1361,10 @@ func computeRealmRan
kings(tx *sql.Tx) error {
 	for _, pool := range pools {
 		// Get seasons for this pool
 		seasonRows, err := tx.Query(`
-			SELECT DISTINCT COALESCE(s_agg.season_nu
mber, 1) as season_id
+			SELECT DISTINCT cr.season_id as season_i
d
 			FROM challenge_runs cr
 			INNER JOIN realms r ON cr.realm_id = r.i
d
 			LEFT JOIN realms parent_r ON r.parent_re
alm_slug = parent_r.slug AND r.region = pare
nt_r.region
-			LEFT JOIN (
-				SELECT ps.period_id, MIN(s.season_numbe
r) as season_number
-				FROM period_seasons ps
-				JOIN seasons s ON ps.season_id = s.id
-				GROUP BY ps.period_id
-			) s_agg ON cr.period_id = s_agg.period_i
d
 			WHERE r.region = ? AND COALESCE(parent_r
.slug, r.slug) = ?
 		`, pool.Region, pool.PoolSlug)
 		if err != nil {
@@ -1468,16 +1393,10 @@ func computeRealmRan
kings(tx *sql.Tx) error {
 						FROM challenge_runs cr
 						INNER JOIN realms r ON cr.realm_id = 
r.id
 						LEFT JOIN realms parent_r ON r.parent
_realm_slug = parent_r.slug AND r.region = p
arent_r.region
-						LEFT JOIN (
-							SELECT ps.period_id, MIN(s.season_nu
mber) as season_number
-							FROM period_seasons ps
-							JOIN seasons s ON ps.season_id = s.i
d
-							GROUP BY ps.period_id
-						) s_agg ON cr.period_id = s_agg.perio
d_id
 						WHERE cr.dungeon_id = ?
 							AND r.region = ?
 							AND COALESCE(parent_r.slug, r.slug) 
= ?
-							AND COALESCE(s_agg.season_number, 1)
 = ?
+							AND cr.season_id = ?
 						GROUP BY cr.team_signature
 					),
 					filtered_runs AS (
@@ -1489,18 +1408,12 @@ func computeRealmRan
kings(tx *sql.Tx) error {
 						FROM challenge_runs cr
 						INNER JOIN realms r ON cr.realm_id = 
r.id
 						LEFT JOIN realms parent_r ON r.parent
_realm_slug = parent_r.slug AND r.region = p
arent_r.region
-						LEFT JOIN (
-							SELECT ps.period_id, MIN(s.season_nu
mber) as season_number
-							FROM period_seasons ps
-							JOIN seasons s ON ps.season_id = s.i
d
-							GROUP BY ps.period_id
-						) s_agg ON cr.period_id = s_agg.perio
d_id
 						INNER JOIN best_team_runs btr ON cr.t
eam_signature = btr.team_signature
 														AND cr.duration = btr.best_du
ration
 						WHERE cr.dungeon_id = ?
 							AND r.region = ?
 							AND COALESCE(parent_r.slug, r.slug) 
= ?
-							AND COALESCE(s_agg.season_number, 1)
 = ?
+							AND cr.season_id = ?
 						GROUP BY cr.team_signature
 						HAVING cr.id = MIN(cr.id)
 					)
 

diff --git a/web/src/components/Leaderboard/LeaderboardScopeFilter/LeaderboardScop
eFilter.astro b/web/src/components/Leaderboard/LeaderboardScopeFilter/LeaderboardS
copeFilter.astro
index f1877ea..a8eecff 100644
--- a/web/src/components/Leaderboard/LeaderboardScopeFilter/LeaderboardScopeFilter
.astro
+++ b/web/src/components/Leaderboard/LeaderboardScopeFilter/LeaderboardScopeFilter
.astro
@@ -21,6 +21,25 @@ const {
   currentClass = "all",
 } = Astro.props;
 
+// Fetch available seasons from API
+let seasonOptions: { id: number; name: string }[] = [];
+try {
+  const response = await fetch(
+    `${Astro.url.origin}/api/leaderboard/season/index.json`,
+  );
+  if (response.ok) {
+    const data = await response.json();
+    seasonOptions = data.data.map((season: any) => ({
+      id: season.id,
+      name: season.name || `Season ${season.id}`,
+    }));
+  }
+} catch (error) {
+  console.error("Failed to fetch seasons:", error);
+  // Fallback to current season if API fails
+  seasonOptions = [{ id: currentSeason, name: `Season ${currentSeason}` }];
+}
+
 // Scope options: Global + individual regions
 const scopeOptions = [
   { value: "global", label: "Global" },
@@ -82,7 +101,20 @@ function buildUrl(region: string, realm: string = ""): string 
{
   <div class="filter-group">
     <label for="season-filter">Season</label>
     <select id="season-filter" data-param="season">
-      <option value="1" selected={currentSeason === 1}>Season 1</option>
+      {
+        seasonOptions.map((season) => (
+          <option
+            value={season.id}
+            selected={season.id === currentSeason}
+            data-url={buildUrl(currentRegion, currentRealm).replace(
+              `season${currentSeason}`,
+              `season${season.id}`,
+            )}
+          >
+            {season.name}
+          </option>
+        ))
+      }
     </select>
   </div>
 
@@ -127,6 +159,9 @@ function buildUrl(region: string, realm: string = ""): string 
{
 </div>
 
 <script>
+  const seasonSelect = document.getElementById(
+    "season-filter",
+  ) as HTMLSelectElement;
   const scopeSelect = document.getElementById(
     "scope-filter",
   ) as HTMLSelectElement;
@@ -134,6 +169,18 @@ function buildUrl(region: string, realm: string = ""): string
 {
     "realm-filter",
   ) as HTMLSelectElement | null;
 
+  // Handle season change - just read the URL from the data attribute
+  if (seasonSelect) {
+    seasonSelect.addEventListener("change", (e) => {
+      const target = e.target as HTMLSelectElement;
+      const selectedOption = target.options[target.selectedIndex];
+      const url = selectedOption.dataset.url;
+      if (url) {
+        window.location.href = url;
+      }
+    });
+  }
+
   // Handle scope change - just read the URL from the data attribute
   if (scopeSelect) {
     scopeSelect.addEventListener("change", (e) => {
@@ -157,6 +204,4 @@ function buildUrl(region: string, realm: string = ""): string 
{
       }
     });
   }
-
-  // TODO: Handle season change (for now, only season 1 exists)
 </script>

diff --git a/web/src/components/Leaderboard/
LeaderboardScopeFilter/LeaderboardScopeFilte
r.astro b/web/src/components/Leaderboard/Lea
derboardScopeFilter/LeaderboardScopeFilter.a
stro
index f1877ea..a8eecff 100644
--- a/web/src/components/Leaderboard/Leaderb
oardScopeFilter/LeaderboardScopeFilter.astro
+++ b/web/src/components/Leaderboard/Leaderb
oardScopeFilter/LeaderboardScopeFilter.astro
@@ -21,6 +21,25 @@ const {
   currentClass = "all",
 } = Astro.props;
 
+// Fetch available seasons from API
+let seasonOptions: { id: number; name: stri
ng }[] = [];
+try {
+  const response = await fetch(
+    `${Astro.url.origin}/api/leaderboard/se
ason/index.json`,
+  );
+  if (response.ok) {
+    const data = await response.json();
+    seasonOptions = data.data.map((season: 
any) => ({
+      id: season.id,
+      name: season.name || `Season ${season
.id}`,
+    }));
+  }
+} catch (error) {
+  console.error("Failed to fetch seasons:",
 error);
+  // Fallback to current season if API fail
s
+  seasonOptions = [{ id: currentSeason, nam
e: `Season ${currentSeason}` }];
+}
+
 // Scope options: Global + individual regio
ns
 const scopeOptions = [
   { value: "global", label: "Global" },
@@ -82,7 +101,20 @@ function buildUrl(region
: string, realm: string = ""): string {
   <div class="filter-group">
     <label for="season-filter">Season</labe
l>
     <select id="season-filter" data-param="
season">
-      <option value="1" selected={currentSe
ason === 1}>Season 1</option>
+      {
+        seasonOptions.map((season) => (
+          <option
+            value={season.id}
+            selected={season.id === current
Season}
+            data-url={buildUrl(currentRegio
n, currentRealm).replace(
+              `season${currentSeason}`,
+              `season${season.id}`,
+            )}
+          >
+            {season.name}
+          </option>
+        ))
+      }
     </select>
   </div>
 
@@ -127,6 +159,9 @@ function buildUrl(region
: string, realm: string = ""): string {
 </div>
 
 <script>
+  const seasonSelect = document.getElementB
yId(
+    "season-filter",
+  ) as HTMLSelectElement;
   const scopeSelect = document.getElementBy
Id(
     "scope-filter",
   ) as HTMLSelectElement;
@@ -134,6 +169,18 @@ function buildUrl(regio
n: string, realm: string = ""): string {
     "realm-filter",
   ) as HTMLSelectElement | null;
 
+  // Handle season change - just read the U
RL from the data attribute
+  if (seasonSelect) {
+    seasonSelect.addEventListener("change",
 (e) => {
+      const target = e.target as HTMLSelect
Element;
+      const selectedOption = target.options
[target.selectedIndex];
+      const url = selectedOption.dataset.ur
l;
+      if (url) {
+        window.location.href = url;
+      }
+    });
+  }
+
   // Handle scope change - just read the UR
L from the data attribute
   if (scopeSelect) {
     scopeSelect.addEventListener("change", 
(e) => {
@@ -157,6 +204,4 @@ function buildUrl(region
: string, realm: string = ""): string {
       }
     });
   }
-
-  // TODO: Handle season change (for now, o
nly season 1 exists)
 </script>
 

diff --git a/web/src/pages/challenge-mode/index.astro b/web/src/pages/challenge-mo
de/index.astro
index ee71b51..8b7409e 100644
--- a/web/src/pages/challenge-mode/index.astro
+++ b/web/src/pages/challenge-mode/index.astro
@@ -1,6 +1,27 @@
 ---
-// redirect to default dungeon leaderboard
+// fetch current season and redirect to default dungeon leaderboard
+let currentSeasonId = 1; // fallback
+try {
+  const response = await fetch(
+    `${Astro.url.origin}/api/leaderboard/season/index.json`,
+  );
+  if (response.ok) {
+    const data = await response.json();
+    // find the most recent current season (highest ID with is_current: true)
+    const currentSeasons = data.data.filter((s: any) => s.is_current);
+    const currentSeason =
+      currentSeasons.length > 0
+        ? currentSeasons[currentSeasons.length - 1] // last current season (highe
st ID)
+        : data.data[data.data.length - 1]; // or just the last season
+    if (currentSeason) {
+      currentSeasonId = currentSeason.id;
+    }
+  }
+} catch (error) {
+  console.error("Failed to fetch current season:", error);
+}
+
 return Astro.redirect(
-  "/challenge-mode/season1/global/temple-of-the-jade-serpent",
+  `/challenge-mode/season${currentSeasonId}/global/temple-of-the-jade-serpent`,
 );
 ---

diff --git a/web/src/pages/challenge-mode/in
dex.astro b/web/src/pages/challenge-mode/ind
ex.astro
index ee71b51..8b7409e 100644
--- a/web/src/pages/challenge-mode/index.ast
ro
+++ b/web/src/pages/challenge-mode/index.ast
ro
@@ -1,6 +1,27 @@
 ---
-// redirect to default dungeon leaderboard
+// fetch current season and redirect to def
ault dungeon leaderboard
+let currentSeasonId = 1; // fallback
+try {
+  const response = await fetch(
+    `${Astro.url.origin}/api/leaderboard/se
ason/index.json`,
+  );
+  if (response.ok) {
+    const data = await response.json();
+    // find the most recent current season 
(highest ID with is_current: true)
+    const currentSeasons = data.data.filter
((s: any) => s.is_current);
+    const currentSeason =
+      currentSeasons.length > 0
+        ? currentSeasons[currentSeasons.len
gth - 1] // last current season (highest ID)
+        : data.data[data.data.length - 1]; 
// or just the last season
+    if (currentSeason) {
+      currentSeasonId = currentSeason.id;
+    }
+  }
+} catch (error) {
+  console.error("Failed to fetch current se
ason:", error);
+}
+
 return Astro.redirect(
-  "/challenge-mode/season1/global/temple-of
-the-jade-serpent",
+  `/challenge-mode/season${currentSeasonId}
/global/temple-of-the-jade-serpent`,
 );
 ---
 
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET