OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
HASH      47b99f55dc19
DATE      2026-05-03
SUBJECT   ookstats: add home page json generator
FILES     3 CHANGED
HASH      47b99f55dc19
DATE      2026-05-03
SUBJECT   ookstats: add home page json generator
FILES     3 CHANGED
 

diff --git a/nix/pkgs/ookstats/src/cmd/build.go b/nix/pkgs/ookstats/src/cmd/build.
go
index 381476d..f00f009 100644
--- a/nix/pkgs/ookstats/src/cmd/build.go
+++ b/nix/pkgs/ookstats/src/cmd/build.go
@@ -194,6 +194,12 @@ var buildCmd = &cobra.Command{
 			return err
 		}
 
+		// 9a) Generate home page JSON
+		log.Info("generating home page JSON")
+		if err := generator.GenerateHome(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 381476d..f00f009 100644
--- a/nix/pkgs/ookstats/src/cmd/build.go
+++ b/nix/pkgs/ookstats/src/cmd/build.go
@@ -194,6 +194,12 @@ var buildCmd = &cobra.C
ommand{
 			return err
 		}
 
+		// 9a) Generate home page JSON
+		log.Info("generating home page JSON")
+		if err := generator.GenerateHome(db, norm
alizedOut); 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 179359f..d35f6c5 100644
--- a/nix/pkgs/ookstats/src/cmd/generate.go
+++ b/nix/pkgs/ookstats/src/cmd/generate.go
@@ -90,6 +90,32 @@ var generateAPICmd = &cobra.Command{
 	},
 }
 
+var generateHomeCmd = &cobra.Command{
+	Use:   "home",
+	Short: "Generate home page JSON (top runs + top players + recent feed)",
+	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.GenerateHome(db, outDir); err != nil {
+			return err
+		}
+		fmt.Printf("\nHome JSON generated at %s/api/home.json\n", outDir)
+		return nil
+	},
+}
+
 func init() {
 	rootCmd.AddCommand(generateCmd)
 	generateCmd.AddCommand(generateAPICmd)
@@ -102,4 +128,7 @@ func init() {
 	generateAPICmd.Flags().Int("shard-size", 5000, "Search index shard size")
 	generateAPICmd.Flags().String("regions", "us,eu,kr,tw", "Regions to include for 
regional leaderboards")
 	generateAPICmd.Flags().Int("workers", 10, "Number of parallel workers for leader
board generation")
+
+	generateCmd.AddCommand(generateHomeCmd)
+	generateHomeCmd.Flags().String("out", "web/public", "Output directory (api/home.
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 179359f..d35f6c5 100644
--- a/nix/pkgs/ookstats/src/cmd/generate.go
+++ b/nix/pkgs/ookstats/src/cmd/generate.go
@@ -90,6 +90,32 @@ var generateAPICmd = &cob
ra.Command{
 	},
 }
 
+var generateHomeCmd = &cobra.Command{
+	Use:   "home",
+	Short: "Generate home page JSON (top runs 
+ top players + recent feed)",
+	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.GenerateHome(db, outD
ir); err != nil {
+			return err
+		}
+		fmt.Printf("\nHome JSON generated at %s/a
pi/home.json\n", outDir)
+		return nil
+	},
+}
+
 func init() {
 	rootCmd.AddCommand(generateCmd)
 	generateCmd.AddCommand(generateAPICmd)
@@ -102,4 +128,7 @@ func init() {
 	generateAPICmd.Flags().Int("shard-size", 5
000, "Search index shard size")
 	generateAPICmd.Flags().String("regions", "
us,eu,kr,tw", "Regions to include for region
al leaderboards")
 	generateAPICmd.Flags().Int("workers", 10, 
"Number of parallel workers for leaderboard 
generation")
+
+	generateCmd.AddCommand(generateHomeCmd)
+	generateHomeCmd.Flags().String("out", "web
/public", "Output directory (api/home.json w
ill be written under it)")
 }
 

diff --git a/nix/pkgs/ookstats/src/internal/generator/home.go b/nix/pkgs/ookstats/
src/internal/generator/home.go
new file mode 100644
index 0000000..ca2153a
--- /dev/null
+++ b/nix/pkgs/ookstats/src/internal/generator/home.go
@@ -0,0 +1,487 @@
+package generator
+
+import (
+	"database/sql"
+	"fmt"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"ookstats/internal/wow"
+	"ookstats/internal/writer"
+)
+
+// HomeJSON is the top-level structure for web/public/api/home.json.
+// Layout: a cross-season recent-top-runs feed, plus per-season blocks
+// containing top-3 dungeon records and top-10 player lists per scope.
+type HomeJSON struct {
+	GeneratedAt   int64                     `json:"generated_at"`
+	RecentTopRuns []HomeRunEntry            `json:"recent_top_runs"`
+	Seasons       map[string]HomeSeasonJSON `json:"seasons"`
+}
+
+// HomeSeasonJSON holds per-season top-3 runs and top-10 player lists.
+type HomeSeasonJSON struct {
+	SeasonID          int                       `json:"season_id"`
+	SeasonName        string                    `json:"season_name,omitempty"`
+	TopRunsPerDungeon map[string][]HomeRunEntry `json:"top_runs_per_dungeon"`
+	TopPlayers        HomePlayerLists           `json:"top_players"`
+}
+
+// HomePlayerLists groups the top-10 player lists by scope.
+type HomePlayerLists struct {
+	Global []HomePlayerEntry `json:"global"`
+	US     []HomePlayerEntry `json:"us"`
+	EU     []HomePlayerEntry `json:"eu"`
+	KR     []HomePlayerEntry `json:"kr"`
+	TW     []HomePlayerEntry `json:"tw"`
+}
+
+// HomeRunEntry is one top run, used both for per-dungeon top-3 and the recent fe
ed.
+type HomeRunEntry struct {
+	Rank               int              `json:"rank,omitempty"`
+	Bracket            string           `json:"bracket,omitempty"`
+	RunID              int64            `json:"run_id"`
+	DungeonID          int              `json:"dungeon_id"`
+	DungeonName        string           `json:"dungeon_name"`
+	DungeonSlug        string           `json:"dungeon_slug"`
+	DurationMs         int64            `json:"duration_ms"`
+	CompletedTimestamp int64            `json:"completed_timestamp"`
+	SeasonID           int              `json:"season_id,omitempty"`
+	Rankings           *HomeRunRankings `json:"rankings,omitempty"`
+	TeamMembers        []HomeRunMember  `json:"team_members"`
+}
+
+// HomeRunRankings carries the rank-bucket(s) a run qualified for.
+// Only populated on entries in RecentTopRuns. Currently global-only by design.
+type HomeRunRankings struct {
+	Global int `json:"global"`
+}
+
+// HomeRunMember is one of the 5 players in a run.
+// Field shapes mirror the canonical web/src/lib/types.ts TeamMember (id, name, s
pec_id, etc.)
+// so the TeamComposition component can consume these without adapter mapping.
+type HomeRunMember struct {
+	ID        int64  `json:"id"`
+	Name      string `json:"name"`
+	RealmSlug string `json:"realm_slug"`
+	Region    string `json:"region"`
+	ClassName string `json:"class_name,omitempty"`
+	SpecID    int    `json:"spec_id,omitempty"`
+	SpecName  string `json:"spec_name,omitempty"`
+	Faction   string `json:"faction,omitempty"`
+}
+
+// HomePlayerEntry is one player in a top-10 list.
+type HomePlayerEntry struct {
+	Rank                   int    `json:"rank"`
+	PlayerID               int64  `json:"player_id"`
+	Name                   string `json:"name"`
+	RealmSlug              string `json:"realm_slug"`
+	RealmName              string `json:"realm_name,omitempty"`
+	Region                 string `json:"region"`
+	ClassName              string `json:"class_name,omitempty"`
+	ActiveSpecID           int    `json:"active_spec_id,omitempty"`
+	ActiveSpecName         string `json:"active_spec_name,omitempty"`
+	CombinedBestTimeMs     int64  `json:"combined_best_time_ms"`
+	GlobalRanking          int    `json:"global_ranking,omitempty"`
+	GlobalRankingBracket   string `json:"global_ranking_bracket,omitempty"`
+	RegionalRanking        int    `json:"regional_ranking,omitempty"`
+	RegionalRankingBracket string `json:"regional_ranking_bracket,omitempty"`
+	AvatarURL              string `json:"avatar_url,omitempty"`
+}
+
+// Seasons we surface on the home page. Mirrors run_rankings.season_id values.
+var homeSeasons = []int{1, 2}
+
+// regions for the regional top-player lists
+var homeRegions = []string{"us", "eu", "kr", "tw"}
+
+// GenerateHome writes outDir/api/home.json with the home page payload.
+func GenerateHome(db *sql.DB, outDir string) error {
+	home := HomeJSON{
+		GeneratedAt: time.Now().UnixMilli(),
+		Seasons:     make(map[string]HomeSeasonJSON),
+	}
+
+	for _, seasonID := range homeSeasons {
+		topRuns, err := loadTopRunsPerDungeon(db, seasonID, 3)
+		if err != nil {
+			return fmt.Errorf("top runs for season %d: %w", seasonID, err)
+		}
+
+		topPlayers, err := loadTopPlayers(db, seasonID, 10)
+		if err != nil {
+			return fmt.Errorf("top players for season %d: %w", seasonID, err)
+		}
+
+		seasonName, err := loadSeasonName(db, seasonID)
+		if err != nil {
+			return fmt.Errorf("season name for %d: %w", seasonID, err)
+		}
+
+		home.Seasons[fmt.Sprintf("%d", seasonID)] = HomeSeasonJSON{
+			SeasonID:          seasonID,
+			SeasonName:        seasonName,
+			TopRunsPerDungeon: topRuns,
+			TopPlayers:        topPlayers,
+		}
+	}
+
+	recent, err := loadRecentTopGlobalRuns(db, 10)
+	if err != nil {
+		return fmt.Errorf("recent top global runs: %w", err)
+	}
+	home.RecentTopRuns = recent
+
+	outPath := filepath.Join(outDir, "api", "home.json")
+	if err := writer.WriteJSONFileCompact(outPath, home); err != nil {
+		return fmt.Errorf("write home.json: %w", err)
+	}
+	return nil
+}
+
+// loadTopRunsPerDungeon returns up to topN team-filtered global runs per dungeon
 for the season,
+// keyed by dungeon slug.
+func loadTopRunsPerDungeon(db *sql.DB, seasonID, topN int) (map[string][]HomeRunE
ntry, error) {
+	rows, err := db.Query(`
+		SELECT
+			rr.run_id, rr.ranking, rr.percentile_bracket,
+			cr.dungeon_id, cr.duration, cr.completed_timestamp,
+			d.slug, d.name
+		FROM run_rankings rr
+		JOIN challenge_runs cr ON rr.run_id = cr.id
+		JOIN dungeons d ON cr.dungeon_id = d.id
+		WHERE rr.ranking_type = 'global'
+		  AND rr.ranking_scope = 'filtered'
+		  AND rr.season_id = ?
+		  AND rr.ranking <= ?
+		ORDER BY cr.dungeon_id, rr.ranking
+	`, seasonID, topN)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+
+	out := make(map[string][]HomeRunEntry)
+	var runIDs []int64
+	for rows.Next() {
+		var e HomeRunEntry
+		var bracket sql.NullString
+		if err := rows.Scan(&e.RunID, &e.Rank, &bracket, &e.DungeonID, &e.DurationMs, &
e.CompletedTimestamp, &e.DungeonSlug, &e.DungeonName); err != nil {
+			return nil, err
+		}
+		if bracket.Valid {
+			e.Bracket = bracket.String
+		}
+		out[e.DungeonSlug] = append(out[e.DungeonSlug], e)
+		runIDs = append(runIDs, e.RunID)
+	}
+	if err := rows.Err(); err != nil {
+		return nil, err
+	}
+
+	members, err := loadTeamMembersForRuns(db, runIDs)
+	if err != nil {
+		return nil, err
+	}
+	for slug, entries := range out {
+		for i := range entries {
+			entries[i].TeamMembers = members[entries[i].RunID]
+		}
+		out[slug] = entries
+	}
+	return out, nil
+}
+
+// loadRecentTopGlobalRuns returns up to limit recent runs that hit top-10 global
ly
+// (team-filtered) across either season, ordered newest-first by completion time.
+func loadRecentTopGlobalRuns(db *sql.DB, limit int) ([]HomeRunEntry, error) {
+	rows, err := db.Query(`
+		SELECT
+			rr.run_id, rr.ranking, rr.percentile_bracket, rr.season_id,
+			cr.dungeon_id, cr.duration, cr.completed_timestamp,
+			d.slug, d.name
+		FROM run_rankings rr
+		JOIN challenge_runs cr ON rr.run_id = cr.id
+		JOIN dungeons d ON cr.dungeon_id = d.id
+		WHERE rr.ranking_type = 'global'
+		  AND rr.ranking_scope = 'filtered'
+		  AND rr.ranking <= 10
+		ORDER BY cr.completed_timestamp DESC
+		LIMIT ?
+	`, limit)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+
+	var out []HomeRunEntry
+	var runIDs []int64
+	for rows.Next() {
+		var e HomeRunEntry
+		var globalRank int
+		var bracket sql.NullString
+		if err := rows.Scan(&e.RunID, &globalRank, &bracket, &e.SeasonID, &e.DungeonID,
 &e.DurationMs, &e.CompletedTimestamp, &e.DungeonSlug, &e.DungeonName); err != nil
 {
+			return nil, err
+		}
+		e.Rankings = &HomeRunRankings{Global: globalRank}
+		if bracket.Valid {
+			e.Bracket = bracket.String
+		}
+		out = append(out, e)
+		runIDs = append(runIDs, e.RunID)
+	}
+	if err := rows.Err(); err != nil {
+		return nil, err
+	}
+
+	members, err := loadTeamMembersForRuns(db, runIDs)
+	if err != nil {
+		return nil, err
+	}
+	for i := range out {
+		out[i].TeamMembers = members[out[i].RunID]
+	}
+	return out, nil
+}
+
+// loadTeamMembersForRuns fetches member metadata for the given run IDs in one ba
tched query,
+// returning a map keyed by run_id.
+func loadTeamMembersForRuns(db *sql.DB, runIDs []int64) (map[int64][]HomeRunMembe
r, error) {
+	out := make(map[int64][]HomeRunMember)
+	if len(runIDs) == 0 {
+		return out, nil
+	}
+	placeholders := make([]string, len(runIDs))
+	args := make([]any, len(runIDs))
+	for i, id := range runIDs {
+		placeholders[i] = "?"
+		args[i] = id
+	}
+
+	q := fmt.Sprintf(`
+		SELECT
+			rm.run_id, rm.spec_id, rm.faction,
+			p.id, p.name,
+			r.slug, r.region,
+			pd.class_name
+		FROM run_members rm
+		JOIN players p ON rm.player_id = p.id
+		JOIN realms r ON p.realm_id = r.id
+		LEFT JOIN player_details pd ON p.id = pd.player_id
+		WHERE rm.run_id IN (%s)
+	`, strings.Join(placeholders, ","))
+
+	rows, err := db.Query(q, args...)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+
+	for rows.Next() {
+		var runID int64
+		var specID sql.NullInt64
+		var faction sql.NullString
+		var className sql.NullString
+		var m HomeRunMember
+		if err := rows.Scan(&runID, &specID, &faction, &m.ID, &m.Name, &m.RealmSlug, &m
.Region, &className); err != nil {
+			return nil, err
+		}
+		if specID.Valid {
+			m.SpecID = int(specID.Int64)
+		}
+		if faction.Valid {
+			m.Faction = faction.String
+		}
+		if className.Valid {
+			m.ClassName = className.String
+		}
+		// Backfill class+spec name from spec_id if either is missing
+		if m.SpecID > 0 {
+			specPtr := m.SpecID
+			cls, spec := wow.FallbackClassAndSpec(m.ClassName, m.SpecName, &specPtr)
+			m.ClassName, m.SpecName = cls, spec
+		}
+		out[runID] = append(out[runID], m)
+	}
+	return out, rows.Err()
+}
+
+// loadTopPlayers returns the global top-N plus per-region top-N player lists for
 the season.
+func loadTopPlayers(db *sql.DB, seasonID, topN int) (HomePlayerLists, error) {
+	var lists HomePlayerLists
+	global, err := loadTopGlobalPlayers(db, seasonID, topN)
+	if err != nil {
+		return lists, fmt.Errorf("global: %w", err)
+	}
+	lists.Global = global
+
+	for _, region := range homeRegions {
+		regional, err := loadTopRegionalPlayers(db, seasonID, region, topN)
+		if err != nil {
+			return lists, fmt.Errorf("region %s: %w", region, err)
+		}
+		switch region {
+		case "us":
+			lists.US = regional
+		case "eu":
+			lists.EU = regional
+		case "kr":
+			lists.KR = regional
+		case "tw":
+			lists.TW = regional
+		}
+	}
+	return lists, nil
+}
+
+func loadTopGlobalPlayers(db *sql.DB, seasonID, topN int) ([]HomePlayerEntry, err
or) {
+	return queryTopPlayers(db, `
+		SELECT
+			pp.player_id, pp.name, pp.class_name, pp.main_spec_id,
+			pp.global_ranking, pp.global_ranking_bracket,
+			pp.regional_ranking, pp.regional_ranking_bracket,
+			pp.combined_best_time,
+			r.slug, r.name, r.region,
+			pd.avatar_url
+		FROM player_profiles pp
+		JOIN realms r ON pp.realm_id = r.id
+		LEFT JOIN player_details pd ON pp.player_id = pd.player_id
+		WHERE pp.season_id = ?
+		  AND pp.has_complete_coverage = 1
+		  AND pp.global_ranking IS NOT NULL
+		  AND pp.global_ranking <= ?
+		ORDER BY pp.global_ranking
+	`, seasonID, topN)
+}
+
+func loadTopRegionalPlayers(db *sql.DB, seasonID int, region string, topN int) ([
]HomePlayerEntry, error) {
+	return queryTopPlayers(db, `
+		SELECT
+			pp.player_id, pp.name, pp.class_name, pp.main_spec_id,
+			pp.global_ranking, pp.global_ranking_bracket,
+			pp.regional_ranking, pp.regional_ranking_bracket,
+			pp.combined_best_time,
+			r.slug, r.name, r.region,
+			pd.avatar_url
+		FROM player_profiles pp
+		JOIN realms r ON pp.realm_id = r.id
+		LEFT JOIN player_details pd ON pp.player_id = pd.player_id
+		WHERE pp.season_id = ?
+		  AND pp.has_complete_coverage = 1
+		  AND r.region = ?
+		  AND pp.regional_ranking IS NOT NULL
+		  AND pp.regional_ranking <= ?
+		ORDER BY pp.regional_ranking
+	`, seasonID, region, topN)
+}
+
+// queryTopPlayers executes a player-list query whose columns match the SELECT li
st above
+// and returns enriched HomePlayerEntry rows. Last positional arg is interpreted 
as the rank
+// to populate Rank from (global_ranking for the global list, regional_ranking fo
r regional).
+func queryTopPlayers(db *sql.DB, q string, args ...any) ([]HomePlayerEntry, error
) {
+	rows, err := db.Query(q, args...)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+
+	var out []HomePlayerEntry
+	for rows.Next() {
+		var (
+			e              HomePlayerEntry
+			className      sql.NullString
+			mainSpecID     sql.NullInt64
+			globalRank     sql.NullInt64
+			globalBracket  sql.NullString
+			regionalRank   sql.NullInt64
+			regionalBracket sql.NullString
+			combinedBest   sql.NullInt64
+			realmName      sql.NullString
+			avatarURL      sql.NullString
+		)
+		if err := rows.Scan(
+			&e.PlayerID, &e.Name, &className, &mainSpecID,
+			&globalRank, &globalBracket,
+			&regionalRank, &regionalBracket,
+			&combinedBest,
+			&e.RealmSlug, &realmName, &e.Region,
+			&avatarURL,
+		); err != nil {
+			return nil, err
+		}
+		if className.Valid {
+			e.ClassName = className.String
+		}
+		if mainSpecID.Valid {
+			e.ActiveSpecID = int(mainSpecID.Int64)
+		}
+		if globalRank.Valid {
+			e.GlobalRanking = int(globalRank.Int64)
+		}
+		if globalBracket.Valid {
+			e.GlobalRankingBracket = globalBracket.String
+		}
+		if regionalRank.Valid {
+			e.RegionalRanking = int(regionalRank.Int64)
+		}
+		if regionalBracket.Valid {
+			e.RegionalRankingBracket = regionalBracket.String
+		}
+		if combinedBest.Valid {
+			e.CombinedBestTimeMs = combinedBest.Int64
+		}
+		if realmName.Valid {
+			e.RealmName = realmName.String
+		}
+		if avatarURL.Valid {
+			e.AvatarURL = avatarURL.String
+		}
+		// Resolve spec name; class_name comes from player_profiles directly
+		if e.ActiveSpecID > 0 {
+			specPtr := e.ActiveSpecID
+			cls, spec := wow.FallbackClassAndSpec(e.ClassName, "", &specPtr)
+			e.ClassName, e.ActiveSpecName = cls, spec
+		}
+		// Rank for the entry: regional ranking takes precedence (the per-region lists
+		// query for it); the global list will populate from GlobalRanking via the
+		// fallback below.
+		if e.RegionalRanking > 0 && isRegionalQuery(q) {
+			e.Rank = e.RegionalRanking
+		} else {
+			e.Rank = e.GlobalRanking
+		}
+		out = append(out, e)
+	}
+	return out, rows.Err()
+}
+
+// isRegionalQuery is a tiny heuristic on the SELECT/ORDER text so queryTopPlayer
s can
+// decide which rank value to surface as `rank`. Cheap and safe - keeps queryTopP
layers
+// reusable for both global and regional list queries.
+func isRegionalQuery(q string) bool {
+	return strings.Contains(q, "ORDER BY pp.regional_ranking")
+}
+
+// loadSeasonName picks a representative season_name for the season number.
+// Seasons are stored per-region; names tend to match across regions, so any non-
empty
+// name for that season number is fine.
+func loadSeasonName(db *sql.DB, seasonNumber int) (string, error) {
+	var name sql.NullString
+	err := db.QueryRow(`
+		SELECT season_name FROM seasons
+		WHERE season_number = ? AND season_name IS NOT NULL AND season_name != ''
+		LIMIT 1
+	`, seasonNumber).Scan(&name)
+	if err == sql.ErrNoRows {
+		return "", nil
+	}
+	if err != nil {
+		return "", err
+	}
+	if name.Valid {
+		return name.String, nil
+	}
+	return "", nil
+}

diff --git a/nix/pkgs/ookstats/src/internal/
generator/home.go b/nix/pkgs/ookstats/src/in
ternal/generator/home.go
new file mode 100644
index 0000000..ca2153a
--- /dev/null
+++ b/nix/pkgs/ookstats/src/internal/generat
or/home.go
@@ -0,0 +1,487 @@
+package generator
+
+import (
+	"database/sql"
+	"fmt"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"ookstats/internal/wow"
+	"ookstats/internal/writer"
+)
+
+// HomeJSON is the top-level structure for 
web/public/api/home.json.
+// Layout: a cross-season recent-top-runs f
eed, plus per-season blocks
+// containing top-3 dungeon records and top
-10 player lists per scope.
+type HomeJSON struct {
+	GeneratedAt   int64                     `j
son:"generated_at"`
+	RecentTopRuns []HomeRunEntry            `j
son:"recent_top_runs"`
+	Seasons       map[string]HomeSeasonJSON `j
son:"seasons"`
+}
+
+// HomeSeasonJSON holds per-season top-3 ru
ns and top-10 player lists.
+type HomeSeasonJSON struct {
+	SeasonID          int                     
  `json:"season_id"`
+	SeasonName        string                  
  `json:"season_name,omitempty"`
+	TopRunsPerDungeon map[string][]HomeRunEntr
y `json:"top_runs_per_dungeon"`
+	TopPlayers        HomePlayerLists         
  `json:"top_players"`
+}
+
+// HomePlayerLists groups the top-10 player
 lists by scope.
+type HomePlayerLists struct {
+	Global []HomePlayerEntry `json:"global"`
+	US     []HomePlayerEntry `json:"us"`
+	EU     []HomePlayerEntry `json:"eu"`
+	KR     []HomePlayerEntry `json:"kr"`
+	TW     []HomePlayerEntry `json:"tw"`
+}
+
+// HomeRunEntry is one top run, used both f
or per-dungeon top-3 and the recent feed.
+type HomeRunEntry struct {
+	Rank               int              `json:
"rank,omitempty"`
+	Bracket            string           `json:
"bracket,omitempty"`
+	RunID              int64            `json:
"run_id"`
+	DungeonID          int              `json:
"dungeon_id"`
+	DungeonName        string           `json:
"dungeon_name"`
+	DungeonSlug        string           `json:
"dungeon_slug"`
+	DurationMs         int64            `json:
"duration_ms"`
+	CompletedTimestamp int64            `json:
"completed_timestamp"`
+	SeasonID           int              `json:
"season_id,omitempty"`
+	Rankings           *HomeRunRankings `json:
"rankings,omitempty"`
+	TeamMembers        []HomeRunMember  `json:
"team_members"`
+}
+
+// HomeRunRankings carries the rank-bucket(
s) a run qualified for.
+// Only populated on entries in RecentTopRu
ns. Currently global-only by design.
+type HomeRunRankings struct {
+	Global int `json:"global"`
+}
+
+// HomeRunMember is one of the 5 players in
 a run.
+// Field shapes mirror the canonical web/sr
c/lib/types.ts TeamMember (id, name, spec_id
, etc.)
+// so the TeamComposition component can con
sume these without adapter mapping.
+type HomeRunMember struct {
+	ID        int64  `json:"id"`
+	Name      string `json:"name"`
+	RealmSlug string `json:"realm_slug"`
+	Region    string `json:"region"`
+	ClassName string `json:"class_name,omitemp
ty"`
+	SpecID    int    `json:"spec_id,omitempty"
`
+	SpecName  string `json:"spec_name,omitempt
y"`
+	Faction   string `json:"faction,omitempty"
`
+}
+
+// HomePlayerEntry is one player in a top-1
0 list.
+type HomePlayerEntry struct {
+	Rank                   int    `json:"rank"
`
+	PlayerID               int64  `json:"playe
r_id"`
+	Name                   string `json:"name"
`
+	RealmSlug              string `json:"realm
_slug"`
+	RealmName              string `json:"realm
_name,omitempty"`
+	Region                 string `json:"regio
n"`
+	ClassName              string `json:"class
_name,omitempty"`
+	ActiveSpecID           int    `json:"activ
e_spec_id,omitempty"`
+	ActiveSpecName         string `json:"activ
e_spec_name,omitempty"`
+	CombinedBestTimeMs     int64  `json:"combi
ned_best_time_ms"`
+	GlobalRanking          int    `json:"globa
l_ranking,omitempty"`
+	GlobalRankingBracket   string `json:"globa
l_ranking_bracket,omitempty"`
+	RegionalRanking        int    `json:"regio
nal_ranking,omitempty"`
+	RegionalRankingBracket string `json:"regio
nal_ranking_bracket,omitempty"`
+	AvatarURL              string `json:"avata
r_url,omitempty"`
+}
+
+// Seasons we surface on the home page. Mir
rors run_rankings.season_id values.
+var homeSeasons = []int{1, 2}
+
+// regions for the regional top-player list
s
+var homeRegions = []string{"us", "eu", "kr"
, "tw"}
+
+// GenerateHome writes outDir/api/home.json
 with the home page payload.
+func GenerateHome(db *sql.DB, outDir string
) error {
+	home := HomeJSON{
+		GeneratedAt: time.Now().UnixMilli(),
+		Seasons:     make(map[string]HomeSeasonJS
ON),
+	}
+
+	for _, seasonID := range homeSeasons {
+		topRuns, err := loadTopRunsPerDungeon(db,
 seasonID, 3)
+		if err != nil {
+			return fmt.Errorf("top runs for season %
d: %w", seasonID, err)
+		}
+
+		topPlayers, err := loadTopPlayers(db, sea
sonID, 10)
+		if err != nil {
+			return fmt.Errorf("top players for seaso
n %d: %w", seasonID, err)
+		}
+
+		seasonName, err := loadSeasonName(db, sea
sonID)
+		if err != nil {
+			return fmt.Errorf("season name for %d: %
w", seasonID, err)
+		}
+
+		home.Seasons[fmt.Sprintf("%d", seasonID)]
 = HomeSeasonJSON{
+			SeasonID:          seasonID,
+			SeasonName:        seasonName,
+			TopRunsPerDungeon: topRuns,
+			TopPlayers:        topPlayers,
+		}
+	}
+
+	recent, err := loadRecentTopGlobalRuns(db,
 10)
+	if err != nil {
+		return fmt.Errorf("recent top global runs
: %w", err)
+	}
+	home.RecentTopRuns = recent
+
+	outPath := filepath.Join(outDir, "api", "h
ome.json")
+	if err := writer.WriteJSONFileCompact(outP
ath, home); err != nil {
+		return fmt.Errorf("write home.json: %w", 
err)
+	}
+	return nil
+}
+
+// loadTopRunsPerDungeon returns up to topN
 team-filtered global runs per dungeon for t
he season,
+// keyed by dungeon slug.
+func loadTopRunsPerDungeon(db *sql.DB, seas
onID, topN int) (map[string][]HomeRunEntry, 
error) {
+	rows, err := db.Query(`
+		SELECT
+			rr.run_id, rr.ranking, rr.percentile_bra
cket,
+			cr.dungeon_id, cr.duration, cr.completed
_timestamp,
+			d.slug, d.name
+		FROM run_rankings rr
+		JOIN challenge_runs cr ON rr.run_id = cr.
id
+		JOIN dungeons d ON cr.dungeon_id = d.id
+		WHERE rr.ranking_type = 'global'
+		  AND rr.ranking_scope = 'filtered'
+		  AND rr.season_id = ?
+		  AND rr.ranking <= ?
+		ORDER BY cr.dungeon_id, rr.ranking
+	`, seasonID, topN)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+
+	out := make(map[string][]HomeRunEntry)
+	var runIDs []int64
+	for rows.Next() {
+		var e HomeRunEntry
+		var bracket sql.NullString
+		if err := rows.Scan(&e.RunID, &e.Rank, &b
racket, &e.DungeonID, &e.DurationMs, &e.Comp
letedTimestamp, &e.DungeonSlug, &e.DungeonNa
me); err != nil {
+			return nil, err
+		}
+		if bracket.Valid {
+			e.Bracket = bracket.String
+		}
+		out[e.DungeonSlug] = append(out[e.Dungeon
Slug], e)
+		runIDs = append(runIDs, e.RunID)
+	}
+	if err := rows.Err(); err != nil {
+		return nil, err
+	}
+
+	members, err := loadTeamMembersForRuns(db,
 runIDs)
+	if err != nil {
+		return nil, err
+	}
+	for slug, entries := range out {
+		for i := range entries {
+			entries[i].TeamMembers = members[entries
[i].RunID]
+		}
+		out[slug] = entries
+	}
+	return out, nil
+}
+
+// loadRecentTopGlobalRuns returns up to li
mit recent runs that hit top-10 globally
+// (team-filtered) across either season, or
dered newest-first by completion time.
+func loadRecentTopGlobalRuns(db *sql.DB, li
mit int) ([]HomeRunEntry, error) {
+	rows, err := db.Query(`
+		SELECT
+			rr.run_id, rr.ranking, rr.percentile_bra
cket, rr.season_id,
+			cr.dungeon_id, cr.duration, cr.completed
_timestamp,
+			d.slug, d.name
+		FROM run_rankings rr
+		JOIN challenge_runs cr ON rr.run_id = cr.
id
+		JOIN dungeons d ON cr.dungeon_id = d.id
+		WHERE rr.ranking_type = 'global'
+		  AND rr.ranking_scope = 'filtered'
+		  AND rr.ranking <= 10
+		ORDER BY cr.completed_timestamp DESC
+		LIMIT ?
+	`, limit)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+
+	var out []HomeRunEntry
+	var runIDs []int64
+	for rows.Next() {
+		var e HomeRunEntry
+		var globalRank int
+		var bracket sql.NullString
+		if err := rows.Scan(&e.RunID, &globalRank
, &bracket, &e.SeasonID, &e.DungeonID, &e.Du
rationMs, &e.CompletedTimestamp, &e.DungeonS
lug, &e.DungeonName); err != nil {
+			return nil, err
+		}
+		e.Rankings = &HomeRunRankings{Global: glo
balRank}
+		if bracket.Valid {
+			e.Bracket = bracket.String
+		}
+		out = append(out, e)
+		runIDs = append(runIDs, e.RunID)
+	}
+	if err := rows.Err(); err != nil {
+		return nil, err
+	}
+
+	members, err := loadTeamMembersForRuns(db,
 runIDs)
+	if err != nil {
+		return nil, err
+	}
+	for i := range out {
+		out[i].TeamMembers = members[out[i].RunID
]
+	}
+	return out, nil
+}
+
+// loadTeamMembersForRuns fetches member me
tadata for the given run IDs in one batched 
query,
+// returning a map keyed by run_id.
+func loadTeamMembersForRuns(db *sql.DB, run
IDs []int64) (map[int64][]HomeRunMember, err
or) {
+	out := make(map[int64][]HomeRunMember)
+	if len(runIDs) == 0 {
+		return out, nil
+	}
+	placeholders := make([]string, len(runIDs)
)
+	args := make([]any, len(runIDs))
+	for i, id := range runIDs {
+		placeholders[i] = "?"
+		args[i] = id
+	}
+
+	q := fmt.Sprintf(`
+		SELECT
+			rm.run_id, rm.spec_id, rm.faction,
+			p.id, p.name,
+			r.slug, r.region,
+			pd.class_name
+		FROM run_members rm
+		JOIN players p ON rm.player_id = p.id
+		JOIN realms r ON p.realm_id = r.id
+		LEFT JOIN player_details pd ON p.id = pd.
player_id
+		WHERE rm.run_id IN (%s)
+	`, strings.Join(placeholders, ","))
+
+	rows, err := db.Query(q, args...)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+
+	for rows.Next() {
+		var runID int64
+		var specID sql.NullInt64
+		var faction sql.NullString
+		var className sql.NullString
+		var m HomeRunMember
+		if err := rows.Scan(&runID, &specID, &fac
tion, &m.ID, &m.Name, &m.RealmSlug, &m.Regio
n, &className); err != nil {
+			return nil, err
+		}
+		if specID.Valid {
+			m.SpecID = int(specID.Int64)
+		}
+		if faction.Valid {
+			m.Faction = faction.String
+		}
+		if className.Valid {
+			m.ClassName = className.String
+		}
+		// Backfill class+spec name from spec_id 
if either is missing
+		if m.SpecID > 0 {
+			specPtr := m.SpecID
+			cls, spec := wow.FallbackClassAndSpec(m.
ClassName, m.SpecName, &specPtr)
+			m.ClassName, m.SpecName = cls, spec
+		}
+		out[runID] = append(out[runID], m)
+	}
+	return out, rows.Err()
+}
+
+// loadTopPlayers returns the global top-N 
plus per-region top-N player lists for the s
eason.
+func loadTopPlayers(db *sql.DB, seasonID, t
opN int) (HomePlayerLists, error) {
+	var lists HomePlayerLists
+	global, err := loadTopGlobalPlayers(db, se
asonID, topN)
+	if err != nil {
+		return lists, fmt.Errorf("global: %w", er
r)
+	}
+	lists.Global = global
+
+	for _, region := range homeRegions {
+		regional, err := loadTopRegionalPlayers(d
b, seasonID, region, topN)
+		if err != nil {
+			return lists, fmt.Errorf("region %s: %w"
, region, err)
+		}
+		switch region {
+		case "us":
+			lists.US = regional
+		case "eu":
+			lists.EU = regional
+		case "kr":
+			lists.KR = regional
+		case "tw":
+			lists.TW = regional
+		}
+	}
+	return lists, nil
+}
+
+func loadTopGlobalPlayers(db *sql.DB, seaso
nID, topN int) ([]HomePlayerEntry, error) {
+	return queryTopPlayers(db, `
+		SELECT
+			pp.player_id, pp.name, pp.class_name, pp
.main_spec_id,
+			pp.global_ranking, pp.global_ranking_bra
cket,
+			pp.regional_ranking, pp.regional_ranking
_bracket,
+			pp.combined_best_time,
+			r.slug, r.name, r.region,
+			pd.avatar_url
+		FROM player_profiles pp
+		JOIN realms r ON pp.realm_id = r.id
+		LEFT JOIN player_details pd ON pp.player_
id = pd.player_id
+		WHERE pp.season_id = ?
+		  AND pp.has_complete_coverage = 1
+		  AND pp.global_ranking IS NOT NULL
+		  AND pp.global_ranking <= ?
+		ORDER BY pp.global_ranking
+	`, seasonID, topN)
+}
+
+func loadTopRegionalPlayers(db *sql.DB, sea
sonID int, region string, topN int) ([]HomeP
layerEntry, error) {
+	return queryTopPlayers(db, `
+		SELECT
+			pp.player_id, pp.name, pp.class_name, pp
.main_spec_id,
+			pp.global_ranking, pp.global_ranking_bra
cket,
+			pp.regional_ranking, pp.regional_ranking
_bracket,
+			pp.combined_best_time,
+			r.slug, r.name, r.region,
+			pd.avatar_url
+		FROM player_profiles pp
+		JOIN realms r ON pp.realm_id = r.id
+		LEFT JOIN player_details pd ON pp.player_
id = pd.player_id
+		WHERE pp.season_id = ?
+		  AND pp.has_complete_coverage = 1
+		  AND r.region = ?
+		  AND pp.regional_ranking IS NOT NULL
+		  AND pp.regional_ranking <= ?
+		ORDER BY pp.regional_ranking
+	`, seasonID, region, topN)
+}
+
+// queryTopPlayers executes a player-list q
uery whose columns match the SELECT list abo
ve
+// and returns enriched HomePlayerEntry row
s. Last positional arg is interpreted as the
 rank
+// to populate Rank from (global_ranking fo
r the global list, regional_ranking for regi
onal).
+func queryTopPlayers(db *sql.DB, q string, 
args ...any) ([]HomePlayerEntry, error) {
+	rows, err := db.Query(q, args...)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+
+	var out []HomePlayerEntry
+	for rows.Next() {
+		var (
+			e              HomePlayerEntry
+			className      sql.NullString
+			mainSpecID     sql.NullInt64
+			globalRank     sql.NullInt64
+			globalBracket  sql.NullString
+			regionalRank   sql.NullInt64
+			regionalBracket sql.NullString
+			combinedBest   sql.NullInt64
+			realmName      sql.NullString
+			avatarURL      sql.NullString
+		)
+		if err := rows.Scan(
+			&e.PlayerID, &e.Name, &className, &mainS
pecID,
+			&globalRank, &globalBracket,
+			&regionalRank, &regionalBracket,
+			&combinedBest,
+			&e.RealmSlug, &realmName, &e.Region,
+			&avatarURL,
+		); err != nil {
+			return nil, err
+		}
+		if className.Valid {
+			e.ClassName = className.String
+		}
+		if mainSpecID.Valid {
+			e.ActiveSpecID = int(mainSpecID.Int64)
+		}
+		if globalRank.Valid {
+			e.GlobalRanking = int(globalRank.Int64)
+		}
+		if globalBracket.Valid {
+			e.GlobalRankingBracket = globalBracket.S
tring
+		}
+		if regionalRank.Valid {
+			e.RegionalRanking = int(regionalRank.Int
64)
+		}
+		if regionalBracket.Valid {
+			e.RegionalRankingBracket = regionalBrack
et.String
+		}
+		if combinedBest.Valid {
+			e.CombinedBestTimeMs = combinedBest.Int6
4
+		}
+		if realmName.Valid {
+			e.RealmName = realmName.String
+		}
+		if avatarURL.Valid {
+			e.AvatarURL = avatarURL.String
+		}
+		// Resolve spec name; class_name comes fr
om player_profiles directly
+		if e.ActiveSpecID > 0 {
+			specPtr := e.ActiveSpecID
+			cls, spec := wow.FallbackClassAndSpec(e.
ClassName, "", &specPtr)
+			e.ClassName, e.ActiveSpecName = cls, spe
c
+		}
+		// Rank for the entry: regional ranking t
akes precedence (the per-region lists
+		// query for it); the global list will po
pulate from GlobalRanking via the
+		// fallback below.
+		if e.RegionalRanking > 0 && isRegionalQue
ry(q) {
+			e.Rank = e.RegionalRanking
+		} else {
+			e.Rank = e.GlobalRanking
+		}
+		out = append(out, e)
+	}
+	return out, rows.Err()
+}
+
+// isRegionalQuery is a tiny heuristic on t
he SELECT/ORDER text so queryTopPlayers can
+// decide which rank value to surface as `r
ank`. Cheap and safe - keeps queryTopPlayers
+// reusable for both global and regional li
st queries.
+func isRegionalQuery(q string) bool {
+	return strings.Contains(q, "ORDER BY pp.re
gional_ranking")
+}
+
+// loadSeasonName picks a representative se
ason_name for the season number.
+// Seasons are stored per-region; names ten
d to match across regions, so any non-empty
+// name for that season number is fine.
+func loadSeasonName(db *sql.DB, seasonNumbe
r int) (string, error) {
+	var name sql.NullString
+	err := db.QueryRow(`
+		SELECT season_name FROM seasons
+		WHERE season_number = ? AND season_name I
S NOT NULL AND season_name != ''
+		LIMIT 1
+	`, seasonNumber).Scan(&name)
+	if err == sql.ErrNoRows {
+		return "", nil
+	}
+	if err != nil {
+		return "", err
+	}
+	if name.Valid {
+		return name.String, nil
+	}
+	return "", nil
+}
 
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET