Duplicates the team's Google Sheets matchup-matrix template once per
opposing team at a loaded event, pre-filled with names, factions,
career win% (all already computed by this page), and cleaned army
lists. New "Which team is yours?" selector + Generate Matrix button,
team-events only.
- Auth: Google Identity Services token client (drive.file +
spreadsheets scopes) — the viewer signs in with their own Google
account via a real OAuth popup, no backend, no client secret.
- Template layout is located at runtime by searching each tab for its
own placeholder text ("Player N - Faction", "Opponent N") rather
than hardcoded A1 ranges, so it survives the template changing later.
Distinguishes the real input area from the template's own "mobile
matrix" mirror and reference sections (which repeat the same
placeholder text) by checking for literal typed values vs formulas —
CSV export can't tell these apart, only the real Sheets API response
can, which is how this was actually verified before writing this.
Auto-picks between the template's 5-man/8-man tab variants by team
size.
- List cleaning via a vendored copy of desjani's 40k-compactor
(src/vendor/40k-compactor/, see NOTICE.md) — it isn't actually
published to npm despite documenting `npm install 40k-compactor`
(verified: 404 against the registry under every plausible name), so
it can't be a normal dependency; vendored instead, MIT per direct
confirmation from the maintainer. Verified against 40k-compactor's
own sample list fixtures with real Node before committing.
- Generating a matrix needs the same BCP auth token already used for
faction/disposition backfill (list text is a subscriber-gated
endpoint) — reuses the existing token UI/storage as-is; missing
lists are reported, not a hard failure.
- nginx CSP updated (script/connect/frame-src) for Google's identity
script and the Drive/Sheets REST APIs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SBzcNkW7JcYipnAX6HfgmK
193 lines
8.0 KiB
HTML
193 lines
8.0 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en" data-theme="dark">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Gateway Gamers — Scouting</title>
|
|
<link rel="stylesheet" href="/src/style.css" />
|
|
<!-- Google Identity Services — powers the "Generate Matrix" Google sign-in popup -->
|
|
<script src="https://accounts.google.com/gsi/client" async defer></script>
|
|
</head>
|
|
<body class="min-h-screen flex flex-col">
|
|
|
|
<div id="header"></div>
|
|
|
|
<main class="flex-1 max-w-5xl w-full mx-auto px-6 py-10">
|
|
|
|
<!-- Page heading -->
|
|
<div class="mb-6">
|
|
<h1 class="text-4xl font-bold uppercase tracking-wide">BCP Roster Scout</h1>
|
|
<p class="text-sm opacity-50 uppercase tracking-widest mt-1">Paste a Best Coast Pairings event link for instant roster stats</p>
|
|
</div>
|
|
|
|
<!-- Input row -->
|
|
<form id="scout-form" class="flex gap-2 mb-8">
|
|
<input
|
|
id="event-input"
|
|
type="text"
|
|
placeholder="https://www.bestcoastpairings.com/event/… or just the event ID"
|
|
class="input input-bordered flex-1 font-mono text-sm"
|
|
autocomplete="off"
|
|
/>
|
|
<button id="scout-btn" type="submit" class="btn btn-primary uppercase tracking-wide">Scout</button>
|
|
</form>
|
|
|
|
<!-- Optional: BCP auth token for backfilling missing faction/disposition from submitted lists -->
|
|
<details class="mb-6 bg-base-200 rounded-box">
|
|
<summary class="cursor-pointer px-4 py-2 text-sm opacity-70 select-none">
|
|
Have a BCP subscription? Add your auth token to backfill missing faction/disposition data
|
|
</summary>
|
|
<div class="px-4 pb-2 flex gap-2 items-center flex-wrap">
|
|
<input
|
|
id="bcp-token-input"
|
|
type="password"
|
|
placeholder="Paste your BCP auth token…"
|
|
class="input input-bordered input-sm flex-1 font-mono"
|
|
autocomplete="off"
|
|
/>
|
|
<button id="bcp-token-save" type="button" class="btn btn-sm">Save</button>
|
|
<button id="bcp-token-clear" type="button" class="btn btn-sm btn-ghost">Clear</button>
|
|
</div>
|
|
<p class="px-4 pb-1 text-xs opacity-40">
|
|
Stored only in your browser (localStorage) — sent only to BCP's own API, never committed or deployed.
|
|
Tokens expire after about an hour, so you may need to paste a fresh one occasionally.
|
|
</p>
|
|
<p class="px-4 pb-3 text-xs" id="bcp-token-status"></p>
|
|
</details>
|
|
|
|
<!-- Banner (warnings / errors) -->
|
|
<div class="alert mb-6 hidden" id="banner"></div>
|
|
|
|
<!-- Loading skeleton -->
|
|
<div class="hidden space-y-4" id="loading">
|
|
<div class="skeleton h-8 w-2/3"></div>
|
|
<div class="skeleton h-4 w-1/3"></div>
|
|
<div class="skeleton h-24 w-full"></div>
|
|
<div class="skeleton h-48 w-full"></div>
|
|
</div>
|
|
|
|
<!-- Empty state -->
|
|
<div class="text-center text-base-content/50 py-16" id="empty">
|
|
Paste a BCP event link above to see roster stats — faction spread, dispositions, checked-in rate, and team breakdowns.
|
|
</div>
|
|
|
|
<!-- Results -->
|
|
<div class="hidden" id="results">
|
|
|
|
<!-- Event header -->
|
|
<div class="card card-border bg-base-200 mb-6">
|
|
<div class="card-body py-5">
|
|
<div class="flex items-start justify-between gap-4 flex-wrap">
|
|
<div>
|
|
<h2 class="text-2xl font-bold" id="ev-name">—</h2>
|
|
<p class="text-sm opacity-60 mt-1" id="ev-sub">—</p>
|
|
</div>
|
|
<div class="badge badge-lg" id="ev-status">—</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stats strip -->
|
|
<div class="stats shadow w-full mb-6 bg-base-200 stats-vertical sm:stats-horizontal">
|
|
<div class="stat">
|
|
<div class="stat-title">Players</div>
|
|
<div class="stat-value text-2xl" id="s-players">—</div>
|
|
</div>
|
|
<div class="stat" id="s-teams-wrap">
|
|
<div class="stat-title">Teams</div>
|
|
<div class="stat-value text-2xl" id="s-teams">—</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-title">Checked In</div>
|
|
<div class="stat-value text-2xl" id="s-checkin">—</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-title">Lists Submitted</div>
|
|
<div class="stat-value text-2xl" id="s-lists">—</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Insights -->
|
|
<div class="mb-8">
|
|
<h3 class="text-lg font-bold uppercase tracking-wide mb-3">Insights</h3>
|
|
<ul class="space-y-2" id="insights"></ul>
|
|
</div>
|
|
|
|
<!-- Faction distribution -->
|
|
<div class="mb-8">
|
|
<h3 class="text-lg font-bold uppercase tracking-wide mb-3">Faction Distribution</h3>
|
|
<div id="faction-bars"></div>
|
|
</div>
|
|
|
|
<!-- Disposition distribution (across all factions) -->
|
|
<div class="mb-8">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<h3 class="text-lg font-bold uppercase tracking-wide">Top Dispositions</h3>
|
|
<button id="disposition-toggle" class="btn btn-ghost btn-xs">Show all</button>
|
|
</div>
|
|
<div id="disposition-bars"></div>
|
|
</div>
|
|
|
|
<!-- Roster: per-team dropdowns for team events, flat table for individual events -->
|
|
<div class="mb-8">
|
|
<div class="flex items-center justify-between mb-3 flex-wrap gap-2">
|
|
<h3 class="text-lg font-bold uppercase tracking-wide">Roster</h3>
|
|
<input id="player-filter" type="text" placeholder="Filter by name, team, or faction…" class="input input-bordered input-sm w-full sm:w-72" />
|
|
</div>
|
|
|
|
<!-- Generate Matrix (team events only) -->
|
|
<div class="card card-border bg-base-200 mb-4 hidden" id="matrix-card">
|
|
<div class="card-body py-4">
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
<label class="text-sm opacity-70" for="matrix-team-select">Which team is yours?</label>
|
|
<select id="matrix-team-select" class="select select-bordered select-sm max-w-xs"></select>
|
|
<button id="matrix-generate-btn" type="button" class="btn btn-sm btn-warning uppercase tracking-wide" disabled>
|
|
Generate Matrix
|
|
</button>
|
|
</div>
|
|
<p class="text-xs opacity-50 mt-1">
|
|
Duplicates the team matrix template, once per opposing team, pre-filled with names, factions, career win%, and cleaned lists.
|
|
List formatting by <a href="https://desjani.github.io/40kCompactor/" target="_blank" rel="noopener" class="link">40k Compactor</a> (Desjani), MIT licensed.
|
|
</p>
|
|
<p class="text-sm mt-2 hidden" id="matrix-status"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Team accordion (team events) -->
|
|
<div class="space-y-2 hidden" id="team-list"></div>
|
|
|
|
<!-- Flat table (individual events) -->
|
|
<div class="overflow-x-auto hidden" id="player-table-wrap">
|
|
<table class="table table-zebra w-full">
|
|
<thead>
|
|
<tr>
|
|
<th>Player</th>
|
|
<th>Faction</th>
|
|
<th class="hidden sm:table-cell">Disposition</th>
|
|
<th class="text-right">ITC Rank</th>
|
|
<th class="text-right">Career W%</th>
|
|
<th class="text-right">List</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="player-rows"></tbody>
|
|
</table>
|
|
</div>
|
|
<p class="text-xs text-base-content/40 mt-2" id="player-count"></p>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="divider mt-8"></div>
|
|
|
|
<p class="text-xs text-base-content/40 text-center">
|
|
Data from Best Coast Pairings · roster-only (this tool doesn't yet read pairings/placings)
|
|
</p>
|
|
|
|
</main>
|
|
|
|
<div id="footer"></div>
|
|
|
|
<script type="module" src="/src/main.js"></script>
|
|
</body>
|
|
</html>
|