mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-17 08:00:13 +01:00
Finalize MDFC follow-ups, docs, and diagnostics tooling
document deck summary DFC badges, exporter annotations, and per-face metadata across README/DOCKER/release notes record completion of all MDFC roadmap follow-ups and add the authoring guide for multi-face CSV entries wire in optional DFC_PER_FACE_SNAPSHOT env support, exporter regression tests, and diagnostics updates noted in the changelog
This commit is contained in:
parent
6fefda714e
commit
88cf832bf2
46 changed files with 3292 additions and 86 deletions
|
|
@ -662,7 +662,7 @@
|
|||
window.__dfcFlipCard = function(card){ if(!card) return; flip(card, card.querySelector('.dfc-toggle')); };
|
||||
window.__dfcGetFace = function(card){ if(!card) return 'front'; return card.getAttribute(FACE_ATTR) || 'front'; };
|
||||
function scan(){
|
||||
document.querySelectorAll('.card-sample, .commander-cell, .card-tile, .candidate-tile, .stack-card, .card-preview, .owned-row, .list-row').forEach(ensureButton);
|
||||
document.querySelectorAll('.card-sample, .commander-cell, .commander-thumb, .card-tile, .candidate-tile, .stack-card, .card-preview, .owned-row, .list-row').forEach(ensureButton);
|
||||
}
|
||||
document.addEventListener('pointermove', function(e){ window.__lastPointerEvent = e; }, { passive:true });
|
||||
document.addEventListener('DOMContentLoaded', scan);
|
||||
|
|
@ -1206,9 +1206,9 @@
|
|||
if(!el) return null;
|
||||
// If inside flip button
|
||||
var btn = el.closest && el.closest('.dfc-toggle');
|
||||
if(btn) return btn.closest('.card-sample, .commander-cell, .card-tile, .candidate-tile, .card-preview, .stack-card');
|
||||
if(btn) return btn.closest('.card-sample, .commander-cell, .commander-thumb, .card-tile, .candidate-tile, .card-preview, .stack-card');
|
||||
// Recognized container classes (add .stack-card for finished/random deck thumbnails)
|
||||
var container = el.closest && el.closest('.card-sample, .commander-cell, .card-tile, .candidate-tile, .card-preview, .stack-card');
|
||||
var container = el.closest && el.closest('.card-sample, .commander-cell, .commander-thumb, .card-tile, .candidate-tile, .card-preview, .stack-card');
|
||||
if(container) return container;
|
||||
// Image-based detection (any card image carrying data-card-name)
|
||||
if(el.matches && (el.matches('img.card-thumb') || el.matches('img[data-card-name]') || el.classList.contains('commander-img'))){
|
||||
|
|
@ -1264,12 +1264,12 @@
|
|||
window.hoverShowByName = function(name){
|
||||
try {
|
||||
var el = document.querySelector('[data-card-name="'+CSS.escape(name)+'"]');
|
||||
if(el){ window.__hoverShowCard(el.closest('.card-sample, .commander-cell, .card-tile, .candidate-tile, .card-preview, .stack-card') || el); }
|
||||
if(el){ window.__hoverShowCard(el.closest('.card-sample, .commander-cell, .commander-thumb, .card-tile, .candidate-tile, .card-preview, .stack-card') || el); }
|
||||
} catch(_) {}
|
||||
};
|
||||
// Keyboard accessibility & focus traversal (P2 UI Hover keyboard accessibility)
|
||||
document.addEventListener('focusin', function(e){ var card=e.target.closest && e.target.closest('.card-sample, .commander-cell'); if(card){ show(card, {clientX:card.getBoundingClientRect().left+10, clientY:card.getBoundingClientRect().top+10}); }});
|
||||
document.addEventListener('focusout', function(e){ var next=e.relatedTarget && e.relatedTarget.closest && e.relatedTarget.closest('.card-sample, .commander-cell'); if(!next) hide(); });
|
||||
document.addEventListener('focusin', function(e){ var card=e.target.closest && e.target.closest('.card-sample, .commander-cell, .commander-thumb'); if(card){ show(card, {clientX:card.getBoundingClientRect().left+10, clientY:card.getBoundingClientRect().top+10}); }});
|
||||
document.addEventListener('focusout', function(e){ var next=e.relatedTarget && e.relatedTarget.closest && e.relatedTarget.closest('.card-sample, .commander-cell, .commander-thumb'); if(!next) hide(); });
|
||||
document.addEventListener('keydown', function(e){ if(e.key==='Escape') hide(); });
|
||||
// Compact mode event listener
|
||||
document.addEventListener('mtg:hoverCompactToggle', function(){ panel.classList.toggle('compact-img', !!window.__hoverCompactMode); });
|
||||
|
|
|
|||
|
|
@ -1,13 +1,19 @@
|
|||
{% if candidates and candidates|length %}
|
||||
<ul style="list-style:none; padding:0; margin:.35rem 0; display:grid; gap:.25rem;" role="listbox" aria-label="Commander suggestions" tabindex="-1">
|
||||
{% for name, score, colors in candidates %}
|
||||
{% for cand in candidates %}
|
||||
<li>
|
||||
<button type="button" id="cand-{{ loop.index0 }}" class="chip candidate-btn" role="option" data-idx="{{ loop.index0 }}" data-name="{{ name|e }}"
|
||||
hx-get="/build/new/inspect?name={{ name|urlencode }}"
|
||||
<button type="button" id="cand-{{ loop.index0 }}" class="chip candidate-btn" role="option" data-idx="{{ loop.index0 }}" data-name="{{ cand.value|e }}" data-display="{{ cand.display|e }}"
|
||||
hx-get="/build/new/inspect?name={{ cand.display|urlencode }}"
|
||||
hx-target="#newdeck-tags-slot" hx-swap="innerHTML"
|
||||
hx-on="htmx:afterOnLoad: (function(){ try{ var n=this.getAttribute('data-name')||''; var ci = document.querySelector('input[name=commander]'); if(ci){ ci.value=n; try{ ci.selectionStart = ci.selectionEnd = ci.value.length; }catch(_){} } var nm = document.querySelector('input[name=name]'); if(nm && (!nm.value || !nm.value.trim())){ nm.value=n; } }catch(_){ } }).call(this)">
|
||||
{{ name }}
|
||||
hx-on="htmx:afterOnLoad: (function(){ try{ var preferred=this.getAttribute('data-name')||''; var displayed=this.getAttribute('data-display')||preferred; var ci = document.querySelector('input[name=commander]'); if(ci){ ci.value=preferred; try{ ci.selectionStart = ci.selectionEnd = ci.value.length; }catch(_){} try{ ci.dispatchEvent(new Event('input', { bubbles: true })); }catch(_){ } } var nm = document.querySelector('input[name=name]'); if(nm && (!nm.value || !nm.value.trim())){ nm.value=displayed; } }catch(_){ } }).call(this)">
|
||||
{{ cand.display }}
|
||||
{% if cand.warning %}
|
||||
<span aria-hidden="true" style="margin-left:.35rem; font-size:11px; color:#facc15;">⚠</span>
|
||||
{% endif %}
|
||||
</button>
|
||||
{% if cand.warning %}
|
||||
<div class="muted" style="font-size:11px; margin:.25rem 0 0 .5rem; color:#facc15;" role="note">⚠ {{ cand.warning }}</div>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -55,9 +55,9 @@
|
|||
<fieldset>
|
||||
<legend>Preferences</legend>
|
||||
<div style="text-align: left;">
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: inline-flex; align-items: center; gap: 0.5rem; margin: 0;" title="When enabled, the builder will try to auto-complete missing combo partners near the end of the build (respecting owned-only and locks).">
|
||||
<input type="checkbox" name="prefer_combos" id="pref-combos-chk" style="margin: 0;" />
|
||||
<div style="margin-bottom: 1rem; display:flex; flex-direction:column; gap:0.75rem;">
|
||||
<label for="pref-combos-chk" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; margin:0; width:100%; cursor:pointer; text-align:left;" title="When enabled, the builder will try to auto-complete missing combo partners near the end of the build (respecting owned-only and locks).">
|
||||
<input type="checkbox" name="prefer_combos" id="pref-combos-chk" value="1" style="margin:0; cursor:pointer;" {% if form and form.prefer_combos %}checked{% endif %} />
|
||||
<span>Prioritize combos</span>
|
||||
</label>
|
||||
<div id="pref-combos-config" style="margin-top: 0.5rem; margin-left: 1.5rem; padding: 0.5rem; border: 1px solid var(--border); border-radius: 8px; display: none;">
|
||||
|
|
@ -80,12 +80,24 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: inline-flex; align-items: center; gap: 0.5rem; margin: 0;" title="When enabled, include a Multi-Copy package for matching archetypes (e.g., tokens/tribal).">
|
||||
<input type="checkbox" name="enable_multicopy" id="pref-mc-chk" style="margin: 0;" />
|
||||
<label for="pref-mc-chk" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; margin:0; width:100%; cursor:pointer; text-align:left;" title="When enabled, include a Multi-Copy package for matching archetypes (e.g., tokens/tribal).">
|
||||
<input type="checkbox" name="enable_multicopy" id="pref-mc-chk" value="1" style="margin:0; cursor:pointer;" {% if form and form.enable_multicopy %}checked{% endif %} />
|
||||
<span>Enable Multi-Copy package</span>
|
||||
</label>
|
||||
<div style="display:flex; flex-direction:column; gap:0.5rem; margin-top:0.75rem;">
|
||||
<label for="use-owned-chk" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; margin:0; width:100%; cursor:pointer; text-align:left;" title="Limit the pool to cards you already own. Cards outside your owned library will be skipped.">
|
||||
<input type="checkbox" name="use_owned_only" id="use-owned-chk" value="1" style="margin:0; cursor:pointer;" {% if form and form.use_owned_only %}checked{% endif %} />
|
||||
<span>Use only owned cards</span>
|
||||
</label>
|
||||
<label for="prefer-owned-chk" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; margin:0; width:100%; cursor:pointer; text-align:left;" title="Still allow unowned cards, but rank owned cards higher when choosing picks.">
|
||||
<input type="checkbox" name="prefer_owned" id="prefer-owned-chk" value="1" style="margin:0; cursor:pointer;" {% if form and form.prefer_owned %}checked{% endif %} />
|
||||
<span>Prefer owned cards (allow unowned fallback)</span>
|
||||
</label>
|
||||
<label for="swap-mdfc-chk" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; margin:0; width:100%; cursor:pointer; text-align:left;" title="When enabled, modal DFC lands will replace a matching basic land as they are added so land counts stay level without manual trims.">
|
||||
<input type="checkbox" name="swap_mdfc_basics" id="swap-mdfc-chk" value="1" style="margin:0; cursor:pointer;" {% if form and form.swap_mdfc_basics %}checked{% endif %} />
|
||||
<span>Swap basics for MDFC lands</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,27 @@
|
|||
</script>
|
||||
</div>
|
||||
|
||||
{% set exclusion = commander.exclusion if commander is defined and commander.exclusion is defined else None %}
|
||||
{% if exclusion %}
|
||||
{% set eligible_raw = exclusion.eligible_faces if exclusion.eligible_faces is defined else [] %}
|
||||
{% set eligible_list = eligible_raw if eligible_raw is iterable else [] %}
|
||||
{% set eligible_lower = eligible_list | map('lower') | list %}
|
||||
{% set current_lower = commander.name|lower %}
|
||||
{% if eligible_list and (current_lower not in eligible_lower or exclusion.reason == 'secondary_face_only') %}
|
||||
<div class="muted" style="font-size:12px; margin-top:.35rem; color:#facc15;" role="note">
|
||||
{% if eligible_list|length == 1 %}
|
||||
⚠ This commander only works from '{{ eligible_list[0] }}'.
|
||||
{% if exclusion.primary_face and exclusion.primary_face|lower != eligible_list[0]|lower %}
|
||||
Front face '{{ exclusion.primary_face }}' can't lead a deck.
|
||||
{% endif %}
|
||||
We'll build using the supported face automatically.
|
||||
{% else %}
|
||||
⚠ This commander only works from these faces: {{ eligible_list | join(', ') }}. We'll build using the supported faces automatically.
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
{% if tags and tags|length %}
|
||||
<div class="muted" style="font-size:12px; margin-bottom:.35rem;">Pick up to three themes. Toggle AND/OR to control how themes combine.</div>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@
|
|||
<input type="checkbox" name="prefer_owned" value="1" {% if prefer_owned %}checked{% endif %} onchange="this.form.requestSubmit();" />
|
||||
Prefer owned cards (allow unowned fallback)
|
||||
</label>
|
||||
<label style="display:flex; align-items:center; gap:.35rem;" title="When enabled, modal DFC lands will replace a matching basic land as they are added so land counts stay level without manual trims.">
|
||||
<input type="checkbox" name="swap_mdfc_basics" value="1" {% if swap_mdfc_basics %}checked{% endif %} onchange="this.form.requestSubmit();" />
|
||||
Swap basics for MDFC lands
|
||||
</label>
|
||||
<a href="/owned" target="_blank" rel="noopener" class="btn">Manage Owned Library</a>
|
||||
</form>
|
||||
<div class="muted" style="font-size:12px; margin-top:-.25rem;">Tip: Locked cards are respected on reruns in Step 5.</div>
|
||||
|
|
|
|||
|
|
@ -74,9 +74,10 @@
|
|||
<p>Tags: {{ deck_theme_tags|default([])|join(', ') }}</p>
|
||||
<div style="margin:.35rem 0; color: var(--muted); display:flex; gap:.5rem; align-items:center; flex-wrap:wrap;">
|
||||
<span>Owned-only: <strong>{{ 'On' if owned_only else 'Off' }}</strong></span>
|
||||
<div style="display:flex;align-items:center;gap:1rem;">
|
||||
<div style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap;">
|
||||
<button type="button" hx-get="/build/step4" hx-target="#wizard" hx-swap="innerHTML" style="background:#374151; color:#e5e7eb; border:none; border-radius:6px; padding:.25rem .5rem; cursor:pointer; font-size:12px;" title="Change owned settings in Review">Edit in Review</button>
|
||||
<div>Prefer-owned: <strong>{{ 'On' if prefer_owned else 'Off' }}</strong></div>
|
||||
<div>MDFC swap: <strong>{{ 'On' if swap_mdfc_basics else 'Off' }}</strong></div>
|
||||
</div>
|
||||
<span style="margin-left:auto;"><a href="/owned" target="_blank" rel="noopener" class="btn">Manage Owned Library</a></span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@
|
|||
.commander-list { display:flex; flex-direction:column; gap:1rem; margin-top:.5rem; }
|
||||
|
||||
.commander-row { display:flex; gap:1rem; padding:1rem; border:1px solid var(--border); border-radius:14px; background:var(--panel); align-items:stretch; }
|
||||
.commander-thumb { width:160px; flex:0 0 auto; }
|
||||
.commander-thumb { width:160px; flex:0 0 auto; position:relative; }
|
||||
.commander-thumb img { width:160px; height:auto; border-radius:10px; border:1px solid var(--border); background:#0b0d12; display:block; }
|
||||
.commander-main { flex:1 1 auto; display:flex; flex-direction:column; gap:.6rem; min-width:0; }
|
||||
.commander-header { display:flex; flex-wrap:wrap; align-items:center; gap:.5rem .75rem; }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
{# Commander row partial fed by CommanderView entries #}
|
||||
{% from "partials/_macros.html" import color_identity %}
|
||||
{% set record = entry.record %}
|
||||
{% set display_label = record.name if '//' in record.name else record.display_name %}
|
||||
<article class="commander-row" data-commander-slug="{{ record.slug }}" data-hover-simple="true">
|
||||
<div class="commander-thumb">
|
||||
{% set small = record.image_small_url or record.image_normal_url %}
|
||||
|
|
@ -12,12 +13,13 @@
|
|||
loading="lazy"
|
||||
decoding="async"
|
||||
data-card-name="{{ record.display_name }}"
|
||||
data-original-name="{{ record.name }}"
|
||||
data-hover-simple="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="commander-main">
|
||||
<div class="commander-header">
|
||||
<h3 class="commander-name">{{ record.display_name }}</h3>
|
||||
<h3 class="commander-name">{{ display_label }}</h3>
|
||||
{{ color_identity(record.color_identity, record.is_colorless, entry.color_aria_label, entry.color_label) }}
|
||||
</div>
|
||||
<p class="commander-context muted">{{ record.type_line or 'Legendary Creature' }}</p>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,62 @@
|
|||
<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>
|
||||
<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.get('colors') if merge_summary else {} %}
|
||||
{% 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>
|
||||
{% for color, payload in colors.items()|dictsort %}
|
||||
<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;">
|
||||
{% set entries = payload.entries or [] %}
|
||||
{% if entries %}
|
||||
<details>
|
||||
<summary style="cursor:pointer;">{{ entries|length }} recorded</summary>
|
||||
<ul style="margin:.35rem 0 0 .75rem; padding:0; list-style:disc; max-height:180px; overflow:auto;">
|
||||
{% for entry in entries %}
|
||||
{% if loop.index0 < 5 %}
|
||||
<li style="margin-bottom:.25rem;">
|
||||
<strong>{{ entry.name }}</strong> — {{ entry.total_faces }} faces (dropped {{ entry.dropped_faces }})
|
||||
</li>
|
||||
{% elif loop.index0 == 5 %}
|
||||
<li style="font-size:11px; opacity:.75;">… {{ entries|length - 5 }} more entries</li>
|
||||
{% break %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</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>
|
||||
</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>
|
||||
|
|
@ -193,6 +249,71 @@
|
|||
.catch(function(){ tokenEl.textContent = 'Theme stats unavailable'; });
|
||||
}
|
||||
loadTokenStats();
|
||||
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();
|
||||
// Theme status and reset
|
||||
try{
|
||||
var tEl = document.getElementById('themeSummary');
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@
|
|||
.stack-card:hover { z-index: 999; transform: translateY(-2px); box-shadow: 0 10px 22px rgba(0,0,0,.6); }
|
||||
.count-badge { position:absolute; top:6px; right:6px; background:rgba(17,24,39,.9); color:#e5e7eb; border:1px solid var(--border); border-radius:12px; font-size:12px; line-height:18px; height:18px; padding:0 6px; pointer-events:none; }
|
||||
.owned-badge { position:absolute; top:6px; left:6px; background:rgba(17,24,39,.9); color:#e5e7eb; border:1px solid var(--border); border-radius:12px; font-size:12px; line-height:18px; height:18px; min-width:18px; padding:0 6px; text-align:center; pointer-events:none; z-index: 2; }
|
||||
.dfc-thumb-badge { position:absolute; bottom:8px; left:6px; background:rgba(15,23,42,.92); border:1px solid #34d399; color:#bbf7d0; border-radius:12px; font-size:11px; line-height:18px; height:18px; padding:0 6px; pointer-events:none; }
|
||||
.dfc-thumb-badge.counts { border-color:#60a5fa; color:#bfdbfe; }
|
||||
.owned-flag { font-size:.95rem; opacity:.9; }
|
||||
</style>
|
||||
<div id="typeview-list" class="typeview">
|
||||
|
|
@ -47,8 +49,11 @@
|
|||
.list-row .count { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-variant-numeric: tabular-nums; font-feature-settings: 'tnum'; text-align:right; color:#94a3b8; }
|
||||
.list-row .times { color:#94a3b8; text-align:center; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
||||
.list-row .name { display:inline-block; padding: 2px 4px; border-radius: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.list-row .flip-slot { min-width:2.4em; display:flex; justify-content:center; align-items:center; }
|
||||
.list-row .flip-slot { min-width:2.4em; display:flex; justify-content:flex-start; align-items:center; }
|
||||
.list-row .owned-flag { width: 1.6em; min-width: 1.6em; text-align:center; display:inline-block; }
|
||||
.dfc-land-chip { display:inline-flex; align-items:center; gap:.25rem; padding:2px 6px; border-radius:999px; font-size:11px; font-weight:600; background:#0f172a; border:1px solid #334155; color:#e5e7eb; line-height:1; }
|
||||
.dfc-land-chip.extra { border-color:#34d399; color:#a7f3d0; }
|
||||
.dfc-land-chip.counts { border-color:#60a5fa; color:#bfdbfe; }
|
||||
</style>
|
||||
<div class="list-grid">
|
||||
{% for c in clist %}
|
||||
|
|
@ -69,7 +74,11 @@
|
|||
<span class="count">{{ cnt }}</span>
|
||||
<span class="times">x</span>
|
||||
<span class="name dfc-anchor" title="{{ c.name }}" data-card-name="{{ c.name }}" data-count="{{ cnt }}" data-role="{{ c.role }}" data-tags="{{ (c.tags|map('trim')|join(', ')) if c.tags else '' }}"{% if overlaps %} data-overlaps="{{ overlaps|join(', ') }}"{% endif %}>{{ c.name }}</span>
|
||||
<span class="flip-slot" aria-hidden="true"></span>
|
||||
<span class="flip-slot" aria-hidden="true">
|
||||
{% if c.dfc_land %}
|
||||
<span class="dfc-land-chip {% if c.dfc_adds_extra_land %}extra{% else %}counts{% endif %}" title="{{ c.dfc_note or 'Modal double-faced land' }}">DFC land{% if c.dfc_adds_extra_land %} +1{% endif %}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="owned-flag" title="{{ 'Owned' if owned else 'Not owned' }}" aria-label="{{ 'Owned' if owned else 'Not owned' }}">{% if owned %}✔{% else %}✖{% endif %}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
|
@ -106,6 +115,9 @@
|
|||
sizes="(max-width: 1200px) 160px, 240px" />
|
||||
<div class="count-badge">{{ cnt }}x</div>
|
||||
<div class="owned-badge" title="{{ 'Owned' if owned else 'Not owned' }}" aria-label="{{ 'Owned' if owned else 'Not owned' }}">{% if owned %}✔{% else %}✖{% endif %}</div>
|
||||
{% if c.dfc_land %}
|
||||
<div class="dfc-thumb-badge {% if c.dfc_adds_extra_land %}extra{% else %}counts{% endif %}" title="{{ c.dfc_note or 'Modal double-faced land' }}">DFC{% if c.dfc_adds_extra_land %}+1{% endif %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
|
@ -122,6 +134,60 @@
|
|||
|
||||
<!-- Deck Summary initializer script moved below markup for proper element availability -->
|
||||
|
||||
<!-- Land Summary -->
|
||||
{% set land = summary.land_summary if summary else None %}
|
||||
{% if land %}
|
||||
<section style="margin-top:1rem;">
|
||||
<h5>Land Summary</h5>
|
||||
<div class="muted" style="font-weight:600; margin-bottom:.35rem;">
|
||||
{{ land.headline or ('Lands: ' ~ (land.traditional or 0)) }}
|
||||
</div>
|
||||
<div style="display:flex; flex-wrap:wrap; gap:.75rem; align-items:flex-start;">
|
||||
<div class="muted">Traditional land slots: <strong>{{ land.traditional or 0 }}</strong></div>
|
||||
<div class="muted">MDFC land additions: <strong>{{ land.dfc_lands or 0 }}</strong></div>
|
||||
<div class="muted">Total with MDFCs: <strong>{{ land.with_dfc or land.traditional or 0 }}</strong></div>
|
||||
</div>
|
||||
{% if land.dfc_cards %}
|
||||
<details style="margin-top:.5rem;">
|
||||
<summary>MDFC mana sources ({{ land.dfc_cards|length }})</summary>
|
||||
<ul style="list-style:none; padding:0; margin:.35rem 0 0; display:grid; gap:.35rem;">
|
||||
{% for card in land.dfc_cards %}
|
||||
{% set extra = card.adds_extra_land or card.counts_as_extra %}
|
||||
{% set colors = card.colors or [] %}
|
||||
<li class="muted" style="display:flex; gap:.5rem; flex-wrap:wrap; align-items:flex-start;">
|
||||
<span class="chip"><span class="dot" style="background:#10b981;"></span> {{ card.name }} ×{{ card.count or 1 }}</span>
|
||||
<span>Colors: {{ colors|join(', ') if colors else '–' }}</span>
|
||||
{% if extra %}
|
||||
<span class="chip" style="background:#0f172a; border-color:#34d399; color:#a7f3d0;">{{ card.note or 'Adds extra land slot' }}</span>
|
||||
{% else %}
|
||||
<span class="chip" style="background:#111827; border-color:#60a5fa; color:#bfdbfe;">{{ card.note or 'Counts as land slot' }}</span>
|
||||
{% endif %}
|
||||
{% if card.faces %}
|
||||
<ul style="list-style:none; padding:0; margin:.2rem 0 0; display:grid; gap:.15rem; flex:1 0 100%;">
|
||||
{% for face in card.faces %}
|
||||
{% set face_name = face.get('face') or face.get('faceName') or 'Face' %}
|
||||
{% set face_type = face.get('type') or '–' %}
|
||||
{% set mana_cost = face.get('mana_cost') %}
|
||||
{% set mana_value = face.get('mana_value') %}
|
||||
{% set produces = face.get('produces_mana') %}
|
||||
<li style="font-size:0.85rem; color:#e5e7eb; opacity:.85;">
|
||||
<span>{{ face_name }}</span>
|
||||
<span>— {{ face_type }}</span>
|
||||
{% if mana_cost %}<span>• Mana Cost: {{ mana_cost }}</span>{% endif %}
|
||||
{% if mana_value is not none %}<span>• MV: {{ mana_value }}</span>{% endif %}
|
||||
{% if produces %}<span>• Produces mana</span>{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<!-- Mana Overview Row: Pips • Sources • Curve -->
|
||||
<section style="margin-top:1rem;">
|
||||
<h5>Mana Overview</h5>
|
||||
|
|
@ -144,7 +210,11 @@
|
|||
{% set c_cards = (pc[color] if pc and (color in pc) else []) %}
|
||||
{% set parts = [] %}
|
||||
{% for c in c_cards %}
|
||||
{% set _ = parts.append(c.name ~ ((" ×" ~ c.count) if c.count and c.count>1 else '')) %}
|
||||
{% set label = c.name ~ ((" ×" ~ c.count) if c.count and c.count>1 else '') %}
|
||||
{% if c.dfc %}
|
||||
{% set label = label ~ ' (DFC)' %}
|
||||
{% endif %}
|
||||
{% set _ = parts.append(label) %}
|
||||
{% endfor %}
|
||||
{% set cards_line = parts|join(' • ') %}
|
||||
{% set pct_f = (pd.weights[color] * 100) if pd.weights and color in pd.weights else 0 %}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue