54465f751c
- Serie-detail side: sæt under serie + enkelt/bulk opret - Billedhentning: URL→lokal download med source_url til re-sync - Advarselsbadge (⚠) på kort med eksterne URLs (ingen streaming) - Sync/Verify endpoints + knapper i admin UI - Brugerstyring: opret/slet (self-delete beskyttet) - Dark mode flash fix: render-blokerende script i <head> - Persistent volume til billeder (pokedexter_images)
140 lines
5.5 KiB
HTML
140 lines
5.5 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Admin{% endblock %}
|
|
{% block content %}
|
|
<h1>🔧 Adminpanel</h1>
|
|
<p style="color:var(--text-muted);margin-bottom:24px;">Velkommen, {{ request.state.user.username }}!</p>
|
|
|
|
<div class="card-grid">
|
|
<a href="/admin/series" class="card-set">
|
|
<div class="card" style="text-align:center;">
|
|
<p style="font-size:2rem;margin-bottom:8px;">📦</p>
|
|
<h3>Serier</h3>
|
|
<p style="font-size:0.85rem;color:var(--text-muted);">Administrér serier</p>
|
|
</div>
|
|
</a>
|
|
<a href="/admin/sets" class="card-set">
|
|
<div class="card" style="text-align:center;">
|
|
<p style="font-size:2rem;margin-bottom:8px;">📁</p>
|
|
<h3>Sæt</h3>
|
|
<p style="font-size:0.85rem;color:var(--text-muted);">Administrér sæt</p>
|
|
</div>
|
|
</a>
|
|
<a href="/search" class="card-set">
|
|
<div class="card" style="text-align:center;">
|
|
<p style="font-size:2rem;margin-bottom:8px;">🔍</p>
|
|
<h3>Søg</h3>
|
|
<p style="font-size:0.85rem;color:var(--text-muted);">Søg efter kort</p>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
|
|
<h2 style="margin-top:36px;">🖼️ Billedvedligeholdelse</h2>
|
|
<div style="display:flex;gap:12px;flex-wrap:wrap;margin:16px 0;">
|
|
<button class="btn btn-outline" onclick="verifyImages(false)">
|
|
🔍 Verificér & genopret manglende billeder
|
|
</button>
|
|
<button class="btn btn-outline" onclick="verifyImages(true)">
|
|
📥 Force-genhent alle billeder
|
|
</button>
|
|
</div>
|
|
<div id="verify-result" style="margin-top:8px;font-size:0.9rem;"></div>
|
|
|
|
<h2 style="margin-top:36px;">👥 Brugere</h2>
|
|
<div style="display:flex;gap:24px;flex-wrap:wrap;margin:20px 0;">
|
|
<div class="card" style="flex:1;min-width:280px;">
|
|
<h3>Opret bruger</h3>
|
|
<form onsubmit="createUser(event)" style="display:flex;flex-direction:column;gap:10px;margin-top:12px;">
|
|
<input type="text" id="new-username" placeholder="Brugernavn" required>
|
|
<input type="password" id="new-password" placeholder="Adgangskode" required>
|
|
<select id="new-role">
|
|
<option value="user">Bruger</option>
|
|
<option value="admin">Administrator</option>
|
|
</select>
|
|
<button class="btn btn-primary">Opret</button>
|
|
</form>
|
|
<div id="user-creation-result" style="margin-top:8px;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr><th>Brugernavn</th><th>Rolle</th><th>Oprettet</th><th>Handling</th></tr>
|
|
</thead>
|
|
<tbody id="users-list">
|
|
{% for u in users %}
|
|
<tr id="user-{{ u.id }}">
|
|
<td><strong>{{ u.username }}</strong></td>
|
|
<td><span class="tag">{{ u.role }}</span></td>
|
|
<td>{{ u.created_at.strftime('%Y-%m-%d %H:%M') if u.created_at else '—' }}</td>
|
|
<td>
|
|
{% if u.username != request.state.user.username %}
|
|
<button class="btn btn-danger btn-sm" onclick="deleteUser({{ u.id }}, '{{ u.username }}')">Slet</button>
|
|
{% else %}
|
|
<span style="color:var(--text-muted);font-size:0.85rem;">dig</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
|
|
<script>
|
|
async function verifyImages(force) {
|
|
const btn = event.target;
|
|
const resultEl = document.getElementById('verify-result');
|
|
btn.disabled = true;
|
|
btn.textContent = force ? '📥 Genhenter...' : '🔍 Verificerer...';
|
|
resultEl.innerHTML = '<p style="color:var(--text-muted);">Arbejder...</p>';
|
|
|
|
const res = await fetch('/api/admin/verify-images' + (force ? '?force=true' : ''), {method: 'POST'});
|
|
const data = await res.json();
|
|
|
|
btn.disabled = false;
|
|
btn.textContent = force ? '📥 Force-genhent alle billeder' : '🔍 Verificér & genopret manglende billeder';
|
|
|
|
if (data.ok) {
|
|
let msg = `✅ ${data.restored} billeder genhentet, ${data.skipped} sprunget over.`;
|
|
if (data.errors.length) {
|
|
msg += `<br><span style="color:var(--accent);">⚠️ ${data.errors.length} fejl:</span><ul>`;
|
|
data.errors.forEach(e => { msg += `<li>${e}</li>`; });
|
|
msg += '</ul>';
|
|
}
|
|
resultEl.innerHTML = '<p>' + msg + '</p>';
|
|
} else {
|
|
resultEl.innerHTML = '<p style="color:var(--accent);">❌ ' + (data.detail || 'Ukendt fejl') + '</p>';
|
|
}
|
|
}
|
|
|
|
async function createUser(e) {
|
|
e.preventDefault();
|
|
const username = document.getElementById('new-username').value.trim();
|
|
const password = document.getElementById('new-password').value;
|
|
const role = document.getElementById('new-role').value;
|
|
if (!username || !password) return;
|
|
const res = await fetch('/api/admin/users', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({username, password, role})
|
|
});
|
|
const data = await res.json();
|
|
const el = document.getElementById('user-creation-result');
|
|
if (data.ok) {
|
|
el.innerHTML = '<p style="color:green;">✓ Bruger oprettet</p>';
|
|
setTimeout(() => location.reload(), 1000);
|
|
} else {
|
|
el.innerHTML = '<p style="color:var(--accent);">' + (data.detail || 'Fejl') + '</p>';
|
|
}
|
|
}
|
|
|
|
async function deleteUser(id, username) {
|
|
if (!confirm('Slet bruger "' + username + '"?')) return;
|
|
const res = await fetch('/api/admin/users/' + id, {method: 'DELETE'});
|
|
const data = await res.json();
|
|
if (data.ok) {
|
|
document.getElementById('user-' + id).remove();
|
|
} else {
|
|
alert(data.detail || 'Fejl ved sletning');
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %} |