mtg_python_deckbuilder/code/web/templates/setup/index.html

604 lines
25 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% from 'partials/_buttons.html' import button %}
{% block content %}
<style>
/* Setup page-specific styling */
#content details > summary {
color: #3b82f6;
cursor: pointer;
font-weight: 500;
}
#content details > summary:hover {
color: #60a5fa;
}
#content details > div {
background: var(--panel) !important;
border-color: var(--border) !important;
}
#content .muted {
color: #94a3b8;
}
/* Make all buttons on this page blue */
#content button,
#content input[type="submit"] {
background: #3b82f6 !important;
border-color: #3b82f6 !important;
color: white !important;
}
#content button:hover,
#content input[type="submit"]:hover {
background: #2563eb !important;
border-color: #2563eb !important;
}
#content button:active,
#content input[type="submit"]:active {
background: #1d4ed8 !important;
}
/* Progress bars */
#content [id$="-progress-bar"] {
background: var(--bg) !important;
}
/* Log output areas */
#content pre {
background: var(--panel) !important;
border-color: var(--border) !important;
}
</style>
<section>
<h2>Setup / Tagging</h2>
<p class="muted" style="max-width:70ch;">Prepare or refresh the card database and apply tags. You can run this anytime.</p>
<details open style="margin-top:.5rem;">
<summary>Current Status</summary>
<div id="setup-status" style="margin-top:.5rem; padding:1rem; border:1px solid var(--border); background:var(--panel); border-radius:8px;">
<div class="muted">Status:</div>
<div id="setup-status-line" style="margin-top:.25rem;">Checking…</div>
<div id="setup-progress-line" class="muted" style="margin-top:.25rem; display:none;"></div>
<div id="setup-progress-bar" style="margin-top:.25rem; width:100%; height:10px; background:var(--bg); border:1px solid var(--border); border-radius:6px; overflow:hidden; display:none;">
<div id="setup-progress-bar-inner" style="height:100%; width:0%; background:#3b82f6;"></div>
</div>
<div id="setup-time-line" class="muted" style="margin-top:.25rem; display:none;"></div>
<div id="setup-color-line" class="muted" style="margin-top:.25rem; display:none;"></div>
<details id="setup-log-wrap" style="margin-top:.5rem; display:none;">
<summary id="setup-log-summary" class="muted" style="cursor:pointer;">Show logs</summary>
<pre id="setup-log-tail" style="margin-top:.5rem; max-height:240px; overflow:auto; background:var(--panel); border:1px solid var(--border); padding:.5rem; border-radius:6px;"></pre>
</details>
</div>
</details>
<details style="margin-top:1rem;">
<summary>Download Pre-tagged Database from GitHub (Optional)</summary>
<div style="margin-top:.5rem; padding:1rem; border:1px solid var(--border); background:var(--panel); border-radius:8px;">
<p class="muted" style="margin:0 0 .75rem 0; font-size:.9rem;">
Download pre-tagged card database and similarity cache from GitHub (updated weekly).
<strong>Note:</strong> A fresh local tagging run will be most up-to-date with the latest card data.
</p>
{{ button('Download from GitHub', variant='priamry', onclick='downloadFromGitHub()', attrs='id="btn-download-github"') }}
<div id="download-status" class="muted" style="margin-top:.5rem; display:none;"></div>
</div>
</details>
{% if image_cache_enabled %}
<details style="margin-top:1rem;">
<summary>Download Card Images (Optional)</summary>
<div style="margin-top:.5rem; padding:1rem; border:1px solid var(--border); background:var(--panel); border-radius:8px;">
<p class="muted" style="margin:0 0 .75rem 0; font-size:.9rem;">
Download card images from Scryfall CDN for faster loading and offline use.
<strong>Note:</strong> Requires ~3-6 GB disk space and 1-2 hours download time (~30k cards).
</p>
<div id="image-cache-status" style="margin-bottom:.75rem;">
<div class="muted">Status:</div>
<div id="image-status-line" style="margin-top:.25rem;">Checking…</div>
<div class="muted" id="image-stats-line" style="margin-top:.25rem; display:none;"></div>
</div>
{{ button('Download Card Images', variant='priamry', onclick='downloadCardImages()', attrs='id="btn-download-images"') }}
<div id="image-download-status" class="muted" style="margin-top:.5rem; display:none;"></div>
<div id="image-progress-bar" style="margin-top:.5rem; width:100%; height:10px; background:var(--bg); border:1px solid var(--border); border-radius:6px; overflow:hidden; display:none;">
<div id="image-progress-bar-inner" style="height:100%; width:0%; background:#3b82f6;"></div>
</div>
</div>
</details>
{% endif %}
<div style="margin-top:1rem; display:flex; gap:.5rem; flex-wrap:wrap;">
<form id="frm-start-setup" action="/setup/start" method="post" onsubmit="event.preventDefault(); startSetup();">
{{ button('Run Setup/Tagging', variant='primary', type='submit', attrs='id="btn-start-setup"') }}
<label class="muted" style="margin-left:.75rem; font-size:.9rem;">
<input type="checkbox" id="chk-force" checked /> Force run
</label>
</form>
<form method="get" action="/setup/running?start=1&force=1">
{{ button('Open Progress Page', variant='primary', type='submit') }}
</form>
</div>
<details style="margin-top:1.25rem;" open>
<summary>Theme Catalog Status</summary>
<div id="themes-status" style="margin-top:.5rem; padding:1rem; border:1px solid var(--border); background:var(--panel); border-radius:8px;">
<div class="muted">Status:</div>
<div id="themes-status-line" style="margin-top:.25rem;">Checking…</div>
<div class="muted" id="themes-meta-line" style="margin-top:.25rem; display:none;"></div>
<div class="muted" id="themes-stale-line" style="margin-top:.25rem; display:none; color:#f87171;"></div>
</div>
</details>
<div style="margin-top:.75rem; display:flex; gap:.5rem; flex-wrap:wrap;">
{{ button('Refresh Themes Only', variant='priamry', onclick='refreshThemes()', attrs='id="btn-refresh-themes"') }}
</div>
{% if similarity_enabled %}
<details style="margin-top:1.25rem;" open>
<summary>Similarity Cache Status</summary>
<div id="similarity-status" style="margin-top:.5rem; padding:1rem; border:1px solid var(--border); background:var(--panel); border-radius:8px;">
<div class="muted">Status:</div>
<div id="similarity-status-line" style="margin-top:.25rem;">Checking…</div>
<div class="muted" id="similarity-meta-line" style="margin-top:.25rem; display:none;"></div>
<div class="muted" id="similarity-warning-line" style="margin-top:.25rem; display:none; color:#f59e0b;"></div>
</div>
</details>
<div style="margin-top:.75rem; display:flex; gap:.5rem; flex-wrap:wrap;">
{{ button('Build Similarity Cache', variant='priamry', onclick='buildSimilarityCache()', attrs='id="btn-build-similarity"') }}
<label class="muted" style="align-self:center; font-size:.85rem;">
<input type="checkbox" id="chk-skip-download" /> Skip GitHub download (build locally)
</label>
<span class="muted" style="align-self:center; font-size:.85rem;">(~15-20 min local, instant if cached on GitHub)</span>
</div>
{% endif %}
</section>
<script>
(function(){
// Minimal styling helper to unify button widths
try {
var style = document.createElement('style');
style.textContent = '.btn{min-width:180px;}';
document.head.appendChild(style);
} catch(e){}
function update(data){
var line = document.getElementById('setup-status-line');
var colorEl = document.getElementById('setup-color-line');
var logEl = document.getElementById('setup-log-tail');
var progEl = document.getElementById('setup-progress-line');
var timeEl = document.getElementById('setup-time-line');
var bar = document.getElementById('setup-progress-bar');
var barIn = document.getElementById('setup-progress-bar-inner');
var logWrap = document.getElementById('setup-log-wrap');
var logSummary = document.getElementById('setup-log-summary');
if (!line) return;
if (data && data.running) {
line.textContent = (data.message || 'Working…');
if (typeof data.percent === 'number') {
progEl.style.display = '';
var p = Math.max(0, Math.min(100, data.percent));
progEl.textContent = 'Progress: ' + p + '%';
if (bar && barIn) { bar.style.display = ''; barIn.style.width = p + '%'; }
if (typeof data.color_idx === 'number' && typeof data.color_total === 'number') {
progEl.textContent += ' • Colors: ' + data.color_idx + ' / ' + data.color_total;
}
if (typeof data.eta_seconds === 'number') {
var mins = Math.floor(data.eta_seconds / 60); var secs = data.eta_seconds % 60;
progEl.textContent += ' • ETA: ~' + mins + 'm ' + secs + 's';
}
} else {
progEl.style.display = 'none';
if (bar) bar.style.display = 'none';
}
if (data.started_at) {
timeEl.style.display = '';
timeEl.textContent = 'Started: ' + data.started_at;
} else {
timeEl.style.display = 'none';
}
if (data.color) {
colorEl.style.display = '';
colorEl.textContent = 'Current color: ' + data.color;
} else {
colorEl.style.display = 'none';
}
if (data.log_tail) {
var lines = data.log_tail.split(/\r?\n/).filter(function(x){ return x.trim() !== ''; });
if (logWrap) logWrap.style.display = '';
if (logSummary) logSummary.textContent = 'Show logs (' + lines.length + ' lines)';
logEl.textContent = data.log_tail;
} else {
if (logWrap) logWrap.style.display = 'none';
}
} else if (data && data.phase === 'done') {
line.textContent = 'Setup complete.';
if (typeof data.percent === 'number') {
progEl.style.display = '';
var p2 = Math.max(0, Math.min(100, data.percent));
progEl.textContent = 'Progress: ' + p2 + '%';
if (bar && barIn) { bar.style.display = ''; barIn.style.width = p2 + '%'; }
} else {
progEl.style.display = 'none';
if (bar) bar.style.display = 'none';
}
if (data.started_at || data.finished_at) {
timeEl.style.display = '';
var t = [];
if (data.started_at) t.push('Started: ' + data.started_at);
if (data.finished_at) t.push('Finished: ' + data.finished_at);
timeEl.textContent = t.join(' • ');
} else {
timeEl.style.display = 'none';
}
colorEl.style.display = 'none';
if (logWrap) logWrap.style.display = 'none';
} else if (data && data.phase === 'error') {
line.textContent = (data.message || 'Setup error.');
if (data.color) {
colorEl.style.display = '';
colorEl.textContent = 'While working on: ' + data.color;
}
} else {
line.textContent = 'Idle';
progEl.style.display = 'none';
timeEl.style.display = 'none';
colorEl.style.display = 'none';
if (logWrap) logWrap.style.display = 'none';
}
}
function poll(){
fetch('/status/setup', { cache: 'no-store' })
.then(function(r){ return r.json(); })
.then(update)
.catch(function(){});
pollThemes();
}
function pollThemes(){
fetch('/themes/status', { cache: 'no-store' })
.then(function(r){ return r.json(); })
.then(updateThemes)
.catch(function(){});
}
function updateThemes(data){
var line = document.getElementById('themes-status-line');
var meta = document.getElementById('themes-meta-line');
var staleEl = document.getElementById('themes-stale-line');
var btn = document.getElementById('btn-refresh-themes');
if(!line) return;
if(!data || !data.ok){ line.textContent = 'Unavailable'; return; }
var parts = [];
if (typeof data.theme_count === 'number') parts.push('Themes: '+data.theme_count);
if (typeof data.yaml_file_count === 'number') parts.push('YAML: '+data.yaml_file_count);
if (data.last_export_at) parts.push('Last Export: '+data.last_export_at);
line.textContent = (data.theme_list_exists ? 'Ready' : 'Not generated');
if(parts.length){ meta.style.display=''; meta.textContent = parts.join(' • '); } else { meta.style.display='none'; }
if(data.stale){ staleEl.style.display=''; staleEl.textContent='Stale: needs refresh'; }
else { staleEl.style.display='none'; }
// Disable refresh while a theme export phase is active (in orchestrator phases 'themes' / 'themes-fast')
try {
if(btn){
if(data.phase === 'themes' || data.phase === 'themes-fast'){
btn.disabled = true; btn.textContent='Refreshing…';
} else {
if(!data.running){ btn.disabled = false; btn.textContent='Refresh Themes Only'; }
}
}
} catch(e){}
}
window.refreshThemes = function(){
var btn = document.getElementById('btn-refresh-themes');
if(btn) btn.disabled = true;
fetch('/themes/refresh', { method:'POST' })
.then(function(){ setTimeout(function(){ pollThemes(); if(btn) btn.disabled=false; }, 1200); })
.catch(function(){ if(btn) btn.disabled=false; });
};
// Card image download functions
function pollImageStatus(){
fetch('/api/images/status', { cache: 'no-store' })
.then(function(r){ return r.json(); })
.then(function(data){
var statusLine = document.getElementById('image-status-line');
var statsLine = document.getElementById('image-stats-line');
var downloadStatus = document.getElementById('image-download-status');
var progressBar = document.getElementById('image-progress-bar');
var progressBarInner = document.getElementById('image-progress-bar-inner');
if (!statusLine) return;
if (data.running) {
// Download in progress
var phase = data.phase || 'unknown';
var message = data.message || 'Downloading...';
var percentage = data.percentage || 0;
statusLine.textContent = 'Download in progress';
statusLine.style.color = '#3b82f6';
if (downloadStatus) {
downloadStatus.style.display = '';
downloadStatus.textContent = message + ' (' + percentage + '%)';
}
if (progressBar && progressBarInner) {
progressBar.style.display = '';
progressBarInner.style.width = percentage + '%';
}
} else if (data.stats) {
// Show cache statistics
var stats = data.stats;
if (stats.enabled === false) {
statusLine.textContent = 'Image caching disabled';
statusLine.style.color = '#94a3b8';
if (statsLine) statsLine.style.display = 'none';
} else {
var totalCount = 0;
var totalSizeMB = 0;
if (stats.small) {
totalCount += stats.small.count || 0;
totalSizeMB += stats.small.size_mb || 0;
}
if (stats.normal) {
totalCount += stats.normal.count || 0;
totalSizeMB += stats.normal.size_mb || 0;
}
if (totalCount > 0) {
statusLine.textContent = 'Cache exists';
statusLine.style.color = '#34d399';
if (statsLine) {
statsLine.style.display = '';
statsLine.textContent = totalCount.toLocaleString() + ' images cached • ' + totalSizeMB.toFixed(1) + ' MB';
}
} else {
statusLine.textContent = 'No images cached';
statusLine.style.color = '#94a3b8';
if (statsLine) statsLine.style.display = 'none';
}
}
// Hide download progress
if (downloadStatus) downloadStatus.style.display = 'none';
if (progressBar) progressBar.style.display = 'none';
}
})
.catch(function(){
var statusLine = document.getElementById('image-status-line');
if (statusLine) {
statusLine.textContent = 'Status unavailable';
statusLine.style.color = '#94a3b8';
}
});
}
window.downloadCardImages = function(){
var btn = document.getElementById('btn-download-images');
var statusEl = document.getElementById('image-download-status');
if (!confirm('Download card images from Scryfall? This will download ~30k images (~3-6 GB) and may take 1-2 hours.')) {
return;
}
if (btn) btn.disabled = true;
if (statusEl) {
statusEl.style.display = '';
statusEl.textContent = 'Starting download...';
}
fetch('/api/images/download', { method: 'POST' })
.then(function(r){
if (!r.ok) {
return r.json().then(function(data){
throw new Error(data.message || 'Download failed');
});
}
return r.json();
})
.then(function(data){
if (statusEl) {
statusEl.style.color = '#34d399';
statusEl.textContent = '✓ ' + (data.message || 'Download started');
}
// Poll status every 2 seconds while downloading
var pollCount = 0;
var imagePoll = setInterval(function(){
pollImageStatus();
pollCount++;
// Stop intensive polling after 2 hours (3600 polls)
if (pollCount > 3600) clearInterval(imagePoll);
}, 2000);
setTimeout(function(){
if (btn) btn.disabled = false;
}, 3000);
})
.catch(function(err){
if (statusEl) {
statusEl.style.color = '#f87171';
statusEl.textContent = '✗ ' + (err.message || 'Download failed');
}
if (btn) btn.disabled = false;
});
};
function rapidPoll(times, delay){
var i = 0;
function tick(){
poll();
i++;
if (i < times) setTimeout(tick, delay);
}
tick();
}
window.downloadFromGitHub = function(){
var btn = document.getElementById('btn-download-github');
var statusEl = document.getElementById('download-status');
if (btn) btn.disabled = true;
if (statusEl) {
statusEl.style.display = '';
statusEl.textContent = 'Downloading from GitHub...';
}
fetch('/setup/download-github', { method: 'POST' })
.then(function(r){
if (!r.ok) throw new Error('Download failed');
return r.json();
})
.then(function(data){
if (statusEl) {
statusEl.style.color = '#34d399';
statusEl.textContent = '✓ ' + (data.message || 'Download complete');
}
// Refresh status displays
poll();
setTimeout(function(){ if (btn) btn.disabled = false; }, 2000);
})
.catch(function(err){
if (statusEl) {
statusEl.style.color = '#f87171';
statusEl.textContent = '✗ Download failed: ' + (err.message || 'Unknown error');
}
if (btn) btn.disabled = false;
});
};
window.startSetup = function(){
var btn = document.getElementById('btn-start-setup');
var line = document.getElementById('setup-status-line');
var force = document.getElementById('chk-force') && document.getElementById('chk-force').checked;
if (btn) btn.disabled = true;
if (line) line.textContent = 'Starting setup/tagging…';
// First try POST with JSON body
fetch('/setup/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ force: !!force }) })
.then(function(r){ if (!r.ok) throw new Error('POST failed'); return r.json().catch(function(){ return {}; }); })
.then(function(){ rapidPoll(5, 600); setTimeout(function(){ window.location.href = '/setup/running?start=1' + (force ? '&force=1' : ''); }, 500); })
.catch(function(){
// Fallback to GET if POST fails (proxy/middleware issues)
var url = '/setup/start' + (force ? '?force=1' : '');
fetch(url, { method: 'GET', cache: 'no-store' })
.then(function(){ rapidPoll(5, 600); setTimeout(function(){ window.location.href = '/setup/running?start=1' + (force ? '&force=1' : ''); }, 500); })
.catch(function(){});
})
.finally(function(){ if (btn) btn.disabled = false; });
};
// Similarity cache status polling
{% if similarity_enabled %}
function pollSimilarityStatus(){
fetch('/status/similarity', { cache: 'no-store' })
.then(function(r){ return r.json(); })
.then(function(data){
var line = document.getElementById('similarity-status-line');
var metaLine = document.getElementById('similarity-meta-line');
var warnLine = document.getElementById('similarity-warning-line');
if (!line) return;
if (data.exists && data.valid) {
var cardCount = data.card_count ? data.card_count.toLocaleString() : '?';
var sizeMB = data.size_mb ? data.size_mb.toFixed(1) : '?';
var ageDays = data.age_days !== null ? data.age_days.toFixed(1) : '?';
line.textContent = 'Cache exists and is valid';
line.style.color = '#34d399';
if (metaLine) {
metaLine.style.display = '';
metaLine.textContent = cardCount + ' cards cached • ' + sizeMB + ' MB • ' + ageDays + ' days old';
}
if (warnLine && data.needs_refresh) {
warnLine.style.display = '';
warnLine.textContent = '⚠ Cache is ' + ageDays + ' days old. Consider rebuilding for fresher data.';
} else if (warnLine) {
warnLine.style.display = 'none';
}
} else if (data.exists && !data.valid) {
line.textContent = 'Cache file is invalid or corrupted';
line.style.color = '#f87171';
if (metaLine) metaLine.style.display = 'none';
if (warnLine) {
warnLine.style.display = '';
warnLine.textContent = '⚠ Rebuild cache to fix.';
}
} else {
line.textContent = 'No cache found';
line.style.color = '#94a3b8';
if (metaLine) metaLine.style.display = 'none';
if (warnLine) {
warnLine.style.display = '';
warnLine.textContent = ' Build cache to enable similar card features.';
}
}
})
.catch(function(){});
}
window.buildSimilarityCache = function(){
var btn = document.getElementById('btn-build-similarity');
var skipDownloadCheckbox = document.getElementById('chk-skip-download');
if (!btn) return;
var skipDownload = skipDownloadCheckbox && skipDownloadCheckbox.checked;
var confirmMsg = skipDownload
? 'Build similarity cache locally for ~30k cards? This will take approximately 15-20 minutes and uses parallel processing.'
: 'Build similarity cache? This will first try to download a pre-built cache from GitHub (instant), or build locally if unavailable (~15-20 minutes).';
if (!confirm(confirmMsg)) {
return;
}
btn.disabled = true;
btn.textContent = 'Building... (check terminal for progress)';
var body = skipDownload ? JSON.stringify({ skip_download: true }) : '{}';
fetch('/similarity/build', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: body
})
.then(function(r){
if (!r.ok) throw new Error('Build failed');
return r.json();
})
.then(function(data){
if (data.success) {
btn.textContent = 'Build Started! Check terminal for progress...';
// Poll status more frequently while building
var pollCount = 0;
var buildPoll = setInterval(function(){
pollSimilarityStatus();
pollCount++;
// Stop intensive polling after 2 minutes, rely on normal polling
if (pollCount > 40) clearInterval(buildPoll);
}, 3000);
setTimeout(function(){
btn.textContent = 'Build Similarity Cache';
btn.disabled = false;
}, 8000);
} else {
btn.textContent = 'Build Failed: ' + (data.error || 'Unknown error');
setTimeout(function(){
btn.textContent = 'Build Similarity Cache';
btn.disabled = false;
}, 3000);
}
})
.catch(function(err){
btn.textContent = 'Build Failed';
setTimeout(function(){
btn.textContent = 'Build Similarity Cache';
btn.disabled = false;
}, 3000);
});
};
pollSimilarityStatus();
setInterval(pollSimilarityStatus, 10000); // Poll every 10s
{% endif %}
// Initialize image status polling
pollImageStatus();
setInterval(pollImageStatus, 10000); // Poll every 10s
setInterval(poll, 3000);
poll();
pollThemes();
})();
</script>
{% endblock %}