Skip to content

About

Paste a mplus.run LFG export and get a sortable Mythic+ applicant table with median and average Warcraft Logs parses on every row.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

gray-matter

Turn a mplus.run LFG export into a sortable applicant table with median and average Mythic+ parse on every row — so you can judge a whole queue at a glance instead of clicking into each character.

┌──────────────┬─────────┬────────┬───────┬────────┬──────────┬──────────────┬───────────┐
│ Character    │ Class   │ Role   │ iLvl  │ M+ Scr │ Dungeons │ Median Parse │ Avg Parse │
├──────────────┼─────────┼────────┼───────┼────────┼──────────┼──────────────┼───────────┤
│ Furywar      │ Warrior │ DPS    │   630 │   3586 │        8 │         87.5 │      84.5 │
│ Holyheals    │ Paladin │ Healer │   625 │   3000 │        3 │         68.0 │      62.0 │
│ Brewmonk     │ Monk    │ Tank   │   628 │   3200 │        4 │         22.5 │      22.0 │
└──────────────┴─────────┴────────┴───────┴────────┴──────────┴──────────────┴───────────┘

Click any row for a per-dungeon breakdown, click any header to sort, and parse cells are coloured on the usual Warcraft Logs percentile scale.

Requirements

  • Node 23.6 or newer — the code runs as TypeScript directly through Node's built-in type stripping, so there is no build step and no runtime dependencies. Node 24 LTS is a good choice.

Install

git clone https://github.com/beaurancourt/gray-matter.git
cd gray-matter

cp .env.example .env
$EDITOR .env          # paste your Warcraft Logs credentials (see below)

npm start             # http://localhost:3000

There is no npm install step: nothing is needed at runtime. Only install dev dependencies if you want to run the tests or typecheck:

npm install           # optional: typescript + @types/node
npm test
npm run typecheck

Getting an export out of the game

  1. Install the mplus.run addon from CurseForge (also in the CurseForge app).
  2. Open the group finder and let applicants queue up.
  3. Click the mplus.run icon next to the Refresh button in the applicant list.
  4. Press Ctrl+C to copy the link, then paste it into gray-matter and hit Load roster.

The addon also has a right-click menu to copy a single player's profile URL, but those links carry no parse data — use the bulk export for the table.

Getting a Warcraft Logs API key

Parses come straight from the Warcraft Logs API v2, so you need a free API client. It takes about a minute.

  1. Sign in at https://www.warcraftlogs.com, then open https://www.warcraftlogs.com/api/clients.
  2. Click Create Client.
  3. Name it anything, e.g. gray-matter.
  4. Redirect URL — this app never uses it. The client-credentials flow this app uses never redirects anywhere, so any placeholder is fine: http://localhost:3000/callback. (If the form insists on HTTPS, https://localhost:3000/callback works too.)
  5. Make sure you are creating a client credentials client. You do not need the user-authorization / PKCE flow — that is for reading private logs, which this app doesn't do.
  6. Create it, then copy the Client ID and Client Secret immediately. Warcraft Logs shows the secret exactly once; if you lose it you must delete the client and create a new one.
  7. Paste both into .env:
WCL_CLIENT_ID=01a0a067-0000-0000-0000-000000000000
WCL_CLIENT_SECRET=your-secret-here

.env is gitignored, so your credentials never end up in git.

Verify the setup

npm run doctor
Authenticating with Warcraft Logs...
Fetched 66 zones.

Auto-detected Mythic+ zone:
   55  Mythic+ Season 2  [Midnight]

Rate limit: 14.02/3600 points used, resets in 58 min.

If that prints a zone, you're done. Useful variants:

npm run doctor -- --zones              # list every zone WCL knows about
npm run doctor -- "<mplus.run link>"   # decode a roster and look up its first character
npm run doctor -- --raw "<link>"       # dump the raw zoneRankings JSON

--raw is the quickest way to see what Warcraft Logs actually returns if you want to change how the columns are computed.

Configuration

Everything is optional; the defaults are sensible.

Variable Default Meaning
PORT 3000 HTTP port.
WCL_CLIENT_ID — Required. Warcraft Logs API client ID.
WCL_CLIENT_SECRET — Required. Warcraft Logs API client secret.
WCL_MPLUS_ZONE_ID auto Pin a Warcraft Logs zone. Blank auto-detects the current season.
WCL_METRIC auto auto, points_and_damage, points_and_healing, dps, hps.
CACHE_TTL_MINUTES 720 How long a character's parses stay cached.
WCL_CONCURRENCY 4 How many characters to look up in parallel.

auto asks for healing rankings for characters who can only heal, and damage rankings for everyone else, so a healer's row never shows a meaningless damage parse.

How it works

The export URL

The addon base64-encodes the applicant list into the link:

https://mplus.run/lfg?d=<url-safe base64 of the JSON>
{
  "r": "eu",                       // region: us | eu | kr | tw
  "p": [                           // players
    {
      "re": "tarren-mill",         // realm slug
      "n": "Veryalive",            // character name
      "c": 1,                      // class id
      "ra": 1,                     // race id
      "s": 3586.41,                // Mythic+ score
      "i": 630,                    // item level
      "ro": 4,                     // role bitfield: 1 tank, 2 healer, 4 dps
      "f": "a",                    // faction: a | h
      "g": 1                       // optional group id
    }
  ]
}

