A static dashboard for the World of Warcraft guild Undaunted. This is for the team Relentless on EU-Draenor. It tracks Mythic+ and raid performance for the active roster and is rebuilt automatically twice a day from live data.
- What the dashboard shows
- How data gets updated
- Tech stack
- Local development
- Running the fetch script manually
- Backfilling historical data
- Managing the roster
- Blocking pug detection
- Secrets and environment variables
- Deploying
- Tests
- Project structure
- Raid tier progression: boss pip row, progress bar, and current difficulty status
- Roster composition: roles, classes, and specs at a glance
- M+ on-track count for the current reset
- One row per raider, one column per boss
- Cells are colour-coded using WarcraftLogs parse colours (grey through orange/pink)
- Each cell links to the relevant WarcraftLogs report
- Boss names are abbreviated to fit the table
- RIO score badge per raider
- Key level colours match Raider.io cutoffs
- Tracks the record week (highest total key count) for the season
- Raid boss parse cards with bar charts and links to WoWAnalyzer
- M+ streak and weekly compliance history
- Lockout callout if the raider has a progression-blocking pug kill
- Full team event log: joins, leaves, rerolls, spec changes, pug flags, exemptions
A GitHub Actions cron job runs at 05:00 UTC and 17:00 UTC every day. It:
- Fetches M+ scores and weekly key runs from Raider.io
- Fetches raid parses and boss kill data from WarcraftLogs
- Writes updated JSON files under
data/ - Commits the snapshot to
main - Triggers a new deployment via the deploy workflow
The site is fully static. There is no server or database. All data lives in JSON files in the repository.
| Tool | Purpose |
|---|---|
| SvelteKit + adapter-static | Static site framework |
| Svelte 5 runes | Reactive component model |
| PicoCSS | Base styles |
| TypeScript | Type safety throughout |
| Biome | Linting and formatting for JS/TS/JSON |
| Prettier + prettier-plugin-svelte | Formatting for .svelte files |
| Vitest | Unit and component tests |
| Playwright | End-to-end tests |
| pnpm | Package manager |
- Node.js LTS
- pnpm (
npm install -g pnpm) - A
.envfile at the project root (see Secrets and environment variables)
-
Install dependencies:
pnpm install
-
Start the dev server:
pnpm dev
-
Open
http://localhost:5173in your browser.
The dev server reads data from the JSON files already committed in data/. It does not fetch live data on its own.
Use this to pull fresh data outside the scheduled cron, for example after editing the roster.
-
Make sure your
.envfile contains valid credentials (see Secrets and environment variables). -
Run:
pnpm fetch
The script writes updated files to data/. Commit the changes and push to main to trigger a deployment.
Use the backfill script when you need parse data for past weeks — for example when adding a new raider mid-tier or when historical records are missing.
The backfill script uses WarcraftLogs encounterRankings(timeframe: Historical) to retrieve week-specific parses with exact report codes.
Run:
pnpm backfillCommit the resulting changes to data/ and push to trigger a deployment.
All officer tasks are done by editing data/roster.json directly and pushing to main. There is no UI — this is intentional so only officers with repository access can make changes.
After any edit to roster.json, run the fetch script to apply changes and trigger a deployment:
pnpm fetch
git add data/
git commit -m "chore: roster update"
git push-
Generate a new UUID for the raider:
node scripts/generate-uuid.mjs
-
Add a new block to the
playersarray indata/roster.json, filling inraider_id,display_name,status: "active",team_designation, their character name, realm, class, spec, and role. -
Add a
joinedevent tomembership_history:{ "event": "joined", "date": "YYYY-MM-DD", "note": "Reason for joining" } -
Add an entry to
role_historywithfromset to today andto: null. -
Run
pnpm fetch, commit, and push.
Do not delete the player block. This preserves their historical parse data and changelog entries.
-
Set
statusto"inactive"on their player block. -
Set
toon their currentrole_historyentry to today's date. -
Add a
leftevent tomembership_history:{ "event": "left", "date": "YYYY-MM-DD", "note": "Reason for leaving" } -
Run
pnpm fetch, commit, and push.
- Update
specon their active character. - Add a new entry to
role_historywith the new spec,fromset to today, andto: null. Settoon the previous entry to today's date. - The changelog will pick up the spec change automatically on the next fetch.
- Run
pnpm fetch, commit, and push.
- Set
active: falseon the old character in theircharactersarray. - Add the new character to the
charactersarray withactive: true. - Add a new entry to
role_historyfor the new character withfromset to today andto: null. Settoon the previous entry to today's date. - Run
pnpm fetch, commit, and push. The changelog will log the reroll automatically.
-
Change
team_designationon their player block to"main"or"alt". -
Add a
team_changedentry tomembership_history:{ "event": "team_changed", "date": "YYYY-MM-DD", "note": "Moving to alt team" } -
Run
pnpm fetch, commit, and push.
If a raider has a progression-blocking pug kill but had a valid reason to miss the raid, officers can grant a retrospective exemption. This removes the warning from their raider page and marks the changelog entry as exempt.
-
Add an
exemptionsarray to their player block inroster.json:"exemptions": [ { "week": "YYYY-WW", "granted_by": "OfficerName", "granted_at": "YYYY-MM-DDTHH:MM:SSZ", "reason": "Could not attend — personal commitment" } ]
Replace
YYYY-WWwith the ISO week number (for example2026-21). Add additional objects to the array if the raider needs exemptions for multiple weeks. -
Run
pnpm fetch, commit, and push. The blocking warning will no longer appear on their page.
The raid schedule controls when kills are considered in-raid vs blocking pugs. To change raid nights, start times, or timezone, edit the raid_schedule block in roster.json:
"raid_schedule": {
"timezone": "Europe/Paris",
"sessions": [
{ "day": "monday", "start": "20:30", "end": "23:30", "grace_minutes": 30 },
{ "day": "wednesday", "start": "20:30", "end": "23:30", "grace_minutes": 30 }
],
"safe_pug_windows": [
{ "day": "tuesday", "start": "00:00", "end": "23:59" },
{ "day": "wednesday", "start": "00:00", "end": "05:59" }
]
}grace_minutes extends the window on either side of the session. safe_pug_windows are periods when pugging a progression boss is allowed without a warning.
To change how many keys or what key level counts toward the weekly requirement, edit these fields in roster.json:
"mplus_weekly_minimum": 4,
"mplus_minimum_key_level": 10Run the validation script to check for common errors before committing:
node scripts/validate-roster.mjsA kill is flagged as a blocking pug if all of the following are true:
- The boss difficulty is
mythic - The kill happened outside all configured raid sessions (including grace windows)
- The kill did not fall within a
safe_pug_windowsentry - The raider does not have an active exemption for that ISO week
Blocking pug kills appear as a warning on the raider's detail page and are logged in the changelog. They disappear automatically after the weekly reset.
Exempt kills show as a blue informational note on the raider page and are logged in the changelog separately. They do not appear as warnings.
Parse data from blocking or exempt pug kills is excluded from the boss cards, progress charts, and the raid parses table — only kills done with Relentless count.
| Secret | Where to set it | What it is |
|---|---|---|
WCL_CLIENT_ID |
GitHub Actions secrets + local .env |
WarcraftLogs OAuth client ID |
WCL_CLIENT_SECRET |
GitHub Actions secrets + local .env |
WarcraftLogs OAuth client secret |
GH_PAT |
GitHub Actions secrets only | Personal access token used by the fetch workflow to commit data back to main |
- Go to https://www.warcraftlogs.com/api/clients.
- Create a new client. The redirect URI can be anything for a server-side client.
- Copy the client ID and secret.
Create .env at the project root:
WCL_CLIENT_ID=your_client_id_here
WCL_CLIENT_SECRET=your_client_secret_here
Do not commit this file. It is listed in .gitignore.
The personal access token needs:
reposcope (to push data commits back tomain)
The project is deployed to Cloudflare Pages. The deploy.yml GitHub Actions workflow builds and tests the site; Cloudflare Pages watches the main branch and deploys automatically on every push.
In the Cloudflare Pages dashboard, set:
| Setting | Value |
|---|---|
| Build command | pnpm build |
| Build output directory | build |
| Root directory | / |
No environment variables are needed in Cloudflare Pages — all secrets are used only at fetch time in GitHub Actions.
Security headers (X-Frame-Options, CSP, etc.) are applied via static/_headers, which Cloudflare Pages reads automatically.
Every push to main triggers two things simultaneously:
- The CI pipeline (
ci.yml) runs lint, type checks, unit tests, a build, and e2e tests. - Cloudflare Pages detects the push via Git integration and deploys the latest
mainbranch.
The fetch cron commits new data to main twice a day. Cloudflare Pages picks this up automatically — the live site reflects up-to-date data without any manual steps.
To deploy without waiting for the cron, push any change to main or trigger the fetch workflow manually via Actions → Fetch Data → Run workflow.
The build command is pnpm build. Output goes to build/. All routes are prerendered at build time from the JSON data files — the result is a folder of static HTML, CSS, and JS with no runtime server.
Run with Vitest:
pnpm test:unitWatch mode:
pnpm test:unit:watchRun with Playwright against the built site:
pnpm build
pnpm test:e2epnpm testpnpm lint
pnpm check:typesFix auto-fixable lint issues:
pnpm lint:fixrelentless/
├── data/
│ ├── roster.json # Raider list, characters, raid schedule, M+ config
│ ├── changelog.json # Team event log
│ └── seasons/ # Fetched data snapshots, one directory per zone/season
│ ├── index.json # Season and zone metadata
│ └── <season-id>/ # Weekly snapshots, compliance, parse data
├── scripts/
│ ├── fetch.ts # Daily data fetch (run by cron and manually)
│ ├── backfill.ts # Historical data fetch for past weeks
│ ├── generate-uuid.mjs # Generates a UUID for new raider entries
│ └── validate-roster.mjs # Validates roster.json structure
├── src/
│ ├── lib/
│ │ ├── components/ # Svelte components
│ │ ├── types/ # TypeScript type definitions
│ │ ├── utils/ # Pure utility functions (fetch helpers, parsers, etc.)
│ │ └── styles/ # Global CSS and colour tokens
│ └── routes/
│ ├── +page.svelte # Dashboard (home)
│ ├── +layout.svelte # Site shell
│ ├── raider/[uuid]/ # Per-raider detail page
│ ├── season/[id]/ # Season summary page
│ └── changelog/ # Team event log page
├── e2e/ # Playwright end-to-end tests
├── .github/workflows/
│ ├── fetch-data.yml # Cron: fetch data, commit, trigger deploy
│ └── deploy.yml # Build, test — Cloudflare Pages deploys on push
├── svelte.config.js # SvelteKit config (static adapter, prerender entries)
├── biome.json # Biome lint and format config
└── data/roster.json # Edit this to manage the roster
| Task | Command or action |
|---|---|
| Start local dev server | pnpm dev |
| Pull fresh data locally | pnpm fetch |
| Backfill historical data | pnpm backfill |
| Run all tests | pnpm test |
| Lint and format | pnpm lint:fix |
| Add a raider | Edit data/roster.json, run pnpm fetch, commit and push |
| Trigger a manual deploy | Push to main or use Actions → Run workflow |