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

605 lines
24 KiB
HTML
Raw Normal View History

{% 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: #050607 !important;
border-color: #1e293b !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: #0a0c10 !important;
}
/* Log output areas */
#content pre {
background: #030405 !important;
border-color: #1e293b !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:#0f1115; 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:#151821; 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:#0b0d12; 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:#0f1115; 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:#0f1115; 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:#151821; 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:#0f1115; 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:#0f1115; 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 %}