mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-24 03:20:12 +01:00
Bracket enforcement + inline gating; global pool prune; compliance JSON artifacts; UI combos gating; compose envs consolidated; fix YAML; bump version to 2.2.5
This commit is contained in:
parent
42c8fc9f9e
commit
4e03997923
32 changed files with 2819 additions and 125 deletions
46
code/web/templates/build/_compliance_panel.html
Normal file
46
code/web/templates/build/_compliance_panel.html
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
{% if compliance %}
|
||||
<details id="compliance-panel" style="margin-top:.75rem;">
|
||||
<summary>Bracket compliance</summary>
|
||||
<div class="muted" style="margin:.35rem 0;">Overall: <strong>{{ compliance.overall }}</strong> (Bracket: {{ compliance.bracket|title }}{{ ' #' ~ compliance.level if compliance.level is defined }})</div>
|
||||
{% if compliance.messages and compliance.messages|length > 0 %}
|
||||
<ul style="margin:.25rem 0; padding-left:1.25rem;">
|
||||
{% for m in compliance.messages %}
|
||||
<li>{{ m }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{# Flagged tiles by category, in the same card grid style #}
|
||||
{% if flagged_meta and flagged_meta|length > 0 %}
|
||||
<h5 style="margin:.75rem 0 .35rem 0;">Flagged cards</h5>
|
||||
<div class="card-grid">
|
||||
{% for f in flagged_meta %}
|
||||
<div class="card-tile" data-card-name="{{ f.name }}" data-role="{{ f.role or '' }}">
|
||||
<a href="https://scryfall.com/search?q={{ f.name|urlencode }}" target="_blank" rel="noopener" class="img-btn" title="{{ f.name }}">
|
||||
<img class="card-thumb" src="https://api.scryfall.com/cards/named?fuzzy={{ f.name|urlencode }}&format=image&version=normal" alt="{{ f.name }} image" width="160" loading="lazy" decoding="async" data-lqip="1"
|
||||
srcset="https://api.scryfall.com/cards/named?fuzzy={{ f.name|urlencode }}&format=image&version=small 160w, https://api.scryfall.com/cards/named?fuzzy={{ f.name|urlencode }}&format=image&version=normal 488w, https://api.scryfall.com/cards/named?fuzzy={{ f.name|urlencode }}&format=image&version=large 672w"
|
||||
sizes="160px" />
|
||||
</a>
|
||||
<div class="owned-badge" title="{{ 'Owned' if f.owned else 'Not owned' }}" aria-label="{{ 'Owned' if f.owned else 'Not owned' }}">{% if f.owned %}✔{% else %}✖{% endif %}</div>
|
||||
<div class="name">{{ f.name }}</div>
|
||||
<div class="muted" style="text-align:center; font-size:12px;">{{ f.category }}{% if f.role %} • {{ f.role }}{% endif %}</div>
|
||||
<div style="display:flex; justify-content:center; margin-top:.25rem;">
|
||||
{# Role-aware alternatives: pass the flagged name; server will infer role and exclude in-deck/locked #}
|
||||
<button type="button" class="btn" hx-get="/build/alternatives" hx-vals='{"name": "{{ f.name }}"}' hx-target="#alts-flag-{{ loop.index0 }}" hx-swap="innerHTML" title="Suggest role-consistent replacements">Pick replacement…</button>
|
||||
</div>
|
||||
<div id="alts-flag-{{ loop.index0 }}" class="alts" style="margin-top:.25rem;"></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if compliance.enforcement %}
|
||||
<div style="margin-top:.75rem; display:flex; gap:1rem; flex-wrap:wrap; align-items:center;">
|
||||
<form hx-post="/build/enforce/apply" hx-target="#wizard" hx-swap="innerHTML" style="display:inline;">
|
||||
<button type="submit" class="btn-rerun">Apply enforcement now</button>
|
||||
</form>
|
||||
<div class="muted">Tip: pick replacements first; your choices are honored during enforcement.</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</details>
|
||||
{% endif %}
|
||||
|
|
@ -40,11 +40,13 @@
|
|||
<input type="hidden" name="tag_mode" value="AND" />
|
||||
</div>
|
||||
<div id="newdeck-multicopy-slot" class="muted" style="margin-top:.5rem; min-height:1rem;"></div>
|
||||
<div style="margin-top:.5rem;">
|
||||
<div style="margin-top:.5rem;" id="newdeck-bracket-slot">
|
||||
<label>Bracket
|
||||
<select name="bracket">
|
||||
{% for b in brackets %}
|
||||
<option value="{{ b.level }}" {% if (form and form.bracket and form.bracket == b.level) or (not form and b.level == 3) %}selected{% endif %}>Bracket {{ b.level }}: {{ b.name }}</option>
|
||||
{% if not gc_commander or b.level >= 3 %}
|
||||
<option value="{{ b.level }}" {% if (form and form.bracket and form.bracket == b.level) or (not form and b.level == 3) %}selected{% endif %}>Bracket {{ b.level }}: {{ b.name }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
|
|
|
|||
|
|
@ -62,6 +62,22 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{# Always update the bracket dropdown on commander change; hide 1–2 only when gc_commander is true #}
|
||||
<div id="newdeck-bracket-slot" hx-swap-oob="true">
|
||||
<label>Bracket
|
||||
<select name="bracket">
|
||||
{% for b in brackets %}
|
||||
{% if not gc_commander or b.level >= 3 %}
|
||||
<option value="{{ b.level }}" {% if b.level == 3 %}selected{% endif %}>Bracket {{ b.level }}: {{ b.name }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
{% if gc_commander %}
|
||||
<div class="muted" style="font-size:12px; margin-top:.25rem;">Commander is a Game Changer; brackets 1–2 are unavailable.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
var list = document.getElementById('modal-tag-list');
|
||||
|
|
|
|||
|
|
@ -76,10 +76,12 @@
|
|||
<legend>Budget/Power Bracket</legend>
|
||||
<div style="display:grid; gap:.5rem;">
|
||||
{% for b in brackets %}
|
||||
{% if not gc_commander or b.level >= 3 %}
|
||||
<label style="display:flex; gap:.5rem; align-items:flex-start;">
|
||||
<input type="radio" name="bracket" value="{{ b.level }}" {% if (selected_bracket is defined and selected_bracket == b.level) or (selected_bracket is not defined and loop.first) %}checked{% endif %} />
|
||||
<span><strong>{{ b.name }}</strong> — <small>{{ b.desc }}</small></span>
|
||||
</label>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="muted" style="margin-top:.35rem; font-size:.9em;">
|
||||
|
|
|
|||
|
|
@ -79,7 +79,14 @@
|
|||
<strong>Status:</strong> {{ status }}{% if stage_label %} — <em>{{ stage_label }}</em>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if gated and (not status or not status.startswith('Build complete')) %}
|
||||
<div class="alert" style="margin-top:.5rem; color:#fecaca; background:#7f1d1d; border:1px solid #991b1b; padding:.5rem .75rem; border-radius:8px;">
|
||||
Compliance gating active — resolve violations above (replace or remove cards) to continue.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Load compliance panel as soon as the page renders, regardless of final status #}
|
||||
<div hx-get="/build/compliance" hx-trigger="load" hx-swap="afterend"></div>
|
||||
{% if status and status.startswith('Build complete') %}
|
||||
<div hx-get="/build/combos" hx-trigger="load" hx-swap="afterend"></div>
|
||||
{% endif %}
|
||||
|
|
@ -144,11 +151,11 @@
|
|||
</form>
|
||||
<form hx-post="/build/step5/continue" hx-target="#wizard" hx-swap="innerHTML" style="display:inline; display:flex; align-items:center; gap:.5rem;" onsubmit="try{ toast('Continuing…'); }catch(_){}">
|
||||
<input type="hidden" name="show_skipped" value="{{ '1' if show_skipped else '0' }}" />
|
||||
<button type="submit" class="btn-continue" data-action="continue" {% if status and status.startswith('Build complete') %}disabled{% endif %}>Continue</button>
|
||||
<button type="submit" class="btn-continue" data-action="continue" {% if (status and status.startswith('Build complete')) or gated %}disabled{% endif %}>Continue</button>
|
||||
</form>
|
||||
<form hx-post="/build/step5/rerun" hx-target="#wizard" hx-swap="innerHTML" style="display:inline; display:flex; align-items:center; gap:.5rem;" onsubmit="try{ toast('Rerunning stage…'); }catch(_){}">
|
||||
<input type="hidden" name="show_skipped" value="{{ '1' if show_skipped else '0' }}" />
|
||||
<button type="submit" class="btn-rerun" data-action="rerun" {% if status and status.startswith('Build complete') %}disabled{% endif %}>Rerun Stage</button>
|
||||
<button type="submit" class="btn-rerun" data-action="rerun" {% if (status and status.startswith('Build complete')) or gated %}disabled{% endif %}>Rerun Stage</button>
|
||||
</form>
|
||||
<span class="sep"></span>
|
||||
<div class="replace-toggle" role="group" aria-label="Replace toggle">
|
||||
|
|
@ -305,11 +312,9 @@
|
|||
|
||||
<!-- controls now above -->
|
||||
|
||||
{% if status and status.startswith('Build complete') %}
|
||||
{% if summary %}
|
||||
{% if status and status.startswith('Build complete') and summary %}
|
||||
{% include "partials/deck_summary.html" %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
29
code/web/templates/build/enforcement.html
Normal file
29
code/web/templates/build/enforcement.html
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
<h2>Bracket compliance — Enforcement review</h2>
|
||||
<p class="muted">Choose replacements for flagged cards, then click Apply enforcement.</p>
|
||||
<div style="margin:.5rem 0 1rem 0;">
|
||||
<a href="/build" class="btn">Back to Builder</a>
|
||||
</div>
|
||||
{% include "build/_compliance_panel.html" %}
|
||||
</section>
|
||||
<script>
|
||||
// In full-page mode, submit enforcement as a normal form POST (not HTMX swap)
|
||||
try{
|
||||
document.querySelectorAll('form[hx-post="/build/enforce/apply"]').forEach(function(f){
|
||||
f.removeAttribute('hx-post');
|
||||
f.removeAttribute('hx-target');
|
||||
f.removeAttribute('hx-swap');
|
||||
f.setAttribute('action', '/build/enforce/apply');
|
||||
f.setAttribute('method', 'post');
|
||||
});
|
||||
}catch(_){ }
|
||||
// Auto-open the compliance details when shown on this dedicated page
|
||||
try{
|
||||
var det = document.querySelector('details');
|
||||
if(det){ det.setAttribute('open', 'open'); }
|
||||
}catch(_){ }
|
||||
</script>
|
||||
{% endblock %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue