refactor: backend standardization (service layer, validation, route splitting) + image cache and Scryfall API fixes

This commit is contained in:
matt 2026-03-17 16:34:50 -07:00
parent e81b47bccf
commit f784741416
35 changed files with 7054 additions and 4344 deletions

View file

@ -10,11 +10,6 @@
</aside>
<div class="grow" data-skeleton>
<div hx-get="/build/banner" hx-trigger="load"></div>
{% if locks_restored and locks_restored > 0 %}
<div class="muted" style="margin:.35rem 0;">
<span class="chip" title="Locks restored from permalink">🔒 {{ locks_restored }} locks restored</span>
</div>
{% endif %}
<h4>Chosen Ideals</h4>
<ul>
{% for key, label in labels.items() %}

View file

@ -186,9 +186,7 @@
<span class="chip" title="Multi-Copy package summary"><span class="dot dot-purple"></span> {{ mc_summary }}</span>
{% endif %}
<span id="locks-chip">{% if locks and locks|length > 0 %}<span class="chip" title="Locked cards">🔒 {{ locks|length }} locked</span>{% endif %}</span>
<button type="button" class="btn ml-auto" title="Copy permalink"
onclick="(async()=>{try{const r=await fetch('/build/permalink');const j=await r.json();const url=(j.permalink?location.origin+j.permalink:location.href+'#'+btoa(JSON.stringify(j.state||{}))); await navigator.clipboard.writeText(url); toast && toast('Permalink copied');}catch(e){alert('Copied state to console'); console.log(e);}})()">Copy Permalink</button>
<button type="button" class="btn" title="Open a saved permalink" onclick="(function(){try{var token = prompt('Paste a /build/from?state=... URL or token:'); if(!token) return; var m = token.match(/state=([^&]+)/); var t = m? m[1] : token.trim(); if(!t) return; window.location.href = '/build/from?state=' + encodeURIComponent(t); }catch(_){}})()">Open Permalink…</button>
</div>
{% set pct = ((deck_count / 100.0) * 100.0) if deck_count else 0 %}
{% set pct_clamped = (pct if pct <= 100 else 100) %}

View file

@ -27,7 +27,6 @@
<a href="/decks/compare" class="btn" role="button" title="Compare two finished decks">Compare</a>
<button id="deck-compare-selected" type="button" title="Compare two selected decks" disabled>Compare selected</button>
<button id="deck-compare-latest" type="button" title="Pick the latest two decks">Latest two</button>
<button id="deck-open-permalink" type="button" title="Open a saved permalink">Open Permalink…</button>
<button id="deck-reset-all" type="button" title="Reset filter, sort, and theme">Reset all</button>
<button id="deck-help" type="button" title="Keyboard shortcuts and tips" aria-haspopup="dialog" aria-controls="deck-help-modal">Help</button>
<span id="deck-count" class="muted" aria-live="polite"></span>
@ -127,7 +126,6 @@
var txtOnlyCb = document.getElementById('deck-txt-only');
var cmpSelBtn = document.getElementById('deck-compare-selected');
var cmpLatestBtn = document.getElementById('deck-compare-latest');
var openPermalinkBtn = document.getElementById('deck-open-permalink');
if (!list) return;
// Panels and themes discovery from data-tags-pipe
@ -416,18 +414,6 @@
} catch(_){ }
});
// Open permalink prompt
if (openPermalinkBtn) openPermalinkBtn.addEventListener('click', function(){
try{
var token = prompt('Paste a /build/from?state=... URL or token:');
if(!token) return;
var m = token.match(/state=([^&]+)/);
var t = m ? m[1] : token.trim();
if(!t) return;
window.location.href = '/build/from?state=' + encodeURIComponent(t);
}catch(_){ }
});
if (resetAllBtn) resetAllBtn.addEventListener('click', function(){
// Clear UI state
try {

View file

@ -18,9 +18,7 @@
<div class="random-meta" style="display:flex; gap:12px; align-items:center; flex-wrap:wrap;">
<span class="seed">Seed: <strong>{{ seed }}</strong></span>
{% if theme %}<span class="theme">Theme: <strong>{{ theme }}</strong></span>{% endif %}
{% if permalink %}
<button class="btn btn-compact" type="button" aria-label="Copy permalink for this exact build" onclick="(async()=>{try{await navigator.clipboard.writeText(location.origin + '{{ permalink }}');(window.toast&&toast('Permalink copied'))||console.log('Permalink copied');}catch(e){alert('Copy failed');}})()">Copy Permalink</button>
{% endif %}
{% if show_diagnostics and diagnostics %}
<span class="diag-badges" aria-label="Diagnostics" role="status" aria-live="polite" aria-atomic="true">
<span class="diag-badge" title="Attempts tried before acceptance" aria-label="Attempts tried before acceptance">

View file

@ -148,10 +148,10 @@
</section>
<script>
(function(){
// Minimal styling helper to unify button widths
// Minimal styling helper to unify button widths (only for content buttons)
try {
var style = document.createElement('style');
style.textContent = '.btn{min-width:180px;}';
style.textContent = '.content .btn{min-width:180px;}';
document.head.appendChild(style);
} catch(e){}
function update(data){
@ -325,27 +325,38 @@
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) {
// Card count = use the max across sizes (each card has one image per size, so avoid double-counting)
var cardCount = Math.max(
(stats.small && stats.small.count) || 0,
(stats.normal && stats.normal.count) || 0
);
var totalSizeMB = ((stats.small && stats.small.size_mb) || 0) + ((stats.normal && stats.normal.size_mb) || 0);
var sizeCount = (stats.small ? 1 : 0) + (stats.normal ? 1 : 0);
if (cardCount > 0) {
statusLine.textContent = 'Cache exists';
statusLine.style.color = '#34d399';
if (statsLine) {
statsLine.style.display = '';
statsLine.textContent = totalCount.toLocaleString() + ' images cached • ' + totalSizeMB.toFixed(1) + ' MB';
var statsText = cardCount.toLocaleString() + ' cards cached • ' + totalSizeMB.toFixed(1) + ' MB';
// If we have last download info, append new card count
if (data.last_download) {
var ld = data.last_download;
if (ld.stats && typeof ld.stats.downloaded === 'number') {
var newCards = sizeCount > 0 ? Math.round(ld.stats.downloaded / sizeCount) : ld.stats.downloaded;
if (newCards > 0) {
statsText += ' • Last run: +' + newCards.toLocaleString() + ' new cards';
} else {
statsText += ' • Last run: fully up to date';
}
} else if (ld.message) {
statsText += ' • ' + ld.message;
}
}
statsLine.textContent = statsText;
}
} else {
statusLine.textContent = 'No images cached';
statusLine.textContent = 'No cards cached';
statusLine.style.color = '#94a3b8';
if (statsLine) statsLine.style.display = 'none';
}
@ -354,6 +365,23 @@
// Hide download progress
if (downloadStatus) downloadStatus.style.display = 'none';
if (progressBar) progressBar.style.display = 'none';
} else if (data.phase === 'error' || data.message) {
// Previous download failed - show error and allow retry
statusLine.textContent = 'Last download failed';
statusLine.style.color = '#f87171';
if (statsLine) {
statsLine.style.display = '';
statsLine.textContent = data.message || 'Unknown error';
}
if (downloadStatus) downloadStatus.style.display = 'none';
if (progressBar) progressBar.style.display = 'none';
} else {
// No stats, no error - likely no download attempted yet
statusLine.textContent = 'No cards cached';
statusLine.style.color = '#94a3b8';
if (statsLine) statsLine.style.display = 'none';
if (downloadStatus) downloadStatus.style.display = 'none';
if (progressBar) progressBar.style.display = 'none';
}
})
.catch(function(){