Two things worth knowing about it:

  • The payload contains no parse data — only identity, score and item level. Everything shown in the parse columns is fetched afterwards.
  • mplus.run's own data routes sit behind a Cloudflare challenge that returns 403 to non-browser clients, so this app never calls mplus.run. It decodes the link locally and goes to the same upstream source mplus.run uses.

Where the parses come from

One GraphQL query per character:

query ZoneRankings($name: String!, $serverSlug: String!, $serverRegion: String!, $zoneID: Int!, $metric: CharacterPageRankingMetricType) {
  characterData {
    character(name: $name, serverSlug: $serverSlug, serverRegion: $serverRegion) {
      zoneRankings(zoneID: $zoneID, metric: $metric)
    }
  }
}

The points_and_damage / points_and_healing metrics attach a throughputRankings block, keyed by encounter id, alongside the ordinary score rankings:

{
  "zone": 55, "metric": "points_and_damage",
  "rankings": [ /* score rankings: used here only for names, kills and specs */ ],
  "throughputRankings": {
    "12993": {
      "best_level": 14,                    // their highest timed key for this dungeon
      "best_historical_percentile": 80,    // <- the per-dungeon "key%" parse
      "median_historical_percentile": 80,
      "best_historical_low_parses": false
    }
  }
}

How the two columns are computed

For each dungeon, one number goes into the series: the character's best key% parse at their highest timed keystone level for that dungeon. Then:

Column Definition
Median Parse median of those per-dungeon numbers
Avg Parse mean of those per-dungeon numbers

That per-dungeon number is best_historical_percentile, and best_level is the key it came from — surfaced as the Key column in the expanded row.

Verified end to end against live data: for a real Arcane Mage the eight values came out 80, 66, 97, 95, 93, 64, 97, 55, reproducing Warcraft Logs' own per-dungeon figures exactly and giving median 86.5 / average 80.875. test/parses.test.ts pins that fixture, so a regression in field selection fails the suite.

Note this deliberately does not use WCL's medianPerformanceAverage / bestPerformanceAverage. Those aggregate the per-dungeon median percentiles, which answers a different question ("how do their typical runs look"). For the same character the two disagree sharply: 86.5 by this app's definition versus 51.7 for medianPerformanceAverage.

Two caveats worth knowing:

  • It is not always 8 numbers. A dungeon the character has never logged has no throughputRankings entry at all and is excluded. The Dungeons column shows how many actually contributed — a median from 4 dungeons is shakier than one from 8.
  • WCL only knows about logged runs. If a key wasn't logged, or was logged privately, it won't appear, so best_level can come out lower than the highest key shown on Raider.io.

The expanded row shows the key level, the Best parse that feeds the columns, and the raw Median kill percentile from the same block.

Caching

Warcraft Logs bills by points per hour (3600 by default), not by request, and each character lookup spends a couple of points. Results are cached in memory and on disk (.cache/parses.json) for CACHE_TTL_MINUTES, keyed by character + zone + metric, so re-opening the same roster costs nothing. Tick Ignore cache to force a refresh. A full 40-applicant roster costs roughly 80 points.

Deployment

It is a single Node process with no dependencies, so it runs anywhere Node 23.6+ does. A systemd unit is included at deploy/gray-matter.service — edit User and WorkingDirectory for your box, then:

sudo cp deploy/gray-matter.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now gray-matter
journalctl -u gray-matter -f

The server keeps the Warcraft Logs secret server-side, so do not expose it directly to the internet without putting an auth layer (or a private network such as a VPN or Tailscale) in front of it — anyone who can reach it can spend your Warcraft Logs points.

Layout

src/
  mplus.ts     decode/encode the addon export payload
  wcl.ts       OAuth client-credentials flow, GraphQL, Mythic+ zone resolution
  parses.ts    zoneRankings JSON -> median/average parse summary
  server.ts    static host + /api/roster + streaming /api/parses
  cache.ts     TTL cache with lazy disk persistence
  metadata.ts  class/role lookup tables
  config.ts    env handling
  doctor.ts    setup diagnostics CLI
public/        the table UI (no framework, no build step)
test/          node:test suites

Development

npm run dev        # watch mode
npm test           # 27 tests
npm run typecheck

Tests cover payload round-tripping, malformed links, the parse summariser against a real fixture, metric selection, and the exact per-dungeon values for a known character.

Notes

  • mplus.run's robots.txt carries Cloudflare content signals (ai-train=no) and disallows a number of AI crawlers. This project does not crawl it: it decodes a link you already have and queries the Warcraft Logs API, which is the same upstream source mplus.run itself uses.
  • Character names and realms come out of your own in-game LFG queue. The only outbound requests are to Warcraft Logs.

License

MIT — see LICENSE.

About

Paste a mplus.run LFG export and get a sortable Mythic+ applicant table with median and average Warcraft Logs parses on every row.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages