Web UI polish: thumbnail-hover preview, white thumbnail selection, Themes bullet list; global Scryfall image retry (thumbs+previews) with fallbacks and cache-bust; standardized data-card-name. Deck Summary alignment overhaul (count//name/owned grid, tabular numerals, inset highlight, tooltips, starts under header). Added diagnostics (health + logs pages, error pages, request-id propagation), global HTMX error toasts, and docs updates. Update DOCKER guide and add run-web scripts. Update CHANGELOG and release notes template.
2025-08-27 11:21:46 -07:00
{% extends "base.html" %}
{% block content %}
< section >
< h2 > Diagnostics< / h2 >
< p class = "muted" > Use these tools to verify error handling surfaces.< / p >
2026-03-20 09:03:20 -07:00
< 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 >
2025-10-03 10:43:24 -07:00
< div id = "envFlags" style = "margin-top:.5rem" > < / div >
< div id = "themeSuppMetrics" class = "muted" style = "margin-top:.5rem" > Loading theme metrics…< / div >
2025-08-28 16:44:58 -07:00
< div id = "themeSummary" style = "margin-top:.5rem" > < / div >
2025-09-26 18:15:52 -07:00
< div id = "themeTokenStats" class = "muted" style = "margin-top:.5rem" > Loading theme stats…< / div >
2025-08-28 16:44:58 -07:00
< div style = "margin-top:.35rem" >
< button class = "btn" id = "diag-theme-reset" > Reset theme preference< / button >
< / div >
2026-03-20 09:03:20 -07:00
< / 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 >
2025-10-02 15:31:05 -07:00
< div class = "muted" style = "margin-bottom:.35rem" > Pulls from < code > logs/dfc_merge_summary.json< / code > to verify merge coverage.< / div >
2025-10-03 10:43:24 -07:00
{% set colors = (merge_summary.colors if merge_summary else {}) | default({}) %}
2025-10-02 15:31:05 -07:00
{% if colors %}
< div class = "muted" style = "margin-bottom:.35rem" > Last updated: {{ merge_summary.updated_at or 'unknown' }}< / 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 .25rem;" > Color< / th >
< th style = "padding:.35rem .25rem;" > Groups merged< / th >
< th style = "padding:.35rem .25rem;" > Faces dropped< / th >
< th style = "padding:.35rem .25rem;" > Multi-face rows< / th >
< th style = "padding:.35rem .25rem;" > Latest entries< / th >
< / tr >
< / thead >
< tbody >
2025-10-03 10:43:24 -07:00
{% for item in colors|dictsort %}
{% set color = item[0] %}
{% set payload = item[1] | default({}) %}
2025-10-02 15:31:05 -07:00
< tr style = "border-bottom:1px solid rgba(148,163,184,0.2);" >
< td style = "padding:.35rem .25rem; font-weight:600;" > {{ color|title }}< / td >
< td style = "padding:.35rem .25rem;" > {{ payload.group_count or 0 }}< / td >
< td style = "padding:.35rem .25rem;" > {{ payload.faces_dropped or 0 }}< / td >
< td style = "padding:.35rem .25rem;" > {{ payload.multi_face_rows or 0 }}< / td >
< td style = "padding:.35rem .25rem;" >
2025-10-03 10:43:24 -07:00
{% set entries = (payload.entries | default([])) %}
2025-10-02 15:31:05 -07:00
{% if entries %}
< details >
< summary style = "cursor:pointer;" > {{ entries|length }} recorded< / summary >
2025-10-03 10:43:24 -07:00
{% set preview = entries[:5] %}
2025-10-02 15:31:05 -07:00
< ul style = "margin:.35rem 0 0 .75rem; padding:0; list-style:disc; max-height:180px; overflow:auto;" >
2025-10-03 10:43:24 -07:00
{% for entry in preview %}
< li style = "margin-bottom:.25rem;" >
< strong > {{ entry.name }}< / strong > — {{ entry.total_faces }} faces (dropped {{ entry.dropped_faces }})
< / li >
2025-10-02 15:31:05 -07:00
{% endfor %}
2025-10-03 10:43:24 -07:00
{% if entries|length > preview|length %}
< li style = "font-size:11px; opacity:.75;" > … {{ entries|length - preview|length }} more entries< / li >
{% endif %}
2025-10-02 15:31:05 -07:00
< / ul >
< / details >
{% else %}
< span class = "muted" > No groups recorded< / span >
{% endif %}
< / td >
< / tr >
{% endfor %}
< / tbody >
< / table >
< / div >
{% else %}
< 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 >
2026-03-20 09:03:20 -07:00
< / 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 >
2025-10-06 09:17:59 -07:00
< 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 >
2026-03-20 09:03:20 -07:00
< / 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 >
2025-08-28 14:57:22 -07:00
< 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 >
2026-03-20 09:03:20 -07:00
< / 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 >
2025-09-01 16:55:24 -07:00
< 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" >
< button class = "btn" id = "diag-combos-run" > Detect< / button >
< small class = "muted" > Runs in diagnostics mode only.< / small >
< / div >
< pre id = "diag-combos-out" style = "margin-top:.5rem; white-space:pre-wrap" > < / pre >
2026-03-20 09:03:20 -07:00
< / details >
2025-08-28 16:44:58 -07:00
{% if enable_pwa %}
2026-03-20 09:03:20 -07:00
< 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 >
2025-08-28 16:44:58 -07:00
{% endif %}
2026-03-20 09:03:20 -07:00
< 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 >
Web UI polish: thumbnail-hover preview, white thumbnail selection, Themes bullet list; global Scryfall image retry (thumbs+previews) with fallbacks and cache-bust; standardized data-card-name. Deck Summary alignment overhaul (count//name/owned grid, tabular numerals, inset highlight, tooltips, starts under header). Added diagnostics (health + logs pages, error pages, request-id propagation), global HTMX error toasts, and docs updates. Update DOCKER guide and add run-web scripts. Update CHANGELOG and release notes template.
2025-08-27 11:21:46 -07:00
< 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 >
2026-03-20 09:03:20 -07:00
< / details >
Web UI polish: thumbnail-hover preview, white thumbnail selection, Themes bullet list; global Scryfall image retry (thumbs+previews) with fallbacks and cache-bust; standardized data-card-name. Deck Summary alignment overhaul (count//name/owned grid, tabular numerals, inset highlight, tooltips, starts under header). Added diagnostics (health + logs pages, error pages, request-id propagation), global HTMX error toasts, and docs updates. Update DOCKER guide and add run-web scripts. Update CHANGELOG and release notes template.
2025-08-27 11:21:46 -07:00
{% if show_logs %}
< p style = "margin-top:.75rem" > < a class = "btn" href = "/logs" > Open Logs< / a > < / p >
{% endif %}
< / section >
< script >
(function(){
var el = document.getElementById('sysSummary');
function render(data){
if (!el) return;
try {
var v = (data & & data.version) || 'dev';
var up = (data & & data.uptime_seconds) || 0;
var st = (data & & data.server_time_utc) || '';
var flags = (data & & data.flags) || {};
el.innerHTML = '< div > < strong > Version:< / strong > '+String(v)+'< / div > '+
(st ? '< div > < strong > Server time (UTC):< / strong > '+String(st)+'< / div > ' : '')+
'< div > < strong > Uptime:< / strong > '+String(up)+'s< / div > '+
2025-09-17 13:23:27 -07:00
'< div > < strong > Flags:< / strong > '
+ 'SHOW_LOGS='+ (flags.SHOW_LOGS? '1':'0')
+ ', SHOW_DIAGNOSTICS='+ (flags.SHOW_DIAGNOSTICS? '1':'0')
+ ', SHOW_SETUP='+ (flags.SHOW_SETUP? '1':'0')
2025-09-30 15:49:08 -07:00
+ ', SHOW_COMMANDERS='+ (flags.SHOW_COMMANDERS? '1':'0')
2025-09-17 13:23:27 -07:00
+ ', RANDOM_MODES='+ (flags.RANDOM_MODES? '1':'0')
+ ', RANDOM_UI='+ (flags.RANDOM_UI? '1':'0')
+ ', RANDOM_MAX_ATTEMPTS='+ String(flags.RANDOM_MAX_ATTEMPTS ?? '')
+ ', RANDOM_TIMEOUT_MS='+ String(flags.RANDOM_TIMEOUT_MS ?? '')
+ '< / div > ';
Web UI polish: thumbnail-hover preview, white thumbnail selection, Themes bullet list; global Scryfall image retry (thumbs+previews) with fallbacks and cache-bust; standardized data-card-name. Deck Summary alignment overhaul (count//name/owned grid, tabular numerals, inset highlight, tooltips, starts under header). Added diagnostics (health + logs pages, error pages, request-id propagation), global HTMX error toasts, and docs updates. Update DOCKER guide and add run-web scripts. Update CHANGELOG and release notes template.
2025-08-27 11:21:46 -07:00
} catch(_){ el.textContent = 'Unavailable'; }
}
function load(){
try { fetch('/status/sys', { cache: 'no-store' }).then(function(r){ return r.json(); }).then(render).catch(function(){ el.textContent='Unavailable'; }); } catch(_){ el.textContent='Unavailable'; }
}
load();
2025-10-03 10:43:24 -07:00
// Environment flags card
(function(){
var target = document.getElementById('envFlags');
if (!target) return;
function renderEnv(data){
if (!data || !data.flags) { target.textContent = 'Flags unavailable'; return; }
var f = data.flags;
function as01(v){ return (v ? '1' : '0'); }
var lines = [];
lines.push('< div > < strong > Homepage & UI:< / strong > '
+ 'SHOW_SETUP=' + as01(f.SHOW_SETUP)
+ ', SHOW_LOGS=' + as01(f.SHOW_LOGS)
+ ', SHOW_DIAGNOSTICS=' + as01(f.SHOW_DIAGNOSTICS)
+ ', SHOW_COMMANDERS=' + as01(f.SHOW_COMMANDERS)
+ ', ENABLE_THEMES=' + as01(f.ENABLE_THEMES)
+ ', ENABLE_CUSTOM_THEMES=' + as01(f.ENABLE_CUSTOM_THEMES)
+ ', ALLOW_MUST_HAVES=' + as01(f.ALLOW_MUST_HAVES)
+ ', THEME=' + String(f.DEFAULT_THEME || '')
+ ', THEME_MATCH_MODE=' + String(f.THEME_MATCH_MODE || '')
+ ', USER_THEME_LIMIT=' + String(f.USER_THEME_LIMIT || '')
+ '< / div > ');
lines.push('< div > < strong > Random:< / strong > '
+ 'RANDOM_MODES=' + as01(f.RANDOM_MODES)
+ ', RANDOM_UI=' + as01(f.RANDOM_UI)
+ ', RANDOM_MAX_ATTEMPTS=' + String(f.RANDOM_MAX_ATTEMPTS || '')
+ ', RANDOM_TIMEOUT_MS=' + String(f.RANDOM_TIMEOUT_MS || '')
+ ', RANDOM_REROLL_THROTTLE_MS=' + String(f.RANDOM_REROLL_THROTTLE_MS || '')
+ ', RANDOM_TELEMETRY=' + as01(f.RANDOM_TELEMETRY)
+ ', RANDOM_STRUCTURED_LOGS=' + as01(f.RANDOM_STRUCTURED_LOGS)
+ '< / div > ');
lines.push('< div > < strong > Rate limiting (random):< / strong > '
+ 'RATE_LIMIT_ENABLED=' + as01(f.RATE_LIMIT_ENABLED)
+ ', WINDOW_S=' + String(f.RATE_LIMIT_WINDOW_S || '')
+ ', RANDOM=' + String(f.RANDOM_RATE_LIMIT_RANDOM || '')
+ ', BUILD=' + String(f.RANDOM_RATE_LIMIT_BUILD || '')
+ ', SUGGEST=' + String(f.RANDOM_RATE_LIMIT_SUGGEST || '')
+ '< / div > ');
target.innerHTML = lines.join('');
}
try { fetch('/status/sys', { cache: 'no-store' }).then(function(r){ return r.json(); }).then(renderEnv).catch(function(){ target.textContent='Flags unavailable'; }); } catch(_){ target.textContent='Flags unavailable'; }
})();
var themeSuppEl = document.getElementById('themeSuppMetrics');
function renderThemeSupp(payload){
if (!themeSuppEl) return;
try {
if (!payload || payload.ok !== true) {
themeSuppEl.textContent = 'Theme metrics unavailable';
return;
}
var metrics = payload.metrics || {};
var total = metrics.total_builds != null ? Number(metrics.total_builds) : 0;
if (!total) {
themeSuppEl.textContent = 'No deck builds recorded yet.';
return;
}
var withUser = metrics.with_user_themes != null ? Number(metrics.with_user_themes) : 0;
var share = metrics.user_theme_share != null ? Number(metrics.user_theme_share) : 0;
var sharePct = !Number.isNaN(share) ? (share * 100).toFixed(1) + '%' : '0%';
var summary = metrics.last_summary || {};
var commander = Array.isArray(summary.commanderThemes) ? summary.commanderThemes : [];
var user = Array.isArray(summary.userThemes) ? summary.userThemes : [];
var merged = Array.isArray(summary.mergedThemes) ? summary.mergedThemes : [];
var unresolvedCount = summary.unresolvedCount != null ? Number(summary.unresolvedCount) : 0;
var unresolved = Array.isArray(summary.unresolved) ? summary.unresolved : [];
var mode = summary.mode || 'AND';
var weight = summary.weight != null ? Number(summary.weight) : 1;
var updated = metrics.last_updated || '';
var topUser = Array.isArray(metrics.top_user_themes) ? metrics.top_user_themes : [];
function joinList(arr){
if (!arr || !arr.length) return '—';
return arr.join(', ');
}
var html = '';
html += '< div > < strong > Total builds:< / strong > ' + String(total) + ' (user themes ' + String(withUser) + '\u00a0| ' + sharePct + ')< / div > ';
if (updated) {
html += '< div style = "font-size:11px;" > Last updated: ' + String(updated) + '< / div > ';
}
html += '< div > < strong > Commander themes:< / strong > ' + joinList(commander) + '< / div > ';
html += '< div > < strong > User themes:< / strong > ' + joinList(user) + '< / div > ';
html += '< div > < strong > Merged:< / strong > ' + joinList(merged) + '< / div > ';
var unresolvedLabel = '0';
if (unresolvedCount > 0) {
unresolvedLabel = String(unresolvedCount) + ' (' + joinList(unresolved) + ')';
} else {
unresolvedLabel = '0';
}
html += '< div > < strong > Unresolved:< / strong > ' + unresolvedLabel + '< / div > ';
html += '< div style = "font-size:11px;" > Mode ' + String(mode) + ' · Weight ' + weight.toFixed(2) + '< / div > ';
if (topUser.length) {
var topLine = topUser.slice(0, 5).map(function(item){
if (!item) return '';
var t = item.theme != null ? String(item.theme) : '';
var c = item.count != null ? String(item.count) : '0';
return t + ' (' + c + ')';
}).filter(Boolean);
if (topLine.length) {
html += '< div style = "font-size:11px; opacity:0.75;" > Top user themes: ' + topLine.join(', ') + '< / div > ';
}
}
themeSuppEl.innerHTML = html;
} catch (_){
themeSuppEl.textContent = 'Theme metrics unavailable';
}
}
function loadThemeSupp(){
if (!themeSuppEl) return;
themeSuppEl.textContent = 'Loading theme metrics…';
fetch('/status/theme_metrics', { cache: 'no-store' })
.then(function(resp){
if (resp.status === 404) {
themeSuppEl.textContent = 'Diagnostics disabled (metrics unavailable)';
return null;
}
return resp.json();
})
.then(function(data){ if (data) renderThemeSupp(data); })
.catch(function(){ themeSuppEl.textContent = 'Theme metrics unavailable'; });
}
loadThemeSupp();
2025-09-26 18:15:52 -07:00
var tokenEl = document.getElementById('themeTokenStats');
function renderTokens(payload){
if (!tokenEl) return;
try {
if (!payload || payload.ok !== true) {
tokenEl.textContent = 'Theme stats unavailable';
return;
}
var stats = payload.stats || {};
var top = Array.isArray(stats.top_tokens) ? stats.top_tokens.slice(0, 5) : [];
var html = '';
var commanders = (stats & & stats.commanders != null) ? stats.commanders : '0';
var withTags = (stats & & stats.with_tags != null) ? stats.with_tags : '0';
var uniqueTokens = (stats & & stats.unique_tokens != null) ? stats.unique_tokens : '0';
var assignments = (stats & & stats.total_assignments != null) ? stats.total_assignments : '0';
var avgTokens = (stats & & stats.avg_tokens_per_commander != null) ? stats.avg_tokens_per_commander : '0';
var medianTokens = (stats & & stats.median_tokens_per_commander != null) ? stats.median_tokens_per_commander : '0';
html += '< div > < strong > Commanders indexed:< / strong > ' + String(commanders) + ' (' + String(withTags) + ' with tags)< / div > ';
html += '< div > < strong > Theme tokens:< / strong > ' + String(uniqueTokens) + ' unique; ' + String(assignments) + ' assignments< / div > ';
html += '< div > < strong > Tokens per commander:< / strong > avg ' + String(avgTokens) + ', median ' + String(medianTokens) + '< / div > ';
if (top.length) {
var parts = [];
top.forEach(function(item){
parts.push(String(item.token) + ' (' + String(item.count) + ')');
});
html += '< div > < strong > Top tokens:< / strong > ' + parts.join(', ') + '< / div > ';
}
var pool = stats.random_pool || {};
if (pool & & typeof pool.size !== 'undefined'){
var coveragePct = null;
if (pool.coverage_ratio != null){
var cov = Number(pool.coverage_ratio);
if (!Number.isNaN(cov)){ coveragePct = (cov * 100).toFixed(1); }
}
html += '< div style = "margin-top:0.35rem;" > < strong > Curated random pool:< / strong > ' + String(pool.size) + ' tokens';
if (coveragePct !== null){ html += ' (' + coveragePct + '% of catalog tokens)'; }
html += '< / div > ';
var rules = pool.rules || {};
var threshold = rules.overrepresented_share_threshold;
if (threshold != null){
var thrPct = Number(threshold);
if (!Number.isNaN(thrPct)){ html += '< div style = "font-size:11px;" > Over-represented threshold: ≥ ' + (thrPct * 100).toFixed(1) + '% of commanders< / div > '; }
}
var excludedCounts = pool.excluded_counts || {};
var reasonKeys = Object.keys(excludedCounts);
if (reasonKeys.length){
var badges = reasonKeys.map(function(reason){
return reason + ' (' + excludedCounts[reason] + ')';
});
html += '< div style = "font-size:11px;" > Exclusions: ' + badges.join(', ') + '< / div > ';
}
var samples = pool.excluded_samples || {};
var sampleKeys = Object.keys(samples);
if (sampleKeys.length){
var sampleLines = [];
sampleKeys.slice(0, 3).forEach(function(reason){
var tokens = samples[reason] || [];
var sampleTokens = (tokens || []).slice(0, 3);
var remainder = Math.max((tokens || []).length - sampleTokens.length, 0);
var tokenLabel = sampleTokens.join(', ');
if (remainder > 0){ tokenLabel += ' +' + remainder; }
sampleLines.push(reason + ': ' + tokenLabel);
});
html += '< div style = "font-size:11px; opacity:0.75;" > Samples → ' + sampleLines.join(' | ') + '< / div > ';
}
var manualDetail = pool.manual_exclusion_detail || {};
var manualKeys = Object.keys(manualDetail);
if (manualKeys.length){
var manualSamples = manualKeys.slice(0, 3).map(function(token){
var info = manualDetail[token] || {};
var label = info.display || token;
var cat = info.category ? (' [' + info.category + ']') : '';
return label + cat;
});
var manualRemainder = Math.max(manualKeys.length - manualSamples.length, 0);
var manualLine = manualSamples.join(', ');
if (manualRemainder > 0){ manualLine += ' +' + manualRemainder; }
html += '< div style = "font-size:11px;" > Manual exclusions: ' + manualLine + '< / div > ';
}
var manualGroups = Array.isArray(rules.manual_exclusions) ? rules.manual_exclusions : [];
if (manualGroups.length){
var categoryList = manualGroups.map(function(group){ return group.category || 'manual'; });
html += '< div style = "font-size:11px; opacity:0.75;" > Manual categories: ' + categoryList.join(', ') + '< / div > ';
}
}
var telemetry = stats.index_telemetry || {};
if (telemetry & & typeof telemetry.token_count !== 'undefined'){
var hitRate = telemetry.hit_rate != null ? Number(telemetry.hit_rate) : null;
var hitPct = (hitRate !== null & & !Number.isNaN(hitRate)) ? (hitRate * 100).toFixed(1) : null;
var teleLine = '< div style = "font-size:11px; margin-top:0.25rem;" > Tag index: ' + String(telemetry.token_count || 0) + ' tokens · lookups ' + String(telemetry.lookups || 0);
if (hitPct !== null){ teleLine += ' · hit rate ' + hitPct + '%'; }
if (telemetry.substring_checks){ teleLine += ' · substring checks ' + String(telemetry.substring_checks || 0); }
teleLine += '< / div > ';
html += teleLine;
}
tokenEl.innerHTML = html;
} catch(_){
tokenEl.textContent = 'Theme stats unavailable';
}
}
function loadTokenStats(){
if (!tokenEl) return;
tokenEl.textContent = 'Loading theme stats…';
fetch('/status/random_theme_stats', { cache: 'no-store' })
.then(function(resp){
if (resp.status === 404) {
tokenEl.textContent = 'Diagnostics disabled (stats unavailable)';
return null;
}
return resp.json();
})
.then(function(data){ if (data) renderTokens(data); })
.catch(function(){ tokenEl.textContent = 'Theme stats unavailable'; });
}
loadTokenStats();
2025-10-02 15:31:05 -07:00
var dfcMetricsEl = document.getElementById('dfcMetrics');
function renderDfcMetrics(payload){
if (!dfcMetricsEl) return;
try {
if (!payload || payload.ok !== true) {
dfcMetricsEl.textContent = 'MDFC metrics unavailable';
return;
}
var metrics = payload.metrics || {};
var html = '';
html += '< div > < strong > Deck summaries observed:< / strong > ' + String(metrics.total_builds || 0) + '< / div > ';
var withDfc = Number(metrics.builds_with_mdfc || 0);
var share = metrics.build_share != null ? Number(metrics.build_share) : null;
if (!Number.isNaN(share) & & share !== null) {
share = (share * 100).toFixed(1);
} else {
share = null;
}
html += '< div > < strong > With MDFCs:< / strong > ' + String(withDfc);
if (share !== null) {
html += ' (' + share + '%)';
}
html += '< / div > ';
var totalLands = Number(metrics.total_mdfc_lands || 0);
var avg = metrics.avg_mdfc_lands != null ? Number(metrics.avg_mdfc_lands) : null;
html += '< div > < strong > Total MDFC lands:< / strong > ' + String(totalLands);
if (avg !== null & & !Number.isNaN(avg)) {
html += ' (avg ' + avg.toFixed(2) + ')';
}
html += '< / div > ';
var top = metrics.top_cards || {};
var topKeys = Object.keys(top);
if (topKeys.length) {
var items = topKeys.slice(0, 5).map(function(name){
return name + ' (' + String(top[name]) + ')';
});
html += '< div style = "font-size:11px;" > Top MDFC sources: ' + items.join(', ') + '< / div > ';
}
var last = metrics.last_summary || {};
if (typeof last.dfc_lands !== 'undefined') {
html += '< div style = "font-size:11px; margin-top:0.25rem;" > Last summary: ' + String(last.dfc_lands || 0) + ' MDFC lands · total with MDFCs ' + String(last.with_dfc || 0) + '< / div > ';
}
if (metrics.last_updated) {
html += '< div style = "font-size:11px;" > Updated: ' + String(metrics.last_updated) + '< / div > ';
}
dfcMetricsEl.innerHTML = html;
} catch (_){
dfcMetricsEl.textContent = 'MDFC metrics unavailable';
}
}
function loadDfcMetrics(){
if (!dfcMetricsEl) return;
dfcMetricsEl.textContent = 'Loading MDFC metrics…';
fetch('/status/dfc_metrics', { cache: 'no-store' })
.then(function(resp){
if (resp.status === 404) {
dfcMetricsEl.textContent = 'Diagnostics disabled (metrics unavailable)';
return null;
}
return resp.json();
})
.then(function(data){ if (data) renderDfcMetrics(data); })
.catch(function(){ dfcMetricsEl.textContent = 'MDFC metrics unavailable'; });
}
loadDfcMetrics();
2025-10-06 09:17:59 -07:00
var partnerSummaryEl = document.getElementById('partnerMetricsSummary');
var partnerModesEl = document.getElementById('partnerMetricsModes');
var partnerSourcesEl = document.getElementById('partnerColorSources');
function escapeHtml(str){
return String(str == null ? '' : str).replace(/[& < >"']/g, function(ch){
return ({"& ": "& ", "< ": "< ", ">": "> ", "\"": "" ", "'": "' "}[ch]) || ch;
});
}
function labelForPartnerRole(role){
var key = role == null ? '' : String(role).toLowerCase();
var map = {
'primary': 'Primary',
'partner': 'Partner commander',
'partner_with': 'Partner With',
'background': 'Background',
'companion': "Doctor's Companion",
'doctor_companion': "Doctor's Companion",
'doctor': 'Doctor',
'secondary': 'Secondary',
};
if (map[key]) return map[key];
if (!key) return '';
return key.replace(/_/g, ' ').replace(/\b\w/g, function(ch){ return ch.toUpperCase(); });
}
function labelForPartnerMode(mode){
var key = mode == null ? 'none' : String(mode).toLowerCase();
var map = {
'none': 'Single commander',
'partner': 'Partner',
'partner_with': 'Partner With',
'background': 'Choose a Background',
'doctor_companion': "Doctor & Companion",
'doctor': 'Doctor',
};
return map[key] || labelForPartnerRole(key) || key;
}
function buildModeCountsHtml(modeCounts, total){
var html = '< div > < strong > Total pairings observed:< / strong > ' + String(total || 0) + '< / div > ';
var keys = Object.keys(modeCounts || {}).filter(function(k){ return Number(modeCounts[k] || 0) > 0; });
if (keys.length){
var parts = keys.sort().map(function(k){
return labelForPartnerMode(k) + ': ' + String(modeCounts[k]);
});
html += '< div style = "font-size:12px;" > Mode breakdown: ' + parts.join(' · ') + '< / div > ';
}
return html;
}
function renderPartnerMetrics(payload){
if (!partnerSummaryEl) return;
try{
if (!payload || payload.ok !== true){
partnerSummaryEl.textContent = 'Partner metrics unavailable';
if (partnerModesEl) partnerModesEl.textContent = '';
if (partnerSourcesEl) partnerSourcesEl.innerHTML = '';
return;
}
var metrics = payload.metrics || {};
var total = Number(metrics.total_pairs || 0);
var modeCounts = metrics.mode_counts || {};
var last = metrics.last_summary || null;
var updated = metrics.last_updated || '';
if (!total || !last){
partnerSummaryEl.textContent = 'No partner/background builds recorded yet.';
if (partnerModesEl) partnerModesEl.innerHTML = buildModeCountsHtml(modeCounts, total);
if (partnerSourcesEl) partnerSourcesEl.innerHTML = '';
return;
}
var primary = last.primary != null ? String(last.primary) : '';
var secondary = last.secondary != null ? String(last.secondary) : '';
if (!primary & & Array.isArray(last.names) & & last.names.length){ primary = String(last.names[0] || ''); }
if (!secondary & & Array.isArray(last.names) & & last.names.length > 1){ secondary = String(last.names[1] || ''); }
var header = '< div > < strong > Latest pairing:< / strong > ' + escapeHtml(primary || '—');
if (secondary){ header += ' + ' + escapeHtml(secondary); }
header += '< / div > ';
header += '< div > < strong > Mode:< / strong > ' + escapeHtml(labelForPartnerMode(last.partner_mode)) + '< / div > ';
var colorLabel = last.color_label != null ? String(last.color_label) : '';
var colorCode = last.color_code != null ? String(last.color_code) : '';
var colors = Array.isArray(last.color_identity) ? last.color_identity.filter(Boolean).map(String).join(' / ') : '';
if (colorLabel || colorCode || colors){
var labelText = colorLabel || colors || colorCode;
var extra = (!colorLabel & & colorCode & & colorCode !== labelText) ? ' (' + escapeHtml(colorCode) + ')' : '';
if (colorLabel & & colorCode & & colorLabel.indexOf(colorCode) === -1){ extra = ' (' + escapeHtml(colorCode) + ')'; }
header += '< div > < strong > Colors:< / strong > ' + escapeHtml(labelText) + extra + '< / div > ';
}
if (updated){
header += '< div style = "font-size:11px; opacity:0.75;" > Last updated: ' + escapeHtml(updated) + '< / div > ';
}
partnerSummaryEl.innerHTML = header;
if (partnerModesEl){
partnerModesEl.innerHTML = buildModeCountsHtml(modeCounts, total);
}
if (partnerSourcesEl){
var sources = Array.isArray(last.color_sources) ? last.color_sources : [];
if (!sources.length){
partnerSourcesEl.innerHTML = '< div class = "muted" > No color source breakdown recorded.< / div > ';
} else {
var html = '< div > < strong > Color sources< / strong > < / div > ';
html += '< ul style = "list-style:none; padding:0; margin:.35rem 0 0; display:grid; gap:.25rem;" > ';
sources.forEach(function(entry){
var color = entry & & entry.color != null ? String(entry.color) : '?';
var providers = Array.isArray(entry & & entry.providers) ? entry.providers : [];
var providerParts = providers.map(function(provider){
var name = provider & & provider.name != null ? String(provider.name) : 'Unknown';
var roleLabel = labelForPartnerRole(provider & & provider.role);
if (roleLabel){
return escapeHtml(name) + ' [' + escapeHtml(roleLabel) + ']';
}
return escapeHtml(name);
});
if (!providerParts.length){ providerParts.push('—'); }
html += '< li class = "muted" > < span class = "chip" style = "display:inline-flex; align-items:center; gap:.25rem;" > < span class = "dot" style = "background: var(--border);" > < / span > ' + escapeHtml(color) + '< / span > ' + providerParts.join(', ') + '< / li > ';
});
html += '< / ul > ';
var delta = last.color_delta || {};
try{
var deltaParts = [];
var added = Array.isArray(delta.added) ? delta.added.filter(Boolean) : [];
var removed = Array.isArray(delta.removed) ? delta.removed.filter(Boolean) : [];
if (added.length){ deltaParts.push('Added ' + added.map(escapeHtml).join(', ')); }
if (removed.length){ deltaParts.push('Removed ' + removed.map(escapeHtml).join(', ')); }
if (deltaParts.length){
html += '< div class = "muted" style = "font-size:12px; margin-top:.35rem;" > ' + deltaParts.join(' · ') + '< / div > ';
}
}catch(_){ }
partnerSourcesEl.innerHTML = html;
}
}
}catch(_){
partnerSummaryEl.textContent = 'Partner metrics unavailable';
if (partnerModesEl) partnerModesEl.textContent = '';
if (partnerSourcesEl) partnerSourcesEl.innerHTML = '';
}
}
function loadPartnerMetrics(){
if (!partnerSummaryEl) return;
partnerSummaryEl.textContent = 'Loading partner metrics…';
fetch('/status/partner_metrics', { cache: 'no-store' })
.then(function(resp){
if (resp.status === 404){
partnerSummaryEl.textContent = 'Diagnostics disabled (partner metrics unavailable)';
if (partnerModesEl) partnerModesEl.textContent = '';
if (partnerSourcesEl) partnerSourcesEl.innerHTML = '';
return null;
}
return resp.json();
})
.then(function(data){ if (data) renderPartnerMetrics(data); })
.catch(function(){
partnerSummaryEl.textContent = 'Partner metrics unavailable';
if (partnerModesEl) partnerModesEl.textContent = '';
if (partnerSourcesEl) partnerSourcesEl.innerHTML = '';
});
}
loadPartnerMetrics();
2025-08-28 16:44:58 -07:00
// Theme status and reset
try{
var tEl = document.getElementById('themeSummary');
var resetBtn = document.getElementById('diag-theme-reset');
function renderTheme(){
if (!tEl) return;
var key = 'mtg:theme';
var stored = localStorage.getItem(key);
var html = '';
var resolved = document.documentElement.getAttribute('data-theme') || '';
html += '< div > < strong > Resolved theme:< / strong > ' + resolved + '< / div > ';
html += '< div > < strong > Preference:< / strong > ' + (stored ? stored : '(none)') + '< / div > ';
tEl.innerHTML = html;
}
renderTheme();
if (resetBtn){
resetBtn.addEventListener('click', function(){
try{ localStorage.removeItem('mtg:theme'); }catch(_){ }
// Re-apply from server default via base script by simulating system apply
try{
var prefersDark = window.matchMedia & & window.matchMedia('(prefers-color-scheme: dark)').matches;
var v = prefersDark ? 'dark' : 'light-blend';
document.documentElement.setAttribute('data-theme', v);
}catch(_){ }
renderTheme();
});
}
}catch(_){ }
2025-09-01 16:55:24 -07:00
// Combos & synergies ad-hoc tester
try{
var runBtn = document.getElementById('diag-combos-run');
var ta = document.getElementById('diag-combos-input');
var out = document.getElementById('diag-combos-out');
function parseLines(){
var v = (ta & & ta.value) || '';
return v.split(/\r?\n/).map(function(s){ return s.trim(); }).filter(Boolean);
}
async function run(){
if (!ta || !out) return;
out.textContent = 'Running…';
try{
var resp = await fetch('/diagnostics/combos', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ names: parseLines() })});
if (!resp.ok){ out.textContent = 'Error '+resp.status; return; }
var data = await resp.json();
var lines = [];
// Versions
try{
if (data.versions){
var vLine = 'List versions: ';
if (data.versions.combos) vLine += 'combos v'+ String(data.versions.combos);
if (data.versions.synergies) vLine += (data.versions.combos? ', ' : '') + 'synergies v'+ String(data.versions.synergies);
lines.push(vLine);
}
}catch(_){ }
lines.push('Combos: '+ data.counts.combos);
(data.combos||[]).forEach(function(c){
var badges = [];
if (c.cheap_early) badges.push('cheap/early');
if (c.setup_dependent) badges.push('setup-dependent');
var tagStr = (c.tags & & c.tags.length? ' ['+c.tags.join(', ')+']' : '');
var badgeStr = badges.length ? ' {'+badges.join(', ')+'}' : '';
lines.push(' - '+c.a+' + '+c.b+ tagStr + badgeStr);
});
lines.push('Synergies: '+ data.counts.synergies);
(data.synergies||[]).forEach(function(s){ lines.push(' - '+s.a+' + '+s.b+(s.tags & & s.tags.length? ' ['+s.tags.join(', ')+']':'')); });
out.textContent = lines.join('\n');
}catch(e){ out.textContent = 'Failed: '+ (e & & e.message? e.message : 'Unknown error'); }
}
if (runBtn){ runBtn.addEventListener('click', run); }
}catch(_){ }
2025-08-28 16:44:58 -07:00
try{
var p = document.getElementById('pwaStatus');
if (p){
function renderPwa(){
try{
var st = window.__pwaStatus || {};
p.innerHTML = '< div > < strong > Registered:< / strong > '+ (st.registered? 'Yes':'No') +'< / div > ' + (st.scope? '< div > < strong > Scope:< / strong > '+ st.scope +'< / div > ' : '');
}catch(_){ p.textContent = 'Unavailable'; }
}
setTimeout(renderPwa, 500);
}
}catch(_){ }
2025-08-28 14:57:22 -07:00
// Perf probe: listen to scroll on a card grid if present
try{
var fpsEl = document.getElementById('perf-fps');
var visEl = document.getElementById('perf-visible');
var rcEl = document.getElementById('perf-renders');
var grid = document.querySelector('.card-grid');
var last = performance.now();
var frames = 0; var renders = 0;
function tick(){
frames++;
var now = performance.now();
if (now - last >= 500){
var fps = Math.round((frames * 1000) / (now - last));
if (fpsEl) fpsEl.textContent = String(fps);
frames = 0; last = now;
}
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
function updateVisible(){
try{
if (!grid) return;
var tiles = grid.querySelectorAll('.card-tile');
var c = 0; tiles.forEach(function(t){ if (t.style.display !== 'none') c++; });
if (visEl) visEl.textContent = String(c);
}catch(_){ }
}
if (grid){
grid.addEventListener('scroll', updateVisible);
var mo = new MutationObserver(function(){ renders++; if (rcEl) rcEl.textContent = String(renders); updateVisible(); });
mo.observe(grid, { childList: true, subtree: true, attributes: false });
updateVisible();
}
}catch(_){ }
Web UI polish: thumbnail-hover preview, white thumbnail selection, Themes bullet list; global Scryfall image retry (thumbs+previews) with fallbacks and cache-bust; standardized data-card-name. Deck Summary alignment overhaul (count//name/owned grid, tabular numerals, inset highlight, tooltips, starts under header). Added diagnostics (health + logs pages, error pages, request-id propagation), global HTMX error toasts, and docs updates. Update DOCKER guide and add run-web scripts. Update CHANGELOG and release notes template.
2025-08-27 11:21:46 -07:00
})();
< / script >
{% endblock %}