mtg_python_deckbuilder/code/web/templates/diagnostics/index.html

314 lines
No EOL
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block content %}
<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>
<div id="themeSummary" style="margin-top:.5rem"></div>
<div id="themeTokenStats" class="muted" style="margin-top:.5rem">Loading theme stats…</div>
<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">Performance (local)</h3>
<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>
<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>
</div>
{% 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>
{% 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>
<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>
{% 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>'+
'<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')
+ ', SHOW_COMMANDERS='+ (flags.SHOW_COMMANDERS? '1':'0')
+ ', 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>';
} 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();
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();
// 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(_){ }
// 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(_){ }
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(_){ }
// 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(_){ }
})();
</script>
{% endblock %}