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.
A GitHub Actions cron job runs at 05:00 UTC and 17:00 UTC every day. It:
data/mainThe 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 |
npm install -g pnpm).env file at the project root (see Secrets and environment variables)Install dependencies:
pnpm install
Start the dev server:
pnpm dev
Open http://localhost:5173 in 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 .env file 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 backfill
Commit 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 players array in data/roster.json, filling in raider_id, display_name, status: "active", team_designation, their character name, realm, class, spec, and role.
Add a joined event to membership_history:
{ "event": "joined", "date": "YYYY-MM-DD", "note": "Reason for joining" }
Add an entry to role_history with from set to today and to: null.
Run pnpm fetch, commit, and push.
Do not delete the player block. This preserves their historical parse data and changelog entries.
Set status to "inactive" on their player block.
Set to on their current role_history entry to today's date.
Add a left event to membership_history:
{ "event": "left", "date": "YYYY-MM-DD", "note": "Reason for leaving" }
Run pnpm fetch, commit, and push.
spec on their active character.role_history with the new spec, from set to today, and to: null. Set to on the previous entry to today's date.pnpm fetch, commit, and push.active: false on the old character in their characters array.characters array with active: true.role_history for the new character with from set to today and to: null. Set to on the previous entry to today's date.pnpm fetch, commit, and push. The changelog will log the reroll automatically.Change team_designation on their player block to "main" or "alt".
Add a team_changed entry to membership_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 exemptions array to their player block in roster.json:
"exemptions": [
{
"week": "YYYY-WW",
"granted_by": "OfficerName",
"granted_at": "YYYY-MM-DDTHH:MM:SSZ",
"reason": "Could not attend — personal commitment"
}
]
Replace YYYY-WW with the ISO week number (for example 2026-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": 10
Run the validation script to check for common errors before committing:
node scripts/validate-roster.mjs
A kill is flagged as a blocking pug if all of the following are true:
mythicsafe_pug_windows entryBlocking 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 |
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:
repo scope (to push data commits back to main)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:
ci.yml) runs lint, type checks, unit tests, a build, and e2e tests.main branch.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:unit
Watch mode:
pnpm test:unit:watch
Run with Playwright against the built site:
pnpm build
pnpm test:e2e
pnpm test
pnpm lint
pnpm check:types
Fix auto-fixable lint issues:
pnpm lint:fix
relentless/
├── 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 |