Compare commits

..

2 Commits

View File

@ -740,6 +740,9 @@
<a href="/quellen">📚 Quellen</a>
<a href="/methodik">🔍 Methodik</a>
<hr style="margin:0.3rem 0;border:none;border-top:1px solid #eee;">
<button onclick="event.stopPropagation();document.getElementById('queue-panel').style.display='block';document.getElementById('hamburger-menu').classList.remove('open');loadQueuePanel();">📊 Queue</button>
<button onclick="event.stopPropagation();document.getElementById('batch-panel').style.display='block';document.getElementById('hamburger-menu').classList.remove('open');">📦 Batch-Analyse</button>
<hr style="margin:0.3rem 0;border:none;border-top:1px solid #eee;">
<button id="auth-btn" onclick="event.stopPropagation();">🔑 Anmelden</button>
</div>
</div>
@ -781,6 +784,14 @@
<select id="partei-filter" onchange="setParteiFilter(this.value)" style="padding: 0.25rem 0.5rem; border-radius: 20px; border: 1px solid var(--color-lightgray); font-size: 0.8rem; cursor: pointer;">
<option value="">Alle Parteien</option>
</select>
<select id="sort-select" onchange="setSortOrder(this.value)" style="padding: 0.25rem 0.5rem; border-radius: 20px; border: 1px solid var(--color-lightgray); font-size: 0.8rem; cursor: pointer;">
<option value="score-desc">↓ GWÖ-Score</option>
<option value="score-asc">↑ GWÖ-Score</option>
<option value="date-desc">↓ Datum</option>
<option value="date-asc">↑ Datum</option>
<option value="nr-desc">↓ Nummer</option>
<option value="title-asc">A-Z Titel</option>
</select>
</div>
</div>
<div class="stats-bar" style="padding: 0.5rem 1rem; gap: 1rem; flex-wrap: wrap; align-items: center;">
@ -930,6 +941,7 @@
let selectedTags = new Set();
let allTags = {};
let currentUser = null; // #43: Auth-State
let currentSort = localStorage.getItem('sortOrder') || 'score-desc';
// #43: Auth prüfen beim Load. Steuert ob "Jetzt prüfen" aktiv ist.
async function initAuth() {
@ -955,7 +967,7 @@
loadAssessments(); // Liste neu rendern (Buttons deaktivieren)
};
// Bestehende Liste neu rendern damit Buttons aktiv werden
if (allAssessments.length > 0) renderList(allAssessments);
if (allAssessments.length > 0) renderList(sortAssessments(allAssessments));
} else {
authBtn.textContent = '🔑 Anmelden';
authBtn.style.color = '';
@ -968,6 +980,29 @@
}
}
// Sortierung (#100)
function setSortOrder(order) {
currentSort = order;
localStorage.setItem('sortOrder', order);
applyFilters();
}
function sortAssessments(items) {
const sorted = [...items];
switch (currentSort) {
case 'score-desc': return sorted.sort((a, b) => (b.gwoeScore || 0) - (a.gwoeScore || 0));
case 'score-asc': return sorted.sort((a, b) => (a.gwoeScore || 0) - (b.gwoeScore || 0));
case 'date-desc': return sorted.sort((a, b) => (b.datum || '').localeCompare(a.datum || ''));
case 'date-asc': return sorted.sort((a, b) => (a.datum || '').localeCompare(b.datum || ''));
case 'nr-desc': return sorted.sort((a, b) => {
const na = parseInt((a.drucksache || '').split('/')[1]) || 0;
const nb = parseInt((b.drucksache || '').split('/')[1]) || 0;
return nb - na;
});
case 'title-asc': return sorted.sort((a, b) => (a.title || '').localeCompare(b.title || '', 'de'));
default: return sorted;
}
}
// Hamburger-Menü schließen bei Klick außerhalb
document.addEventListener('click', (e) => {
const menu = document.getElementById('hamburger-menu');
@ -992,6 +1027,12 @@
// Load assessments on page load — localStorage-Auswahl wiederherstellen
document.addEventListener('DOMContentLoaded', () => {
initAuth(); // #43: Auth-State prüfen
// Sort-Auswahl aus localStorage wiederherstellen
const savedSort = localStorage.getItem('sortOrder');
if (savedSort) {
document.getElementById('sort-select').value = savedSort;
currentSort = savedSort;
}
const saved = localStorage.getItem('selectedBundesland');
const select = document.getElementById('bundesland-select');
if (saved) {
@ -1155,7 +1196,7 @@
const resp = await fetch(url);
allAssessments = await resp.json();
updateStats();
renderList(allAssessments);
renderList(sortAssessments(allAssessments));
buildParteienFilter();
buildTagCloud();
} catch (e) {
@ -1749,7 +1790,7 @@
);
}
renderList(filtered);
renderList(sortAssessments(filtered));
}
function setScoreFilter(filter, btn) {
@ -1779,7 +1820,7 @@
);
}
renderList(filtered);
renderList(sortAssessments(filtered));
}
function applyScoreFilter(items, filter) {
@ -2229,5 +2270,96 @@
}
}
</script>
<!-- Queue-Panel Overlay -->
<div id="queue-panel" style="display:none;position:fixed;top:0;right:0;width:400px;height:100vh;background:white;box-shadow:-4px 0 12px rgba(0,0,0,0.15);z-index:200;overflow-y:auto;padding:1.5rem;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
<h3 style="color:var(--color-blue);margin:0;">📊 Queue</h3>
<button onclick="document.getElementById('queue-panel').style.display='none'" style="background:none;border:none;font-size:1.2rem;cursor:pointer;"></button>
</div>
<div id="queue-panel-content"><span style="color:#aaa;">Lade...</span></div>
</div>
<!-- Batch-Panel Overlay -->
<div id="batch-panel" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:450px;background:white;box-shadow:0 8px 24px rgba(0,0,0,0.2);z-index:200;border-radius:8px;padding:1.5rem;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
<h3 style="color:var(--color-blue);margin:0;">📦 Batch-Analyse</h3>
<button onclick="document.getElementById('batch-panel').style.display='none'" style="background:none;border:none;font-size:1.2rem;cursor:pointer;"></button>
</div>
<p style="font-size:0.85rem;color:#666;margin-bottom:1rem;">Analysiert automatisch die neuesten ungeprüften Anträge.</p>
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;">
<select id="batch-bl-modal" style="padding:0.4rem;border:1px solid #ddd;border-radius:4px;">
{% for bl in bundeslaender if bl.code != 'ALL' and bl.active %}
<option value="{{ bl.code }}">{{ bl.name }}</option>
{% endfor %}
</select>
<select id="batch-limit-modal" style="padding:0.4rem;border:1px solid #ddd;border-radius:4px;">
<option value="5">5</option>
<option value="10" selected>10</option>
<option value="20">20</option>
<option value="50">50</option>
</select>
<button onclick="startBatchModal()" style="padding:0.4rem 1rem;background:var(--color-green);color:white;border:none;border-radius:4px;cursor:pointer;">🚀 Starten</button>
</div>
<div id="batch-modal-status" style="margin-top:0.75rem;font-size:0.85rem;"></div>
</div>
<script>
function loadQueuePanel() {
const el = document.getElementById('queue-panel-content');
async function refresh() {
try {
const qs = await fetch('/api/queue/status').then(r => r.json());
const jobs = qs.jobs || [];
if (jobs.length === 0 && qs.pending === 0) {
el.innerHTML = '<p style="color:#888;">Keine Aufträge in der Warteschlange.</p>';
return;
}
const completed = jobs.filter(j => j.status === 'completed').length;
const pct = jobs.length > 0 ? Math.round(completed / jobs.length * 100) : 0;
el.innerHTML = `
<div style="margin-bottom:0.5rem;font-size:0.85rem;color:#666;">
${qs.concurrency} Worker · ${qs.pending} wartend · ${qs.processed_total} verarbeitet
${qs.shutting_down ? '<br><span style="color:#dc3545;">⚠ Server wird heruntergefahren</span>' : ''}
</div>
<div style="background:#eee;border-radius:4px;height:6px;margin-bottom:0.75rem;">
<div style="background:var(--color-green);height:100%;width:${pct}%;transition:width 0.5s;border-radius:4px;"></div>
</div>
<table style="width:100%;font-size:0.8rem;border-collapse:collapse;">
${jobs.map(j => {
const icon = j.status === 'completed' ? '✅' : j.status === 'processing' ? '⏳' : j.status === 'failed' ? '❌' : '⏸';
return '<tr style="border-top:1px solid #f0f0f0;"><td>' + (j.drucksache || j.job_id.substring(0,8)) + '</td><td>' + icon + '</td><td>' + (j.duration ? j.duration + 's' : '') + '</td></tr>';
}).join('')}
</table>`;
} catch { el.innerHTML = '<span style="color:#c00;">Fehler</span>'; }
}
refresh();
// Auto-refresh solange Panel offen
const iv = setInterval(() => {
if (document.getElementById('queue-panel').style.display === 'none') { clearInterval(iv); return; }
refresh();
}, 3000);
}
async function startBatchModal() {
const bl = document.getElementById('batch-bl-modal').value;
const limit = document.getElementById('batch-limit-modal').value;
const status = document.getElementById('batch-modal-status');
status.innerHTML = '<span style="color:var(--color-blue);">⏳ Wird gestartet...</span>';
try {
const resp = await fetch('/api/batch-analyze', {
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'bundesland=' + bl + '&limit=' + limit
});
if (resp.status === 401 || resp.status === 403) {
status.innerHTML = '<span style="color:#dc3545;">🔒 Admin-Berechtigung erforderlich.</span>';
return;
}
const data = await resp.json();
status.innerHTML = '<span style="color:var(--color-green);">✓ ' + (data.enqueued || 0) + ' Anträge eingereiht. Queue-Panel öffnen um Fortschritt zu sehen.</span>';
} catch (e) {
status.innerHTML = '<span style="color:#dc3545;">❌ ' + e.message + '</span>';
}
}
</script>
</body>
</html>