mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-24 11:30:12 +01:00
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
This commit is contained in:
parent
8fa040a05a
commit
0f73a85a4e
43 changed files with 4515 additions and 105 deletions
93
code/web/templates/configs/index.html
Normal file
93
code/web/templates/configs/index.html
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h2>Build from JSON</h2>
|
||||
<div style="display:grid; grid-template-columns: 1fr minmax(360px, 520px); gap:16px; align-items:start;">
|
||||
<p class="muted" style="max-width: 70ch; margin:0;">
|
||||
Run a non-interactive deck build using a saved JSON configuration. Upload a JSON file, view its details, or run it headlessly to generate deck exports and a build summary.
|
||||
</p>
|
||||
<div>
|
||||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<strong style="font-size:14px;">Example: {{ example_name }}</strong>
|
||||
</div>
|
||||
<pre style="margin-top:.35rem; background:#0f1115; border:1px solid var(--border); padding:.75rem; border-radius:8px; max-height:300px; overflow:auto; white-space:pre;">{{ example_json or '{\n "commander": "Your Commander Name",\n "primary_tag": "Your Main Theme",\n "secondary_tag": null,\n "tertiary_tag": null,\n "bracket_level": 0,\n "ideal_counts": {\n "ramp": 10,\n "lands": 35,\n "basic_lands": 20,\n "fetch_lands": 3,\n "creatures": 28,\n "removal": 10,\n "wipes": 2,\n "card_advantage": 8,\n "protection": 4\n }\n}' }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% if error %}<div class="error">{{ error }}</div>{% endif %}
|
||||
{% if notice %}<div class="notice">{{ notice }}</div>{% endif %}
|
||||
<div class="config-actions" style="margin-bottom:1rem; display:flex; gap:12px; align-items:center;">
|
||||
<form hx-post="/configs/upload" hx-target="#config-list" hx-swap="outerHTML" enctype="multipart/form-data">
|
||||
<button type="button" class="btn" onclick="this.nextElementSibling.click();">Upload JSON</button>
|
||||
<input id="upload-json" type="file" name="file" accept="application/json" style="display:none" onchange="this.form.requestSubmit();">
|
||||
</form>
|
||||
<input id="config-filter" type="search" placeholder="Filter by commander or tag..." style="flex:1; max-width:360px; padding:.4rem .6rem; border-radius:8px; border:1px solid var(--border); background:#0f1115; color:#e5e7eb;" />
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
// Drag and drop upload support
|
||||
var form = document.querySelector('.config-actions form[enctype="multipart/form-data"]');
|
||||
var fileInput = document.getElementById('upload-json');
|
||||
if (form && fileInput) {
|
||||
form.addEventListener('dragover', function(e){ e.preventDefault(); form.style.outline = '2px dashed #334155'; });
|
||||
form.addEventListener('dragleave', function(){ form.style.outline = ''; });
|
||||
form.addEventListener('drop', function(e){
|
||||
e.preventDefault(); form.style.outline = '';
|
||||
if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length) {
|
||||
fileInput.files = e.dataTransfer.files;
|
||||
form.requestSubmit();
|
||||
}
|
||||
});
|
||||
}
|
||||
// Client-side filter of config list
|
||||
var filter = document.getElementById('config-filter');
|
||||
var list = document.getElementById('config-list');
|
||||
function applyFilter() {
|
||||
if (!list) return;
|
||||
var q = (filter.value || '').toLowerCase().trim();
|
||||
var items = list.querySelectorAll('li');
|
||||
items.forEach(function(li){
|
||||
var txt = (li.textContent || '').toLowerCase();
|
||||
li.style.display = (!q || txt.indexOf(q) !== -1) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
if (filter) {
|
||||
filter.addEventListener('input', applyFilter);
|
||||
}
|
||||
document.addEventListener('htmx:afterSwap', function(e){ if (e && e.target && e.target.id === 'config-list') applyFilter(); });
|
||||
})();
|
||||
</script>
|
||||
<div id="config-list">
|
||||
{% if not items %}
|
||||
<p>No configs found in /config. Export a run config from a build, or upload one here.</p>
|
||||
{% else %}
|
||||
<ul class="file-list" style="list-style: none; margin: 0; padding: 0;">
|
||||
{% for it in items %}
|
||||
<li>
|
||||
<div class="row" style="display:flex; justify-content:space-between; align-items:center; gap:12px;">
|
||||
<div style="display:flex; align-items:center; gap:10px;">
|
||||
<strong>
|
||||
{% if it.commander %}
|
||||
<span data-card-name="{{ it.commander }}" data-tags="{{ (it.tags|join(', ')) if it.tags else '' }}">{{ it.commander }}</span>
|
||||
{% else %}
|
||||
{{ it.name }}
|
||||
{% endif %}
|
||||
</strong>
|
||||
{% if it.tags %}<span style="color:#64748b;">[{{ ', '.join(it.tags) }}]</span>{% endif %}
|
||||
{% if it.bracket_level is not none %}<span class="badge">Bracket {{ it.bracket_level }}</span>{% endif %}
|
||||
</div>
|
||||
<div class="actions" style="display:flex; gap:8px;">
|
||||
<form method="get" action="/configs/view" style="display:inline;">
|
||||
<input type="hidden" name="name" value="{{ it.name }}" />
|
||||
<button type="submit" class="btn">View</button>
|
||||
</form>
|
||||
<form method="post" action="/configs/run" style="display:inline;">
|
||||
<input type="hidden" name="name" value="{{ it.name }}" />
|
||||
<button type="submit">Run</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
56
code/web/templates/configs/run_result.html
Normal file
56
code/web/templates/configs/run_result.html
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h2>Build from JSON: {{ cfg_name }}</h2>
|
||||
<p class="muted" style="max-width: 70ch;">This page shows the results of a non-interactive build from the selected JSON configuration.</p>
|
||||
{% if commander %}
|
||||
<div class="muted">Commander: <strong data-card-name="{{ commander }}">{{ commander }}</strong></div>
|
||||
{% endif %}
|
||||
|
||||
<div class="two-col two-col-left-rail">
|
||||
<aside class="card-preview">
|
||||
{% if commander %}
|
||||
<a href="https://scryfall.com/search?q={{ commander|urlencode }}" target="_blank" rel="noopener">
|
||||
<img src="https://api.scryfall.com/cards/named?fuzzy={{ commander|urlencode }}&format=image&version=normal" alt="{{ commander }} card image" width="320" />
|
||||
</a>
|
||||
{% endif %}
|
||||
<div style="margin-top:.75rem; display:flex; gap:.35rem; flex-wrap:wrap;">
|
||||
{% if ok and csv_path %}
|
||||
<form action="/files" method="get" target="_blank" style="display:inline; margin:0;">
|
||||
<input type="hidden" name="path" value="{{ csv_path }}" />
|
||||
<button type="submit">Download CSV</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if ok and txt_path %}
|
||||
<form action="/files" method="get" target="_blank" style="display:inline; margin:0;">
|
||||
<input type="hidden" name="path" value="{{ txt_path }}" />
|
||||
<button type="submit">Download TXT</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="get" action="/configs" style="display:inline; margin:0;">
|
||||
<button type="submit">Back to Build from JSON</button>
|
||||
</form>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="grow">
|
||||
{% if not ok %}
|
||||
<div class="error">Build failed: {{ error }}</div>
|
||||
{% else %}
|
||||
<div class="notice">Build completed{% if commander %} — <strong>{{ commander }}</strong>{% endif %}</div>
|
||||
|
||||
|
||||
{% if summary %}
|
||||
{% include "partials/deck_summary.html" %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if show_logs %}
|
||||
<details style="margin-top:1rem;">
|
||||
<summary>Show logs</summary>
|
||||
<pre style="margin-top:.5rem; white-space:pre-wrap; background:#0f1115; border:1px solid var(--border); padding:1rem; border-radius:8px; max-height:40vh; overflow:auto;">{{ log }}</pre>
|
||||
</details>
|
||||
{% endif %}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
24
code/web/templates/configs/run_summary.html
Normal file
24
code/web/templates/configs/run_summary.html
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h2>Run from Config: {{ cfg_name }}</h2>
|
||||
{% if not ok %}
|
||||
<div class="error">Build failed: {{ error }}</div>
|
||||
{% else %}
|
||||
<div class="notice">Build completed.</div>
|
||||
<div style="margin:.5rem 0;">
|
||||
{% if csv_path %}<a class="btn" href="/files?path={{ csv_path | urlencode }}">Download CSV</a>{% endif %}
|
||||
{% if txt_path %}<a class="btn" href="/files?path={{ txt_path | urlencode }}">Download TXT</a>{% endif %}
|
||||
</div>
|
||||
{% if summary %}
|
||||
<hr style="margin:1.25rem 0; border-color: var(--border);" />
|
||||
<h4>Deck Summary</h4>
|
||||
{# Reuse the same inner sections as the build summary page by including its markup #}
|
||||
{% set __summary = summary %}
|
||||
{% set summary = __summary %}
|
||||
{% include "build/_step5.html" ignore missing %}
|
||||
{% else %}
|
||||
<p class="muted">No summary data available.</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<p style="margin-top:1rem;"><a href="/configs">Back to Configs</a></p>
|
||||
{% endblock %}
|
||||
22
code/web/templates/configs/view.html
Normal file
22
code/web/templates/configs/view.html
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h2>Build from JSON: {{ name }}</h2>
|
||||
<p class="muted" style="max-width: 70ch;">Review the configuration details below, then run a non-interactive build to produce deck exports and a summary.</p>
|
||||
<details open>
|
||||
<summary>Overview</summary>
|
||||
<div class="grid" style="display:grid; grid-template-columns: 200px 1fr; gap:6px; max-width: 920px;">
|
||||
<div>Commander</div><div>{{ data.commander }}</div>
|
||||
<div>Tags</div><div>{{ data.primary_tag }}{% if data.secondary_tag %}, {{ data.secondary_tag }}{% endif %}{% if data.tertiary_tag %}, {{ data.tertiary_tag }}{% endif %}</div>
|
||||
<div>Bracket</div><div>{{ data.bracket_level }}</div>
|
||||
</div>
|
||||
</details>
|
||||
<details style="margin-top:1rem;" open>
|
||||
<summary>Ideal Counts</summary>
|
||||
<pre style="background:#0f1115; border:1px solid var(--border); padding:.75rem; border-radius:8px;">{{ data.ideal_counts | tojson(indent=2) }}</pre>
|
||||
</details>
|
||||
<form method="post" action="/configs/run" style="margin-top:1rem;">
|
||||
<input type="hidden" name="name" value="{{ name }}" />
|
||||
<button type="submit">Run Headless</button>
|
||||
<button type="submit" formaction="/configs" formmethod="get" class="btn" style="margin-left:.5rem;">Back</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue