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:
mwisnowski 2025-09-03 18:00:06 -07:00
parent 42c8fc9f9e
commit 4e03997923
32 changed files with 2819 additions and 125 deletions

View 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 %}

View file

@ -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>

View file

@ -62,6 +62,22 @@
</div>
</div>
{# Always update the bracket dropdown on commander change; hide 12 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 12 are unavailable.</div>
{% endif %}
</div>
<script>
(function(){
var list = document.getElementById('modal-tag-list');

View file

@ -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;">

View file

@ -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>

View 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 %}