feat: add theme quality, pool size, and popularity badges with filtering

This commit is contained in:
matt 2026-03-20 08:50:54 -07:00
parent 03e2846882
commit 0149fc2df9
21 changed files with 1165 additions and 64 deletions

View file

@ -34,7 +34,7 @@
<script>
window.__telemetryEndpoint = '/telemetry/events';
</script>
<link rel="stylesheet" href="/static/styles.css?v=20250911-1" />
<link rel="stylesheet" href="/static/styles.css?v=20260319-3" />
<link rel="stylesheet" href="/static/shared-components.css?v=20251021-1" />
<style>
/* Disable all transitions until page is loaded to prevent sidebar flash */

View file

@ -3,9 +3,9 @@
<section>
<h2>Diagnostics</h2>
<p class="muted">Use these tools to verify error handling surfaces.</p>
<div class="card" style="background: var(--panel); border:1px solid var(--border); border-radius:10px; padding:.75rem; margin-bottom:.75rem">
<h3 style="margin-top:0">System summary</h3>
<div id="sysSummary" class="muted">Loading…</div>
<details class="card" style="background: var(--panel); border:1px solid var(--border); border-radius:10px; padding:.75rem; margin-bottom:.75rem">
<summary style="cursor:pointer; user-select:none; margin-top:0; font-size:1.17em; font-weight:bold;">System summary</summary>
<div id="sysSummary" class="muted" style="margin-top:.5rem">Loading…</div>
<div id="envFlags" style="margin-top:.5rem"></div>
<div id="themeSuppMetrics" class="muted" style="margin-top:.5rem">Loading theme metrics…</div>
<div id="themeSummary" style="margin-top:.5rem"></div>
@ -13,9 +13,49 @@
<div style="margin-top:.35rem">
<button class="btn" id="diag-theme-reset">Reset theme preference</button>
</div>
</div>
<div class="card" style="background: var(--panel); border:1px solid var(--border); border-radius:10px; padding:.75rem; margin-bottom:.75rem">
<h3 style="margin-top:0">Multi-face merge snapshot</h3>
</details>
{# Theme Quality Overview #}
{% if quality_stats %}
<details class="card" style="background: var(--panel); border:1px solid var(--border); border-radius:10px; padding:.75rem; margin-bottom:.75rem">
<summary style="cursor:pointer; user-select:none; margin-top:0; font-size:1.17em; font-weight:bold;">Theme Catalog Quality</summary>
<div class="muted" style="margin-bottom:.5rem; margin-top:.5rem">Quick overview of theme quality metrics</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; margin-bottom: 1rem;">
<div>
<div class="muted" style="font-size: 11px;">Total Themes</div>
<div style="font-size: 20px; font-weight: 600;">{{ quality_stats.total_themes }}</div>
</div>
<div>
<div class="muted" style="font-size: 11px;">Average Quality</div>
<div style="font-size: 20px; font-weight: 600;">{{ (quality_stats.avg_quality_score * 100)|round|int }}%</div>
</div>
<div>
<div class="muted" style="font-size: 11px; color: #10b981;">Excellent</div>
<div style="font-size: 20px; font-weight: 600; color: #10b981;">{{ quality_stats.tier_counts.Excellent }}</div>
</div>
<div>
<div class="muted" style="font-size: 11px; color: #3b82f6;">Good</div>
<div style="font-size: 20px; font-weight: 600; color: #3b82f6;">{{ quality_stats.tier_counts.Good }}</div>
</div>
<div>
<div class="muted" style="font-size: 11px; color: #f59e0b;">Fair</div>
<div style="font-size: 20px; font-weight: 600; color: #f59e0b;">{{ quality_stats.tier_counts.Fair }}</div>
</div>
<div>
<div class="muted" style="font-size: 11px; color: #ef4444;">Poor</div>
<div style="font-size: 20px; font-weight: 600; color: #ef4444;">{{ quality_stats.tier_counts.Poor }}</div>
</div>
</div>
<div style="margin-top: .75rem;">
<a href="/diagnostics/quality" class="btn" style="text-decoration: none;">View Full Quality Dashboard →</a>
</div>
</details>
{% endif %}
<details class="card" style="background: var(--panel); border:1px solid var(--border); border-radius:10px; padding:.75rem; margin-bottom:.75rem">
<summary style="cursor:pointer; user-select:none; margin-top:0; font-size:1.17em; font-weight:bold;">Multi-face merge snapshot</summary>
<div class="muted" style="margin-bottom:.35rem">Pulls from <code>logs/dfc_merge_summary.json</code> to verify merge coverage.</div>
{% set colors = (merge_summary.colors if merge_summary else {}) | default({}) %}
{% if colors %}
@ -70,25 +110,25 @@
<div class="muted">No merge summary has been recorded. Run the tagger with multi-face merging enabled.</div>
{% endif %}
<div id="dfcMetrics" class="muted" style="margin-top:.5rem;">Loading MDFC metrics…</div>
</div>
<div class="card" style="background: var(--panel); border:1px solid var(--border); border-radius:10px; padding:.75rem; margin-bottom:.75rem">
<h3 style="margin-top:0">Dual-Commander diagnostics</h3>
<div class="muted" style="margin-bottom:.35rem;">Latest partner, partner-with, doctor, and background pairings with color sources.</div>
</details>
<details class="card" style="background: var(--panel); border:1px solid var(--border); border-radius:10px; padding:.75rem; margin-bottom:.75rem">
<summary style="cursor:pointer; user-select:none; margin-top:0; font-size:1.17em; font-weight:bold;">Dual-Commander diagnostics</summary>
<div class="muted" style="margin-bottom:.35rem; margin-top:.5rem;">Latest partner, partner-with, doctor, and background pairings with color sources.</div>
<div id="partnerMetricsSummary" class="muted">Loading partner metrics…</div>
<div id="partnerMetricsModes" class="muted" style="margin-top:.5rem;"></div>
<div id="partnerColorSources" style="margin-top:.5rem;"></div>
</div>
<div class="card" style="background: var(--panel); border:1px solid var(--border); border-radius:10px; padding:.75rem; margin-bottom:.75rem">
<h3 style="margin-top:0">Performance (local)</h3>
</details>
<details class="card" style="background: var(--panel); border:1px solid var(--border); border-radius:10px; padding:.75rem; margin-bottom:.75rem">
<summary style="cursor:pointer; user-select:none; margin-top:0; font-size:1.17em; font-weight:bold;">Performance (local)</summary>
<div class="muted" style="margin-bottom:.35rem">Scroll the Step 5 list; this panel shows a rough FPS estimate and virtualization renders.</div>
<div style="display:flex; gap:1rem; flex-wrap:wrap">
<div><strong>Scroll FPS:</strong> <span id="perf-fps"></span></div>
<div><strong>Visible tiles:</strong> <span id="perf-visible"></span></div>
<div><strong>Render count:</strong> <span id="perf-renders">0</span></div>
</div>
</div>
<div class="card" style="background: var(--panel); border:1px solid var(--border); border-radius:10px; padding:.75rem; margin-bottom:.75rem">
<h3 style="margin-top:0">Combos & Synergies (ad-hoc)</h3>
</details>
<details class="card" style="background: var(--panel); border:1px solid var(--border); border-radius:10px; padding:.75rem; margin-bottom:.75rem">
<summary style="cursor:pointer; user-select:none; margin-top:0; font-size:1.17em; font-weight:bold;">Combos & Synergies (ad-hoc)</summary>
<div class="muted" style="margin-bottom:.35rem">Paste card names (one per line) and detect two-card combos and synergies using current lists.</div>
<textarea id="diag-combos-input" rows="6" style="width:100%; resize:vertical; font-family: var(--mono);"></textarea>
<div style="margin-top:.5rem; display:flex; gap:.5rem; align-items:center">
@ -96,21 +136,21 @@
<small class="muted">Runs in diagnostics mode only.</small>
</div>
<pre id="diag-combos-out" style="margin-top:.5rem; white-space:pre-wrap"></pre>
</div>
</details>
{% if enable_pwa %}
<div class="card" style="background:#0f1115; border:1px solid var(--border); border-radius:10px; padding:.75rem; margin-bottom:.75rem">
<h3 style="margin-top:0">PWA status</h3>
<div id="pwaStatus" class="muted">Checking…</div>
</div>
<details class="card" style="background:#0f1115; border:1px solid var(--border); border-radius:10px; padding:.75rem; margin-bottom:.75rem">
<summary style="cursor:pointer; user-select:none; margin-top:0; font-size:1.17em; font-weight:bold;">PWA status</summary>
<div id="pwaStatus" class="muted" style="margin-top:.5rem">Checking…</div>
</details>
{% endif %}
<div class="card" style="background: var(--panel); border:1px solid var(--border); border-radius:10px; padding:.75rem;">
<h3 style="margin-top:0">Error triggers</h3>
<details class="card" style="background: var(--panel); border:1px solid var(--border); border-radius:10px; padding:.75rem;">
<summary style="cursor:pointer; user-select:none; margin-top:0; font-size:1.17em; font-weight:bold;">Error triggers</summary>
<div class="row" style="display:flex; gap:.5rem; align-items:center">
<button class="btn" hx-get="/diagnostics/trigger-error" hx-trigger="click" hx-target="this" hx-swap="none">Trigger HTTP error (418)</button>
<button class="btn" hx-get="/diagnostics/trigger-error?kind=unhandled" hx-trigger="click" hx-target="this" hx-swap="none">Trigger unhandled error (500)</button>
<small class="muted">You should see a toast and an inline banner with Request-ID.</small>
</div>
</div>
</details>
{% if show_logs %}
<p style="margin-top:.75rem"><a class="btn" href="/logs">Open Logs</a></p>
{% endif %}

View file

@ -0,0 +1,159 @@
{% extends "base.html" %}
{% block content %}
<section>
<h2>Theme Quality Dashboard</h2>
<p class="muted">Monitor theme catalog health and quality metrics</p>
{# Summary Statistics #}
<div class="card" style="background: var(--panel); border:1px solid var(--border); border-radius:10px; padding:.75rem; margin-bottom:.75rem">
<h3 style="margin-top:0">Catalog Statistics</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 1rem;">
<div>
<div class="muted" style="font-size: 12px;">Total Themes</div>
<div style="font-size: 24px; font-weight: 600;">{{ total_themes }}</div>
</div>
<div>
<div class="muted" style="font-size: 12px;">Average Quality Score</div>
<div style="font-size: 24px; font-weight: 600;">{{ (avg_quality_score * 100)|round|int }}%</div>
</div>
</div>
</div>
{# Quality Distribution #}
<div class="card" style="background: var(--panel); border:1px solid var(--border); border-radius:10px; padding:.75rem; margin-bottom:.75rem">
<h3 style="margin-top:0">Quality Tier Distribution</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem;">
<div>
<div style="font-size: 12px; color: #10b981; font-weight: 600;">Excellent (≥75%)</div>
<div style="font-size: 32px; font-weight: 700; color: #10b981;">{{ tier_counts.Excellent }}</div>
<div class="muted" style="font-size: 11px;">{{ ((tier_counts.Excellent / total_themes) * 100)|round(1) }}% of catalog</div>
</div>
<div>
<div style="font-size: 12px; color: #3b82f6; font-weight: 600;">Good (60-74%)</div>
<div style="font-size: 32px; font-weight: 700; color: #3b82f6;">{{ tier_counts.Good }}</div>
<div class="muted" style="font-size: 11px;">{{ ((tier_counts.Good / total_themes) * 100)|round(1) }}% of catalog</div>
</div>
<div>
<div style="font-size: 12px; color: #f59e0b; font-weight: 600;">Fair (40-59%)</div>
<div style="font-size: 32px; font-weight: 700; color: #f59e0b;">{{ tier_counts.Fair }}</div>
<div class="muted" style="font-size: 11px;">{{ ((tier_counts.Fair / total_themes) * 100)|round(1) }}% of catalog</div>
</div>
<div>
<div style="font-size: 12px; color: #ef4444; font-weight: 600;">Poor (&lt;40%)</div>
<div style="font-size: 32px; font-weight: 700; color: #ef4444;">{{ tier_counts.Poor }}</div>
<div class="muted" style="font-size: 11px;">{{ ((tier_counts.Poor / total_themes) * 100)|round(1) }}% of catalog</div>
</div>
</div>
</div>
{# Top 10 Highest Quality Themes #}
<div class="card" style="background: var(--panel); border:1px solid var(--border); border-radius:10px; padding:.75rem; margin-bottom:.75rem">
<h3 style="margin-top:0">Top 10 Highest Quality Themes</h3>
<div class="muted" style="margin-bottom:.5rem">Well-curated themes with high scores</div>
<div style="overflow-x:auto">
<table style="width:100%; border-collapse:collapse; font-size:13px;">
<thead>
<tr style="border-bottom:1px solid var(--border); text-align:left;">
<th style="padding:.35rem .5rem;">Rank</th>
<th style="padding:.35rem .5rem;">Theme</th>
<th style="padding:.35rem .5rem;">Tier</th>
<th style="padding:.35rem .5rem;">Score</th>
<th style="padding:.35rem .5rem;">Pool Size</th>
<th style="padding:.35rem .5rem;">Synergies</th>
<th style="padding:.35rem .5rem;">Editorial</th>
</tr>
</thead>
<tbody>
{% for theme in top_themes %}
<tr style="border-bottom:1px solid rgba(148,163,184,0.2);">
<td style="padding:.35rem .5rem; font-weight:600;">{{ loop.index }}</td>
<td style="padding:.35rem .5rem;">
<a href="/themes/{{ theme.slug }}" style="text-decoration: none; color: var(--link-color);">{{ theme.theme }}</a>
</td>
<td style="padding:.35rem .5rem;">
<span class="theme-badge badge-quality-{{ theme.tier|lower }}">{{ theme.tier }}</span>
</td>
<td style="padding:.35rem .5rem; font-weight:600;">{{ (theme.score * 100)|round|int }}%</td>
<td style="padding:.35rem .5rem;">{{ theme.pool_size }}</td>
<td style="padding:.35rem .5rem;">{{ theme.synergy_count }}</td>
<td style="padding:.35rem .5rem; text-transform: capitalize;">{{ theme.editorial_quality }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{# Bottom 10 Lowest Quality Themes #}
<div class="card" style="background: var(--panel); border:1px solid var(--border); border-radius:10px; padding:.75rem; margin-bottom:.75rem">
<h3 style="margin-top:0">Bottom 10 Lowest Quality Themes</h3>
<div class="muted" style="margin-bottom:.5rem">Themes that need improvement</div>
<div style="overflow-x:auto">
<table style="width:100%; border-collapse:collapse; font-size:13px;">
<thead>
<tr style="border-bottom:1px solid var(--border); text-align:left;">
<th style="padding:.35rem .5rem;">Theme</th>
<th style="padding:.35rem .5rem;">Tier</th>
<th style="padding:.35rem .5rem;">Score</th>
<th style="padding:.35rem .5rem;">Pool Size</th>
<th style="padding:.35rem .5rem;">Synergies</th>
<th style="padding:.35rem .5rem;">Issues</th>
<th style="padding:.35rem .5rem;">Suggestions</th>
</tr>
</thead>
<tbody>
{% for theme in bottom_themes %}
<tr style="border-bottom:1px solid rgba(148,163,184,0.2);">
<td style="padding:.35rem .5rem;">
<a href="/themes/{{ theme.slug }}" style="text-decoration: none; color: var(--link-color);">{{ theme.theme }}</a>
</td>
<td style="padding:.35rem .5rem;">
<span class="theme-badge badge-quality-{{ theme.tier|lower }}">{{ theme.tier }}</span>
</td>
<td style="padding:.35rem .5rem; font-weight:600;">{{ (theme.score * 100)|round|int }}%</td>
<td style="padding:.35rem .5rem;">{{ theme.pool_size }}</td>
<td style="padding:.35rem .5rem;">{{ theme.synergy_count }}</td>
<td style="padding:.35rem .5rem; font-size: 11px;">
{% set issues = [] %}
{% if theme.pool_size < 15 %}{% set _ = issues.append('Low card count') %}{% endif %}
{% if theme.synergy_count < 3 %}{% set _ = issues.append('Few synergies') %}{% endif %}
{% if theme.has_fallback_description %}{% set _ = issues.append('Auto-generated desc') %}{% endif %}
{% if theme.editorial_quality == 'auto' %}{% set _ = issues.append('Not reviewed') %}{% endif %}
{{ issues|join(', ') or 'None identified' }}
</td>
<td style="padding:.35rem .5rem; font-size: 11px;">
{% set suggestions = [] %}
{% if theme.pool_size < 15 %}{% set _ = suggestions.append('Add example cards') %}{% endif %}
{% if theme.synergy_count < 3 %}{% set _ = suggestions.append('Define synergies') %}{% endif %}
{% if theme.has_fallback_description %}{% set _ = suggestions.append('Write custom description') %}{% endif %}
{% if theme.editorial_quality == 'auto' %}{% set _ = suggestions.append('Review & curate') %}{% endif %}
{{ suggestions|join('; ') or 'N/A' }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{# Tools and Links #}
<div class="card" style="background: var(--panel); border:1px solid var(--border); border-radius:10px; padding:.75rem; margin-bottom:.75rem">
<h3 style="margin-top:0">Quality Improvement Tools</h3>
<div style="margin-bottom: 1rem;">
<h4 style="margin: 0 0 .5rem 0; font-size: 14px;">Run Linter</h4>
<p class="muted" style="margin: 0 0 .5rem 0; font-size: 12px;">Analyze theme catalog for quality issues and get actionable suggestions</p>
<code style="display: block; background: rgba(0,0,0,0.1); padding: .5rem; border-radius: 6px; font-size: 12px; overflow-x: auto;">
python code/scripts/validate_theme_catalog.py --lint
</code>
</div>
<div>
<h4 style="margin: 1rem 0 .5rem 0; font-size: 14px;">Documentation</h4>
<ul style="margin: 0; padding-left: 1.25rem; font-size: 13px;">
<li><a href="https://github.com/mwisnowski/mtg_python_deckbuilder/blob/main/docs/theme_editorial_guide.md" target="_blank" rel="noopener noreferrer" style="color: var(--link-color);">Theme Editorial Guide</a> - Quality scoring methodology and best practices</li>
<li><a href="/themes" style="color: var(--link-color);">Browse Themes</a> - View all themes with quality badges</li>
</ul>
</div>
</div>
</section>
{% endblock %}

View file

@ -8,7 +8,29 @@
<input type="text" id="theme-search" placeholder="Search themes" aria-label="Search" class="w-full" autocomplete="off" />
<div id="theme-search-results" class="search-suggestions"></div>
</div>
<div class="min-w-[160px]">
{% if show_theme_filters %}
<div class="min-w-[140px]">
<label class="text-[11px] block opacity-70">Quality</label>
<select id="quality-filter" class="w-full text-[13px]">
<option value="">All</option>
<option>Excellent</option>
<option>Good</option>
<option>Fair</option>
<option>Poor</option>
</select>
</div>
<div class="min-w-[140px]">
<label class="text-[11px] block opacity-70">Pool Size</label>
<select id="pool-filter" class="w-full text-[13px]">
<option value="">All</option>
<option>Vast</option>
<option>Large</option>
<option>Moderate</option>
<option>Small</option>
<option>Tiny</option>
</select>
</div>
<div class="min-w-[140px]">
<label class="text-[11px] block opacity-70">Popularity</label>
<select id="pop-filter" class="w-full text-[13px]">
<option value="">All</option>
@ -19,13 +41,78 @@
<option>Rare</option>
</select>
</div>
{% endif %}
<button id="clear-search" class="btn btn-ghost text-xs" hidden>Clear</button>
</div>
<div id="quick-popularity" class="flex gap-1.5 flex-wrap mb-2">
{% for b in ['Very Common','Common','Uncommon','Niche','Rare'] %}
<button class="btn btn-ghost pop-chip text-[11px] px-2 py-0.5" data-pop="{{ b }}">{{ b }}</button>
{% endfor %}
{% if show_theme_filters %}
<div id="quick-filters" class="flex gap-2.5 flex-wrap mb-2">
<div>
<div class="text-[10px] opacity-60 mb-0.5">Quality</div>
<div class="flex gap-1 flex-wrap">
{% for b in ['Excellent','Good','Fair','Poor'] %}
<button class="btn btn-ghost quality-chip text-[11px] px-2 py-0.5" data-quality="{{ b }}">{{ b[0] }}</button>
{% endfor %}
</div>
</div>
<div>
<div class="text-[10px] opacity-60 mb-0.5">Pool Size</div>
<div class="flex gap-1 flex-wrap">
{% for b in ['Vast','Large','Moderate','Small','Tiny'] %}
<button class="btn btn-ghost pool-chip text-[11px] px-2 py-0.5" data-pool="{{ b }}">{{ b[0] }}</button>
{% endfor %}
</div>
</div>
<div>
<div class="text-[10px] opacity-60 mb-0.5">Popularity</div>
<div class="flex gap-1 flex-wrap">
{% for b in ['Very Common','Common','Uncommon','Niche','Rare'] %}
<button class="btn btn-ghost pop-chip text-[11px] px-2 py-0.5" data-pop="{{ b }}">{{ b }}</button>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{# Badge Legend #}
<details class="mb-3 text-xs" style="max-width: 800px;">
<summary class="cursor-pointer opacity-70 hover:opacity-100" style="user-select: none;">
Badge Legend
</summary>
<div class="mt-2 p-3 rounded" style="background: var(--bg-secondary); border: 1px solid var(--border);">
<div class="flex flex-col gap-2.5">
{% if show_theme_quality_badges %}
<div>
<div class="font-semibold mb-1">Quality Badges <span class="theme-badge badge-quality-excellent">E</span> <span class="theme-badge badge-quality-good">G</span> <span class="theme-badge badge-quality-fair">F</span> <span class="theme-badge badge-quality-poor">P</span></div>
<div class="opacity-85">Editorial quality measures how well we've documented and curated each theme. The score is calculated from multiple factors:</div>
<ul class="mt-1 mb-1 opacity-85 text-[11px] list-disc ml-4 space-y-0.5">
<li><strong>Card Count & Uniqueness</strong>: Themes with sufficient representative cards and unique synergies score higher</li>
<li><strong>Description Quality</strong>: Custom descriptions that avoid generic patterns score higher than auto-generated fallbacks</li>
<li><strong>Metadata Completeness</strong>: Presence of primary/secondary colors, archetype classification, and popularity data</li>
<li><strong>Curation Status</strong>: Themes marked as reviewed or final have higher quality than draft entries</li>
</ul>
<div class="opacity-70 text-[11px]">Higher quality = better-documented, more thoroughly curated, easier to build around</div>
<div class="mt-1 opacity-70 text-[11px]"><strong>Tiers:</strong> Excellent (75%+) • Good (60-74%) • Fair (40-59%) • Poor (&lt;40%)</div>
<div class="mt-1.5 opacity-70 text-[11px]">💡 <a href="https://github.com/mwisnowski/mtg_python_deckbuilder" target="_blank" rel="noopener noreferrer" class="underline hover:opacity-100">Help improve theme quality on GitHub</a></div>
</div>
{% endif %}
{% if show_theme_pool_badges %}
<div>
<div class="font-semibold mb-1">Pool Size Badges <span class="theme-badge badge-pool-vast">V</span> <span class="theme-badge badge-pool-large">L</span> <span class="theme-badge badge-pool-moderate">M</span> <span class="theme-badge badge-pool-small">S</span> <span class="theme-badge badge-pool-tiny">T</span></div>
<div class="opacity-85">Total cards available with this theme tag. Larger pools offer more variety and flexibility when building.</div>
<div class="mt-1 opacity-70 text-[11px]">• Vast (500+) • Large (200-499) • Moderate (50-199) • Small (15-49) • Tiny (&lt;15)</div>
</div>
{% endif %}
{% if show_theme_popularity_badges %}
<div>
<div class="font-semibold mb-1">Popularity <span class="theme-badge badge-pop-vc">Very Common</span> <span class="theme-badge badge-pop-c">Common</span> <span class="theme-badge badge-pop-u">Uncommon</span> <span class="theme-badge badge-pop-n">Niche</span> <span class="theme-badge badge-pop-r">Rare</span></div>
<div class="opacity-85">Usage frequency based on how many commanders and cards are associated with this theme. More popular themes appear in more decks.</div>
</div>
{% endif %}
</div>
</div>
</details>
<div id="active-filters" class="flex gap-1.5 flex-wrap mb-2 text-[11px]"></div>
<div id="theme-results" aria-live="polite" aria-busy="true">
<div class="flex flex-col gap-2">
@ -38,7 +125,11 @@
const input = document.getElementById('theme-search');
const resultsBox = document.getElementById('theme-search-results');
const clearBtn = document.getElementById('clear-search');
const qualityFilter = document.getElementById('quality-filter');
const poolFilter = document.getElementById('pool-filter');
const popSel = document.getElementById('pop-filter');
const qualityChips = document.querySelectorAll('.quality-chip');
const poolChips = document.querySelectorAll('.pool-chip');
const popChips = document.querySelectorAll('.pop-chip');
const activeFilters = document.getElementById('active-filters');
const resultsHost = document.getElementById('theme-results');
@ -48,7 +139,9 @@
function buildParams(){
const params = new URLSearchParams();
const q = input.value.trim(); if(q) params.set('q', q);
const pop = popSel.value; if(pop) params.set('bucket', pop);
const quality = qualityFilter ? qualityFilter.value : ''; if(quality) params.set('quality_tier', quality);
const pool = poolFilter ? poolFilter.value : ''; if(pool) params.set('pool_tier', pool);
const pop = popSel ? popSel.value : ''; if(pop) params.set('bucket', pop);
params.set('limit','50'); params.set('offset','0');
return params.toString();
}
@ -62,7 +155,9 @@
function renderActive(){
activeFilters.innerHTML='';
const q = input.value.trim(); if(q) addChip('Search: '+q, ()=>{ input.value=''; fetchList(); });
const pop = popSel.value; if(pop) addChip('Popularity: '+pop, ()=>{ popSel.value=''; fetchList(); });
const quality = qualityFilter ? qualityFilter.value : ''; if(quality) addChip('Quality: '+quality, ()=>{ qualityFilter.value=''; fetchList(); });
const pool = poolFilter ? poolFilter.value : ''; if(pool) addChip('Pool: '+pool, ()=>{ poolFilter.value=''; fetchList(); });
const pop = popSel ? popSel.value : ''; if(pop) addChip('Popularity: '+pop, ()=>{ popSel.value=''; fetchList(); });
}
function fetchList(){
const ps = buildParams();
@ -154,7 +249,18 @@
fetch('/themes/fragment/detail/'+id,{cache:'reload'}).catch(()=>{});
});
document.addEventListener('click', function(ev){ if(!resultsBox.contains(ev.target) && ev.target!==input){ hideResults(); } });
popSel.addEventListener('change', fetchList); popChips.forEach(ch=> ch.addEventListener('click', ()=>{ popSel.value=ch.getAttribute('data-pop'); fetchList(); }));
if (qualityFilter) {
qualityFilter.addEventListener('change', fetchList);
qualityChips.forEach(ch=> ch.addEventListener('click', ()=>{ qualityFilter.value=ch.getAttribute('data-quality'); fetchList(); }));
}
if (poolFilter) {
poolFilter.addEventListener('change', fetchList);
poolChips.forEach(ch=> ch.addEventListener('click', ()=>{ poolFilter.value=ch.getAttribute('data-pool'); fetchList(); }));
}
if (popSel) {
popSel.addEventListener('change', fetchList);
popChips.forEach(ch=> ch.addEventListener('click', ()=>{ popSel.value=ch.getAttribute('data-pop'); fetchList(); }));
}
// Initial load
fetchList();
})();

View file

@ -18,10 +18,148 @@
{% endif %}
{% endif %}
<div class="text-xs mb-2 flex gap-2 flex-wrap">
{% if theme.popularity_bucket %}<span class="theme-badge {% if theme.popularity_bucket=='Very Common' %}badge-pop-vc{% elif theme.popularity_bucket=='Common' %}badge-pop-c{% elif theme.popularity_bucket=='Uncommon' %}badge-pop-u{% elif theme.popularity_bucket=='Niche' %}badge-pop-n{% elif theme.popularity_bucket=='Rare' %}badge-pop-r{% endif %}" title="Popularity: {{ theme.popularity_bucket }}" aria-label="Popularity bucket: {{ theme.popularity_bucket }}">{{ theme.popularity_bucket }}</span>{% endif %}
{% if show_theme_popularity_badges and theme.popularity_bucket %}<span class="theme-badge {% if theme.popularity_bucket=='Very Common' %}badge-pop-vc{% elif theme.popularity_bucket=='Common' %}badge-pop-c{% elif theme.popularity_bucket=='Uncommon' %}badge-pop-u{% elif theme.popularity_bucket=='Niche' %}badge-pop-n{% elif theme.popularity_bucket=='Rare' %}badge-pop-r{% endif %}" title="Popularity: {{ theme.popularity_bucket }}" aria-label="Popularity bucket: {{ theme.popularity_bucket }}">{{ theme.popularity_bucket }}</span>{% endif %}
{% if show_theme_quality_badges and theme.quality_tier %}<span class="theme-badge badge-quality-{{ theme.quality_tier|lower }}" title="Quality: {{ theme.quality_tier }} ({{ (theme.quality_score * 100)|round|int }}%)" aria-label="Quality tier: {{ theme.quality_tier }}">{{ theme.quality_tier }}</span>{% endif %}
{% if show_theme_pool_badges and theme.pool_tier %}<span class="theme-badge badge-pool-{{ theme.pool_tier|lower }}" title="Pool: {{ theme.pool_tier }} (~{{ theme.pool_size }} cards)" aria-label="Pool tier: {{ theme.pool_tier }}">{{ theme.pool_tier }}</span>{% endif %}
{% if diagnostics and theme.editorial_quality %}<span class="theme-badge badge-quality-{{ theme.editorial_quality }}" title="Editorial quality: {{ theme.editorial_quality }}" aria-label="Editorial quality: {{ theme.editorial_quality }}">{{ theme.editorial_quality }}</span>{% endif %}
{% if diagnostics and theme.has_fallback_description %}<span class="theme-badge badge-fallback" title="Fallback generic description" aria-label="Fallback generic description">Fallback</span>{% endif %}
</div>
<!-- Badge Explanations -->
{% if (show_theme_quality_badges and theme.quality_tier) or (show_theme_pool_badges and theme.pool_tier) or (show_theme_popularity_badges and theme.popularity_bucket) %}
<details class="mt-3 text-xs" style="max-width: 800px;" open>
<summary class="cursor-pointer opacity-70 hover:opacity-100 font-semibold" style="user-select: none;">
Badge Details
</summary>
<div class="mt-2 space-y-3 px-4 py-3 bg-gray-50 dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-gray-700">
{% if show_theme_quality_badges and theme.quality_tier %}
<div class="badge-explanation">
<div class="flex items-start gap-2">
<span class="theme-badge badge-quality-{{ theme.quality_tier|lower }} flex-shrink-0"
title="Quality: {{ theme.quality_tier }} ({{ (theme.quality_score * 100)|round|int }}%)">
{{ theme.quality_tier }}
</span>
<div class="flex-1 text-sm text-gray-700 dark:text-gray-300">
<strong>Quality Score:</strong> {{ (theme.quality_score * 100)|round|int }}%
<ul class="mt-1 ml-4 list-disc space-y-0.5 text-xs text-gray-600 dark:text-gray-400">
{% if theme.quality_score >= 0.70 %}
<li>High card count and unique synergies ({{ theme.synergy_count }} synergies)</li>
{% elif theme.quality_score >= 0.60 %}
<li>Good card selection with {{ theme.synergy_count }} documented synergies</li>
{% elif theme.quality_score >= 0.40 %}
<li>Moderate card pool with {{ theme.synergy_count }} synergies</li>
{% else %}
<li>Limited card pool and synergies ({{ theme.synergy_count }} synergies)</li>
{% endif %}
{% if theme.has_fallback_description %}
<li>Auto-generated description (could benefit from custom curation)</li>
{% else %}
<li>Custom curated description</li>
{% endif %}
{% if theme.curated_synergies or theme.enforced_synergies or theme.inferred_synergies %}
<li>Synergy breakdown: {{ theme.curated_synergies|length }} curated, {{ theme.enforced_synergies|length }} enforced, {{ theme.inferred_synergies|length }} inferred</li>
{% else %}
<li>No synergy breakdown available</li>
{% endif %}
{% if theme.deck_archetype %}
<li>Deck archetype: {{ theme.deck_archetype }}</li>
{% else %}
<li>No deck archetype classification</li>
{% endif %}
{% if theme.editorial_quality == 'final' %}
<li>Fully reviewed and curated (editorial status: final)</li>
{% elif theme.editorial_quality == 'refined' %}
<li>Reviewed and refined (editorial status: refined)</li>
{% elif theme.editorial_quality == 'draft' %}
<li>Draft quality (editorial status: draft)</li>
{% elif theme.editorial_quality == 'auto' %}
<li>Auto-generated content (editorial status: auto)</li>
{% endif %}
</ul>
<div class="mt-2 text-xs text-gray-500 dark:text-gray-500">💡 <a href="https://github.com/mwisnowski/mtg_python_deckbuilder" target="_blank" rel="noopener noreferrer" class="underline hover:text-gray-700 dark:hover:text-gray-300">Help improve theme quality on GitHub</a></div>
</div>
</div>
</div>
{% endif %}
{% if show_theme_pool_badges and theme.pool_tier %}
<div class="badge-explanation">
<div class="flex items-start gap-2">
<span class="theme-badge badge-pool-{{ theme.pool_tier|lower }} flex-shrink-0"
title="Pool: {{ theme.pool_tier }} (~{{ theme.pool_size }} cards)">
{{ theme.pool_tier }}
</span>
<div class="flex-1 text-sm text-gray-700 dark:text-gray-300">
<strong>Card Pool Size:</strong> ~{{ theme.pool_size }} cards available
<ul class="mt-1 ml-4 list-disc space-y-0.5 text-xs text-gray-600 dark:text-gray-400">
{% if theme.pool_tier == 'Vast' %}
<li>Extensive card selection with {{ theme.pool_size }}+ cards available for deckbuilding</li>
<li>Provides maximum flexibility and optimization potential</li>
{% elif theme.pool_tier == 'Large' %}
<li>Large card selection with {{ theme.pool_size }} cards available</li>
<li>Offers strong flexibility for different deck strategies</li>
{% elif theme.pool_tier == 'Moderate' %}
<li>Moderate selection with {{ theme.pool_size }} cards available</li>
<li>Sufficient options for focused deck strategies</li>
{% elif theme.pool_tier == 'Small' %}
<li>Limited selection with {{ theme.pool_size }} cards available</li>
<li>May require creative deckbuilding approaches</li>
{% elif theme.pool_tier == 'Tiny' %}
<li>Very limited pool with only {{ theme.pool_size }} cards available</li>
<li>Highly focused niche theme with restricted card choices</li>
{% endif %}
</ul>
</div>
</div>
</div>
{% endif %}
{% if show_theme_popularity_badges and theme.popularity_bucket %}
<div class="badge-explanation">
<div class="flex items-start gap-2">
<span class="theme-badge {% if theme.popularity_bucket=='Very Common' %}badge-pop-vc{% elif theme.popularity_bucket=='Common' %}badge-pop-c{% elif theme.popularity_bucket=='Uncommon' %}badge-pop-u{% elif theme.popularity_bucket=='Niche' %}badge-pop-n{% elif theme.popularity_bucket=='Rare' %}badge-pop-r{% endif %} flex-shrink-0"
title="Popularity: {{ theme.popularity_bucket }}">
{{ theme.popularity_bucket }}
</span>
<div class="flex-1 text-sm text-gray-700 dark:text-gray-300">
<strong>Theme Popularity:</strong> {{ theme.popularity_bucket }}
<ul class="mt-1 ml-4 list-disc space-y-0.5 text-xs text-gray-600 dark:text-gray-400">
{% if theme.popularity_bucket == 'Very Common' %}
<li>Extremely popular theme seen in many commander decks</li>
<li>High adoption rate across diverse commanders and strategies</li>
<li>Well-established with extensive community support and resources</li>
{% elif theme.popularity_bucket == 'Common' %}
<li>Popular theme frequently used in commander deckbuilding</li>
<li>Strong adoption rate with good community support</li>
<li>Well-documented strategies and card synergies available</li>
{% elif theme.popularity_bucket == 'Uncommon' %}
<li>Moderately popular theme with regular usage</li>
<li>Decent adoption rate, particularly in specific archetypes</li>
<li>Some community resources and discussion available</li>
{% elif theme.popularity_bucket == 'Niche' %}
<li>Specialized theme with limited but dedicated following</li>
<li>Lower adoption rate, often used in specific deck strategies</li>
<li>May require more research to optimize effectively</li>
{% elif theme.popularity_bucket == 'Rare' %}
<li>Rarely used theme with minimal adoption</li>
<li>Very low usage rate across commander decks</li>
<li>May offer unique deckbuilding opportunities for brewers</li>
{% endif %}
</ul>
</div>
</div>
</div>
{% endif %}
</div>
</details>
{% endif %}
<div class="synergy-section">
<h4>Synergies {% if not uncapped %}(capped){% endif %}</h4>
<div class="theme-synergies">
@ -46,7 +184,7 @@
{% if theme.example_cards %}
{% for c in theme.example_cards %}
{% set base_c = (c.split(' - Synergy (')[0] if ' - Synergy (' in c else c) %}
<div class="ex-card card-sample text-center" data-card-name="{{ base_c }}" data-role="example_card" data-tags="{{ theme.synergies|join(', ') }}" data-original-name="{{ c }}">
<div class="ex-card text-center" data-card-name="{{ base_c }}" data-role="example_card" data-tags="{{ theme.synergies|join(', ') }}" data-original-name="{{ c }}">
<img class="card-thumb w-full h-auto border border-[var(--border)] rounded-[10px]" loading="lazy" decoding="async" alt="{{ c }} image" src="{{ base_c|card_image('small') }}" />
<div class="text-[11px] mt-1 whitespace-nowrap overflow-hidden text-ellipsis font-semibold card-ref" data-card-name="{{ base_c }}" data-tags="{{ theme.synergies|join(', ') }}" data-original-name="{{ c }}">{{ c }}</div>
</div>

View file

@ -30,7 +30,17 @@
<tbody>
{% for it in items %}
<tr hx-get="/themes/fragment/detail/{{ it.id }}" hx-target="#theme-detail" hx-swap="innerHTML" title="Click for details" class="theme-row" data-theme-id="{{ it.id }}" tabindex="0" role="option" aria-selected="false">
<td title="{{ it.short_description or '' }}">{% set q = request.query_params.get('q') %}{% set name = it.theme %}{% if q %}{% set ql = q.lower() %}{% set nl = name.lower() %}{% if ql in nl %}{% set start = nl.find(ql) %}{% set end = start + q|length %}<span class="trunc-name">{{ name[:start] }}<mark>{{ name[start:end] }}</mark>{{ name[end:] }}</span>{% else %}<span class="trunc-name">{{ name }}</span>{% endif %}{% else %}<span class="trunc-name">{{ name }}</span>{% endif %} {% if diagnostics and it.has_fallback_description %}<span class="theme-badge badge-fallback" title="Fallback description"></span>{% endif %}
<td title="{{ it.short_description or '' }}">{% set q = request.query_params.get('q') %}{% set name = it.theme %}{% if q %}{% set ql = q.lower() %}{% set nl = name.lower() %}{% if ql in nl %}{% set start = nl.find(ql) %}{% set end = start + q|length %}<span class="trunc-name">{{ name[:start] }}<mark>{{ name[start:end] }}</mark>{{ name[end:] }}</span>{% else %}<span class="trunc-name">{{ name }}</span>{% endif %}{% else %}<span class="trunc-name">{{ name }}</span>{% endif %}
{# Quality tier badge #}
{% if show_theme_quality_badges and it.quality_tier %}
<span class="theme-badge badge-quality-{{ it.quality_tier|lower }}" title="Quality: {{ it.quality_tier }} ({{ (it.quality_score * 100)|round|int }}%)" aria-label="Quality tier: {{ it.quality_tier }}">{{ it.quality_tier[0]|upper }}</span>
{% endif %}
{# Pool size badge #}
{% if show_theme_pool_badges and it.pool_tier %}
<span class="theme-badge badge-pool-{{ it.pool_tier|lower }}" title="Pool: {{ it.pool_tier }} (~{{ it.pool_size }} cards)" aria-label="Pool tier: {{ it.pool_tier }}">{{ it.pool_tier[0]|upper }}</span>
{% endif %}
{# Diagnostics-only badges #}
{% if diagnostics and it.has_fallback_description %}<span class="theme-badge badge-fallback" title="Fallback description"></span>{% endif %}
{% if diagnostics and it.editorial_quality %}
<span class="theme-badge badge-quality-{{ it.editorial_quality }}" title="Editorial quality: {{ it.editorial_quality }}">{{ it.editorial_quality[0]|upper }}</span>
{% endif %}
@ -38,7 +48,7 @@
<td>{% if it.primary_color %}<span aria-label="Primary color: {{ it.primary_color }}">{{ it.primary_color }}</span>{% endif %}</td>
<td>{% if it.secondary_color %}<span aria-label="Secondary color: {{ it.secondary_color }}">{{ it.secondary_color }}</span>{% endif %}</td>
<td>
{% if it.popularity_bucket %}
{% if show_theme_popularity_badges and it.popularity_bucket %}
<span class="theme-badge {% if it.popularity_bucket=='Very Common' %}badge-pop-vc{% elif it.popularity_bucket=='Common' %}badge-pop-c{% elif it.popularity_bucket=='Uncommon' %}badge-pop-u{% elif it.popularity_bucket=='Niche' %}badge-pop-n{% elif it.popularity_bucket=='Rare' %}badge-pop-r{% endif %}" title="Popularity: {{ it.popularity_bucket }}">{{ it.popularity_bucket }}</span>
{% endif %}
</td>

View file

@ -13,7 +13,20 @@
<ul class="list-none p-0 m-0 flex flex-col gap-2.5">
{% for it in items %}
<li class="theme-list-card">
<a href="/themes/{{ it.id }}" class="font-semibold text-sm no-underline text-[var(--text)]">{{ it.theme }}</a>
<div class="flex justify-between items-start gap-2">
<a href="/themes/{{ it.id }}" class="font-semibold text-sm no-underline text-[var(--text)]">{{ it.theme }}</a>
<div class="flex gap-1">
{% if show_theme_quality_badges and it.quality_tier %}
<span class="theme-badge badge-quality-{{ it.quality_tier|lower }}" title="Quality: {{ it.quality_tier }} ({{ (it.quality_score * 100)|round|int }}%)">{{ it.quality_tier[0]|upper }}</span>
{% endif %}
{% if show_theme_pool_badges and it.pool_tier %}
<span class="theme-badge badge-pool-{{ it.pool_tier|lower }}" title="Pool: {{ it.pool_tier }} (~{{ it.pool_size }} cards)">{{ it.pool_tier[0]|upper }}</span>
{% endif %}
{% if show_theme_popularity_badges and it.popularity_bucket %}
<span class="theme-badge {% if it.popularity_bucket=='Very Common' %}badge-pop-vc{% elif it.popularity_bucket=='Common' %}badge-pop-c{% elif it.popularity_bucket=='Uncommon' %}badge-pop-u{% elif it.popularity_bucket=='Niche' %}badge-pop-n{% elif it.popularity_bucket=='Rare' %}badge-pop-r{% endif %}" title="Popularity: {{ it.popularity_bucket }}">{{ it.popularity_bucket }}</span>
{% endif %}
</div>
</div>
{% if it.short_description %}<div class="text-xs opacity-85 mt-0.5">{{ it.short_description }}</div>{% endif %}
</li>
{% endfor %}