- React + MUI DataGrid app with faction filter, search, change filter - Biggest movers cards (drops/rises) scoped to current filter view - Historical points graph modal (5 MFM versions: 1.14 → current) - URL state sync (faction, dir, q params — shareable URLs) - Grimdark favicon + OG embed image (Google Imagen) - Multi-stage Dockerfile (node build → nginx serve) - docker-compose.yml with Traefik + Cloudflare TLS - Data pipeline: build_deduped_data.py merges PDF + live scrape - Ynnari merged into Aeldari (shared codex) - Mobile responsive: flex columns, no fixed pixel widths - Color semantics: green=cheaper, red=costlier (consistent everywhere) - 1,449 units across 31 factions
3.1 KiB
3.1 KiB
WH40K Points Comparator
Compare Warhammer 40,000 unit points across Munitorum Field Manual versions.
Features
- Multi-version comparison — Track points changes across 5 MFM versions (1.14 → current)
- Faction filtering — Browse by faction or view all 1,449 units
- Biggest movers — Top 5 price drops and rises for the current view
- Historical graph — Click any unit to see a points history chart across all MFM versions
- Shareable URLs — Filter state is stored in URL query params (
?faction=...&dir=...&q=...) - Mobile responsive — Flex-based layout that scales to any device
- Social embeds — OG/Twitter meta tags with grimdark favicon and preview image
Tech Stack
- React + MUI (Material UI DataGrid)
- Vite build
- nginx (Alpine) in Docker
- Traefik reverse proxy with Cloudflare TLS
Data Sources
- Current MFM — Scraped from warhammer-community.com (live data)
- MFM 4.3 (Jun 2026), 3.2 (Aug 2025), 2.3 (Mar 2025), 1.14 (Dec 2024) — Parsed from PDFs
Deployment
Using Docker Compose (recommended)
- Edit
docker-compose.yml— change theHost()rule to your domain - Build and deploy:
docker compose up -d --build
Without Traefik
Uncomment the ports section in docker-compose.yml:
ports:
- "8080:80"
Then access at http://localhost:8080.
Building from scratch
The Dockerfile is multi-stage:
- Build stage —
node:20-alpineinstalls deps and runsnpm run build - Runtime stage —
nginx:alpineserves the static files
No pre-built artifacts needed — just docker compose build.
Data Pipeline
PDFs + live scrape → per-faction JSON files
↓
build_deduped_data.py
↓
react-app/public/data.json
↓
React app (fetched at runtime)
Run python3 build_deduped_data.py to rebuild data.json from source data.
Project Structure
wh40k-factions/
├── Dockerfile # Multi-stage: node build → nginx serve
├── docker-compose.yml # Traefik + Cloudflare TLS config
├── build_deduped_data.py # Merges all MFM versions into data.json
├── parse_pdf_per_faction.py # PDF → per-faction JSON (MFM 4.3)
├── react-app/
│ ├── index.html # OG meta tags, favicon
│ ├── package.json
│ ├── vite.config.js
│ ├── public/
│ │ ├── data.json # Merged dataset (5 MFM versions)
│ │ ├── favicon.png # Grimdark Aquila icon
│ │ └── og-image.png # Social embed banner
│ └── src/
│ ├── main.jsx
│ └── App.jsx # Main app: filters, movers, DataGrid, graph modal
├── live/ # Current MFM scrape (per-faction JSON)
├── pdf/ # MFM 4.3 PDF parse (per-faction JSON)
├── pdf32/ # MFM 3.2 PDF parse
├── pdf23/ # MFM 2.3 PDF parse
└── pdf114/ # MFM 1.14 PDF parse
License
MIT