Fix: eagerload series.sets i admin og API (MissingGreenlet i async mode)

This commit is contained in:
2026-07-01 22:15:14 +02:00
parent c0952a3bd3
commit 8361066f2f
20 changed files with 1075 additions and 554 deletions
+100 -76
View File
@@ -1,95 +1,119 @@
{% extends "base.html" %}
{% block title %}Admin — Serier{% endblock %}
{% block title %}Admin — Sæt{% endblock %}
{% block content %}
<h1>📦 Administrér serier</h1>
<p style="color:var(--text-muted);margin-bottom:16px;">Opret og slet Pokémon TCG serier/sets.</p>
<a href="/admin" style="color:var(--text-muted);text-decoration:none;font-size:0.9rem;">&larr; Admin</a>
<h1 style="margin-top:12px;">📁 Sæt</h1>
<div class="card" style="margin-bottom:24px;">
<h2>Opret ny serie</h2>
<form id="create-set-form" style="display:flex;gap:8px;flex-wrap:wrap;margin-top:12px;">
<input type="text" name="code" placeholder="Kode (f.eks. CRI)" required style="max-width:120px;text-transform:uppercase;">
<input type="text" name="name" placeholder="Navn (f.eks. Chaos Rising)" required style="max-width:280px;">
<input type="text" name="release_date" placeholder="Udgivelsesdato" style="max-width:160px;">
<button type="submit" class="btn btn-primary">Opret</button>
</form>
<p id="set-msg" style="font-size:0.85rem;margin-top:8px;"></p>
<div style="display:flex;gap:24px;flex-wrap:wrap;margin:20px 0;">
<div class="card" style="flex:1;min-width:280px;">
<h3>Opret sæt</h3>
<form onsubmit="createSet(event)" style="display:flex;flex-direction:column;gap:10px;margin-top:12px;">
<select id="set-serie" required>
<option value="">— Vælg serie —</option>
{% for s in series %}
<option value="{{ s.name }}">{{ s.name }}</option>
{% endfor %}
</select>
<input type="text" id="set-code" placeholder="Kode (f.eks. EVS)" required>
<input type="text" id="set-name" placeholder="Navn (f.eks. Evolving Skies)" required>
<input type="date" id="set-date" placeholder="Udgivelsesdato">
<input type="text" id="set-desc" placeholder="Beskrivelse (valgfri)">
<button class="btn btn-primary">Opret</button>
</form>
</div>
<div class="card" style="flex:1;min-width:280px;">
<h3>CSV-import</h3>
<p style="font-size:0.85rem;color:var(--text-muted);margin:8px 0;">
Kolonner: <code>serie,code,name,release_date,description</code>
<br><a href="/api/admin/templates/sets.csv" style="color:var(--accent);">Download skabelon</a>
</p>
<form onsubmit="importSets(event)" style="display:flex;flex-direction:column;gap:10px;">
<input type="file" id="sets-csv" accept=".csv" required>
<button class="btn btn-primary">Importer</button>
</form>
<div id="sets-import-result" style="margin-top:8px;"></div>
</div>
</div>
<h2>Eksisterende serier</h2>
<div id="set-list"></div>
<h2>Eksisterende sæt</h2>
<table>
<thead>
<tr><th>Kode</th><th>Navn</th><th>Serie</th><th>Kort</th><th>Handling</th></tr>
</thead>
<tbody id="sets-list">
{% for s in sets %}
<tr id="set-{{ s.id }}">
<td><span class="tag">{{ s.code }}</span></td>
<td><a href="/admin/cards/{{ s.code }}" style="color:var(--text);">{{ s.name }}</a></td>
<td>{{ s.serie.name }}</td>
<td>{{ s.cards|length }}</td>
<td>
<a href="/admin/cards/{{ s.code }}" class="btn btn-outline btn-sm">Kort</a>
<button class="btn btn-danger btn-sm" onclick="deleteSet({{ s.id }})">Slet</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if sets|length == 0 %}
<div class="card" style="text-align:center;color:var(--text-muted);margin-top:16px;">
<p>Ingen sæt endnu.</p>
</div>
{% endif %}
<script>
async function loadSets() {
try {
const res = await fetch('/api/admin/sets');
const sets = await res.json();
const container = document.getElementById('set-list');
if (sets.length === 0) {
container.innerHTML = '<div class="card"><p style="color:var(--text-muted);">Ingen serier endnu.</p></div>';
return;
}
let html = '<div style="overflow-x:auto;"><table><thead><tr><th>Kode</th><th>Navn</th><th>Udgivet</th><th style="width:100px;"></th></tr></thead><tbody>';
sets.forEach(s => {
html += '<tr>' +
'<td><span class="tag">' + s.code + '</span></td>' +
'<td><a href="/admin/cards/' + s.code + '" style="color:var(--text);">' + s.name + '</a></td>' +
'<td>' + (s.release_date || '—') + '</td>' +
'<td><button class="btn btn-danger btn-sm" onclick="deleteSet(' + s.id + ', \'' + s.code + '\')">Slet</button></td>' +
'</tr>';
});
html += '</tbody></table></div>';
container.innerHTML = html;
} catch(e) {
document.getElementById('set-list').innerHTML = '<p style="color:var(--accent);">Kunne ikke indlæse serier</p>';
async function createSet(e) {
e.preventDefault();
const serie = document.getElementById('set-serie').value;
const code = document.getElementById('set-code').value.trim().toUpperCase();
const name = document.getElementById('set-name').value.trim();
const date = document.getElementById('set-date').value || null;
const desc = document.getElementById('set-desc').value.trim() || null;
if (!serie || !code || !name) return;
const res = await fetch('/api/admin/series/' + encodeURIComponent(serie) + '/sets', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({code, name, release_date: date, description: desc})
});
const data = await res.json();
if (data.ok) {
location.reload();
} else {
alert(data.detail || 'Fejl');
}
}
async function deleteSet(id, code) {
if (!confirm('Slet serien ' + code + '? Alle kort i serien slettes også.')) return;
try {
const res = await fetch('/api/admin/sets/' + id, { method: 'DELETE' });
if (res.ok) {
loadSets();
} else {
alert('Kunne ikke slette');
}
} catch(e) {
async function deleteSet(id) {
if (!confirm('Slet dette sæt og alle dets kort?')) return;
const res = await fetch('/api/admin/sets/' + id, {method: 'DELETE'});
const data = await res.json();
if (data.ok) {
document.getElementById('set-' + id).remove();
} else {
alert('Fejl ved sletning');
}
}
document.getElementById('create-set-form').addEventListener('submit', async (e) => {
async function importSets(e) {
e.preventDefault();
const form = e.target;
const msg = document.getElementById('set-msg');
try {
const res = await fetch('/api/admin/sets', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code: form.code.value,
name: form.name.value,
release_date: form.release_date.value || null,
}),
});
if (res.ok) {
form.reset();
msg.style.color = 'var(--text)';
msg.textContent = '✅ Serie oprettet';
loadSets();
} else {
const err = await res.json();
msg.style.color = 'var(--accent)';
msg.textContent = '❌ ' + (err.detail || 'Fejl');
const file = document.getElementById('sets-csv').files[0];
if (!file) return;
const form = new FormData();
form.append('file', file);
const res = await fetch('/api/admin/sets/import', {method: 'POST', body: form});
const data = await res.json();
const el = document.getElementById('sets-import-result');
if (data.ok) {
el.innerHTML = '<p style="color:green;">✓ ' + data.created + ' sæt oprettet</p>';
if (data.errors.length) {
el.innerHTML += '<p style="color:var(--accent);">' + data.errors.join('<br>') + '</p>';
}
} catch(e) {
msg.style.color = 'var(--accent)';
msg.textContent = '❌ Kunne ikke forbinde';
setTimeout(() => location.reload(), 1500);
} else {
el.innerHTML = '<p style="color:var(--accent);">Fejl ved import</p>';
}
});
loadSets();
}
</script>
{% endblock %}