HASH 2447ab9e5e85
DATE 2025-11-22
SUBJECT web: status page
FILES 17 CHANGED
HASH 2447ab9e5e85
DATE 2025-11-22
SUBJECT web: status page
FILES 17 CHANGED
┌─ web/.astro/data-store.json ───────────────────────────────────────────────┐
│ diff --git a/web/.astro/data-store.json b/web/.astro/data-store.json │
│ index 94ea62a..50de610 100644 │
│ --- a/web/.astro/data-store.json │
│ +++ b/web/.astro/data-store.json │
│ @@ -1 +1 @@ │
│ -[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.12.0","astro-config- │
│ digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{} │
│ ,\"site\":\"https://wowsimstats.com\",\"compressHTML\":true,\"base\":\"/\",\"trail │
│ ingSlash\":\"ignore\",\"output\":\"server\",\"scopedStyleStrategy\":\"attribute\", │
│ \"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astr │
│ o\",\"serverEntry\":\"entry.mjs\",\"redirects\":false,\"inlineStylesheets\":\"auto │
│ \",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":37287,\"s │
│ treaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"r │
│ oute\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\" │
│ config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"de │
│ vToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\ │
│ ",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"th │
│ eme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPl │
│ ugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":t │
│ rue},\"security\":{\"checkOrigin\":true},\"env\":{\"schema\":{},\"validateSecrets\ │
│ ":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false │
│ ,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\ │
│ ":false,\"csp\":false,\"rawEnvValues\":false},\"legacy\":{\"collections\":false}}" │
│ ] │
│ \ No newline at end of file │
│ +[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.12.0","astro-config- │
│ digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{} │
│ ,\"site\":\"https://wowsimstats.com\",\"compressHTML\":true,\"base\":\"/\",\"trail │
│ ingSlash\":\"ignore\",\"output\":\"server\",\"scopedStyleStrategy\":\"attribute\", │
│ \"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astr │
│ o\",\"serverEntry\":\"entry.mjs\",\"redirects\":false,\"inlineStylesheets\":\"auto │
│ \",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":26617,\"s │
│ treaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"r │
│ oute\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\" │
│ config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"de │
│ vToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\ │
│ ",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"th │
│ eme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPl │
│ ugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":t │
│ rue},\"security\":{\"checkOrigin\":true},\"env\":{\"schema\":{},\"validateSecrets\ │
│ ":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false │
│ ,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\ │
│ ":false,\"csp\":false,\"rawEnvValues\":false},\"legacy\":{\"collections\":false}}" │
│ ] │
│ \ No newline at end of file │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ web/.astro/data-store.json ─────────┐
│ diff --git a/web/.astro/data-store.json b/we │
│ b/.astro/data-store.json │
│ index 94ea62a..50de610 100644 │
│ --- a/web/.astro/data-store.json │
│ +++ b/web/.astro/data-store.json │
│ @@ -1 +1 @@ │
│ -[["Map",1,2],"meta::meta",["Map",3,4,5,6]," │
│ astro-version","5.12.0","astro-config-digest │
│ ","{\"root\":{},\"srcDir\":{},\"publicDir\": │
│ {},\"outDir\":{},\"cacheDir\":{},\"site\":\" │
│ https://wowsimstats.com\",\"compressHTML\":t │
│ rue,\"base\":\"/\",\"trailingSlash\":\"ignor │
│ e\",\"output\":\"server\",\"scopedStyleStrat │
│ egy\":\"attribute\",\"build\":{\"format\":\" │
│ directory\",\"client\":{},\"server\":{},\"as │
│ sets\":\"_astro\",\"serverEntry\":\"entry.mj │
│ s\",\"redirects\":false,\"inlineStylesheets\ │
│ ":\"auto\",\"concurrency\":1},\"server\":{\" │
│ open\":false,\"host\":false,\"port\":37287,\ │
│ "streaming\":true,\"allowedHosts\":[]},\"red │
│ irects\":{},\"image\":{\"endpoint\":{\"route │
│ \":\"/_image\"},\"service\":{\"entrypoint\": │
│ \"astro/assets/services/sharp\",\"config\":{ │
│ }},\"domains\":[],\"remotePatterns\":[],\"re │
│ sponsiveStyles\":false},\"devToolbar\":{\"en │
│ abled\":true},\"markdown\":{\"syntaxHighligh │
│ t\":{\"type\":\"shiki\",\"excludeLangs\":[\" │
│ math\"]},\"shikiConfig\":{\"langs\":[],\"lan │
│ gAlias\":{},\"theme\":\"github-dark\",\"them │
│ es\":{},\"wrap\":false,\"transformers\":[]}, │
│ \"remarkPlugins\":[],\"rehypePlugins\":[],\" │
│ remarkRehype\":{},\"gfm\":true,\"smartypants │
│ \":true},\"security\":{\"checkOrigin\":true} │
│ ,\"env\":{\"schema\":{},\"validateSecrets\": │
│ false},\"experimental\":{\"clientPrerender\" │
│ :false,\"contentIntellisense\":false,\"headi │
│ ngIdCompat\":false,\"preserveScriptOrder\":f │
│ alse,\"liveContentCollections\":false,\"csp\ │
│ ":false,\"rawEnvValues\":false},\"legacy\":{ │
│ \"collections\":false}}"] │
│ \ No newline at end of file │
│ +[["Map",1,2],"meta::meta",["Map",3,4,5,6]," │
│ astro-version","5.12.0","astro-config-digest │
│ ","{\"root\":{},\"srcDir\":{},\"publicDir\": │
│ {},\"outDir\":{},\"cacheDir\":{},\"site\":\" │
│ https://wowsimstats.com\",\"compressHTML\":t │
│ rue,\"base\":\"/\",\"trailingSlash\":\"ignor │
│ e\",\"output\":\"server\",\"scopedStyleStrat │
│ egy\":\"attribute\",\"build\":{\"format\":\" │
│ directory\",\"client\":{},\"server\":{},\"as │
│ sets\":\"_astro\",\"serverEntry\":\"entry.mj │
│ s\",\"redirects\":false,\"inlineStylesheets\ │
│ ":\"auto\",\"concurrency\":1},\"server\":{\" │
│ open\":false,\"host\":false,\"port\":26617,\ │
│ "streaming\":true,\"allowedHosts\":[]},\"red │
│ irects\":{},\"image\":{\"endpoint\":{\"route │
│ \":\"/_image\"},\"service\":{\"entrypoint\": │
│ \"astro/assets/services/sharp\",\"config\":{ │
│ }},\"domains\":[],\"remotePatterns\":[],\"re │
│ sponsiveStyles\":false},\"devToolbar\":{\"en │
│ abled\":true},\"markdown\":{\"syntaxHighligh │
│ t\":{\"type\":\"shiki\",\"excludeLangs\":[\" │
│ math\"]},\"shikiConfig\":{\"langs\":[],\"lan │
│ gAlias\":{},\"theme\":\"github-dark\",\"them │
│ es\":{},\"wrap\":false,\"transformers\":[]}, │
│ \"remarkPlugins\":[],\"rehypePlugins\":[],\" │
│ remarkRehype\":{},\"gfm\":true,\"smartypants │
│ \":true},\"security\":{\"checkOrigin\":true} │
│ ,\"env\":{\"schema\":{},\"validateSecrets\": │
│ false},\"experimental\":{\"clientPrerender\" │
│ :false,\"contentIntellisense\":false,\"headi │
│ ngIdCompat\":false,\"preserveScriptOrder\":f │
│ alse,\"liveContentCollections\":false,\"csp\ │
│ ":false,\"rawEnvValues\":false},\"legacy\":{ │
│ \"collections\":false}}"] │
│ \ No newline at end of file │
└──────────────────────────────────────────────┘
┌─ web/.astro/types.d.ts ────────────────────────────────────────────────────┐
│ diff --git a/web/.astro/types.d.ts b/web/.astro/types.d.ts │
│ index f964fe0..03d7cc4 100644 │
│ --- a/web/.astro/types.d.ts │
│ +++ b/web/.astro/types.d.ts │
│ @@ -1 +1,2 @@ │
│ /// <reference types="astro/client" /> │
│ +/// <reference path="content.d.ts" /> │
│ \ No newline at end of file │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ web/.astro/types.d.ts ──────────────┐
│ diff --git a/web/.astro/types.d.ts b/web/.as │
│ tro/types.d.ts │
│ index f964fe0..03d7cc4 100644 │
│ --- a/web/.astro/types.d.ts │
│ +++ b/web/.astro/types.d.ts │
│ @@ -1 +1,2 @@ │
│ /// <reference types="astro/client" /> │
│ +/// <reference path="content.d.ts" /> │
│ \ No newline at end of file │
└──────────────────────────────────────────────┘
┌─ .../components/Leaderboard/LeaderboardTypeNav/LeaderboardTypeNav.astro ───┐
│ diff --git a/web/src/components/Leaderboard/LeaderboardTypeNav/LeaderboardTypeNav. │
│ astro b/web/src/components/Leaderboard/LeaderboardTypeNav/LeaderboardTypeNav.astro │
│ index 544cc09..52e29a3 100644 │
│ --- a/web/src/components/Leaderboard/LeaderboardTypeNav/LeaderboardTypeNav.astro │
│ +++ b/web/src/components/Leaderboard/LeaderboardTypeNav/LeaderboardTypeNav.astro │
│ @@ -2,7 +2,8 @@ │
│ import "./LeaderboardTypeNav.scss"; │
│ │
│ interface Props { │
│ - currentType: "dungeon" | "player"; │
│ + currentType?: "dungeon" | "player"; │
│ + currentTab?: "dungeon" | "player" | "status"; │
│ currentRegion?: string; │
│ currentRealm?: string; │
│ currentSeason?: number; │
│ @@ -11,7 +12,8 @@ interface Props { │
│ } │
│ │
│ const { │
│ - currentType, │
│ + currentType = "dungeon", │
│ + currentTab = currentType, │
│ currentRegion = "global", │
│ currentRealm = "", │
│ currentSeason = 1, │
│ @@ -47,16 +49,22 @@ if (currentRegion === "global") { │
│ <nav class="leaderboard-type-nav"> │
│ <a │
│ href={dungeonUrl} │
│ - class:list={["nav-tab", { active: currentType === "dungeon" }]} │
│ + class:list={["nav-tab", { active: currentTab === "dungeon" }]} │
│ > │
│ Dungeons │
│ </a> │
│ <a │
│ href={playerUrl} │
│ - class:list={["nav-tab", { active: currentType === "player" }]} │
│ + class:list={["nav-tab", { active: currentTab === "player" }]} │
│ > │
│ Players │
│ </a> │
│ + <a │
│ + href="/challenge-mode/status" │
│ + class:list={["nav-tab", { active: currentTab === "status" }]} │
│ + > │
│ + Status │
│ + </a> │
│ <a │
│ class="nav-tab discord-link" │
│ href="https://discord.gg/etxRSDNxFr" │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...eNav/LeaderboardTypeNav.astro ───┐
│ diff --git a/web/src/components/Leaderboard/ │
│ LeaderboardTypeNav/LeaderboardTypeNav.astro │
│ b/web/src/components/Leaderboard/Leaderboard │
│ TypeNav/LeaderboardTypeNav.astro │
│ index 544cc09..52e29a3 100644 │
│ --- a/web/src/components/Leaderboard/Leaderb │
│ oardTypeNav/LeaderboardTypeNav.astro │
│ +++ b/web/src/components/Leaderboard/Leaderb │
│ oardTypeNav/LeaderboardTypeNav.astro │
│ @@ -2,7 +2,8 @@ │
│ import "./LeaderboardTypeNav.scss"; │
│ │
│ interface Props { │
│ - currentType: "dungeon" | "player"; │
│ + currentType?: "dungeon" | "player"; │
│ + currentTab?: "dungeon" | "player" | "stat │
│ us"; │
│ currentRegion?: string; │
│ currentRealm?: string; │
│ currentSeason?: number; │
│ @@ -11,7 +12,8 @@ interface Props { │
│ } │
│ │
│ const { │
│ - currentType, │
│ + currentType = "dungeon", │
│ + currentTab = currentType, │
│ currentRegion = "global", │
│ currentRealm = "", │
│ currentSeason = 1, │
│ @@ -47,16 +49,22 @@ if (currentRegion === "g │
│ lobal") { │
│ <nav class="leaderboard-type-nav"> │
│ <a │
│ href={dungeonUrl} │
│ - class:list={["nav-tab", { active: curre │
│ ntType === "dungeon" }]} │
│ + class:list={["nav-tab", { active: curre │
│ ntTab === "dungeon" }]} │
│ > │
│ Dungeons │
│ </a> │
│ <a │
│ href={playerUrl} │
│ - class:list={["nav-tab", { active: curre │
│ ntType === "player" }]} │
│ + class:list={["nav-tab", { active: curre │
│ ntTab === "player" }]} │
│ > │
│ Players │
│ </a> │
│ + <a │
│ + href="/challenge-mode/status" │
│ + class:list={["nav-tab", { active: curre │
│ ntTab === "status" }]} │
│ + > │
│ + Status │
│ + </a> │
│ <a │
│ class="nav-tab discord-link" │
│ href="https://discord.gg/etxRSDNxFr" │
└──────────────────────────────────────────────┘
┌─ ...c/components/Leaderboard/LeaderboardTypeNav/LeaderboardTypeNav.scss ───┐
│ diff --git a/web/src/components/Leaderboard/LeaderboardTypeNav/LeaderboardTypeNav. │
│ scss b/web/src/components/Leaderboard/LeaderboardTypeNav/LeaderboardTypeNav.scss │
│ index 83664a2..0d42c68 100644 │
│ --- a/web/src/components/Leaderboard/LeaderboardTypeNav/LeaderboardTypeNav.scss │
│ +++ b/web/src/components/Leaderboard/LeaderboardTypeNav/LeaderboardTypeNav.scss │
│ @@ -6,6 +6,7 @@ │
│ margin-bottom: $spacing-lg; │
│ border-bottom: 2px solid var(--border-color); │
│ align-items: center; │
│ + flex-wrap: wrap; │
│ } │
│ │
│ .nav-tab { │
│ @@ -49,9 +50,19 @@ │
│ } │
│ │
│ @media (max-width: 768px) { │
│ + .leaderboard-type-nav { │
│ + gap: $spacing-xs; │
│ + } │
│ + │
│ .nav-tab { │
│ - flex: 1; │
│ + flex: 1 1 calc(50% - $spacing-xs); │
│ text-align: center; │
│ padding: $spacing-sm $spacing-md; │
│ } │
│ + │
│ + .discord-link { │
│ + flex: 1 1 100%; │
│ + margin-left: 0; │
│ + margin-top: $spacing-xs; │
│ + } │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...peNav/LeaderboardTypeNav.scss ───┐
│ diff --git a/web/src/components/Leaderboard/ │
│ LeaderboardTypeNav/LeaderboardTypeNav.scss b │
│ /web/src/components/Leaderboard/LeaderboardT │
│ ypeNav/LeaderboardTypeNav.scss │
│ index 83664a2..0d42c68 100644 │
│ --- a/web/src/components/Leaderboard/Leaderb │
│ oardTypeNav/LeaderboardTypeNav.scss │
│ +++ b/web/src/components/Leaderboard/Leaderb │
│ oardTypeNav/LeaderboardTypeNav.scss │
│ @@ -6,6 +6,7 @@ │
│ margin-bottom: $spacing-lg; │
│ border-bottom: 2px solid var(--border-col │
│ or); │
│ align-items: center; │
│ + flex-wrap: wrap; │
│ } │
│ │
│ .nav-tab { │
│ @@ -49,9 +50,19 @@ │
│ } │
│ │
│ @media (max-width: 768px) { │
│ + .leaderboard-type-nav { │
│ + gap: $spacing-xs; │
│ + } │
│ + │
│ .nav-tab { │
│ - flex: 1; │
│ + flex: 1 1 calc(50% - $spacing-xs); │
│ text-align: center; │
│ padding: $spacing-sm $spacing-md; │
│ } │
│ + │
│ + .discord-link { │
│ + flex: 1 1 100%; │
│ + margin-left: 0; │
│ + margin-top: $spacing-xs; │
│ + } │
│ } │
└──────────────────────────────────────────────┘
┌─ ...s/Leaderboard/RegionRealmFilterSelect/RegionRealmFilterSelect.astro ───┐
│ diff --git a/web/src/components/Leaderboard/RegionRealmFilterSelect/RegionRealmFil │
│ terSelect.astro b/web/src/components/Leaderboard/RegionRealmFilterSelect/RegionRea │
│ lmFilterSelect.astro │
│ index 634cfcf..32cab2a 100644 │
│ --- a/web/src/components/Leaderboard/RegionRealmFilterSelect/RegionRealmFilterSele │
│ ct.astro │
│ +++ b/web/src/components/Leaderboard/RegionRealmFilterSelect/RegionRealmFilterSele │
│ ct.astro │
│ @@ -1,10 +1,15 @@ │
│ --- │
│ -import type { LatestRunEntry } from "../../../lib/types"; │
│ import { REGION_OPTIONS } from "../../../lib/wow-constants"; │
│ import "./RegionRealmFilterSelect.scss"; │
│ │
│ +type RealmOptionSource = { │
│ + region: string; │
│ + realm_slug: string; │
│ + realm_name: string; │
│ +}; │
│ + │
│ interface Props { │
│ - runs: LatestRunEntry[]; │
│ + runs: RealmOptionSource[]; │
│ currentRegion?: string; │
│ currentRealm?: string; │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...RegionRealmFilterSelect.astro ───┐
│ diff --git a/web/src/components/Leaderboard/ │
│ RegionRealmFilterSelect/RegionRealmFilterSel │
│ ect.astro b/web/src/components/Leaderboard/R │
│ egionRealmFilterSelect/RegionRealmFilterSele │
│ ct.astro │
│ index 634cfcf..32cab2a 100644 │
│ --- a/web/src/components/Leaderboard/RegionR │
│ ealmFilterSelect/RegionRealmFilterSelect.ast │
│ ro │
│ +++ b/web/src/components/Leaderboard/RegionR │
│ ealmFilterSelect/RegionRealmFilterSelect.ast │
│ ro │
│ @@ -1,10 +1,15 @@ │
│ --- │
│ -import type { LatestRunEntry } from "../../ │
│ ../lib/types"; │
│ import { REGION_OPTIONS } from "../../../li │
│ b/wow-constants"; │
│ import "./RegionRealmFilterSelect.scss"; │
│ │
│ +type RealmOptionSource = { │
│ + region: string; │
│ + realm_slug: string; │
│ + realm_name: string; │
│ +}; │
│ + │
│ interface Props { │
│ - runs: LatestRunEntry[]; │
│ + runs: RealmOptionSource[]; │
│ currentRegion?: string; │
│ currentRealm?: string; │
│ } │
└──────────────────────────────────────────────┘
┌─ web/src/components/Leaderboard/TeamComposition/TeamComposition.astro ─────┐
│ diff --git a/web/src/components/Leaderboard/TeamComposition/TeamComposition.astro │
│ b/web/src/components/Leaderboard/TeamComposition/TeamComposition.astro │
│ index b55252e..cd90cba 100644 │
│ --- a/web/src/components/Leaderboard/TeamComposition/TeamComposition.astro │
│ +++ b/web/src/components/Leaderboard/TeamComposition/TeamComposition.astro │
│ @@ -1,12 +1,12 @@ │
│ --- │
│ -import type { LatestRunMember, TeamMember } from "../../../lib/types"; │
│ +import type { TeamMember } from "../../../lib/types"; │
│ import PlayerLink from "../PlayerLink/PlayerLink.astro"; │
│ import { getSpecInfo } from "../../../lib/wow-constants"; │
│ import { getEffectiveRealmSlug } from "../../../lib/realms"; │
│ import "./TeamComposition.scss"; │
│ │
│ interface Props { │
│ - members: LatestRunMember[] | TeamMember[]; │
│ + members: TeamMember[]; │
│ currentRegion?: string; │
│ currentRealm?: string; │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...osition/TeamComposition.astro ───┐
│ diff --git a/web/src/components/Leaderboard/ │
│ TeamComposition/TeamComposition.astro b/web/ │
│ src/components/Leaderboard/TeamComposition/T │
│ eamComposition.astro │
│ index b55252e..cd90cba 100644 │
│ --- a/web/src/components/Leaderboard/TeamCom │
│ position/TeamComposition.astro │
│ +++ b/web/src/components/Leaderboard/TeamCom │
│ position/TeamComposition.astro │
│ @@ -1,12 +1,12 @@ │
│ --- │
│ -import type { LatestRunMember, TeamMember } │
│ from "../../../lib/types"; │
│ +import type { TeamMember } from "../../../l │
│ ib/types"; │
│ import PlayerLink from "../PlayerLink/Playe │
│ rLink.astro"; │
│ import { getSpecInfo } from "../../../lib/w │
│ ow-constants"; │
│ import { getEffectiveRealmSlug } from "../. │
│ ./../lib/realms"; │
│ import "./TeamComposition.scss"; │
│ │
│ interface Props { │
│ - members: LatestRunMember[] | TeamMember[] │
│ ; │
│ + members: TeamMember[]; │
│ currentRegion?: string; │
│ currentRealm?: string; │
│ } │
└──────────────────────────────────────────────┘
┌─ ...mponents/Status/StatusDungeonContainer/StatusDungeonContainer.astro ───┐
│ diff --git a/web/src/components/Status/StatusDungeonContainer/StatusDungeonContain │
│ er.astro b/web/src/components/Status/StatusDungeonContainer/StatusDungeonContainer │
│ .astro │
│ new file mode 100644 │
│ index 0000000..b76bef3 │
│ --- /dev/null │
│ +++ b/web/src/components/Status/StatusDungeonContainer/StatusDungeonContainer.astr │
│ o │
│ @@ -0,0 +1,69 @@ │
│ +--- │
│ +import type { CoverageHealth, DungeonCoverage } from "../../../lib/types"; │
│ +import "./StatusDungeonContainer.scss"; │
│ +import { getDungeonIconUrl } from "../../../lib/dungeonIcons"; │
│ + │
│ +interface Props { │
│ + dungeon: DungeonCoverage; │
│ + healthLabels: Record<CoverageHealth, string>; │
│ + formatList: (items?: number[]) => string; │
│ + latestPeriod?: number | null; │
│ +} │
│ + │
│ +const { dungeon, healthLabels, formatList, latestPeriod } = Astro.props; │
│ +const latestPeriodNumber = │
│ + typeof latestPeriod === "number" ? latestPeriod : null; │
│ +const coversLatest = │
│ + latestPeriodNumber !== null && │
│ + Array.isArray(dungeon.periods) && │
│ + dungeon.periods.includes(latestPeriodNumber); │
│ +const missingLatest = │
│ + latestPeriodNumber !== null && │
│ + (!coversLatest || │
│ + (Array.isArray(dungeon.missing_periods) && │
│ + dungeon.missing_periods.includes(latestPeriodNumber))); │
│ + │
│ +const latestObserved = (() => { │
│ + const periods = dungeon.periods || []; │
│ + const missing = new Set(dungeon.missing_periods || []); │
│ + const candidates = periods.filter((p) => !missing.has(p)); │
│ + if (candidates.length === 0) { │
│ + return null; │
│ + } │
│ + return candidates.reduce( │
│ + (max, val) => (val > max ? val : max), │
│ + candidates[0], │
│ + ); │
│ +})(); │
│ + │
│ +const dungeonIconUrl = getDungeonIconUrl( │
│ + dungeon.dungeon_slug, │
│ + dungeon.dungeon_name, │
│ +); │
│ +--- │
│ + │
│ +<li │
│ + class={`status-dungeon ${ │
│ + coversLatest ? "status-dungeon--latest" : "" │
│ + } ${missingLatest ? "status-dungeon--missing" : ""}`} │
│ +> │
│ + <div class="status-dungeon__head"> │
│ + <span class="status-dungeon__name"> │
│ + { │
│ + dungeonIconUrl && ( │
│ + <img │
│ + src={dungeonIconUrl} │
│ + alt={dungeon.dungeon_name} │
│ + loading="lazy" │
│ + class="status-dungeon__icon" │
│ + /> │
│ + ) │
│ + } │
│ + {dungeon.dungeon_name} │
│ + </span> │
│ + </div> │
│ + │
│ + <p class="status-dungeon__latest"> │
│ + Latest period seen: {latestObserved ?? "None"} │
│ + </p> │
│ +</li> │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ .../StatusDungeonContainer.astro ───┐
│ diff --git a/web/src/components/Status/Statu │
│ sDungeonContainer/StatusDungeonContainer.ast │
│ ro b/web/src/components/Status/StatusDungeon │
│ Container/StatusDungeonContainer.astro │
│ new file mode 100644 │
│ index 0000000..b76bef3 │
│ --- /dev/null │
│ +++ b/web/src/components/Status/StatusDungeo │
│ nContainer/StatusDungeonContainer.astro │
│ @@ -0,0 +1,69 @@ │
│ +--- │
│ +import type { CoverageHealth, DungeonCovera │
│ ge } from "../../../lib/types"; │
│ +import "./StatusDungeonContainer.scss"; │
│ +import { getDungeonIconUrl } from "../../.. │
│ /lib/dungeonIcons"; │
│ + │
│ +interface Props { │
│ + dungeon: DungeonCoverage; │
│ + healthLabels: Record<CoverageHealth, stri │
│ ng>; │
│ + formatList: (items?: number[]) => string; │
│ + latestPeriod?: number | null; │
│ +} │
│ + │
│ +const { dungeon, healthLabels, formatList, │
│ latestPeriod } = Astro.props; │
│ +const latestPeriodNumber = │
│ + typeof latestPeriod === "number" ? latest │
│ Period : null; │
│ +const coversLatest = │
│ + latestPeriodNumber !== null && │
│ + Array.isArray(dungeon.periods) && │
│ + dungeon.periods.includes(latestPeriodNumb │
│ er); │
│ +const missingLatest = │
│ + latestPeriodNumber !== null && │
│ + (!coversLatest || │
│ + (Array.isArray(dungeon.missing_periods) │
│ && │
│ + dungeon.missing_periods.includes(late │
│ stPeriodNumber))); │
│ + │
│ +const latestObserved = (() => { │
│ + const periods = dungeon.periods || []; │
│ + const missing = new Set(dungeon.missing_p │
│ eriods || []); │
│ + const candidates = periods.filter((p) => │
│ !missing.has(p)); │
│ + if (candidates.length === 0) { │
│ + return null; │
│ + } │
│ + return candidates.reduce( │
│ + (max, val) => (val > max ? val : max), │
│ + candidates[0], │
│ + ); │
│ +})(); │
│ + │
│ +const dungeonIconUrl = getDungeonIconUrl( │
│ + dungeon.dungeon_slug, │
│ + dungeon.dungeon_name, │
│ +); │
│ +--- │
│ + │
│ +<li │
│ + class={`status-dungeon ${ │
│ + coversLatest ? "status-dungeon--latest" │
│ : "" │
│ + } ${missingLatest ? "status-dungeon--miss │
│ ing" : ""}`} │
│ +> │
│ + <div class="status-dungeon__head"> │
│ + <span class="status-dungeon__name"> │
│ + { │
│ + dungeonIconUrl && ( │
│ + <img │
│ + src={dungeonIconUrl} │
│ + alt={dungeon.dungeon_name} │
│ + loading="lazy" │
│ + class="status-dungeon__icon" │
│ + /> │
│ + ) │
│ + } │
│ + {dungeon.dungeon_name} │
│ + </span> │
│ + </div> │
│ + │
│ + <p class="status-dungeon__latest"> │
│ + Latest period seen: {latestObserved ?? │
│ "None"} │
│ + </p> │
│ +</li> │
└──────────────────────────────────────────────┘
┌─ ...omponents/Status/StatusDungeonContainer/StatusDungeonContainer.scss ───┐
│ diff --git a/web/src/components/Status/StatusDungeonContainer/StatusDungeonContain │
│ er.scss b/web/src/components/Status/StatusDungeonContainer/StatusDungeonContainer. │
│ scss │
│ new file mode 100644 │
│ index 0000000..db1b7e0 │
│ --- /dev/null │
│ +++ b/web/src/components/Status/StatusDungeonContainer/StatusDungeonContainer.scss │
│ @@ -0,0 +1,71 @@ │
│ +@use "../../../styles/core/tokens" as *; │
│ + │
│ +.status-dungeon { │
│ + border: 1px solid var(--border-color); │
│ + border-radius: $radius-md; │
│ + padding: $spacing-sm; │
│ + background: rgba(255, 255, 255, 0.01); │
│ + list-style: none; │
│ + display: flex; │
│ + flex-direction: column; │
│ + gap: $spacing-xs; │
│ +} │
│ + │
│ +.status-dungeon--latest { │
│ + border-color: var(--success-color); │
│ +} │
│ + │
│ +.status-dungeon__head { │
│ + display: flex; │
│ + justify-content: space-between; │
│ + align-items: center; │
│ + gap: $spacing-xs; │
│ +} │
│ + │
│ +.status-dungeon__name { │
│ + font-weight: 600; │
│ + display: flex; │
│ + align-items: center; │
│ + gap: $spacing-xs; │
│ +} │
│ + │
│ +.status-dungeon__icon { │
│ + width: 28px; │
│ + height: 28px; │
│ + border-radius: $radius-sm; │
│ + border: 1px solid rgba(255, 255, 255, 0.1); │
│ + object-fit: cover; │
│ +} │
│ + │
│ +.status-dungeon__latest { │
│ + margin: 0; │
│ + font-size: $font-size-sm; │
│ + color: var(--text-secondary); │
│ +} │
│ + │
│ +.status-dungeon__badges { │
│ + display: flex; │
│ + flex-wrap: wrap; │
│ + gap: $spacing-xs; │
│ +} │
│ + │
│ +.status-dungeon__badge { │
│ + padding: 2px 8px; │
│ + border-radius: $radius-sm; │
│ + font-size: $font-size-xs; │
│ + font-weight: 600; │
│ + text-transform: uppercase; │
│ +} │
│ + │
│ +.status-dungeon__badge--positive { │
│ + background: rgba(56, 187, 92, 0.15); │
│ + color: var(--success-color); │
│ +} │
│ + │
│ +.status-dungeon__badge--negative { │
│ + background: rgba(239, 68, 68, 0.15); │
│ + color: var(--error-color); │
│ +} │
│ +.status-dungeon--missing { │
│ + border-color: var(--error-color); │
│ +} │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...r/StatusDungeonContainer.scss ───┐
│ diff --git a/web/src/components/Status/Statu │
│ sDungeonContainer/StatusDungeonContainer.scs │
│ s b/web/src/components/Status/StatusDungeonC │
│ ontainer/StatusDungeonContainer.scss │
│ new file mode 100644 │
│ index 0000000..db1b7e0 │
│ --- /dev/null │
│ +++ b/web/src/components/Status/StatusDungeo │
│ nContainer/StatusDungeonContainer.scss │
│ @@ -0,0 +1,71 @@ │
│ +@use "../../../styles/core/tokens" as *; │
│ + │
│ +.status-dungeon { │
│ + border: 1px solid var(--border-color); │
│ + border-radius: $radius-md; │
│ + padding: $spacing-sm; │
│ + background: rgba(255, 255, 255, 0.01); │
│ + list-style: none; │
│ + display: flex; │
│ + flex-direction: column; │
│ + gap: $spacing-xs; │
│ +} │
│ + │
│ +.status-dungeon--latest { │
│ + border-color: var(--success-color); │
│ +} │
│ + │
│ +.status-dungeon__head { │
│ + display: flex; │
│ + justify-content: space-between; │
│ + align-items: center; │
│ + gap: $spacing-xs; │
│ +} │
│ + │
│ +.status-dungeon__name { │
│ + font-weight: 600; │
│ + display: flex; │
│ + align-items: center; │
│ + gap: $spacing-xs; │
│ +} │
│ + │
│ +.status-dungeon__icon { │
│ + width: 28px; │
│ + height: 28px; │
│ + border-radius: $radius-sm; │
│ + border: 1px solid rgba(255, 255, 255, 0.1 │
│ ); │
│ + object-fit: cover; │
│ +} │
│ + │
│ +.status-dungeon__latest { │
│ + margin: 0; │
│ + font-size: $font-size-sm; │
│ + color: var(--text-secondary); │
│ +} │
│ + │
│ +.status-dungeon__badges { │
│ + display: flex; │
│ + flex-wrap: wrap; │
│ + gap: $spacing-xs; │
│ +} │
│ + │
│ +.status-dungeon__badge { │
│ + padding: 2px 8px; │
│ + border-radius: $radius-sm; │
│ + font-size: $font-size-xs; │
│ + font-weight: 600; │
│ + text-transform: uppercase; │
│ +} │
│ + │
│ +.status-dungeon__badge--positive { │
│ + background: rgba(56, 187, 92, 0.15); │
│ + color: var(--success-color); │
│ +} │
│ + │
│ +.status-dungeon__badge--negative { │
│ + background: rgba(239, 68, 68, 0.15); │
│ + color: var(--error-color); │
│ +} │
│ +.status-dungeon--missing { │
│ + border-color: var(--error-color); │
│ +} │
└──────────────────────────────────────────────┘
┌─ ...c/components/Status/StatusRealmContainer/StatusRealmContainer.astro ───┐
│ diff --git a/web/src/components/Status/StatusRealmContainer/StatusRealmContainer.a │
│ stro b/web/src/components/Status/StatusRealmContainer/StatusRealmContainer.astro │
│ new file mode 100644 │
│ index 0000000..5791995 │
│ --- /dev/null │
│ +++ b/web/src/components/Status/StatusRealmContainer/StatusRealmContainer.astro │
│ @@ -0,0 +1,74 @@ │
│ +--- │
│ +import type { CoverageHealth, RealmCoverage } from "../../../lib/types"; │
│ +import StatusDungeonContainer from "../StatusDungeonContainer/StatusDungeonContai │
│ ner.astro"; │
│ +import "./StatusRealmContainer.scss"; │
│ + │
│ +interface Props { │
│ + realm: RealmCoverage; │
│ + healthLabels: Record<CoverageHealth, string>; │
│ + formatList: (items?: number[]) => string; │
│ + latestPeriod?: number | null; │
│ +} │
│ + │
│ +const { realm, healthLabels, formatList, latestPeriod } = Astro.props; │
│ + │
│ +const regionLabel = realm.region.toUpperCase(); │
│ +const latestPeriodNumber = │
│ + typeof latestPeriod === "number" ? latestPeriod : null; │
│ +const hasGoodLatest = │
│ + latestPeriodNumber !== null && │
│ + realm.dungeons.some((d) => { │
│ + const periods = d.periods || []; │
│ + const missing = d.missing_periods || []; │
│ + return ( │
│ + periods.includes(latestPeriodNumber) && │
│ + !missing.includes(latestPeriodNumber) │
│ + ); │
│ + }); │
│ + │
│ +const latestMissing = │
│ + !hasGoodLatest && │
│ + latestPeriodNumber !== null && │
│ + realm.dungeons.some((d) => │
│ + (d.missing_periods || []).includes(latestPeriodNumber), │
│ + ); │
│ + │
│ +const derivedHealth: CoverageHealth = │
│ + latestPeriodNumber !== null │
│ + ? hasGoodLatest │
│ + ? "ok" │
│ + : latestMissing │
│ + ? "some_missing" │
│ + : realm.health │
│ + : realm.health; │
│ +const healthLabel = healthLabels[derivedHealth]; │
│ +--- │
│ + │
│ +<article │
│ + class={`status-realm ${ │
│ + hasGoodLatest │
│ + ? "status-realm--healthy" │
│ + : latestMissing │
│ + ? "status-realm--missing" │
│ + : "" │
│ + }`} │
│ +> │
│ + <header class="status-realm__header"> │
│ + <div> │
│ + <h3>{realm.realm_name}</h3> │
│ + </div> │
│ + </header> │
│ + │
│ + <ul class="status-realm__dungeons"> │
│ + { │
│ + realm.dungeons.map((dungeon) => ( │
│ + <StatusDungeonContainer │
│ + dungeon={dungeon} │
│ + formatList={formatList} │
│ + healthLabels={healthLabels} │
│ + latestPeriod={latestPeriodNumber} │
│ + /> │
│ + )) │
│ + } │
│ + </ul> │
│ +</article> │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...er/StatusRealmContainer.astro ───┐
│ diff --git a/web/src/components/Status/Statu │
│ sRealmContainer/StatusRealmContainer.astro b │
│ /web/src/components/Status/StatusRealmContai │
│ ner/StatusRealmContainer.astro │
│ new file mode 100644 │
│ index 0000000..5791995 │
│ --- /dev/null │
│ +++ b/web/src/components/Status/StatusRealmC │
│ ontainer/StatusRealmContainer.astro │
│ @@ -0,0 +1,74 @@ │
│ +--- │
│ +import type { CoverageHealth, RealmCoverage │
│ } from "../../../lib/types"; │
│ +import StatusDungeonContainer from "../Stat │
│ usDungeonContainer/StatusDungeonContainer.as │
│ tro"; │
│ +import "./StatusRealmContainer.scss"; │
│ + │
│ +interface Props { │
│ + realm: RealmCoverage; │
│ + healthLabels: Record<CoverageHealth, stri │
│ ng>; │
│ + formatList: (items?: number[]) => string; │
│ + latestPeriod?: number | null; │
│ +} │
│ + │
│ +const { realm, healthLabels, formatList, la │
│ testPeriod } = Astro.props; │
│ + │
│ +const regionLabel = realm.region.toUpperCas │
│ e(); │
│ +const latestPeriodNumber = │
│ + typeof latestPeriod === "number" ? latest │
│ Period : null; │
│ +const hasGoodLatest = │
│ + latestPeriodNumber !== null && │
│ + realm.dungeons.some((d) => { │
│ + const periods = d.periods || []; │
│ + const missing = d.missing_periods || [] │
│ ; │
│ + return ( │
│ + periods.includes(latestPeriodNumber) │
│ && │
│ + !missing.includes(latestPeriodNumber) │
│ + ); │
│ + }); │
│ + │
│ +const latestMissing = │
│ + !hasGoodLatest && │
│ + latestPeriodNumber !== null && │
│ + realm.dungeons.some((d) => │
│ + (d.missing_periods || []).includes(late │
│ stPeriodNumber), │
│ + ); │
│ + │
│ +const derivedHealth: CoverageHealth = │
│ + latestPeriodNumber !== null │
│ + ? hasGoodLatest │
│ + ? "ok" │
│ + : latestMissing │
│ + ? "some_missing" │
│ + : realm.health │
│ + : realm.health; │
│ +const healthLabel = healthLabels[derivedHea │
│ lth]; │
│ +--- │
│ + │
│ +<article │
│ + class={`status-realm ${ │
│ + hasGoodLatest │
│ + ? "status-realm--healthy" │
│ + : latestMissing │
│ + ? "status-realm--missing" │
│ + : "" │
│ + }`} │
│ +> │
│ + <header class="status-realm__header"> │
│ + <div> │
│ + <h3>{realm.realm_name}</h3> │
│ + </div> │
│ + </header> │
│ + │
│ + <ul class="status-realm__dungeons"> │
│ + { │
│ + realm.dungeons.map((dungeon) => ( │
│ + <StatusDungeonContainer │
│ + dungeon={dungeon} │
│ + formatList={formatList} │
│ + healthLabels={healthLabels} │
│ + latestPeriod={latestPeriodNumber} │
│ + /> │
│ + )) │
│ + } │
│ + </ul> │
│ +</article> │
└──────────────────────────────────────────────┘
┌─ ...rc/components/Status/StatusRealmContainer/StatusRealmContainer.scss ───┐
│ diff --git a/web/src/components/Status/StatusRealmContainer/StatusRealmContainer.s │
│ css b/web/src/components/Status/StatusRealmContainer/StatusRealmContainer.scss │
│ new file mode 100644 │
│ index 0000000..7602229 │
│ --- /dev/null │
│ +++ b/web/src/components/Status/StatusRealmContainer/StatusRealmContainer.scss │
│ @@ -0,0 +1,52 @@ │
│ +@use "../../../styles/core/tokens" as *; │
│ + │
│ +.status-realm { │
│ + border-radius: $radius-lg; │
│ + border: 1px solid var(--border-color); │
│ + background: rgba(255, 255, 255, 0.02); │
│ + padding: $spacing-sm $spacing-md; │
│ + display: flex; │
│ + flex-direction: column; │
│ + gap: $spacing-sm; │
│ + min-height: 160px; │
│ +} │
│ + │
│ +.status-realm__header { │
│ + display: flex; │
│ + justify-content: space-between; │
│ + gap: $spacing-md; │
│ + align-items: flex-start; │
│ + │
│ + h3 { │
│ + margin: 0; │
│ + font-size: 1.2rem; │
│ + } │
│ +} │
│ + │
│ +.status-realm__region { │
│ + font-size: $font-size-xs; │
│ + letter-spacing: 0.12em; │
│ + text-transform: uppercase; │
│ + color: var(--text-secondary); │
│ +} │
│ + │
│ +.status-realm__meta { │
│ + margin: $spacing-xs 0 0; │
│ + color: var(--text-secondary); │
│ + font-size: $font-size-sm; │
│ +} │
│ + │
│ +.status-realm__dungeons { │
│ + list-style: none; │
│ + padding: 0; │
│ + margin: $spacing-sm 0 0; │
│ + display: flex; │
│ + flex-direction: column; │
│ + gap: $spacing-sm; │
│ +} │
│ + │
│ +@media (max-width: 768px) { │
│ + .status-realm__header { │
│ + flex-direction: column; │
│ + } │
│ +} │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...ner/StatusRealmContainer.scss ───┐
│ diff --git a/web/src/components/Status/Statu │
│ sRealmContainer/StatusRealmContainer.scss b/ │
│ web/src/components/Status/StatusRealmContain │
│ er/StatusRealmContainer.scss │
│ new file mode 100644 │
│ index 0000000..7602229 │
│ --- /dev/null │
│ +++ b/web/src/components/Status/StatusRealmC │
│ ontainer/StatusRealmContainer.scss │
│ @@ -0,0 +1,52 @@ │
│ +@use "../../../styles/core/tokens" as *; │
│ + │
│ +.status-realm { │
│ + border-radius: $radius-lg; │
│ + border: 1px solid var(--border-color); │
│ + background: rgba(255, 255, 255, 0.02); │
│ + padding: $spacing-sm $spacing-md; │
│ + display: flex; │
│ + flex-direction: column; │
│ + gap: $spacing-sm; │
│ + min-height: 160px; │
│ +} │
│ + │
│ +.status-realm__header { │
│ + display: flex; │
│ + justify-content: space-between; │
│ + gap: $spacing-md; │
│ + align-items: flex-start; │
│ + │
│ + h3 { │
│ + margin: 0; │
│ + font-size: 1.2rem; │
│ + } │
│ +} │
│ + │
│ +.status-realm__region { │
│ + font-size: $font-size-xs; │
│ + letter-spacing: 0.12em; │
│ + text-transform: uppercase; │
│ + color: var(--text-secondary); │
│ +} │
│ + │
│ +.status-realm__meta { │
│ + margin: $spacing-xs 0 0; │
│ + color: var(--text-secondary); │
│ + font-size: $font-size-sm; │
│ +} │
│ + │
│ +.status-realm__dungeons { │
│ + list-style: none; │
│ + padding: 0; │
│ + margin: $spacing-sm 0 0; │
│ + display: flex; │
│ + flex-direction: column; │
│ + gap: $spacing-sm; │
│ +} │
│ + │
│ +@media (max-width: 768px) { │
│ + .status-realm__header { │
│ + flex-direction: column; │
│ + } │
│ +} │
└──────────────────────────────────────────────┘
┌─ web/src/components/Status/StatusRunsTable/StatusRunsTable.astro ──────────┐
│ diff --git a/web/src/components/Status/StatusRunsTable/StatusRunsTable.astro b/web │
│ /src/components/Status/StatusRunsTable/StatusRunsTable.astro │
│ deleted file mode 100644 │
│ index 4f93848..0000000 │
│ --- a/web/src/components/Status/StatusRunsTable/StatusRunsTable.astro │
│ +++ /dev/null │
│ @@ -1,87 +0,0 @@ │
│ ---- │
│ -import type { LatestRunEntry } from "../../../lib/types"; │
│ -import Table from "../../Table/Table.astro"; │
│ -import TableRow from "../../Table/TableRow.astro"; │
│ -import TableCell from "../../Table/TableCell.astro"; │
│ -import { formatDurationMMSS } from "../../../lib/utils"; │
│ -import { getDungeonIconUrl } from "../../../lib/dungeonIcons"; │
│ -import TeamComposition from "../../Leaderboard/TeamComposition/TeamComposition.as │
│ tro"; │
│ -import "./StatusRunsTable.scss"; │
│ - │
│ -interface Props { │
│ - runs: LatestRunEntry[]; │
│ -} │
│ - │
│ -const { runs = [] } = Astro.props; │
│ - │
│ -const timeAgo = (timestamp: number) => { │
│ - if (!timestamp) return ""; │
│ - const sec = Math.max(0, Math.floor((Date.now() - timestamp) / 1000)); │
│ - const mins = Math.floor(sec / 60); │
│ - const hours = Math.floor(mins / 60); │
│ - const days = Math.floor(hours / 24); │
│ - if (days > 0) return `${days}d ago`; │
│ - if (hours > 0) return `${hours}h ago`; │
│ - if (mins > 0) return `${mins}m ago`; │
│ - return `${sec}s ago`; │
│ -}; │
│ ---- │
│ - │
│ -<Table mode="status"> │
│ - <!-- Header slot --> │
│ - <div slot="header" class="table-header-cell">Dungeon</div> │
│ - <div slot="header" class="table-header-cell">Time</div> │
│ - <div slot="header" class="table-header-cell">Team</div> │
│ - <div slot="header" class="table-header-cell">Realm</div> │
│ - <div slot="header" class="table-header-cell">Last Run</div> │
│ - <div slot="header" class="table-header-cell">Period</div> │
│ - │
│ - <!-- Body (default slot) --> │
│ - { │
│ - runs.length > 0 ? ( │
│ - runs.map((run) => { │
│ - const duration = formatDurationMMSS(run.latest_run?.duration_ms || 0); │
│ - const dungeonIconUrl = getDungeonIconUrl( │
│ - run.dungeon_slug, │
│ - run.dungeon_name, │
│ - ); │
│ - const members = run.latest_run?.members || []; │
│ - const timestamp = run.most_recent_iso || ""; │
│ - const ago = timeAgo(run.most_recent); │
│ - │
│ - return ( │
│ - <TableRow data-region={run.region.toLowerCase()}> │
│ - <TableCell label="Dungeon"> │
│ - <div class="dungeon-cell"> │
│ - {dungeonIconUrl && ( │
│ - <img │
│ - src={dungeonIconUrl} │
│ - alt={run.dungeon_name} │
│ - class="dungeon-icon" │
│ - /> │
│ - )} │
│ - <span>{run.dungeon_name}</span> │
│ - </div> │
│ - </TableCell> │
│ - <TableCell label="Time">{duration}</TableCell> │
│ - <TableCell label="Team"> │
│ - <TeamComposition members={members} /> │
│ - </TableCell> │
│ - <TableCell label="Realm">{run.realm_name}</TableCell> │
│ - <TableCell label="Last Run"> │
│ - <span class="timestamp-cell"> │
│ - {timestamp} │
│ - {ago && <span class="time-ago">({ago})</span>} │
│ - </span> │
│ - </TableCell> │
│ - <TableCell label="Period">{run.period_id}</TableCell> │
│ - </TableRow> │
│ - ); │
│ - }) │
│ - ) : ( │
│ - <div class="table-empty"> │
│ - <p>No runs found.</p> │
│ - </div> │
│ - ) │
│ - } │
│ -</Table> │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...nsTable/StatusRunsTable.astro ───┐
│ diff --git a/web/src/components/Status/Statu │
│ sRunsTable/StatusRunsTable.astro b/web/src/c │
│ omponents/Status/StatusRunsTable/StatusRunsT │
│ able.astro │
│ deleted file mode 100644 │
│ index 4f93848..0000000 │
│ --- a/web/src/components/Status/StatusRunsTa │
│ ble/StatusRunsTable.astro │
│ +++ /dev/null │
│ @@ -1,87 +0,0 @@ │
│ ---- │
│ -import type { LatestRunEntry } from "../../ │
│ ../lib/types"; │
│ -import Table from "../../Table/Table.astro" │
│ ; │
│ -import TableRow from "../../Table/TableRow. │
│ astro"; │
│ -import TableCell from "../../Table/TableCel │
│ l.astro"; │
│ -import { formatDurationMMSS } from "../../. │
│ ./lib/utils"; │
│ -import { getDungeonIconUrl } from "../../.. │
│ /lib/dungeonIcons"; │
│ -import TeamComposition from "../../Leaderbo │
│ ard/TeamComposition/TeamComposition.astro"; │
│ -import "./StatusRunsTable.scss"; │
│ - │
│ -interface Props { │
│ - runs: LatestRunEntry[]; │
│ -} │
│ - │
│ -const { runs = [] } = Astro.props; │
│ - │
│ -const timeAgo = (timestamp: number) => { │
│ - if (!timestamp) return ""; │
│ - const sec = Math.max(0, Math.floor((Date. │
│ now() - timestamp) / 1000)); │
│ - const mins = Math.floor(sec / 60); │
│ - const hours = Math.floor(mins / 60); │
│ - const days = Math.floor(hours / 24); │
│ - if (days > 0) return `${days}d ago`; │
│ - if (hours > 0) return `${hours}h ago`; │
│ - if (mins > 0) return `${mins}m ago`; │
│ - return `${sec}s ago`; │
│ -}; │
│ ---- │
│ - │
│ -<Table mode="status"> │
│ - <!-- Header slot --> │
│ - <div slot="header" class="table-header-ce │
│ ll">Dungeon</div> │
│ - <div slot="header" class="table-header-ce │
│ ll">Time</div> │
│ - <div slot="header" class="table-header-ce │
│ ll">Team</div> │
│ - <div slot="header" class="table-header-ce │
│ ll">Realm</div> │
│ - <div slot="header" class="table-header-ce │
│ ll">Last Run</div> │
│ - <div slot="header" class="table-header-ce │
│ ll">Period</div> │
│ - │
│ - <!-- Body (default slot) --> │
│ - { │
│ - runs.length > 0 ? ( │
│ - runs.map((run) => { │
│ - const duration = formatDurationMMSS │
│ (run.latest_run?.duration_ms || 0); │
│ - const dungeonIconUrl = getDungeonIc │
│ onUrl( │
│ - run.dungeon_slug, │
│ - run.dungeon_name, │
│ - ); │
│ - const members = run.latest_run?.mem │
│ bers || []; │
│ - const timestamp = run.most_recent_i │
│ so || ""; │
│ - const ago = timeAgo(run.most_recent │
│ ); │
│ - │
│ - return ( │
│ - <TableRow data-region={run.region │
│ .toLowerCase()}> │
│ - <TableCell label="Dungeon"> │
│ - <div class="dungeon-cell"> │
│ - {dungeonIconUrl && ( │
│ - <img │
│ - src={dungeonIconUrl} │
│ - alt={run.dungeon_name} │
│ - class="dungeon-icon" │
│ - /> │
│ - )} │
│ - <span>{run.dungeon_name}</s │
│ pan> │
│ - </div> │
│ - </TableCell> │
│ - <TableCell label="Time">{durati │
│ on}</TableCell> │
│ - <TableCell label="Team"> │
│ - <TeamComposition members={mem │
│ bers} /> │
│ - </TableCell> │
│ - <TableCell label="Realm">{run.r │
│ ealm_name}</TableCell> │
│ - <TableCell label="Last Run"> │
│ - <span class="timestamp-cell"> │
│ - {timestamp} │
│ - {ago && <span class="time-a │
│ go">({ago})</span>} │
│ - </span> │
│ - </TableCell> │
│ - <TableCell label="Period">{run. │
│ period_id}</TableCell> │
│ - </TableRow> │
│ - ); │
│ - }) │
│ - ) : ( │
│ - <div class="table-empty"> │
│ - <p>No runs found.</p> │
│ - </div> │
│ - ) │
│ - } │
│ -</Table> │
└──────────────────────────────────────────────┘
┌─ web/src/components/Status/StatusRunsTable/StatusRunsTable.scss ───────────┐
│ diff --git a/web/src/components/Status/StatusRunsTable/StatusRunsTable.scss b/web/ │
│ src/components/Status/StatusRunsTable/StatusRunsTable.scss │
│ index 5371042..1d559f0 100644 │
│ --- a/web/src/components/Status/StatusRunsTable/StatusRunsTable.scss │
│ +++ b/web/src/components/Status/StatusRunsTable/StatusRunsTable.scss │
│ @@ -1,36 +1,116 @@ │
│ @use "../../../styles/core/tokens" as *; │
│ │
│ -.status-page { │
│ - max-width: 1200px; │
│ - margin: 0 auto; │
│ - padding: $spacing-xl; │
│ +.status-content { │
│ + display: flex; │
│ + flex-direction: column; │
│ + gap: $spacing-lg; │
│ +} │
│ │
│ - .page-title { │
│ - margin: $spacing-sm 0 $spacing-lg 0; │
│ - } │
│ +.status-empty { │
│ + padding: $spacing-lg; │
│ + border-radius: $radius-lg; │
│ + border: 1px dashed rgba(255, 255, 255, 0.08); │
│ + background: rgba(255, 255, 255, 0.02); │
│ + color: var(--text-secondary); │
│ + text-align: center; │
│ } │
│ │
│ -.dungeon-cell { │
│ +.region-section { │
│ + border: 1px solid var(--border-color); │
│ + border-radius: $radius-lg; │
│ + padding: $spacing-sm $spacing-md; │
│ + margin-bottom: $spacing-lg; │
│ +} │
│ + │
│ +.region-section[open] { │
│ + background: rgba(255, 255, 255, 0.02); │
│ +} │
│ + │
│ +.region-summary { │
│ display: flex; │
│ - align-items: center; │
│ + justify-content: space-between; │
│ + align-items: baseline; │
│ gap: $spacing-sm; │
│ + font-size: 1.1rem; │
│ + letter-spacing: 0.08em; │
│ + text-transform: uppercase; │
│ + color: var(--text-secondary); │
│ + cursor: pointer; │
│ + list-style: none; │
│ } │
│ │
│ -.dungeon-icon { │
│ - width: 24px; │
│ - height: 24px; │
│ - border-radius: $radius-sm; │
│ - flex-shrink: 0; │
│ +.region-summary::-webkit-details-marker { │
│ + display: none; │
│ } │
│ │
│ -.timestamp-cell { │
│ - display: flex; │
│ - align-items: center; │
│ - flex-wrap: wrap; │
│ - gap: $spacing-xs; │
│ +.region-summary::before { │
│ + content: "?"; │
│ + margin-right: $spacing-sm; │
│ + transition: transform 0.2s ease; │
│ +} │
│ + │
│ +.region-section:not([open]) .region-summary::before { │
│ + transform: rotate(-90deg); │
│ } │
│ │
│ -.time-ago { │
│ +.region-summary__latest { │
│ + font-size: $font-size-sm; │
│ + text-transform: none; │
│ color: var(--text-secondary); │
│ + margin-left: auto; │
│ +} │
│ + │
│ +.region-grid { │
│ + display: grid; │
│ + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); │
│ + gap: $spacing-md; │
│ + margin-top: $spacing-md; │
│ +} │
│ + │
│ +.health-pill { │
│ + padding: $spacing-xs $spacing-sm; │
│ + border-radius: $radius-md; │
│ + border: 1px solid rgba(255, 255, 255, 0.18); │
│ + font-weight: 600; │
│ font-size: $font-size-sm; │
│ + text-transform: uppercase; │
│ + │
│ + &.health-ok { │
│ + background: rgba(56, 187, 92, 0.15); │
│ + color: #6ee7b7; │
│ + } │
│ + │
│ + &.health-some_missing { │
│ + background: rgba(251, 191, 36, 0.15); │
│ + color: #fcd34d; │
│ + } │
│ + │
│ + &.health-no_data { │
│ + background: rgba(239, 68, 68, 0.15); │
│ + color: #fca5a5; │
│ + } │
│ +} │
│ + │
│ +.status-pill { │
│ + padding: $spacing-xs $spacing-sm; │
│ + border-radius: $radius-sm; │
│ + border: 1px solid rgba(255, 255, 255, 0.18); │
│ + font-weight: 600; │
│ + font-size: $font-size-xs; │
│ + text-transform: uppercase; │
│ + │
│ + &.status-ok { │
│ + background: rgba(56, 187, 92, 0.15); │
│ + color: #6ee7b7; │
│ + } │
│ + │
│ + &.status-some_missing { │
│ + background: rgba(251, 191, 36, 0.15); │
│ + color: #fcd34d; │
│ + } │
│ + │
│ + &.status-no_data { │
│ + background: rgba(239, 68, 68, 0.15); │
│ + color: #fca5a5; │
│ + } │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...unsTable/StatusRunsTable.scss ───┐
│ diff --git a/web/src/components/Status/Statu │
│ sRunsTable/StatusRunsTable.scss b/web/src/co │
│ mponents/Status/StatusRunsTable/StatusRunsTa │
│ ble.scss │
│ index 5371042..1d559f0 100644 │
│ --- a/web/src/components/Status/StatusRunsTa │
│ ble/StatusRunsTable.scss │
│ +++ b/web/src/components/Status/StatusRunsTa │
│ ble/StatusRunsTable.scss │
│ @@ -1,36 +1,116 @@ │
│ @use "../../../styles/core/tokens" as *; │
│ │
│ -.status-page { │
│ - max-width: 1200px; │
│ - margin: 0 auto; │
│ - padding: $spacing-xl; │
│ +.status-content { │
│ + display: flex; │
│ + flex-direction: column; │
│ + gap: $spacing-lg; │
│ +} │
│ │
│ - .page-title { │
│ - margin: $spacing-sm 0 $spacing-lg 0; │
│ - } │
│ +.status-empty { │
│ + padding: $spacing-lg; │
│ + border-radius: $radius-lg; │
│ + border: 1px dashed rgba(255, 255, 255, 0. │
│ 08); │
│ + background: rgba(255, 255, 255, 0.02); │
│ + color: var(--text-secondary); │
│ + text-align: center; │
│ } │
│ │
│ -.dungeon-cell { │
│ +.region-section { │
│ + border: 1px solid var(--border-color); │
│ + border-radius: $radius-lg; │
│ + padding: $spacing-sm $spacing-md; │
│ + margin-bottom: $spacing-lg; │
│ +} │
│ + │
│ +.region-section[open] { │
│ + background: rgba(255, 255, 255, 0.02); │
│ +} │
│ + │
│ +.region-summary { │
│ display: flex; │
│ - align-items: center; │
│ + justify-content: space-between; │
│ + align-items: baseline; │
│ gap: $spacing-sm; │
│ + font-size: 1.1rem; │
│ + letter-spacing: 0.08em; │
│ + text-transform: uppercase; │
│ + color: var(--text-secondary); │
│ + cursor: pointer; │
│ + list-style: none; │
│ } │
│ │
│ -.dungeon-icon { │
│ - width: 24px; │
│ - height: 24px; │
│ - border-radius: $radius-sm; │
│ - flex-shrink: 0; │
│ +.region-summary::-webkit-details-marker { │
│ + display: none; │
│ } │
│ │
│ -.timestamp-cell { │
│ - display: flex; │
│ - align-items: center; │
│ - flex-wrap: wrap; │
│ - gap: $spacing-xs; │
│ +.region-summary::before { │
│ + content: "?"; │
│ + margin-right: $spacing-sm; │
│ + transition: transform 0.2s ease; │
│ +} │
│ + │
│ +.region-section:not([open]) .region-summary │
│ ::before { │
│ + transform: rotate(-90deg); │
│ } │
│ │
│ -.time-ago { │
│ +.region-summary__latest { │
│ + font-size: $font-size-sm; │
│ + text-transform: none; │
│ color: var(--text-secondary); │
│ + margin-left: auto; │
│ +} │
│ + │
│ +.region-grid { │
│ + display: grid; │
│ + grid-template-columns: repeat(auto-fit, m │
│ inmax(320px, 1fr)); │
│ + gap: $spacing-md; │
│ + margin-top: $spacing-md; │
│ +} │
│ + │
│ +.health-pill { │
│ + padding: $spacing-xs $spacing-sm; │
│ + border-radius: $radius-md; │
│ + border: 1px solid rgba(255, 255, 255, 0.1 │
│ 8); │
│ + font-weight: 600; │
│ font-size: $font-size-sm; │
│ + text-transform: uppercase; │
│ + │
│ + &.health-ok { │
│ + background: rgba(56, 187, 92, 0.15); │
│ + color: #6ee7b7; │
│ + } │
│ + │
│ + &.health-some_missing { │
│ + background: rgba(251, 191, 36, 0.15); │
│ + color: #fcd34d; │
│ + } │
│ + │
│ + &.health-no_data { │
│ + background: rgba(239, 68, 68, 0.15); │
│ + color: #fca5a5; │
│ + } │
│ +} │
│ + │
│ +.status-pill { │
│ + padding: $spacing-xs $spacing-sm; │
│ + border-radius: $radius-sm; │
│ + border: 1px solid rgba(255, 255, 255, 0.1 │
│ 8); │
│ + font-weight: 600; │
│ + font-size: $font-size-xs; │
│ + text-transform: uppercase; │
│ + │
│ + &.status-ok { │
│ + background: rgba(56, 187, 92, 0.15); │
│ + color: #6ee7b7; │
│ + } │
│ + │
│ + &.status-some_missing { │
│ + background: rgba(251, 191, 36, 0.15); │
│ + color: #fcd34d; │
│ + } │
│ + │
│ + &.status-no_data { │
│ + background: rgba(239, 68, 68, 0.15); │
│ + color: #fca5a5; │
│ + } │
│ } │
└──────────────────────────────────────────────┘
┌─ web/src/layouts/PageLayout.astro ─────────────────────────────────────────┐
│ diff --git a/web/src/layouts/PageLayout.astro b/web/src/layouts/PageLayout.astro │
│ index ba6bce6..3e1932f 100644 │
│ --- a/web/src/layouts/PageLayout.astro │
│ +++ b/web/src/layouts/PageLayout.astro │
│ @@ -59,11 +59,13 @@ const fullUrl = new URL(url, Astro.site || Astro.url.origin).t │
│ oString(); │
│ content={new URL(ogImage, Astro.url.origin).toString()} │
│ /> │
│ </head> │
│ - <body> │
│ + <body class="site-body"> │
│ <WoWClassColors /> │
│ <Navigation /> │
│ │
│ - <slot /> │
│ + <div class="site-content"> │
│ + <slot /> │
│ + </div> │
│ │
│ <Footer /> │
│ </body> │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ web/src/layouts/PageLayout.astro ───┐
│ diff --git a/web/src/layouts/PageLayout.astr │
│ o b/web/src/layouts/PageLayout.astro │
│ index ba6bce6..3e1932f 100644 │
│ --- a/web/src/layouts/PageLayout.astro │
│ +++ b/web/src/layouts/PageLayout.astro │
│ @@ -59,11 +59,13 @@ const fullUrl = new URL( │
│ url, Astro.site || Astro.url.origin).toStrin │
│ g(); │
│ content={new URL(ogImage, Astro.url.o │
│ rigin).toString()} │
│ /> │
│ </head> │
│ - <body> │
│ + <body class="site-body"> │
│ <WoWClassColors /> │
│ <Navigation /> │
│ │
│ - <slot /> │
│ + <div class="site-content"> │
│ + <slot /> │
│ + </div> │
│ │
│ <Footer /> │
│ </body> │
└──────────────────────────────────────────────┘
┌─ web/src/lib/types.ts ─────────────────────────────────────────────────────┐
│ diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts │
│ index 521168c..15ed8d5 100644 │
│ --- a/web/src/lib/types.ts │
│ +++ b/web/src/lib/types.ts │
│ @@ -225,80 +225,35 @@ export interface FuseSearchResult<T> { │
│ } │
│ │
│ // Status page types │
│ -export interface LatestRunMember { │
│ - name: string; │
│ - spec_id: number; │
│ - realm_slug: string; │
│ - region: string; │
│ -} │
│ +export type CoverageHealth = "ok" | "some_missing" | "no_data"; │
│ │
│ -export interface LatestRunDetails { │
│ - completed_timestamp: number; │
│ - duration_ms: number; │
│ - keystone_level: number; │
│ - members: LatestRunMember[]; │
│ -} │
│ - │
│ -export interface LatestRunEntry { │
│ - region: string; │
│ - realm_slug: string; │
│ - realm_name: string; │
│ - realm_id: number; │
│ - most_recent: number; │
│ - most_recent_iso: string; │
│ - period_id: string; │
│ - dungeon_slug: string; │
│ - dungeon_name: string; │
│ - run_count: number; │
│ - has_runs: boolean; │
│ - latest_run: LatestRunDetails; │
│ -} │
│ - │
│ -export interface PeriodCoverage { │
│ - period_id: string; │
│ - has_runs: boolean; │
│ - latest_ts: number; │
│ - latest_iso: string; │
│ - run_count: number; │
│ -} │
│ - │
│ -export interface DungeonStatus { │
│ +export interface DungeonCoverage { │
│ + dungeon_id: number; │
│ dungeon_slug: string; │
│ dungeon_name: string; │
│ - latest_ts: number; │
│ - latest_iso: string; │
│ - latest_period: string; │
│ - periods: PeriodCoverage[]; │
│ - missing_periods: string[]; │
│ - latest_run?: LatestRunDetails; │
│ + status: CoverageHealth; │
│ + periods: number[]; │
│ + missing_periods: number[]; │
│ + error_periods: number[]; │
│ } │
│ │
│ -export interface RealmStatus { │
│ +export interface RealmCoverage { │
│ region: string; │
│ realm_slug: string; │
│ realm_name: string; │
│ - realm_id: number; │
│ - dungeons: DungeonStatus[]; │
│ + health: CoverageHealth; │
│ + total_periods: number; │
│ + missing_periods: number; │
│ + error_periods: number; │
│ + dungeons: DungeonCoverage[]; │
│ } │
│ │
│ export interface StatusApiResponse { │
│ - generated_at: number; │
│ - periods: string[]; │
│ - summary: { │
│ - endpoints_tested: number; │
│ - success: number; │
│ - failed: number; │
│ - }; │
│ - latest_runs: LatestRunEntry[]; │
│ - realm_status: RealmStatus[]; │
│ + generated_at: string; │
│ + realms: RealmCoverage[]; │
│ } │
│ │
│ export interface RealmStatusApiResponse { │
│ - generated_at: number; │
│ - region: string; │
│ - realm_slug: string; │
│ - realm_name: string; │
│ - realm_id: number; │
│ - periods: string[]; │
│ - dungeons: DungeonStatus[]; │
│ + generated_at: string; │
│ + realm: RealmCoverage; │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ web/src/lib/types.ts ───────────────┐
│ diff --git a/web/src/lib/types.ts b/web/src/ │
│ lib/types.ts │
│ index 521168c..15ed8d5 100644 │
│ --- a/web/src/lib/types.ts │
│ +++ b/web/src/lib/types.ts │
│ @@ -225,80 +225,35 @@ export interface FuseS │
│ earchResult<T> { │
│ } │
│ │
│ // Status page types │
│ -export interface LatestRunMember { │
│ - name: string; │
│ - spec_id: number; │
│ - realm_slug: string; │
│ - region: string; │
│ -} │
│ +export type CoverageHealth = "ok" | "some_m │
│ issing" | "no_data"; │
│ │
│ -export interface LatestRunDetails { │
│ - completed_timestamp: number; │
│ - duration_ms: number; │
│ - keystone_level: number; │
│ - members: LatestRunMember[]; │
│ -} │
│ - │
│ -export interface LatestRunEntry { │
│ - region: string; │
│ - realm_slug: string; │
│ - realm_name: string; │
│ - realm_id: number; │
│ - most_recent: number; │
│ - most_recent_iso: string; │
│ - period_id: string; │
│ - dungeon_slug: string; │
│ - dungeon_name: string; │
│ - run_count: number; │
│ - has_runs: boolean; │
│ - latest_run: LatestRunDetails; │
│ -} │
│ - │
│ -export interface PeriodCoverage { │
│ - period_id: string; │
│ - has_runs: boolean; │
│ - latest_ts: number; │
│ - latest_iso: string; │
│ - run_count: number; │
│ -} │
│ - │
│ -export interface DungeonStatus { │
│ +export interface DungeonCoverage { │
│ + dungeon_id: number; │
│ dungeon_slug: string; │
│ dungeon_name: string; │
│ - latest_ts: number; │
│ - latest_iso: string; │
│ - latest_period: string; │
│ - periods: PeriodCoverage[]; │
│ - missing_periods: string[]; │
│ - latest_run?: LatestRunDetails; │
│ + status: CoverageHealth; │
│ + periods: number[]; │
│ + missing_periods: number[]; │
│ + error_periods: number[]; │
│ } │
│ │
│ -export interface RealmStatus { │
│ +export interface RealmCoverage { │
│ region: string; │
│ realm_slug: string; │
│ realm_name: string; │
│ - realm_id: number; │
│ - dungeons: DungeonStatus[]; │
│ + health: CoverageHealth; │
│ + total_periods: number; │
│ + missing_periods: number; │
│ + error_periods: number; │
│ + dungeons: DungeonCoverage[]; │
│ } │
│ │
│ export interface StatusApiResponse { │
│ - generated_at: number; │
│ - periods: string[]; │
│ - summary: { │
│ - endpoints_tested: number; │
│ - success: number; │
│ - failed: number; │
│ - }; │
│ - latest_runs: LatestRunEntry[]; │
│ - realm_status: RealmStatus[]; │
│ + generated_at: string; │
│ + realms: RealmCoverage[]; │
│ } │
│ │
│ export interface RealmStatusApiResponse { │
│ - generated_at: number; │
│ - region: string; │
│ - realm_slug: string; │
│ - realm_name: string; │
│ - realm_id: number; │
│ - periods: string[]; │
│ - dungeons: DungeonStatus[]; │
│ + generated_at: string; │
│ + realm: RealmCoverage; │
│ } │
└──────────────────────────────────────────────┘
┌─ web/src/pages/challenge-mode/status.astro ────────────────────────────────┐
│ diff --git a/web/src/pages/challenge-mode/status.astro b/web/src/pages/challenge-m │
│ ode/status.astro │
│ index dbfcf6d..4c7e49a 100644 │
│ --- a/web/src/pages/challenge-mode/status.astro │
│ +++ b/web/src/pages/challenge-mode/status.astro │
│ @@ -1,13 +1,20 @@ │
│ --- │
│ import Callout from "../../components/Callout/Callout.astro"; │
│ -import StatusRunsTable from "../../components/Status/StatusRunsTable/StatusRunsTa │
│ ble.astro"; │
│ import PageLayout from "../../layouts/PageLayout.astro"; │
│ +import PlayerSearch from "../../components/PlayerSearch/PlayerSearch.astro"; │
│ import "../../styles/global.scss"; │
│ import "../../components/Status/StatusRunsTable/StatusRunsTable.scss"; │
│ +import "../../components/Leaderboard/LeaderboardHeader/LeaderboardHeader.scss"; │
│ +import type { │
│ + CoverageHealth, │
│ + RealmCoverage, │
│ + StatusApiResponse, │
│ +} from "../../lib/types"; │
│ import { readFile } from "node:fs/promises"; │
│ import { join } from "node:path"; │
│ +import StatusRealmContainer from "../../components/Status/StatusRealmContainer/St │
│ atusRealmContainer.astro"; │
│ +import LeaderboardTypeNav from "../../components/Leaderboard/LeaderboardTypeNav/L │
│ eaderboardTypeNav.astro"; │
│ │
│ -// Load global status data server-side │
│ const filePath = join( │
│ process.cwd(), │
│ "public", │
│ @@ -16,24 +23,123 @@ const filePath = join( │
│ "latest-runs.json", │
│ ); │
│ const fileContent = await readFile(filePath, "utf-8"); │
│ -const data = JSON.parse(fileContent); │
│ +const data = JSON.parse(fileContent) as StatusApiResponse; │
│ +const realms = Array.isArray(data.realms) ? data.realms : []; │
│ + │
│ +const healthLabels: Record<CoverageHealth, string> = { │
│ + ok: "OK", │
│ + some_missing: "Some missing", │
│ + no_data: "No data", │
│ +}; │
│ + │
│ +const formatList = (items?: number[]) => │
│ + items && items.length ? items.join(", ") : "None"; │
│ + │
│ +const latestPeriodByRegion = realms.reduce<Record<string, number | null>>( │
│ + (acc, realm) => { │
│ + const regionKey = realm.region.toUpperCase(); │
│ + let currentMax = acc[regionKey] ?? null; │
│ + realm.dungeons.forEach((dungeon) => { │
│ + (dungeon.periods || []).forEach((period) => { │
│ + if (currentMax === null || period > currentMax) { │
│ + currentMax = period; │
│ + } │
│ + }); │
│ + }); │
│ + acc[regionKey] = currentMax; │
│ + return acc; │
│ + }, │
│ + {}, │
│ +); │
│ + │
│ +const realmsByRegion = realms.reduce<Record<string, RealmCoverage[]>>( │
│ + (acc, realm) => { │
│ + const regionKey = realm.region.toUpperCase(); │
│ + if (!acc[regionKey]) { │
│ + acc[regionKey] = []; │
│ + } │
│ + acc[regionKey].push(realm); │
│ + acc[regionKey].sort((a, b) => a.realm_name.localeCompare(b.realm_name)); │
│ + return acc; │
│ + }, │
│ + {}, │
│ +); │
│ + │
│ +const regionEntries = Object.entries(realmsByRegion).sort((a, b) => │
│ + a[0].localeCompare(b[0]), │
│ +); │
│ --- │
│ │
│ <PageLayout title="Challenge Mode Status"> │
│ - <main class="status-page"> │
│ - <h1 class="page-title">Challenge Mode Global Status</h1> │
│ + <main class="leaderboard-page"> │
│ + <h1 class="leaderboard-header status-page__title"> │
│ + <span class="header-text">Challenge Mode Fetch Status</span> │
│ + </h1> │
│ + <PlayerSearch /> │
│ + <LeaderboardTypeNav currentTab="status" /> │
│ │
│ - <Callout type="note" title="About"> │
│ + <Callout type="note"> │
│ <p> │
│ - Showing the latest run for each realm across all dungeons. Data │
│ - generated at {new Date(data.generated_at).toLocaleString()}. │
│ + Periods refer to one week long intervals within a challenge mode season. │
│ </p> │
│ </Callout> │
│ │
│ - <div id="status-content"> │
│ - <StatusRunsTable runs={data.latest_runs} /> │
│ + <div class="status-content"> │
│ + { │
│ + regionEntries.length === 0 ? ( │
│ + <p class="status-empty"> │
│ + No fetch results have been recorded yet. Run `ookstats fetch cm` │
│ + first. │
│ + </p> │
│ + ) : ( │
│ + regionEntries.map(([region, realms]) => { │
│ + const latest = latestPeriodByRegion[region] ?? null; │
│ + return ( │
│ + <details │
│ + class="region-section" │
│ + data-region={region.toLowerCase()} │
│ + > │
│ + <summary class="region-summary"> │
│ + <span>{region}</span> │
│ + {latest !== null && ( │
│ + <span class="region-summary__latest"> │
│ + Latest period: {latest} │
│ + </span> │
│ + )} │
│ + </summary> │
│ + <div class="region-grid"> │
│ + {realms.map((realm) => ( │
│ + <StatusRealmContainer │
│ + realm={realm} │
│ + healthLabels={healthLabels} │
│ + formatList={formatList} │
│ + latestPeriod={latest} │
│ + /> │
│ + ))} │
│ + </div> │
│ + </details> │
│ + ); │
│ + }) │
│ + ) │
│ + } │
│ </div> │
│ </main> │
│ - │
│ - <script src="../../client/best-runs-decorate.ts"></script> │
│ </PageLayout> │
│ + │
│ +<style> │
│ + .leaderboard-page { │
│ + max-width: 1200px; │
│ + margin: 0 auto; │
│ + padding: 20px; │
│ + } │
│ + │
│ + .status-page__title { │
│ + margin-top: 0; │
│ + } │
│ + │
│ + @media (max-width: 768px) { │
│ + .leaderboard-page { │
│ + padding: 15px; │
│ + } │
│ + } │
│ +</style> │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...s/challenge-mode/status.astro ───┐
│ diff --git a/web/src/pages/challenge-mode/st │
│ atus.astro b/web/src/pages/challenge-mode/st │
│ atus.astro │
│ index dbfcf6d..4c7e49a 100644 │
│ --- a/web/src/pages/challenge-mode/status.as │
│ tro │
│ +++ b/web/src/pages/challenge-mode/status.as │
│ tro │
│ @@ -1,13 +1,20 @@ │
│ --- │
│ import Callout from "../../components/Callo │
│ ut/Callout.astro"; │
│ -import StatusRunsTable from "../../componen │
│ ts/Status/StatusRunsTable/StatusRunsTable.as │
│ tro"; │
│ import PageLayout from "../../layouts/PageL │
│ ayout.astro"; │
│ +import PlayerSearch from "../../components/ │
│ PlayerSearch/PlayerSearch.astro"; │
│ import "../../styles/global.scss"; │
│ import "../../components/Status/StatusRunsT │
│ able/StatusRunsTable.scss"; │
│ +import "../../components/Leaderboard/Leader │
│ boardHeader/LeaderboardHeader.scss"; │
│ +import type { │
│ + CoverageHealth, │
│ + RealmCoverage, │
│ + StatusApiResponse, │
│ +} from "../../lib/types"; │
│ import { readFile } from "node:fs/promises" │
│ ; │
│ import { join } from "node:path"; │
│ +import StatusRealmContainer from "../../com │
│ ponents/Status/StatusRealmContainer/StatusRe │
│ almContainer.astro"; │
│ +import LeaderboardTypeNav from "../../compo │
│ nents/Leaderboard/LeaderboardTypeNav/Leaderb │
│ oardTypeNav.astro"; │
│ │
│ -// Load global status data server-side │
│ const filePath = join( │
│ process.cwd(), │
│ "public", │
│ @@ -16,24 +23,123 @@ const filePath = join( │
│ "latest-runs.json", │
│ ); │
│ const fileContent = await readFile(filePath │
│ , "utf-8"); │
│ -const data = JSON.parse(fileContent); │
│ +const data = JSON.parse(fileContent) as Sta │
│ tusApiResponse; │
│ +const realms = Array.isArray(data.realms) ? │
│ data.realms : []; │
│ + │
│ +const healthLabels: Record<CoverageHealth, │
│ string> = { │
│ + ok: "OK", │
│ + some_missing: "Some missing", │
│ + no_data: "No data", │
│ +}; │
│ + │
│ +const formatList = (items?: number[]) => │
│ + items && items.length ? items.join(", ") │
│ : "None"; │
│ + │
│ +const latestPeriodByRegion = realms.reduce< │
│ Record<string, number | null>>( │
│ + (acc, realm) => { │
│ + const regionKey = realm.region.toUpperC │
│ ase(); │
│ + let currentMax = acc[regionKey] ?? null │
│ ; │
│ + realm.dungeons.forEach((dungeon) => { │
│ + (dungeon.periods || []).forEach((peri │
│ od) => { │
│ + if (currentMax === null || period > │
│ currentMax) { │
│ + currentMax = period; │
│ + } │
│ + }); │
│ + }); │
│ + acc[regionKey] = currentMax; │
│ + return acc; │
│ + }, │
│ + {}, │
│ +); │
│ + │
│ +const realmsByRegion = realms.reduce<Record │
│ <string, RealmCoverage[]>>( │
│ + (acc, realm) => { │
│ + const regionKey = realm.region.toUpperC │
│ ase(); │
│ + if (!acc[regionKey]) { │
│ + acc[regionKey] = []; │
│ + } │
│ + acc[regionKey].push(realm); │
│ + acc[regionKey].sort((a, b) => a.realm_n │
│ ame.localeCompare(b.realm_name)); │
│ + return acc; │
│ + }, │
│ + {}, │
│ +); │
│ + │
│ +const regionEntries = Object.entries(realms │
│ ByRegion).sort((a, b) => │
│ + a[0].localeCompare(b[0]), │
│ +); │
│ --- │
│ │
│ <PageLayout title="Challenge Mode Status"> │
│ - <main class="status-page"> │
│ - <h1 class="page-title">Challenge Mode G │
│ lobal Status</h1> │
│ + <main class="leaderboard-page"> │
│ + <h1 class="leaderboard-header status-pa │
│ ge__title"> │
│ + <span class="header-text">Challenge M │
│ ode Fetch Status</span> │
│ + </h1> │
│ + <PlayerSearch /> │
│ + <LeaderboardTypeNav currentTab="status" │
│ /> │
│ │
│ - <Callout type="note" title="About"> │
│ + <Callout type="note"> │
│ <p> │
│ - Showing the latest run for each rea │
│ lm across all dungeons. Data │
│ - generated at {new Date(data.generat │
│ ed_at).toLocaleString()}. │
│ + Periods refer to one week long inte │
│ rvals within a challenge mode season. │
│ </p> │
│ </Callout> │
│ │
│ - <div id="status-content"> │
│ - <StatusRunsTable runs={data.latest_ru │
│ ns} /> │
│ + <div class="status-content"> │
│ + { │
│ + regionEntries.length === 0 ? ( │
│ + <p class="status-empty"> │
│ + No fetch results have been reco │
│ rded yet. Run `ookstats fetch cm` │
│ + first. │
│ + </p> │
│ + ) : ( │
│ + regionEntries.map(([region, realm │
│ s]) => { │
│ + const latest = latestPeriodByRe │
│ gion[region] ?? null; │
│ + return ( │
│ + <details │
│ + class="region-section" │
│ + data-region={region.toLower │
│ Case()} │
│ + > │
│ + <summary class="region-summ │
│ ary"> │
│ + <span>{region}</span> │
│ + {latest !== null && ( │
│ + <span class="region-sum │
│ mary__latest"> │
│ + Latest period: {lates │
│ t} │
│ + </span> │
│ + )} │
│ + </summary> │
│ + <div class="region-grid"> │
│ + {realms.map((realm) => ( │
│ + <StatusRealmContainer │
│ + realm={realm} │
│ + healthLabels={healthL │
│ abels} │
│ + formatList={formatLis │
│ t} │
│ + latestPeriod={latest} │
│ + /> │
│ + ))} │
│ + </div> │
│ + </details> │
│ + ); │
│ + }) │
│ + ) │
│ + } │
│ </div> │
│ </main> │
│ - │
│ - <script src="../../client/best-runs-decor │
│ ate.ts"></script> │
│ </PageLayout> │
│ + │
│ +<style> │
│ + .leaderboard-page { │
│ + max-width: 1200px; │
│ + margin: 0 auto; │
│ + padding: 20px; │
│ + } │
│ + │
│ + .status-page__title { │
│ + margin-top: 0; │
│ + } │
│ + │
│ + @media (max-width: 768px) { │
│ + .leaderboard-page { │
│ + padding: 15px; │
│ + } │
│ + } │
│ +</style> │
└──────────────────────────────────────────────┘
┌─ web/src/styles/core/_tokens.scss ─────────────────────────────────────────┐
│ diff --git a/web/src/styles/core/_tokens.scss b/web/src/styles/core/_tokens.scss │
│ index 8d68fbf..e1a5faf 100644 │
│ --- a/web/src/styles/core/_tokens.scss │
│ +++ b/web/src/styles/core/_tokens.scss │
│ @@ -48,6 +48,8 @@ $font-weight-bold: 700; │
│ --text-muted: #a9a9a9; │
│ --blue-color: #7daea3; │
│ --note-color: #7daea3; │
│ + --error-color: #ea6962; │
│ + --success-color: #a9b665; │
│ │
│ /* WoW Class Colors */ │
│ --class-death-knight: #c41e3a; │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ web/src/styles/core/_tokens.scss ───┐
│ diff --git a/web/src/styles/core/_tokens.scs │
│ s b/web/src/styles/core/_tokens.scss │
│ index 8d68fbf..e1a5faf 100644 │
│ --- a/web/src/styles/core/_tokens.scss │
│ +++ b/web/src/styles/core/_tokens.scss │
│ @@ -48,6 +48,8 @@ $font-weight-bold: 700; │
│ --text-muted: #a9a9a9; │
│ --blue-color: #7daea3; │
│ --note-color: #7daea3; │
│ + --error-color: #ea6962; │
│ + --success-color: #a9b665; │
│ │
│ /* WoW Class Colors */ │
│ --class-death-knight: #c41e3a; │
└──────────────────────────────────────────────┘
┌─ web/src/styles/global.scss ───────────────────────────────────────────────┐
│ diff --git a/web/src/styles/global.scss b/web/src/styles/global.scss │
│ index 9b5d2ac..b584f57 100644 │
│ --- a/web/src/styles/global.scss │
│ +++ b/web/src/styles/global.scss │
│ @@ -2,6 +2,11 @@ │
│ @use "./core/utilities"; │
│ @use "./core/best-runs"; │
│ │
│ +html, │
│ +body { │
│ + min-height: 100%; │
│ +} │
│ + │
│ body { │
│ font-family: Arial, sans-serif; │
│ margin: 0; │
│ @@ -10,6 +15,17 @@ body { │
│ color: var(--text-primary); │
│ } │
│ │
│ +.site-body { │
│ + min-height: 100vh; │
│ + display: flex; │
│ + flex-direction: column; │
│ +} │
│ + │
│ +.site-content { │
│ + flex: 1 0 auto; │
│ + width: 100%; │
│ +} │
│ + │
│ .page-header { │
│ text-align: center; │
│ padding: 30px 20px; │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ web/src/styles/global.scss ─────────┐
│ diff --git a/web/src/styles/global.scss b/we │
│ b/src/styles/global.scss │
│ index 9b5d2ac..b584f57 100644 │
│ --- a/web/src/styles/global.scss │
│ +++ b/web/src/styles/global.scss │
│ @@ -2,6 +2,11 @@ │
│ @use "./core/utilities"; │
│ @use "./core/best-runs"; │
│ │
│ +html, │
│ +body { │
│ + min-height: 100%; │
│ +} │
│ + │
│ body { │
│ font-family: Arial, sans-serif; │
│ margin: 0; │
│ @@ -10,6 +15,17 @@ body { │
│ color: var(--text-primary); │
│ } │
│ │
│ +.site-body { │
│ + min-height: 100vh; │
│ + display: flex; │
│ + flex-direction: column; │
│ +} │
│ + │
│ +.site-content { │
│ + flex: 1 0 auto; │
│ + width: 100%; │
│ +} │
│ + │
│ .page-header { │
│ text-align: center; │
│ padding: 30px 20px; │
└──────────────────────────────────────────────┘
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET