gwoe-antragspruefer/app/templates/v2/components/queue_widget.html
Dotty Dotter 553e99d14e feat(v2): globaler BL-Selector im Header + Auth-gated Sidebar + Queue-Widget
Bundesland-Auswahl:
- Topbar: einziger BL-Selektor mit localStorage.gwoe.bl-Persistenz
- BL-Felder entfernt aus durchsuchen.html, landtag_suche.html, neu.html, auswertungen.html
- Screens hoeren auf v2-bl-changed CustomEvent + initial via window.v2GetGlobalBl()

Sichtbarkeit (Sidebar):
- Durchsuchen + Tags: immer
- Merkliste / Neuer Antrag / Landtag-Suche / Auswertungen / Export / Feed: nur eingeloggt
- Cluster + Batch-Analyse + Administration: nur Admin

Server-Side Schutz:
- _v2_template_context()-Helper liefert is_authenticated, is_admin, v2_bundeslaender
- HTML-Routen mit Depends(require_auth) bzw. require_admin
- 401/403-Browser-Requests redirecten auf /?login=1 statt JSON-Error

Queue-Widget (#149):
- Neues Component-Partial v2/components/queue_widget.html
- Statusbar unten links + Hover-Tooltip mit den letzten 20 Jobs
- 5s-Polling auf /api/queue/status, blendet sich aus wenn keine Jobs

Smoke-Test angepasst an neue Auth-Erwartungen (302 fuer auth-protected Routen).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:50:36 +02:00

94 lines
4.4 KiB
HTML

{#
queue_widget.html — Queue-Statusbar mit Hover-Tooltip (#149).
Wird am Ende von base.html eingebunden via {% include %}. Self-contained:
Eigenes <div id="v2-queue-statusbar"> + <div id="v2-queue-tooltip"> +
Polling-Script. Pollt alle 5 s `/api/queue/status` und blendet sich aus,
wenn keine Jobs aktiv/fertig/fehlgeschlagen sind.
Portiert aus classic-UI (#99). Nutzt v2-Tokens statt classic-Variablen.
#}
<div id="v2-queue-statusbar"
style="display:none;position:fixed;bottom:1rem;left:1rem;
background:var(--ecg-card-bg);border:1px solid var(--ecg-light);
border-radius:6px;padding:0.4rem 0.8rem;
font-family:var(--font-mono);font-size:11px;color:var(--ecg-dark);
box-shadow:0 2px 8px rgba(0,0,0,0.1);z-index:100;cursor:default;
transition:all 0.2s;"
onmouseenter="document.getElementById('v2-queue-tooltip').style.display='block'"
onmouseleave="document.getElementById('v2-queue-tooltip').style.display='none'"
aria-label="Analyse-Queue Status">
<span id="v2-queue-status-text"></span>
</div>
<div id="v2-queue-tooltip"
style="display:none;position:fixed;bottom:3.5rem;left:1rem;
background:var(--ecg-card-bg);border:1px solid var(--ecg-light);
border-radius:6px;padding:0.8rem 1rem;
font-family:var(--font-sans);font-size:12px;color:var(--ecg-dark);
box-shadow:0 4px 16px rgba(0,0,0,0.15);z-index:101;
max-width:420px;max-height:320px;overflow-y:auto;">
</div>
<script>
(function () {
function poll() {
fetch('/api/queue/status')
.then(function (r) { return r.json(); })
.then(function (qs) {
var jobs = (qs.jobs || []).filter(function (j) { return j.status !== 'stale'; });
var processing = jobs.filter(function (j) { return j.status === 'processing'; }).length;
var queued = jobs.filter(function (j) { return j.status === 'queued' || j.status === 'pending'; }).length;
var completed = jobs.filter(function (j) { return j.status === 'completed'; }).length;
var failed = jobs.filter(function (j) { return j.status === 'failed'; }).length;
var bar = document.getElementById('v2-queue-statusbar');
var text = document.getElementById('v2-queue-status-text');
if (!bar || !text) return;
if (processing + queued + completed + failed === 0) {
bar.style.display = 'none';
return;
}
bar.style.display = 'block';
var parts = [];
if (processing > 0) parts.push('⏳ ' + processing + ' in Bearbeitung');
if (queued > 0) parts.push('⏸ ' + queued + ' wartend');
if (completed > 0) parts.push('✓ ' + completed + ' fertig');
if (failed > 0) parts.push('✗ ' + failed + ' fehlgeschlagen');
text.textContent = parts.join(' · ');
var tip = document.getElementById('v2-queue-tooltip');
if (!tip) return;
var workers = qs.workers_running != null ? qs.workers_running : '?';
var rows = jobs.slice(0, 20).map(function (j) {
var icon = j.status === 'completed' ? '✓'
: j.status === 'processing' ? '⏳'
: j.status === 'failed' ? '✗'
: '⏸';
var dur = j.duration ? (' · ' + j.duration + 's') : '';
var bl = j.bundesland ? (' · ' + j.bundesland) : '';
var ds = j.drucksache || '?';
var dsLink = j.status === 'completed' && j.drucksache
? '<a href="/antrag/' + encodeURIComponent(j.drucksache) + '" style="color:var(--ecg-blue);">' + ds + '</a>'
: ds;
return '<div style="padding:0.25rem 0;border-bottom:1px solid var(--ecg-light);">'
+ '<span style="font-family:var(--font-mono);">' + icon + '</span> '
+ dsLink
+ '<span style="font-family:var(--font-mono);color:var(--ecg-text-muted);font-size:0.85em;">' + bl + dur + '</span>'
+ '</div>';
}).join('');
tip.innerHTML = '<div style="margin-bottom:0.5rem;font-weight:900;font-size:11px;letter-spacing:0.04em;text-transform:uppercase;color:var(--ecg-blue);">Queue · '
+ workers + ' Worker</div>'
+ (rows || '<div style="color:var(--ecg-text-muted);">leer</div>');
})
.catch(function () { /* still */ });
}
// erster Aufruf direkt + danach alle 5 s
poll();
setInterval(poll, 5000);
})();
</script>