mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-16 07:30:13 +01:00
413 lines
26 KiB
HTML
413 lines
26 KiB
HTML
{% extends 'base.html' %}
|
||
{% block content %}
|
||
<h2 style="position:relative;">Theme Catalog <small id="theme-stale-indicator" style="display:none; font-size:12px; color:#b45309;">(Refreshing…)</small></h2>
|
||
<div id="theme-picker" class="theme-picker" hx-get="/themes/fragment/list?limit=20&offset=0" hx-trigger="load" hx-target="#theme-results" hx-swap="innerHTML" role="region" aria-label="Theme picker">
|
||
<div class="theme-picker-controls">
|
||
<input type="text" id="theme-search" placeholder="Search themes or synergies" aria-label="Search"
|
||
hx-get="/themes/fragment/list" hx-target="#theme-results" hx-trigger="debouncedinput change" name="q"
|
||
data-hx-debounce="260" data-hx-debounce-events="input,keyup" data-hx-debounce-flush="blur" data-hx-debounce-group="theme-search" />
|
||
<select id="theme-archetype" name="archetype" hx-get="/themes/fragment/list" hx-target="#theme-results" hx-trigger="change">
|
||
<option value="">All Archetypes</option>
|
||
{% if archetypes %}{% for a in archetypes %}<option value="{{ a }}">{{ a }}</option>{% endfor %}{% endif %}
|
||
</select>
|
||
<select id="theme-bucket" name="bucket" hx-get="/themes/fragment/list" hx-target="#theme-results" hx-trigger="change">
|
||
<option value="">All Popularity</option>
|
||
<option>Very Common</option>
|
||
<option>Common</option>
|
||
<option>Uncommon</option>
|
||
<option>Niche</option>
|
||
<option>Rare</option>
|
||
</select>
|
||
<select id="theme-limit" name="limit" hx-get="/themes/fragment/list" hx-target="#theme-results" hx-trigger="change" title="Themes per page">
|
||
<option value="20" selected>20</option>
|
||
<option value="30">30</option>
|
||
<option value="50">50</option>
|
||
<option value="75">75</option>
|
||
<option value="100">100</option>
|
||
</select>
|
||
<label title="Show full synergy list (diagnostics only)"><input type="checkbox" id="synergy-full" name="synergy_mode" value="full" hx-get="/themes/fragment/list" hx-target="#theme-results" hx-trigger="change"/> Full Synergies</label>
|
||
<label title="Search input responsiveness experiment">
|
||
Mode:
|
||
<select id="search-mode">
|
||
<option value="throttle" selected>Throttle 250ms</option>
|
||
<option value="debounce">Debounce 250ms</option>
|
||
</select>
|
||
</label>
|
||
<div class="color-filters" role="group" aria-label="Filter by primary/secondary color">
|
||
{% for c in ['W','U','B','R','G'] %}
|
||
<label><input type="checkbox" name="colors" value="{{ c }}"
|
||
hx-get="/themes/fragment/list" hx-target="#theme-results" hx-trigger="change"/> {{ c }}</label>
|
||
{% endfor %}
|
||
</div>
|
||
{% if theme_picker_diagnostics %}
|
||
<label title="Show diagnostics-only badges"><input type="checkbox" id="diag-toggle" name="diagnostics" value="1" hx-get="/themes/fragment/list" hx-target="#theme-results" hx-trigger="change"/> Diagnostics</label>
|
||
{% endif %}
|
||
</div>
|
||
<div id="filter-chips" class="filter-chips" aria-label="Active filters" style="margin-bottom:.25rem;"></div>
|
||
<div id="theme-results" class="theme-results preload-hide" aria-live="polite" aria-busy="true" role="listbox" aria-label="Loading themes">
|
||
<div class="skeleton-table" aria-hidden="true">
|
||
{% for i in range(6) %}
|
||
<div class="skeleton-row skeleton-align">
|
||
<div class="sk-cell sk-col sk-theme"></div>
|
||
<div class="sk-cell sk-col sk-arch"></div>
|
||
<div class="sk-cell sk-col sk-pop"></div>
|
||
<div class="sk-cell sk-col sk-colors"></div>
|
||
<div class="sk-cell sk-col sk-cnt"></div>
|
||
<div class="sk-cell sk-col sk-synergies"></div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
<template id="theme-row-template"></template>
|
||
<div class="legend" style="margin-top:1rem; font-size:12px; line-height:1.3;">
|
||
<strong>Legend:</strong>
|
||
<span class="theme-badge badge-enforced" title="Enforced synergy (whitelist governance)">ENF</span>
|
||
<span class="theme-badge badge-curated" title="Curated synergy (hand authored)">CUR</span>
|
||
<span class="theme-badge badge-inferred" title="Inferred synergy (analytics)">INF</span>
|
||
<span class="theme-badge badge-pop-vc" title="Popularity: Very Common">VC</span>
|
||
<span class="theme-badge badge-pop-c" title="Popularity: Common">C</span>
|
||
<span class="theme-badge badge-pop-u" title="Popularity: Uncommon">U</span>
|
||
<span class="theme-badge badge-pop-n" title="Popularity: Niche">N</span>
|
||
<span class="theme-badge badge-pop-r" title="Popularity: Rare">R</span>
|
||
{% if theme_picker_diagnostics %}
|
||
<span class="theme-badge badge-fallback" title="Generic fallback description">⚠</span>
|
||
<span class="theme-badge badge-quality-draft" title="Editorial quality: draft">D</span>
|
||
<span class="theme-badge badge-quality-reviewed" title="Editorial quality: reviewed">R</span>
|
||
<span class="theme-badge badge-quality-final" title="Editorial quality: final">F</span>
|
||
{% endif %}
|
||
</div>
|
||
<div id="theme-preview-host"></div>
|
||
</div>
|
||
<style>
|
||
.theme-picker-controls { display:flex; flex-wrap:wrap; gap:.5rem; margin-bottom:.75rem; }
|
||
.theme-results table { width:100%; border-collapse: collapse; }
|
||
.theme-results th, .theme-results td { padding:.35rem .5rem; border-bottom:1px solid var(--border); font-size:13px; }
|
||
.theme-results tr:hover { background: var(--hover); cursor:pointer; }
|
||
:root { --focus:#6366f1; }
|
||
@media (prefers-contrast: more){ :root { --focus:#ff9800; } }
|
||
.theme-row.is-active { outline:2px solid var(--focus); outline-offset:-2px; background:var(--hover); }
|
||
/* Long theme name truncation */
|
||
.theme-results td:first-child { max-width:260px; }
|
||
.theme-results td:first-child span.trunc-name { display:inline-block; max-width:240px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; vertical-align:bottom; }
|
||
/* Badge wrapping heuristics */
|
||
.theme-synergies .theme-badge { max-width:120px; overflow:hidden; text-overflow:ellipsis; }
|
||
.theme-synergies { font-size:11px; opacity:.85; display:flex; flex-wrap:wrap; gap:4px; }
|
||
.theme-badge { display:inline-block; padding:2px 6px; border-radius:12px; font-size:10px; background: var(--panel-alt); border:1px solid var(--border); letter-spacing:.5px; }
|
||
.badge-fallback { background:#7f1d1d; color:#fff; }
|
||
.badge-quality-draft { background:#4338ca; color:#fff; }
|
||
.badge-quality-reviewed { background:#065f46; color:#fff; }
|
||
.badge-quality-final { background:#065f46; color:#fff; font-weight:600; }
|
||
.badge-pop-vc { background:#065f46; color:#fff; }
|
||
.badge-pop-c { background:#047857; color:#fff; }
|
||
.badge-pop-u { background:#0369a1; color:#fff; }
|
||
.badge-pop-n { background:#92400e; color:#fff; }
|
||
.badge-pop-r { background:#7f1d1d; color:#fff; }
|
||
.badge-curated { background:#4f46e5; color:#fff; }
|
||
.badge-enforced { background:#334155; color:#fff; }
|
||
.badge-inferred { background:#57534e; color:#fff; }
|
||
/* Preview modal */
|
||
.preview-modal { position:fixed; inset:0; background:rgba(0,0,0,0.55); display:flex; align-items:flex-start; justify-content:center; padding:4vh 2vw; z-index:9000; }
|
||
.preview-modal-content { background:var(--panel); padding:1rem; border-radius:8px; max-width:900px; width:100%; max-height:88vh; overflow:auto; box-shadow:0 4px 18px rgba(0,0,0,0.4); }
|
||
/* Skeleton */
|
||
.skeleton-table { display:flex; flex-direction:column; gap:6px; }
|
||
.skeleton-row { display:grid; grid-template-columns:22% 10% 10% 12% 12% 1fr; gap:8px; align-items:center; }
|
||
.sk-cell { height:14px; background:linear-gradient(90deg, var(--panel-alt) 25%, var(--hover) 50%, var(--panel-alt) 75%); background-size:200% 100%; animation: sk 1.2s ease-in-out infinite; border-radius:4px; opacity:.7; }
|
||
.skeleton-align .sk-col { height:14px; }
|
||
.sk-synergies { height:18px; }
|
||
/* New UX additions */
|
||
.filter-chips { display:flex; gap:6px; flex-wrap:wrap; margin-top:.35rem; }
|
||
.filter-chip { background:var(--panel-alt); border:1px solid var(--border); padding:2px 8px; border-radius:14px; font-size:11px; cursor:pointer; display:inline-flex; align-items:center; gap:4px; }
|
||
.filter-chip button { background:none; border:none; color:inherit; cursor:pointer; font-size:11px; padding:0; line-height:1; }
|
||
mark { background:#fde68a; color:inherit; padding:0 2px; border-radius:2px; }
|
||
@keyframes sk {0%{background-position:0 0;}100%{background-position:-200% 0;}}
|
||
</style>
|
||
<script>
|
||
(function(){
|
||
function serializeFilters(container){
|
||
var params = [];
|
||
var qs = container.querySelector('#theme-search');
|
||
if(qs && qs.value.trim()){ params.push('q='+encodeURIComponent(qs.value.trim())); }
|
||
var as = container.querySelector('#theme-archetype');
|
||
if(as && as.value) params.push('archetype='+encodeURIComponent(as.value));
|
||
var bs = container.querySelector('#theme-bucket');
|
||
if(bs && bs.value) params.push('bucket='+encodeURIComponent(bs.value));
|
||
var lim = container.querySelector('#theme-limit');
|
||
if(lim && lim.value) params.push('limit='+encodeURIComponent(lim.value));
|
||
var diag = container.querySelector('#diag-toggle');
|
||
if(diag && diag.checked) params.push('diagnostics=1');
|
||
var syFull = container.querySelector('#synergy-full');
|
||
if(syFull && syFull.checked) params.push('synergy_mode=full');
|
||
var colorChecks = container.querySelectorAll('input[name="colors"]:checked');
|
||
if(colorChecks.length){
|
||
var vals = Array.prototype.map.call(colorChecks, c=>c.value).join(',');
|
||
params.push('colors='+encodeURIComponent(vals));
|
||
}
|
||
return params.join('&');
|
||
}
|
||
var perfMarks={}; function mark(n){ perfMarks[n]=performance.now(); }
|
||
function fetchList(){
|
||
saveScroll();
|
||
var container = document.getElementById('theme-picker');
|
||
if(!container) return;
|
||
var target = document.getElementById('theme-results');
|
||
if(!target) return;
|
||
// Abort any in-flight request (resilience: rapid search)
|
||
if(window.__themeListAbort){ try { window.__themeListAbort.abort(); } catch(_e){} }
|
||
var controller = new AbortController();
|
||
window.__themeListAbort = controller;
|
||
target.setAttribute('aria-busy','true');
|
||
target.setAttribute('aria-label','Loading themes');
|
||
mark('list_render_start');
|
||
var base = serializeFilters(container);
|
||
if(base.indexOf('offset=') === -1){ base += (base ? '&' : '') + 'offset=0'; }
|
||
toggleRefreshBtn(true);
|
||
fetch('/themes/fragment/list?'+base, {cache:'no-store', signal: controller.signal})
|
||
.then(r=>r.text())
|
||
.then(html=>{
|
||
if(controller.signal.aborted) return;
|
||
target.innerHTML = html;
|
||
if(window.htmx && typeof window.htmx.process==='function'){
|
||
window.htmx.process(target);
|
||
}
|
||
target.removeAttribute('aria-busy');
|
||
target.classList.remove('preload-hide');
|
||
listRenderComplete();
|
||
toggleRefreshBtn(false);
|
||
})
|
||
.catch(err=>{ if(controller.signal.aborted) return; target.innerHTML = '<div class="error" role="alert">Failed loading themes. <button id="retry-fetch" class="btn btn-ghost">Retry</button></div>'; target.removeAttribute('aria-busy'); target.classList.remove('preload-hide'); attachRetry(); structuredLog('list_fetch_error'); announceResultCount(); toggleRefreshBtn(false); });
|
||
}
|
||
function attachRetry(){ var b=document.getElementById('retry-fetch'); if(!b) return; b.addEventListener('click', function(){ fetchList(); }); }
|
||
function listRenderComplete(){ mark('list_ready'); announceResultCount(); if(window.THEME_DIAG_ENABLED){ try { var dur=perfMarks.list_ready - perfMarks.list_render_start; if(navigator.sendBeacon){ navigator.sendBeacon('/themes/metrics/client', new Blob([JSON.stringify({events:[{name:'list_render', duration_ms:Math.round(dur)}]})], {type:'application/json'})); } } catch(_e){} } }
|
||
function injectPrefetchLinks(){
|
||
try {
|
||
var head=document.head; if(!head) return;
|
||
// Remove old dynamic prefetch links
|
||
Array.from(head.querySelectorAll('link[data-dynamic-prefetch]')).forEach(l=>l.remove());
|
||
// Choose top 5 rows (skip currently selected to bias exploration)
|
||
var rows = document.querySelectorAll('#theme-results tr.theme-row[data-theme-id]');
|
||
var current = new URL(window.location.href).searchParams.get('theme');
|
||
var picked=[]; rows.forEach(r=>{ var id=r.getAttribute('data-theme-id'); if(!id) return; if(id===current) return; if(picked.length<5) picked.push(id); });
|
||
picked.forEach(function(id){ var link=document.createElement('link'); link.rel='prefetch'; link.href='/themes/fragment/detail/'+id; link.as='fetch'; link.setAttribute('data-dynamic-prefetch','1'); head.appendChild(link); });
|
||
} catch(e) {}
|
||
}
|
||
function structuredLog(ev){ if(!window.THEME_DIAG_ENABLED) return; try { fetch('/themes/log',{method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({event:ev, ts:Date.now()})}); } catch(_e){} }
|
||
document.addEventListener('htmx:afterOnLoad', function(ev){
|
||
if(ev.target && ev.target.id==='theme-results'){
|
||
var rows = ev.target.querySelectorAll('tr.theme-row[data-theme-id]');
|
||
rows.forEach(function(r){
|
||
var id = r.getAttribute('data-theme-id');
|
||
if(!id) return;
|
||
r.addEventListener('click', function(){
|
||
var url = new URL(window.location.href);
|
||
url.searchParams.set('theme', id);
|
||
var filters = serializeFilters(document.getElementById('theme-picker'));
|
||
if(filters){ url.searchParams.set('filters', filters); }
|
||
history.pushState({ theme:id, filters: filters }, '', url.toString()); window._lastThemeFocusId=id;
|
||
}, { once:false });
|
||
var prefetchTimer;
|
||
r.addEventListener('mouseenter', function(){
|
||
clearTimeout(prefetchTimer);
|
||
prefetchTimer = setTimeout(function(){
|
||
fetch('/themes/fragment/detail/'+id+'?'+(document.getElementById('diag-toggle')?.checked?'diagnostics=1':''), {cache:'force-cache'}).then(function(resp){ structuredLog('prefetch_success'); return resp.text(); }).then(function(_html){ }).catch(function(){ structuredLog('prefetch_error'); });
|
||
}, 180);
|
||
});
|
||
r.addEventListener('mouseleave', function(){ clearTimeout(prefetchTimer); });
|
||
});
|
||
var current = new URL(window.location.href).searchParams.get('theme');
|
||
if(current && !document.getElementById('theme-detail')?.dataset?.loaded){
|
||
htmx.ajax('GET', '/themes/fragment/detail/'+current, '#theme-detail');
|
||
}
|
||
// Restore focus to previously active row if available
|
||
if(window._lastThemeFocusId){
|
||
var targetRow = ev.target.querySelector('tr.theme-row[data-theme-id="'+window._lastThemeFocusId+'"]');
|
||
if(targetRow){ targetRow.focus({preventScroll:false}); }
|
||
}
|
||
buildFilterChips();
|
||
injectPrefetchLinks();
|
||
restoreScroll();
|
||
enableKeyboardNav();
|
||
}
|
||
});
|
||
window.addEventListener('popstate', function(ev){
|
||
var state = ev.state || {};
|
||
if(state.filters){
|
||
var params = new URLSearchParams(state.filters);
|
||
var container = document.getElementById('theme-picker');
|
||
if(container){
|
||
if(params.get('q')) container.querySelector('#theme-search').value = decodeURIComponent(params.get('q'));
|
||
if(params.get('archetype')) container.querySelector('#theme-archetype').value = decodeURIComponent(params.get('archetype'));
|
||
if(params.get('bucket')) container.querySelector('#theme-bucket').value = decodeURIComponent(params.get('bucket'));
|
||
if(params.get('limit')) container.querySelector('#theme-limit').value = decodeURIComponent(params.get('limit'));
|
||
var colorStr = params.get('colors');
|
||
if(colorStr){
|
||
var set = new Set(colorStr.split(','));
|
||
container.querySelectorAll('input[name="colors"]').forEach(function(cb){ cb.checked = set.has(cb.value); });
|
||
}
|
||
if(params.get('diagnostics')==='1'){ var diag=container.querySelector('#diag-toggle'); if(diag){ diag.checked=true; } }
|
||
fetchList();
|
||
}
|
||
}
|
||
if(state.theme){
|
||
htmx.ajax('GET', '/themes/fragment/detail/'+state.theme, '#theme-detail');
|
||
}
|
||
});
|
||
window.addEventListener('load', function(){
|
||
var url = new URL(window.location.href);
|
||
var filters = url.searchParams.get('filters');
|
||
if(filters){
|
||
var params = new URLSearchParams(filters);
|
||
var container = document.getElementById('theme-picker');
|
||
if(container){
|
||
if(params.get('q')) container.querySelector('#theme-search').value = decodeURIComponent(params.get('q'));
|
||
if(params.get('archetype')) container.querySelector('#theme-archetype').value = decodeURIComponent(params.get('archetype'));
|
||
if(params.get('bucket')) container.querySelector('#theme-bucket').value = decodeURIComponent(params.get('bucket'));
|
||
if(params.get('limit')) container.querySelector('#theme-limit').value = decodeURIComponent(params.get('limit'));
|
||
var colorStr = params.get('colors');
|
||
if(colorStr){
|
||
var set = new Set(colorStr.split(','));
|
||
container.querySelectorAll('input[name="colors"]').forEach(function(cb){ cb.checked = set.has(cb.value); });
|
||
}
|
||
if(params.get('diagnostics')==='1'){ var diag=container.querySelector('#diag-toggle'); if(diag){ diag.checked=true; } }
|
||
}
|
||
}
|
||
var theme = url.searchParams.get('theme');
|
||
if(theme){ htmx.ajax('GET','/themes/fragment/detail/'+theme,'#theme-detail'); }
|
||
window.THEME_DIAG_ENABLED = !!document.getElementById('diag-toggle');
|
||
}, { once:true });
|
||
var lastScroll = 0;
|
||
function saveScroll(){ lastScroll = window.scrollY || document.documentElement.scrollTop; }
|
||
function restoreScroll(){ if(typeof lastScroll === 'number'){ window.scrollTo(0, lastScroll); } }
|
||
function buildFilterChips(){
|
||
var host = document.getElementById('filter-chips');
|
||
if(!host) return;
|
||
host.innerHTML='';
|
||
var container = document.getElementById('theme-picker');
|
||
if(!container) return;
|
||
var q = container.querySelector('#theme-search').value.trim();
|
||
var archetype = container.querySelector('#theme-archetype').value;
|
||
var bucket = container.querySelector('#theme-bucket').value;
|
||
var colors = Array.from(container.querySelectorAll('input[name="colors"]:checked')).map(c=>c.value).join(',');
|
||
var diag = container.querySelector('#diag-toggle')?.checked;
|
||
function addChip(label, key){
|
||
var chip = document.createElement('span');
|
||
chip.className='filter-chip';
|
||
chip.innerHTML = '<span>'+label+'</span><button aria-label="Remove '+key+'">×</button>';
|
||
chip.querySelector('button').addEventListener('click', function(){
|
||
if(key==='q'){ container.querySelector('#theme-search').value=''; }
|
||
if(key==='archetype'){ container.querySelector('#theme-archetype').value=''; }
|
||
if(key==='bucket'){ container.querySelector('#theme-bucket').value=''; }
|
||
if(key==='colors'){ container.querySelectorAll('input[name="colors"]').forEach(cb=>cb.checked=false); }
|
||
if(key==='diagnostics'){ var d=container.querySelector('#diag-toggle'); if(d) d.checked=false; }
|
||
fetchList();
|
||
});
|
||
host.appendChild(chip);
|
||
}
|
||
if(q) addChip('Search: '+q, 'q');
|
||
if(archetype) addChip('Archetype: '+archetype, 'archetype');
|
||
if(bucket) addChip('Popularity: '+bucket, 'bucket');
|
||
if(colors) addChip('Colors: '+colors, 'colors');
|
||
if(diag) addChip('Diagnostics', 'diagnostics');
|
||
var syFull = container.querySelector('#synergy-full')?.checked; if(syFull) addChip('Full Synergies','synergy_mode');
|
||
}
|
||
function enableKeyboardNav(){
|
||
var tbody = document.querySelector('#theme-results tbody');
|
||
if(!tbody) return;
|
||
var rows = Array.from(tbody.querySelectorAll('tr.theme-row'));
|
||
if(!rows.length) return;
|
||
var activeIndex = -1;
|
||
function setActive(i){ rows.forEach(r=>r.classList.remove('is-active')); if(i>=0 && rows[i]){ rows[i].classList.add('is-active'); rows[i].focus({preventScroll:true}); activeIndex=i; } }
|
||
document.addEventListener('keydown', function(e){
|
||
if(['ArrowDown','ArrowUp','Enter','Escape'].indexOf(e.key)===-1) return;
|
||
if(e.key==='ArrowDown'){ e.preventDefault(); setActive(Math.min(activeIndex+1, rows.length-1)); }
|
||
else if(e.key==='ArrowUp'){ e.preventDefault(); setActive(Math.max(activeIndex-1, 0)); }
|
||
else if(e.key==='Enter'){ if(activeIndex>=0){ rows[activeIndex].click(); } }
|
||
else if(e.key==='Escape'){ setActive(-1); var detail=document.getElementById('theme-detail'); if(detail){ detail.innerHTML='Selection cleared.'; detail.setAttribute('aria-live','polite'); } }
|
||
}, { once:false });
|
||
document.addEventListener('keydown', function(e){
|
||
if(e.key==='Enter' && e.shiftKey){ var cb=document.getElementById('synergy-full'); if(cb){ cb.checked=!cb.checked; fetchList(); } }
|
||
});
|
||
}
|
||
var searchInput = document.getElementById('theme-search');
|
||
var searchModeSel = document.getElementById('search-mode');
|
||
var lastExec = 0; var pendingTimer = null; var BASE_DELAY = 250;
|
||
function performSearch(){ fetchList(); }
|
||
function throttledHandler(){ var now=Date.now(); if(now - lastExec > BASE_DELAY){ lastExec = now; performSearch(); } }
|
||
function debouncedHandler(){ clearTimeout(pendingTimer); pendingTimer = setTimeout(performSearch, BASE_DELAY); }
|
||
function attachSearchHandler(){
|
||
if(!searchInput) return;
|
||
searchInput.removeEventListener('keyup', throttledHandler);
|
||
searchInput.removeEventListener('keyup', debouncedHandler);
|
||
if(searchModeSel && searchModeSel.value==='debounce'){
|
||
searchInput.addEventListener('keyup', debouncedHandler);
|
||
} else {
|
||
searchInput.addEventListener('keyup', throttledHandler);
|
||
}
|
||
}
|
||
if(searchModeSel){ searchModeSel.addEventListener('change', attachSearchHandler); }
|
||
attachSearchHandler();
|
||
function toggleRefreshBtn(dis){ var btn=document.getElementById('catalog-refresh-btn'); if(btn){ if(dis){ btn.setAttribute('disabled','disabled'); btn.setAttribute('aria-busy','true'); } else { btn.removeAttribute('disabled'); btn.removeAttribute('aria-busy'); } } }
|
||
function checkStatus(){
|
||
fetch('/themes/status',{cache:'no-store'}).then(r=>r.json()).then(js=>{
|
||
if(js.stale){
|
||
var ind=document.getElementById('theme-stale-indicator'); if(ind){ ind.style.display='inline'; }
|
||
toggleRefreshBtn(true);
|
||
fetch('/themes/refresh',{method:'POST'}).then(()=>{
|
||
var attempts=0; var max=20; var iv=setInterval(()=>{
|
||
fetch('/themes/status',{cache:'no-store'}).then(r=>r.json()).then(s2=>{
|
||
if(!s2.stale){ clearInterval(iv); if(ind) ind.style.display='none'; fetchList(); }
|
||
}).catch(()=>{});
|
||
attempts++; if(attempts>max){ clearInterval(iv); }
|
||
},1500);
|
||
}).finally(()=>{ toggleRefreshBtn(false); });
|
||
}
|
||
}).catch(()=>{});
|
||
}
|
||
function announceResultCount(){ var tbody=document.querySelector('#theme-results tbody'); if(!tbody) return; var count=tbody.querySelectorAll('tr.theme-row').length; var host=document.getElementById('theme-results'); if(host){ host.setAttribute('aria-label', count+' themes'); } }
|
||
window.addEventListener('load', checkStatus, {once:true});
|
||
})();
|
||
// Preview modal retry/backoff & logging
|
||
(function(){
|
||
document.addEventListener('click', function(e){ var btn=e.target.closest('button[data-preview-btn]'); if(!btn) return; var theme=btn.getAttribute('data-theme-id'); if(!theme) return; setTimeout(function(){ attach(theme); }, 40); });
|
||
function attach(theme){
|
||
var modal=document.getElementById('theme-preview-modal'); if(!modal) return;
|
||
var cacheKey='preview:'+theme;
|
||
var tStart = performance.now();
|
||
// Stale-While-Revalidate: show cached HTML immediately if present
|
||
try {
|
||
var cached=sessionStorage.getItem(cacheKey);
|
||
if(cached){ modal.innerHTML=cached; modal.setAttribute('data-swr','stale'); }
|
||
} catch(_e){}
|
||
var attempts=0,max=3,back=350; function run(){ attempts++; fetch('/themes/fragment/preview/'+theme,{cache:'no-store'}).then(function(r){ if(r.status===200) return r.text(); if(r.status===304) return ''; throw new Error('bad'); }).then(function(html){ if(!html) return; modal.innerHTML=html; structuredLog('preview_fetch_success'); try { sessionStorage.setItem(cacheKey, html); } catch(_e){} try { recordPreviewLatency(performance.now()-tStart); } catch(_e){} }).catch(function(){ structuredLog('preview_fetch_error'); try { recordPreviewLatency(performance.now()-tStart, true); } catch(_e){} if(attempts<max){ setTimeout(run, back); back*=2; } else { modal.innerHTML='<div class="preview-modal-content"><div role="alert" style="font-size:13px;">Failed to load preview.<br/><button id="retry-preview" class="btn">Retry</button></div></div>'; var rp=document.getElementById('retry-preview'); if(rp){ rp.addEventListener('click', function(){ attempts=0; back=350; run(); }); } } }); }
|
||
if(/Loading/.test(modal.textContent||'') || modal.getAttribute('data-swr')==='stale') run();
|
||
// Inject export buttons (single insertion guard)
|
||
setTimeout(function(){
|
||
try {
|
||
if(!modal.querySelector('.preview-export-bar') && modal.querySelector('.preview-header')){
|
||
var bar = document.createElement('div');
|
||
bar.className='preview-export-bar';
|
||
bar.style.cssText='margin:.5rem 0 .25rem; display:flex; gap:.5rem; flex-wrap:wrap; align-items:center; font-size:11px;';
|
||
bar.innerHTML = '<button class="btn btn-ghost" style="font-size:11px;padding:2px 8px;" data-exp-json>Export JSON</button>'+
|
||
'<button class="btn btn-ghost" style="font-size:11px;padding:2px 8px;" data-exp-csv>Export CSV</button>'+
|
||
'<span style="opacity:.6;">(Respects curated toggle)</span>';
|
||
var header = modal.querySelector('.preview-header');
|
||
header.parentNode.insertBefore(bar, header.nextSibling);
|
||
function curatedOnly(){ try { return localStorage.getItem('mtg:preview.curatedOnly')==='1'; } catch(_){ return false; } }
|
||
bar.querySelector('[data-exp-json]').addEventListener('click', function(){ window.open('/themes/preview/'+encodeURIComponent(theme)+'/export.json?curated_only='+(curatedOnly()?'1':'0'),'_blank'); });
|
||
bar.querySelector('[data-exp-csv]').addEventListener('click', function(){ window.open('/themes/preview/'+encodeURIComponent(theme)+'/export.csv?curated_only='+(curatedOnly()?'1':'0'),'_blank'); });
|
||
}
|
||
} catch(_e){}
|
||
}, 120);
|
||
}
|
||
})();
|
||
</script>
|
||
<style>
|
||
#theme-results.preload-hide { visibility:hidden; }
|
||
#catalog-refresh-btn[disabled]{ opacity:.55; cursor:progress; }
|
||
</style>
|
||
<script>
|
||
// Batch preview latency beacons every 20 events
|
||
(function(){
|
||
var latSamples=[]; var errSamples=0; var BATCH=20; window.recordPreviewLatency=function(ms, isErr){ try { latSamples.push(ms); if(isErr) errSamples++; if(latSamples.length>=BATCH && window.THEME_DIAG_ENABLED){ var avg=Math.round(latSamples.reduce((a,b)=>a+b,0)/latSamples.length); var payload={events:[{name:'preview_load_batch', count:latSamples.length, avg_ms:avg, err:errSamples}]}; if(navigator.sendBeacon){ navigator.sendBeacon('/themes/metrics/client', new Blob([JSON.stringify(payload)],{type:'application/json'})); } latSamples=[]; errSamples=0; } } catch(_e){} };
|
||
})();
|
||
</script>
|
||
{% endblock %}
|