mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-18 00:20:13 +01:00
Web UI polish: thumbnail-hover preview, white thumbnail selection, Themes bullet list; global Scryfall image retry (thumbs+previews) with fallbacks and cache-bust; standardized data-card-name. Deck Summary alignment overhaul (count//name/owned grid, tabular numerals, inset highlight, tooltips, starts under header). Added diagnostics (health + logs pages, error pages, request-id propagation), global HTMX error toasts, and docs updates. Update DOCKER guide and add run-web scripts. Update CHANGELOG and release notes template.
This commit is contained in:
parent
8d1f6a8ac4
commit
f8c6b5c07e
30 changed files with 786 additions and 232 deletions
44
code/web/templates/diagnostics/index.html
Normal file
44
code/web/templates/diagnostics/index.html
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<section>
|
||||
<h2>Diagnostics</h2>
|
||||
<p class="muted">Use these tools to verify error handling surfaces.</p>
|
||||
<div class="card" style="background:#0f1115; border:1px solid var(--border); border-radius:10px; padding:.75rem; margin-bottom:.75rem">
|
||||
<h3 style="margin-top:0">System summary</h3>
|
||||
<div id="sysSummary" class="muted">Loading…</div>
|
||||
</div>
|
||||
<div class="card" style="background:#0f1115; border:1px solid var(--border); border-radius:10px; padding:.75rem;">
|
||||
<h3 style="margin-top:0">Error triggers</h3>
|
||||
<div class="row" style="display:flex; gap:.5rem; align-items:center">
|
||||
<button class="btn" hx-get="/diagnostics/trigger-error" hx-trigger="click" hx-target="this" hx-swap="none">Trigger HTTP error (418)</button>
|
||||
<button class="btn" hx-get="/diagnostics/trigger-error?kind=unhandled" hx-trigger="click" hx-target="this" hx-swap="none">Trigger unhandled error (500)</button>
|
||||
<small class="muted">You should see a toast and an inline banner with Request-ID.</small>
|
||||
</div>
|
||||
</div>
|
||||
{% if show_logs %}
|
||||
<p style="margin-top:.75rem"><a class="btn" href="/logs">Open Logs</a></p>
|
||||
{% endif %}
|
||||
</section>
|
||||
<script>
|
||||
(function(){
|
||||
var el = document.getElementById('sysSummary');
|
||||
function render(data){
|
||||
if (!el) return;
|
||||
try {
|
||||
var v = (data && data.version) || 'dev';
|
||||
var up = (data && data.uptime_seconds) || 0;
|
||||
var st = (data && data.server_time_utc) || '';
|
||||
var flags = (data && data.flags) || {};
|
||||
el.innerHTML = '<div><strong>Version:</strong> '+String(v)+'</div>'+
|
||||
(st ? '<div><strong>Server time (UTC):</strong> '+String(st)+'</div>' : '')+
|
||||
'<div><strong>Uptime:</strong> '+String(up)+'s</div>'+
|
||||
'<div><strong>Flags:</strong> SHOW_LOGS='+ (flags.SHOW_LOGS? '1':'0') +', SHOW_DIAGNOSTICS='+ (flags.SHOW_DIAGNOSTICS? '1':'0') +', SHOW_SETUP='+ (flags.SHOW_SETUP? '1':'0') +'</div>';
|
||||
} catch(_){ el.textContent = 'Unavailable'; }
|
||||
}
|
||||
function load(){
|
||||
try { fetch('/status/sys', { cache: 'no-store' }).then(function(r){ return r.json(); }).then(render).catch(function(){ el.textContent='Unavailable'; }); } catch(_){ el.textContent='Unavailable'; }
|
||||
}
|
||||
load();
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
85
code/web/templates/diagnostics/logs.html
Normal file
85
code/web/templates/diagnostics/logs.html
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<section>
|
||||
<h2>Logs</h2>
|
||||
<form method="get" action="/logs" class="form-row" style="gap:.5rem; align-items: center;">
|
||||
<label>Tail <input type="number" name="tail" value="{{ tail }}" min="1" max="500" style="width:80px"></label>
|
||||
<label>Filter <input type="text" name="q" value="{{ q }}" placeholder="keyword"></label>
|
||||
<label>Level
|
||||
<select name="level" id="levelSel">
|
||||
{% set _lvl = (level or 'all') %}
|
||||
<option value="all" {% if _lvl=='all' %}selected{% endif %}>All</option>
|
||||
<option value="error" {% if _lvl=='error' %}selected{% endif %}>Error</option>
|
||||
<option value="warning" {% if _lvl=='warning' %}selected{% endif %}>Warning</option>
|
||||
<option value="info" {% if _lvl=='info' %}selected{% endif %}>Info</option>
|
||||
<option value="debug" {% if _lvl=='debug' %}selected{% endif %}>Debug</option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="btn" type="submit">Refresh</button>
|
||||
<button class="btn" type="button" id="copyLogsBtn" title="Copy visible logs">Copy</button>
|
||||
<label style="margin-left:1rem; display:inline-flex; align-items:center; gap:.35rem">
|
||||
<input type="checkbox" id="autoRefreshLogs" data-pref="logs:auto" /> Auto-refresh
|
||||
</label>
|
||||
<label style="display:inline-flex; align-items:center; gap:.35rem">
|
||||
every <input type="number" id="autoRefreshInterval" value="3" min="1" max="30" style="width:60px"> s
|
||||
</label>
|
||||
</form>
|
||||
<pre id="logTail" class="log-tail" data-tail="{{ tail }}" data-q="{{ q }}" data-level="{{ level or 'all' }}" style="white-space: pre-wrap; background:#0f1115; color:#e5e7eb; border:1px solid var(--border); border-radius:8px; padding:.75rem; margin-top:.75rem; max-height:60vh; overflow:auto">{{ lines | join('') }}</pre>
|
||||
<script>
|
||||
(function(){
|
||||
var pre = document.getElementById('logTail');
|
||||
var autoCb = document.getElementById('autoRefreshLogs');
|
||||
var intervalInput = document.getElementById('autoRefreshInterval');
|
||||
// hydrate from saved pref
|
||||
try { var saved = window.__mtgState && window.__mtgState.get('logs:auto', false); if (typeof saved === 'boolean') autoCb.checked = saved; } catch(_){ }
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var tailAttr = (pre && pre.getAttribute('data-tail')) || '200';
|
||||
var qAttr = (pre && pre.getAttribute('data-q')) || '';
|
||||
var levelAttr = (pre && pre.getAttribute('data-level')) || 'all';
|
||||
var tail = parseInt(params.get('tail') || tailAttr, 10) || parseInt(tailAttr, 10) || 200;
|
||||
var q = params.get('q') || qAttr;
|
||||
var level = params.get('level') || levelAttr;
|
||||
var timer = null;
|
||||
function fetchLogs(){
|
||||
try {
|
||||
var url = '/status/logs?tail='+encodeURIComponent(String(tail));
|
||||
if (q) url += '&q='+encodeURIComponent(q);
|
||||
if (level && level !== 'all') url += '&level='+encodeURIComponent(level);
|
||||
fetch(url, { cache: 'no-store' })
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(data){ if (pre && data && data.lines){ pre.textContent = (data.lines||[]).join(''); pre.scrollTop = pre.scrollHeight; } });
|
||||
} catch(e){}
|
||||
}
|
||||
function start(){
|
||||
stop();
|
||||
var sec = parseInt(intervalInput.value||'3', 10); if (isNaN(sec) || sec < 1) sec = 3; if (sec > 30) sec = 30;
|
||||
timer = setInterval(fetchLogs, sec * 1000);
|
||||
fetchLogs();
|
||||
}
|
||||
function stop(){ if (timer){ clearInterval(timer); timer = null; } }
|
||||
autoCb.addEventListener('change', function(){
|
||||
try { window.__mtgState && window.__mtgState.set('logs:auto', !!autoCb.checked); } catch(_){ }
|
||||
if (autoCb.checked) start(); else stop();
|
||||
});
|
||||
intervalInput.addEventListener('change', function(){ if (autoCb.checked) start(); });
|
||||
if (autoCb.checked) start();
|
||||
var levelSel = document.getElementById('levelSel');
|
||||
if (levelSel){ levelSel.addEventListener('change', function(){ if (autoCb.checked) fetchLogs(); }); }
|
||||
// Copy button
|
||||
var copyBtn = document.getElementById('copyLogsBtn');
|
||||
function copyText(text){
|
||||
try { navigator.clipboard.writeText(text); return true; } catch(_) {
|
||||
try {
|
||||
var ta = document.createElement('textarea'); ta.value = text; ta.style.position='fixed'; ta.style.opacity='0'; document.body.appendChild(ta); ta.select(); var ok = document.execCommand('copy'); ta.remove(); return ok; } catch(__){ return false; }
|
||||
}
|
||||
}
|
||||
if (copyBtn){
|
||||
copyBtn.addEventListener('click', function(){
|
||||
var ok = copyText(pre ? pre.textContent || '' : '');
|
||||
if (ok){ if (window.toast) window.toast('Copied logs'); copyBtn.textContent = 'Copied'; setTimeout(function(){ copyBtn.textContent='Copy'; }, 1200); }
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</section>
|
||||
{% endblock %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue