HASH 72874ae6e5d4
DATE 2026-07-16
SUBJECT ookstats + web: season 3 defaults, dynamic season scopes
FILES 9 CHANGED
HASH 72874ae6e5d4
DATE 2026-07-16
SUBJECT ookstats + web: season 3 defaults,
dynamic season scopes
FILES 9 CHANGED
┌─ nix/pkgs/ookstats/src/internal/generator/gear.go ─────────────────────────┐
│ diff --git a/nix/pkgs/ookstats/src/internal/generator/gear.go b/nix/pkgs/ookstats/ │
│ src/internal/generator/gear.go │
│ index c7acfe2..6dff3d4 100644 │
│ --- a/nix/pkgs/ookstats/src/internal/generator/gear.go │
│ +++ b/nix/pkgs/ookstats/src/internal/generator/gear.go │
│ @@ -92,7 +92,12 @@ func GenerateGear(db *sql.DB, outDir string) error { │
│ return fmt.Errorf("load item profiles: %w", err) │
│ } │
│ │
│ - for _, season := range []int{1, 2} { │
│ + seasons, err := loadGearSeasons(db) │
│ + if err != nil { │
│ + return fmt.Errorf("load gear seasons: %w", err) │
│ + } │
│ + │
│ + for _, season := range seasons { │
│ seasonKey := fmt.Sprintf("season_%d", season) │
│ bySpec := make(map[string]GearSpecBucket) │
│ │
│ @@ -122,6 +127,23 @@ func GenerateGear(db *sql.DB, outDir string) error { │
│ │
│ // gatherSpecIDs returns every spec id we have a primary-stat mapping for. Using │
│ // a yield-style helper keeps the caller readable without exporting the map. │
│ +func loadGearSeasons(db *sql.DB) ([]int, error) { │
│ + rows, err := db.Query(`SELECT DISTINCT season_number FROM seasons ORDER BY seaso │
│ n_number ASC`) │
│ + if err != nil { │
│ + return nil, err │
│ + } │
│ + defer rows.Close() │
│ + var out []int │
│ + for rows.Next() { │
│ + var n int │
│ + if err := rows.Scan(&n); err != nil { │
│ + return nil, err │
│ + } │
│ + out = append(out, n) │
│ + } │
│ + return out, rows.Err() │
│ +} │
│ + │
│ func gatherSpecIDs() map[int]struct{} { │
│ out := make(map[int]struct{}, 33) │
│ for _, sid := range []int{ │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...rc/internal/generator/gear.go ───┐
│ diff --git a/nix/pkgs/ookstats/src/internal/ │
│ generator/gear.go b/nix/pkgs/ookstats/src/in │
│ ternal/generator/gear.go │
│ index c7acfe2..6dff3d4 100644 │
│ --- a/nix/pkgs/ookstats/src/internal/generat │
│ or/gear.go │
│ +++ b/nix/pkgs/ookstats/src/internal/generat │
│ or/gear.go │
│ @@ -92,7 +92,12 @@ func GenerateGear(db *sql │
│ .DB, outDir string) error { │
│ return fmt.Errorf("load item profiles: %w │
│ ", err) │
│ } │
│ │
│ - for _, season := range []int{1, 2} { │
│ + seasons, err := loadGearSeasons(db) │
│ + if err != nil { │
│ + return fmt.Errorf("load gear seasons: %w" │
│ , err) │
│ + } │
│ + │
│ + for _, season := range seasons { │
│ seasonKey := fmt.Sprintf("season_%d", sea │
│ son) │
│ bySpec := make(map[string]GearSpecBucket) │
│ │
│ @@ -122,6 +127,23 @@ func GenerateGear(db *s │
│ ql.DB, outDir string) error { │
│ │
│ // gatherSpecIDs returns every spec id we h │
│ ave a primary-stat mapping for. Using │
│ // a yield-style helper keeps the caller re │
│ adable without exporting the map. │
│ +func loadGearSeasons(db *sql.DB) ([]int, er │
│ ror) { │
│ + rows, err := db.Query(`SELECT DISTINCT sea │
│ son_number FROM seasons ORDER BY season_numb │
│ er ASC`) │
│ + if err != nil { │
│ + return nil, err │
│ + } │
│ + defer rows.Close() │
│ + var out []int │
│ + for rows.Next() { │
│ + var n int │
│ + if err := rows.Scan(&n); err != nil { │
│ + return nil, err │
│ + } │
│ + out = append(out, n) │
│ + } │
│ + return out, rows.Err() │
│ +} │
│ + │
│ func gatherSpecIDs() map[int]struct{} { │
│ out := make(map[int]struct{}, 33) │
│ for _, sid := range []int{ │
└──────────────────────────────────────────────┘
┌─ nix/pkgs/ookstats/src/internal/generator/stats.go ────────────────────────┐
│ diff --git a/nix/pkgs/ookstats/src/internal/generator/stats.go b/nix/pkgs/ookstats │
│ /src/internal/generator/stats.go │
│ index 3f0a88b..67231d8 100644 │
│ --- a/nix/pkgs/ookstats/src/internal/generator/stats.go │
│ +++ b/nix/pkgs/ookstats/src/internal/generator/stats.go │
│ @@ -113,17 +113,16 @@ type statsScopeDef struct { │
│ SeasonNum int // 0 = all-time │
│ } │
│ │
│ -var statsScopes = []statsScopeDef{ │
│ - {Key: "all_time", SeasonNum: 0}, │
│ - {Key: "season_1", SeasonNum: 1}, │
│ - {Key: "season_2", SeasonNum: 2}, │
│ -} │
│ - │
│ // statsRegions are the region scopes emitted; "global" means no region filter. │
│ var statsRegions = []string{"global", "us", "eu", "kr", "tw"} │
│ │
│ // GenerateStats writes outDir/api/stats.json with aggregated stats per (region, │
│ season) scope. │
│ func GenerateStats(db *sql.DB, outDir string) error { │
│ + statsScopes, err := loadStatsScopes(db) │
│ + if err != nil { │
│ + return fmt.Errorf("load stats scopes: %w", err) │
│ + } │
│ + │
│ out := StatsJSON{ │
│ GeneratedAt: time.Now().UnixMilli(), │
│ Scopes: make(map[string]map[string]StatsScope, len(statsRegions)), │
│ @@ -154,6 +153,24 @@ func GenerateStats(db *sql.DB, outDir string) error { │
│ return nil │
│ } │
│ │
│ +// loadStatsScopes returns all-time plus one scope per season in the DB │
│ +func loadStatsScopes(db *sql.DB) ([]statsScopeDef, error) { │
│ + rows, err := db.Query(`SELECT DISTINCT season_number FROM seasons ORDER BY seaso │
│ n_number ASC`) │
│ + if err != nil { │
│ + return nil, err │
│ + } │
│ + defer rows.Close() │
│ + scopes := []statsScopeDef{{Key: "all_time", SeasonNum: 0}} │
│ + for rows.Next() { │
│ + var n int │
│ + if err := rows.Scan(&n); err != nil { │
│ + return nil, err │
│ + } │
│ + scopes = append(scopes, statsScopeDef{Key: fmt.Sprintf("season_%d", n), SeasonN │
│ um: n}) │
│ + } │
│ + return scopes, rows.Err() │
│ +} │
│ + │
│ // buildStatsScope assembles a single StatsScope. │
│ // seasonNum == 0 means cross-season (all-time). region == "global" means no regi │
│ on filter. │
│ // For all-time, pass allTimeTotalPlayers=0; for per-season builds, pass the all- │
│ time scope's │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...c/internal/generator/stats.go ───┐
│ diff --git a/nix/pkgs/ookstats/src/internal/ │
│ generator/stats.go b/nix/pkgs/ookstats/src/i │
│ nternal/generator/stats.go │
│ index 3f0a88b..67231d8 100644 │
│ --- a/nix/pkgs/ookstats/src/internal/generat │
│ or/stats.go │
│ +++ b/nix/pkgs/ookstats/src/internal/generat │
│ or/stats.go │
│ @@ -113,17 +113,16 @@ type statsScopeDef str │
│ uct { │
│ SeasonNum int // 0 = all-time │
│ } │
│ │
│ -var statsScopes = []statsScopeDef{ │
│ - {Key: "all_time", SeasonNum: 0}, │
│ - {Key: "season_1", SeasonNum: 1}, │
│ - {Key: "season_2", SeasonNum: 2}, │
│ -} │
│ - │
│ // statsRegions are the region scopes emitt │
│ ed; "global" means no region filter. │
│ var statsRegions = []string{"global", "us", │
│ "eu", "kr", "tw"} │
│ │
│ // GenerateStats writes outDir/api/stats.js │
│ on with aggregated stats per (region, season │
│ ) scope. │
│ func GenerateStats(db *sql.DB, outDir strin │
│ g) error { │
│ + statsScopes, err := loadStatsScopes(db) │
│ + if err != nil { │
│ + return fmt.Errorf("load stats scopes: %w" │
│ , err) │
│ + } │
│ + │
│ out := StatsJSON{ │
│ GeneratedAt: time.Now().UnixMilli(), │
│ Scopes: make(map[string]map[string]S │
│ tatsScope, len(statsRegions)), │
│ @@ -154,6 +153,24 @@ func GenerateStats(db * │
│ sql.DB, outDir string) error { │
│ return nil │
│ } │
│ │
│ +// loadStatsScopes returns all-time plus on │
│ e scope per season in the DB │
│ +func loadStatsScopes(db *sql.DB) ([]statsSc │
│ opeDef, error) { │
│ + rows, err := db.Query(`SELECT DISTINCT sea │
│ son_number FROM seasons ORDER BY season_numb │
│ er ASC`) │
│ + if err != nil { │
│ + return nil, err │
│ + } │
│ + defer rows.Close() │
│ + scopes := []statsScopeDef{{Key: "all_time" │
│ , SeasonNum: 0}} │
│ + for rows.Next() { │
│ + var n int │
│ + if err := rows.Scan(&n); err != nil { │
│ + return nil, err │
│ + } │
│ + scopes = append(scopes, statsScopeDef{Key │
│ : fmt.Sprintf("season_%d", n), SeasonNum: n} │
│ ) │
│ + } │
│ + return scopes, rows.Err() │
│ +} │
│ + │
│ // buildStatsScope assembles a single Stats │
│ Scope. │
│ // seasonNum == 0 means cross-season (all-t │
│ ime). region == "global" means no region fil │
│ ter. │
│ // For all-time, pass allTimeTotalPlayers=0 │
│ ; for per-season builds, pass the all-time s │
│ cope's │
└──────────────────────────────────────────────┘
┌─ web/src/components/Home/ChallengeRun/ChallengeRun.astro ──────────────────┐
│ diff --git a/web/src/components/Home/ChallengeRun/ChallengeRun.astro b/web/src/com │
│ ponents/Home/ChallengeRun/ChallengeRun.astro │
│ index b86bbee..687bb8d 100644 │
│ --- a/web/src/components/Home/ChallengeRun/ChallengeRun.astro │
│ +++ b/web/src/components/Home/ChallengeRun/ChallengeRun.astro │
│ @@ -16,6 +16,7 @@ interface Props { │
│ showDungeonName?: boolean; │
│ rank?: number; │
│ bracket?: string; │
│ + seasonId?: number; │
│ } │
│ │
│ const { │
│ @@ -28,6 +29,7 @@ const { │
│ showDungeonName = true, │
│ rank, │
│ bracket, │
│ + seasonId = 3, │
│ } = Astro.props; │
│ │
│ const dungeonIcon = getDungeonIconUrl(dungeonSlug, dungeonName); │
│ @@ -39,7 +41,7 @@ const bracketClass = bracket ? `bracket-${bracket}` : ""; │
│ showDungeonIcon && dungeonIcon && ( │
│ <a │
│ class="challenge-run__dungeon" │
│ - href={`/challenge-mode/season2/global/${dungeonSlug}`} │
│ + href={`/challenge-mode/season${seasonId}/global/${dungeonSlug}`} │
│ title={dungeonName} │
│ > │
│ <img src={dungeonIcon} alt={dungeonName} loading="lazy" /> │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...allengeRun/ChallengeRun.astro ───┐
│ diff --git a/web/src/components/Home/Challen │
│ geRun/ChallengeRun.astro b/web/src/component │
│ s/Home/ChallengeRun/ChallengeRun.astro │
│ index b86bbee..687bb8d 100644 │
│ --- a/web/src/components/Home/ChallengeRun/C │
│ hallengeRun.astro │
│ +++ b/web/src/components/Home/ChallengeRun/C │
│ hallengeRun.astro │
│ @@ -16,6 +16,7 @@ interface Props { │
│ showDungeonName?: boolean; │
│ rank?: number; │
│ bracket?: string; │
│ + seasonId?: number; │
│ } │
│ │
│ const { │
│ @@ -28,6 +29,7 @@ const { │
│ showDungeonName = true, │
│ rank, │
│ bracket, │
│ + seasonId = 3, │
│ } = Astro.props; │
│ │
│ const dungeonIcon = getDungeonIconUrl(dunge │
│ onSlug, dungeonName); │
│ @@ -39,7 +41,7 @@ const bracketClass = brack │
│ et ? `bracket-${bracket}` : ""; │
│ showDungeonIcon && dungeonIcon && ( │
│ <a │
│ class="challenge-run__dungeon" │
│ - href={`/challenge-mode/season2/glob │
│ al/${dungeonSlug}`} │
│ + href={`/challenge-mode/season${seas │
│ onId}/global/${dungeonSlug}`} │
│ title={dungeonName} │
│ > │
│ <img src={dungeonIcon} alt={dungeon │
│ Name} loading="lazy" /> │
└──────────────────────────────────────────────┘
┌─ web/src/components/Home/RecentRuns/RecentRuns.astro ──────────────────────┐
│ diff --git a/web/src/components/Home/RecentRuns/RecentRuns.astro b/web/src/compone │
│ nts/Home/RecentRuns/RecentRuns.astro │
│ index 2a55e88..74a533d 100644 │
│ --- a/web/src/components/Home/RecentRuns/RecentRuns.astro │
│ +++ b/web/src/components/Home/RecentRuns/RecentRuns.astro │
│ @@ -31,6 +31,7 @@ const { runs } = Astro.props; │
│ showDungeonName={false} │
│ rank={r.rankings?.global} │
│ bracket={r.bracket} │
│ + seasonId={r.season_id} │
│ /> │
│ ))} │
│ </div> │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...e/RecentRuns/RecentRuns.astro ───┐
│ diff --git a/web/src/components/Home/RecentR │
│ uns/RecentRuns.astro b/web/src/components/Ho │
│ me/RecentRuns/RecentRuns.astro │
│ index 2a55e88..74a533d 100644 │
│ --- a/web/src/components/Home/RecentRuns/Rec │
│ entRuns.astro │
│ +++ b/web/src/components/Home/RecentRuns/Rec │
│ entRuns.astro │
│ @@ -31,6 +31,7 @@ const { runs } = Astro.pro │
│ ps; │
│ showDungeonName={false} │
│ rank={r.rankings?.global} │
│ bracket={r.bracket} │
│ + seasonId={r.season_id} │
│ /> │
│ ))} │
│ </div> │
└──────────────────────────────────────────────┘
┌─ web/src/pages/challenge-mode/[season]/characters/[...scope].astro ────────┐
│ diff --git a/web/src/pages/challenge-mode/[season]/characters/[...scope].astro b/w │
│ eb/src/pages/challenge-mode/[season]/characters/[...scope].astro │
│ index 1af91e3..38d0cad 100644 │
│ --- a/web/src/pages/challenge-mode/[season]/characters/[...scope].astro │
│ +++ b/web/src/pages/challenge-mode/[season]/characters/[...scope].astro │
│ @@ -17,14 +17,14 @@ import { CLASSES } from "../../../../lib/wow-constants"; │
│ │
│ export const prerender = false; │
│ │
│ -const seasonParam = Astro.params.season || "season2"; │
│ +const seasonParam = Astro.params.season || "season3"; │
│ const seasonMatch = seasonParam.match(/^season(\d+)$/); │
│ const isAllTime = seasonParam === "all-time"; │
│ const currentSeason: number | "all-time" = isAllTime │
│ ? "all-time" │
│ : seasonMatch │
│ ? parseInt(seasonMatch[1], 10) │
│ - : 2; │
│ + : 3; │
│ │
│ const scope = Astro.params.scope || ""; │
│ let scopeParts = scope.split("/").filter(Boolean); │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...]/characters/[...scope].astro ───┐
│ diff --git a/web/src/pages/challenge-mode/[s │
│ eason]/characters/[...scope].astro b/web/src │
│ /pages/challenge-mode/[season]/characters/[. │
│ ..scope].astro │
│ index 1af91e3..38d0cad 100644 │
│ --- a/web/src/pages/challenge-mode/[season]/ │
│ characters/[...scope].astro │
│ +++ b/web/src/pages/challenge-mode/[season]/ │
│ characters/[...scope].astro │
│ @@ -17,14 +17,14 @@ import { CLASSES } from │
│ "../../../../lib/wow-constants"; │
│ │
│ export const prerender = false; │
│ │
│ -const seasonParam = Astro.params.season || │
│ "season2"; │
│ +const seasonParam = Astro.params.season || │
│ "season3"; │
│ const seasonMatch = seasonParam.match(/^sea │
│ son(\d+)$/); │
│ const isAllTime = seasonParam === "all-time │
│ "; │
│ const currentSeason: number | "all-time" = │
│ isAllTime │
│ ? "all-time" │
│ : seasonMatch │
│ ? parseInt(seasonMatch[1], 10) │
│ - : 2; │
│ + : 3; │
│ │
│ const scope = Astro.params.scope || ""; │
│ let scopeParts = scope.split("/").filter(Bo │
│ olean); │
└──────────────────────────────────────────────┘
┌─ web/src/pages/challenge-mode/[season]/gear/[spec].astro ──────────────────┐
│ diff --git a/web/src/pages/challenge-mode/[season]/gear/[spec].astro b/web/src/pag │
│ es/challenge-mode/[season]/gear/[spec].astro │
│ index ac4e80b..ca9c301 100644 │
│ --- a/web/src/pages/challenge-mode/[season]/gear/[spec].astro │
│ +++ b/web/src/pages/challenge-mode/[season]/gear/[spec].astro │
│ @@ -11,9 +11,9 @@ import type { GearJSON, GearSpecBucket } from "../../../../lib/t │
│ ypes"; │
│ │
│ export const prerender = false; │
│ │
│ -const seasonParam = (Astro.params.season || "season2").toLowerCase(); │
│ +const seasonParam = (Astro.params.season || "season3").toLowerCase(); │
│ const seasonMatch = seasonParam.match(/^season(\d+)$/); │
│ -const seasonNumber = seasonMatch ? parseInt(seasonMatch[1], 10) : 2; │
│ +const seasonNumber = seasonMatch ? parseInt(seasonMatch[1], 10) : 3; │
│ const seasonKey = `season_${seasonNumber}`; │
│ │
│ const specSlug = (Astro.params.spec || "monk-mistweaver").toLowerCase(); │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...de/[season]/gear/[spec].astro ───┐
│ diff --git a/web/src/pages/challenge-mode/[s │
│ eason]/gear/[spec].astro b/web/src/pages/cha │
│ llenge-mode/[season]/gear/[spec].astro │
│ index ac4e80b..ca9c301 100644 │
│ --- a/web/src/pages/challenge-mode/[season]/ │
│ gear/[spec].astro │
│ +++ b/web/src/pages/challenge-mode/[season]/ │
│ gear/[spec].astro │
│ @@ -11,9 +11,9 @@ import type { GearJSON, Ge │
│ arSpecBucket } from "../../../../lib/types"; │
│ │
│ export const prerender = false; │
│ │
│ -const seasonParam = (Astro.params.season || │
│ "season2").toLowerCase(); │
│ +const seasonParam = (Astro.params.season || │
│ "season3").toLowerCase(); │
│ const seasonMatch = seasonParam.match(/^sea │
│ son(\d+)$/); │
│ -const seasonNumber = seasonMatch ? parseInt │
│ (seasonMatch[1], 10) : 2; │
│ +const seasonNumber = seasonMatch ? parseInt │
│ (seasonMatch[1], 10) : 3; │
│ const seasonKey = `season_${seasonNumber}`; │
│ │
│ const specSlug = (Astro.params.spec || "mon │
│ k-mistweaver").toLowerCase(); │
└──────────────────────────────────────────────┘
┌─ web/src/pages/challenge-mode/[season]/players/[...scope].astro ───────────┐
│ diff --git a/web/src/pages/challenge-mode/[season]/players/[...scope].astro b/web/ │
│ src/pages/challenge-mode/[season]/players/[...scope].astro │
│ index 93deac7..b84743f 100644 │
│ --- a/web/src/pages/challenge-mode/[season]/players/[...scope].astro │
│ +++ b/web/src/pages/challenge-mode/[season]/players/[...scope].astro │
│ @@ -17,14 +17,14 @@ import { getEffectiveRealmSlug } from "../../../../lib/realms" │
│ ; │
│ │
│ export const prerender = false; │
│ │
│ -const seasonParam = Astro.params.season || "season2"; │
│ +const seasonParam = Astro.params.season || "season3"; │
│ const seasonMatch = seasonParam.match(/^season(\d+)$/); │
│ const isAllTime = seasonParam === "all-time"; │
│ const currentSeason: number | "all-time" = isAllTime │
│ ? "all-time" │
│ : seasonMatch │
│ ? parseInt(seasonMatch[1], 10) │
│ - : 2; │
│ + : 3; │
│ │
│ const scope = Astro.params.scope || ""; │
│ let scopeParts = scope.split("/").filter(Boolean); │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...son]/players/[...scope].astro ───┐
│ diff --git a/web/src/pages/challenge-mode/[s │
│ eason]/players/[...scope].astro b/web/src/pa │
│ ges/challenge-mode/[season]/players/[...scop │
│ e].astro │
│ index 93deac7..b84743f 100644 │
│ --- a/web/src/pages/challenge-mode/[season]/ │
│ players/[...scope].astro │
│ +++ b/web/src/pages/challenge-mode/[season]/ │
│ players/[...scope].astro │
│ @@ -17,14 +17,14 @@ import { getEffectiveRea │
│ lmSlug } from "../../../../lib/realms"; │
│ │
│ export const prerender = false; │
│ │
│ -const seasonParam = Astro.params.season || │
│ "season2"; │
│ +const seasonParam = Astro.params.season || │
│ "season3"; │
│ const seasonMatch = seasonParam.match(/^sea │
│ son(\d+)$/); │
│ const isAllTime = seasonParam === "all-time │
│ "; │
│ const currentSeason: number | "all-time" = │
│ isAllTime │
│ ? "all-time" │
│ : seasonMatch │
│ ? parseInt(seasonMatch[1], 10) │
│ - : 2; │
│ + : 3; │
│ │
│ const scope = Astro.params.scope || ""; │
│ let scopeParts = scope.split("/").filter(Bo │
│ olean); │
└──────────────────────────────────────────────┘
┌─ web/src/pages/challenge-mode/[season]/stats/[region].astro ───────────────┐
│ diff --git a/web/src/pages/challenge-mode/[season]/stats/[region].astro b/web/src/ │
│ pages/challenge-mode/[season]/stats/[region].astro │
│ index 77ed9aa..e53c39e 100644 │
│ --- a/web/src/pages/challenge-mode/[season]/stats/[region].astro │
│ +++ b/web/src/pages/challenge-mode/[season]/stats/[region].astro │
│ @@ -13,7 +13,7 @@ import type { StatsJSON, StatsScope } from "../../../../lib/type │
│ s"; │
│ export const prerender = false; │
│ │
│ // Parse URL params: /challenge-mode/(season{N}|all-time)/stats/{region} │
│ -const seasonParam = (Astro.params.season || "season2").toLowerCase(); │
│ +const seasonParam = (Astro.params.season || "season3").toLowerCase(); │
│ let seasonNumber: number; │
│ let seasonKey: string; │
│ if (seasonParam === "all-time") { │
│ @@ -21,7 +21,7 @@ if (seasonParam === "all-time") { │
│ seasonKey = "all_time"; │
│ } else { │
│ const seasonMatch = seasonParam.match(/^season(\d+)$/); │
│ - seasonNumber = seasonMatch ? parseInt(seasonMatch[1], 10) : 2; │
│ + seasonNumber = seasonMatch ? parseInt(seasonMatch[1], 10) : 3; │
│ seasonKey = `season_${seasonNumber}`; │
│ } │
│ │
│ @@ -54,11 +54,20 @@ const REGION_LABELS: Record<string, string> = { │
│ kr: "KR", │
│ tw: "TW", │
│ }; │
│ -const SEASON_LABELS: Record<string, string> = { │
│ - all_time: "All Time", │
│ - season_1: "Season 1", │
│ - season_2: "Season 2", │
│ -}; │
│ +// labels come from the data so new seasons need no frontend change │
│ +const SEASON_LABELS: Record<string, string> = { all_time: "All Time" }; │
│ +if (stats) { │
│ + for (const regionScope of Object.values(stats.scopes)) { │
│ + for (const key of Object.keys(regionScope)) { │
│ + if (!(key in SEASON_LABELS)) { │
│ + const match = key.match(/^season_(\d+)$/); │
│ + if (match) { │
│ + SEASON_LABELS[key] = `Season ${match[1]}`; │
│ + } │
│ + } │
│ + } │
│ + } │
│ +} │
│ │
│ const generatedAt = stats?.generated_at ? new Date(stats.generated_at) : null; │
│ const scopeLabel = `${REGION_LABELS[region] ?? region.toUpperCase()} - ${SEASON_L │
│ ABELS[seasonKey] ?? seasonKey}`; │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...[season]/stats/[region].astro ───┐
│ diff --git a/web/src/pages/challenge-mode/[s │
│ eason]/stats/[region].astro b/web/src/pages/ │
│ challenge-mode/[season]/stats/[region].astro │
│ index 77ed9aa..e53c39e 100644 │
│ --- a/web/src/pages/challenge-mode/[season]/ │
│ stats/[region].astro │
│ +++ b/web/src/pages/challenge-mode/[season]/ │
│ stats/[region].astro │
│ @@ -13,7 +13,7 @@ import type { StatsJSON, S │
│ tatsScope } from "../../../../lib/types"; │
│ export const prerender = false; │
│ │
│ // Parse URL params: /challenge-mode/(seaso │
│ n{N}|all-time)/stats/{region} │
│ -const seasonParam = (Astro.params.season || │
│ "season2").toLowerCase(); │
│ +const seasonParam = (Astro.params.season || │
│ "season3").toLowerCase(); │
│ let seasonNumber: number; │
│ let seasonKey: string; │
│ if (seasonParam === "all-time") { │
│ @@ -21,7 +21,7 @@ if (seasonParam === "all-t │
│ ime") { │
│ seasonKey = "all_time"; │
│ } else { │
│ const seasonMatch = seasonParam.match(/^s │
│ eason(\d+)$/); │
│ - seasonNumber = seasonMatch ? parseInt(sea │
│ sonMatch[1], 10) : 2; │
│ + seasonNumber = seasonMatch ? parseInt(sea │
│ sonMatch[1], 10) : 3; │
│ seasonKey = `season_${seasonNumber}`; │
│ } │
│ │
│ @@ -54,11 +54,20 @@ const REGION_LABELS: Rec │
│ ord<string, string> = { │
│ kr: "KR", │
│ tw: "TW", │
│ }; │
│ -const SEASON_LABELS: Record<string, string> │
│ = { │
│ - all_time: "All Time", │
│ - season_1: "Season 1", │
│ - season_2: "Season 2", │
│ -}; │
│ +// labels come from the data so new seasons │
│ need no frontend change │
│ +const SEASON_LABELS: Record<string, string> │
│ = { all_time: "All Time" }; │
│ +if (stats) { │
│ + for (const regionScope of Object.values(s │
│ tats.scopes)) { │
│ + for (const key of Object.keys(regionSco │
│ pe)) { │
│ + if (!(key in SEASON_LABELS)) { │
│ + const match = key.match(/^season_(\ │
│ d+)$/); │
│ + if (match) { │
│ + SEASON_LABELS[key] = `Season ${ma │
│ tch[1]}`; │
│ + } │
│ + } │
│ + } │
│ + } │
│ +} │
│ │
│ const generatedAt = stats?.generated_at ? n │
│ ew Date(stats.generated_at) : null; │
│ const scopeLabel = `${REGION_LABELS[region] │
│ ?? region.toUpperCase()} - ${SEASON_LABELS[ │
│ seasonKey] ?? seasonKey}`; │
└──────────────────────────────────────────────┘
┌─ web/src/pages/challenge-mode/stats.astro ─────────────────────────────────┐
│ diff --git a/web/src/pages/challenge-mode/stats.astro b/web/src/pages/challenge-mo │
│ de/stats.astro │
│ index 20b594d..5b38b9a 100644 │
│ --- a/web/src/pages/challenge-mode/stats.astro │
│ +++ b/web/src/pages/challenge-mode/stats.astro │
│ @@ -2,5 +2,28 @@ │
│ // legacy entry point: redirect to the canonical /season{N}/stats/{region} │
│ export const prerender = false; │
│ │
│ -return Astro.redirect("/challenge-mode/season2/stats/global", 301); │
│ +let currentSeasonId = 1; │
│ +try { │
│ + const response = await fetch( │
│ + `${Astro.url.origin}/api/leaderboard/season/index.json`, │
│ + ); │
│ + if (response.ok) { │
│ + const data = await response.json(); │
│ + const currentSeasons = data.data.filter((s: any) => s.is_current); │
│ + const currentSeason = │
│ + currentSeasons.length > 0 │
│ + ? currentSeasons[currentSeasons.length - 1] │
│ + : data.data[data.data.length - 1]; │
│ + if (currentSeason) { │
│ + currentSeasonId = currentSeason.id; │
│ + } │
│ + } │
│ +} catch (error) { │
│ + console.error("Failed to fetch current season:", error); │
│ +} │
│ + │
│ +return Astro.redirect( │
│ + `/challenge-mode/season${currentSeasonId}/stats/global`, │
│ + 301, │
│ +); │
│ --- │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...es/challenge-mode/stats.astro ───┐
│ diff --git a/web/src/pages/challenge-mode/st │
│ ats.astro b/web/src/pages/challenge-mode/sta │
│ ts.astro │
│ index 20b594d..5b38b9a 100644 │
│ --- a/web/src/pages/challenge-mode/stats.ast │
│ ro │
│ +++ b/web/src/pages/challenge-mode/stats.ast │
│ ro │
│ @@ -2,5 +2,28 @@ │
│ // legacy entry point: redirect to the cano │
│ nical /season{N}/stats/{region} │
│ export const prerender = false; │
│ │
│ -return Astro.redirect("/challenge-mode/seas │
│ on2/stats/global", 301); │
│ +let currentSeasonId = 1; │
│ +try { │
│ + const response = await fetch( │
│ + `${Astro.url.origin}/api/leaderboard/se │
│ ason/index.json`, │
│ + ); │
│ + if (response.ok) { │
│ + const data = await response.json(); │
│ + const currentSeasons = data.data.filter │
│ ((s: any) => s.is_current); │
│ + const currentSeason = │
│ + currentSeasons.length > 0 │
│ + ? currentSeasons[currentSeasons.len │
│ gth - 1] │
│ + : data.data[data.data.length - 1]; │
│ + if (currentSeason) { │
│ + currentSeasonId = currentSeason.id; │
│ + } │
│ + } │
│ +} catch (error) { │
│ + console.error("Failed to fetch current se │
│ ason:", error); │
│ +} │
│ + │
│ +return Astro.redirect( │
│ + `/challenge-mode/season${currentSeasonId} │
│ /stats/global`, │
│ + 301, │
│ +); │
│ --- │
└──────────────────────────────────────────────┘
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET