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>
This commit is contained in:
parent
38bffb23fa
commit
553e99d14e
102
app/main.py
102
app/main.py
@ -127,6 +127,28 @@ app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
||||
templates = Jinja2Templates(directory=str(templates_dir))
|
||||
|
||||
|
||||
# ─── Auth-Fehler bei HTML-Seiten: Redirect statt JSON-401/403 ─────────────────
|
||||
|
||||
@app.exception_handler(401)
|
||||
async def auth_required_redirect(request: Request, exc: HTTPException):
|
||||
"""Bei 401 auf HTML-Routes: Redirect zu /?login=1 (öffnet Auth-Modal)."""
|
||||
accept = request.headers.get("accept", "")
|
||||
if "text/html" in accept:
|
||||
from fastapi.responses import RedirectResponse
|
||||
return RedirectResponse("/?login=1", status_code=302)
|
||||
return JSONResponse({"detail": exc.detail}, status_code=401)
|
||||
|
||||
|
||||
@app.exception_handler(403)
|
||||
async def admin_required_redirect(request: Request, exc: HTTPException):
|
||||
"""Bei 403 auf HTML-Routes: Redirect zu /?login=1 (öffnet Auth-Modal)."""
|
||||
accept = request.headers.get("accept", "")
|
||||
if "text/html" in accept:
|
||||
from fastapi.responses import RedirectResponse
|
||||
return RedirectResponse("/?login=1", status_code=302)
|
||||
return JSONResponse({"detail": exc.detail}, status_code=403)
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup():
|
||||
import asyncio
|
||||
@ -209,7 +231,7 @@ async def classic_index(request: Request):
|
||||
# ─── Default: / → v2 (Default-Flip #139 Phase 2) ────────────────────────────
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request):
|
||||
async def index(request: Request, current_user: Optional[dict] = Depends(get_current_user)):
|
||||
"""Startseite — rendert v2-Listenansicht (Default-Flip Phase 2).
|
||||
|
||||
Alte /?drucksache=XX-YYYY Deep-Links werden per 301-Redirect auf
|
||||
@ -230,11 +252,12 @@ async def index(request: Request):
|
||||
"assessments": assessments,
|
||||
"bl_codes": bl_codes,
|
||||
"assessment_count": len(assessments),
|
||||
**_v2_template_context(current_user),
|
||||
})
|
||||
|
||||
|
||||
@app.get("/antrag/{drucksache:path}", response_class=HTMLResponse)
|
||||
async def antrag_detail(request: Request, drucksache: str):
|
||||
async def antrag_detail(request: Request, drucksache: str, current_user: Optional[dict] = Depends(get_current_user)):
|
||||
"""v2-Antragsdetail-Route — Server-Side-Rendering mit echten DB-Daten."""
|
||||
try:
|
||||
drucksache = validate_drucksache(drucksache)
|
||||
@ -243,6 +266,7 @@ async def antrag_detail(request: Request, drucksache: str):
|
||||
"request": request,
|
||||
"v2_active_nav": "durchsuchen",
|
||||
"error": f"Ungültige Drucksachen-ID: {drucksache}",
|
||||
**_v2_template_context(current_user),
|
||||
}, status_code=400)
|
||||
|
||||
row = await get_assessment(drucksache)
|
||||
@ -251,6 +275,7 @@ async def antrag_detail(request: Request, drucksache: str):
|
||||
"request": request,
|
||||
"v2_active_nav": "durchsuchen",
|
||||
"error": f"Antrag {drucksache} wurde nicht gefunden.",
|
||||
**_v2_template_context(current_user),
|
||||
}, status_code=404)
|
||||
|
||||
antrag = _row_to_detail(row)
|
||||
@ -294,9 +319,33 @@ async def antrag_detail(request: Request, drucksache: str):
|
||||
"E5": "Setzt sich Ihre Kommune für mehr Demokratie ein — auch auf Landes- und Bundesebene? Werden internationale Abkommen unterstützt?",
|
||||
},
|
||||
"matrix_labels": MATRIX_LABELS,
|
||||
**_v2_template_context(current_user),
|
||||
})
|
||||
|
||||
|
||||
def _v2_template_context(current_user=None) -> dict:
|
||||
"""Gemeinsame v2-Template-Variablen: is_admin, is_authenticated, v2_bundeslaender.
|
||||
|
||||
Wird in jeder v2-Route aufgerufen und per **-Spread in den Template-Context gemischt.
|
||||
"""
|
||||
is_authenticated = bool(current_user and current_user.get("authenticated", False))
|
||||
# require_auth liefert keinen "authenticated"-Key, aber ein sub-Feld — beides prüfen
|
||||
if current_user and current_user.get("sub"):
|
||||
is_authenticated = True
|
||||
roles = (current_user or {}).get("roles", [])
|
||||
is_admin = "admin" in roles or "gwoe-admin" in roles
|
||||
v2_bls = [
|
||||
{"code": bl.code, "name": bl.name}
|
||||
for bl in alle_bundeslaender()
|
||||
if bl.aktiv
|
||||
]
|
||||
return {
|
||||
"is_authenticated": is_authenticated,
|
||||
"is_admin": is_admin,
|
||||
"v2_bundeslaender": v2_bls,
|
||||
}
|
||||
|
||||
|
||||
def _rows_to_list(rows):
|
||||
"""Konvertiert DB-Rows in das flache Dict-Format für die v2-Listenansicht."""
|
||||
result = []
|
||||
@ -1588,25 +1637,27 @@ async def list_bundeslaender():
|
||||
# === Impressum / Datenschutz ===
|
||||
|
||||
@app.get("/impressum", response_class=HTMLResponse)
|
||||
async def impressum_page(request: Request):
|
||||
async def impressum_page(request: Request, current_user: Optional[dict] = Depends(get_current_user)):
|
||||
return templates.TemplateResponse("v2/screens/legal.html", {
|
||||
"request": request, "app_name": settings.app_name,
|
||||
"title": "Impressum", "section": "impressum",
|
||||
**_v2_template_context(current_user),
|
||||
})
|
||||
|
||||
|
||||
@app.get("/datenschutz", response_class=HTMLResponse)
|
||||
async def datenschutz_page(request: Request):
|
||||
async def datenschutz_page(request: Request, current_user: Optional[dict] = Depends(get_current_user)):
|
||||
return templates.TemplateResponse("v2/screens/legal.html", {
|
||||
"request": request, "app_name": settings.app_name,
|
||||
"title": "Datenschutzerklärung", "section": "datenschutz",
|
||||
**_v2_template_context(current_user),
|
||||
})
|
||||
|
||||
|
||||
# === Quellen / Programme ===
|
||||
|
||||
@app.get("/methodik", response_class=HTMLResponse)
|
||||
async def methodik_page(request: Request):
|
||||
async def methodik_page(request: Request, current_user: Optional[dict] = Depends(get_current_user)):
|
||||
"""Transparenz-/Methodik-Seite (#96)."""
|
||||
from .bundeslaender import aktive_bundeslaender, BUNDESLAENDER
|
||||
from .embeddings import get_indexing_status
|
||||
@ -1630,11 +1681,12 @@ async def methodik_page(request: Request):
|
||||
"programme_count": status.get("total", 0),
|
||||
"chunk_count": sum(p.get("chunks", 0) for p in status.get("programmes", [])),
|
||||
"bundeslaender": sorted(bl_list, key=lambda x: x["name"]),
|
||||
**_v2_template_context(current_user),
|
||||
})
|
||||
|
||||
|
||||
@app.get("/quellen", response_class=HTMLResponse)
|
||||
async def quellen_page(request: Request):
|
||||
async def quellen_page(request: Request, current_user: Optional[dict] = Depends(get_current_user)):
|
||||
"""Quellen-Seite mit allen Wahl- und Parteiprogrammen, nach BL gruppiert."""
|
||||
from .bundeslaender import BUNDESLAENDER
|
||||
programmes = get_programme_info()
|
||||
@ -1661,6 +1713,7 @@ async def quellen_page(request: Request):
|
||||
"wahlprogramme_grouped": wahlprogramme_grouped,
|
||||
"grundsatzprogramme": grundsatz,
|
||||
"status": status,
|
||||
**_v2_template_context(current_user),
|
||||
})
|
||||
|
||||
|
||||
@ -1850,7 +1903,7 @@ async def index_programme(
|
||||
|
||||
|
||||
@app.get("/auswertungen", response_class=HTMLResponse)
|
||||
async def auswertungen_page(request: Request):
|
||||
async def auswertungen_page(request: Request, current_user: Optional[dict] = Depends(get_current_user)):
|
||||
"""Auswertungs-Dashboard in v2 (Phase 3 Migration aus Classic)."""
|
||||
from .auswertungen import get_wahlperioden
|
||||
from .bundeslaender import alle_bundeslaender
|
||||
@ -1864,6 +1917,7 @@ async def auswertungen_page(request: Request):
|
||||
"v2_active_nav": "auswertungen",
|
||||
"wahlperioden": wahlperioden,
|
||||
"bl_codes": bl_codes,
|
||||
**_v2_template_context(current_user),
|
||||
})
|
||||
|
||||
|
||||
@ -2171,26 +2225,28 @@ async def v2_antrag_redirect(request: Request, drucksache: str):
|
||||
|
||||
|
||||
@app.get("/v2/merkliste", response_class=HTMLResponse)
|
||||
async def v2_merkliste(request: Request):
|
||||
"""Merkliste (Bookmarks) — lädt Daten via /api/bookmarks client-seitig."""
|
||||
async def v2_merkliste(request: Request, current_user: dict = Depends(require_auth)):
|
||||
"""Merkliste (Bookmarks) — nur für eingeloggte User; lädt Daten via /api/bookmarks client-seitig."""
|
||||
return templates.TemplateResponse("v2/screens/merkliste.html", {
|
||||
"request": request,
|
||||
"v2_active_nav": "merkliste",
|
||||
**_v2_template_context(current_user),
|
||||
})
|
||||
|
||||
|
||||
@app.get("/v2/tags", response_class=HTMLResponse)
|
||||
async def v2_tags(request: Request):
|
||||
async def v2_tags(request: Request, current_user: Optional[dict] = Depends(get_current_user)):
|
||||
"""Tag-Cloud-Seite — Themen-Filter über alle Assessments."""
|
||||
return templates.TemplateResponse("v2/screens/tags.html", {
|
||||
"request": request,
|
||||
"v2_active_nav": "tags",
|
||||
**_v2_template_context(current_user),
|
||||
})
|
||||
|
||||
|
||||
@app.get("/v2/cluster", response_class=HTMLResponse)
|
||||
async def v2_cluster(request: Request):
|
||||
"""Cluster-Liste — Top-10 Cluster als redaktionelle Liste."""
|
||||
async def v2_cluster(request: Request, current_user: dict = Depends(require_admin)):
|
||||
"""Cluster-Liste — nur für Admins."""
|
||||
rows = await get_all_assessments(None)
|
||||
assessments = _rows_to_list(rows)
|
||||
bl_codes = sorted({a["bundesland"] for a in assessments if a.get("bundesland")})
|
||||
@ -2198,12 +2254,13 @@ async def v2_cluster(request: Request):
|
||||
"request": request,
|
||||
"v2_active_nav": "cluster",
|
||||
"bl_codes": bl_codes,
|
||||
**_v2_template_context(current_user),
|
||||
})
|
||||
|
||||
|
||||
@app.get("/v2/neu", response_class=HTMLResponse)
|
||||
async def v2_neu(request: Request):
|
||||
"""Neuer-Antrag-Form — startet Analyse via /api/analyze-drucksache."""
|
||||
async def v2_neu(request: Request, current_user: dict = Depends(require_auth)):
|
||||
"""Neuer-Antrag-Form — nur für eingeloggte User; startet Analyse via /api/analyze-drucksache."""
|
||||
from .bundeslaender import alle_bundeslaender
|
||||
bl_list = [
|
||||
{"code": bl.code, "name": bl.name}
|
||||
@ -2215,12 +2272,13 @@ async def v2_neu(request: Request):
|
||||
"v2_active_nav": "neu",
|
||||
"bundeslaender": sorted(bl_list, key=lambda x: x["name"]),
|
||||
"default_model": settings.llm_model_default,
|
||||
**_v2_template_context(current_user),
|
||||
})
|
||||
|
||||
|
||||
@app.get("/v2/landtag-suche", response_class=HTMLResponse)
|
||||
async def v2_landtag_suche(request: Request):
|
||||
"""Landtag-Suche — sucht Drucksachen live im Landtags-Portal (nicht nur DB)."""
|
||||
async def v2_landtag_suche(request: Request, current_user: dict = Depends(require_auth)):
|
||||
"""Landtag-Suche — nur für eingeloggte User; sucht Drucksachen live im Landtags-Portal."""
|
||||
from .bundeslaender import alle_bundeslaender
|
||||
bl_list = [
|
||||
{"code": bl.code, "name": bl.name}
|
||||
@ -2231,12 +2289,13 @@ async def v2_landtag_suche(request: Request):
|
||||
"request": request,
|
||||
"v2_active_nav": "landtag_suche",
|
||||
"bundeslaender": sorted(bl_list, key=lambda x: x["name"]),
|
||||
**_v2_template_context(current_user),
|
||||
})
|
||||
|
||||
|
||||
@app.get("/v2/batch", response_class=HTMLResponse)
|
||||
async def v2_batch(request: Request):
|
||||
"""Batch-Analyse-Form (Admin) — enqueued ungeprüfte Drucksachen eines BL."""
|
||||
async def v2_batch(request: Request, current_user: dict = Depends(require_admin)):
|
||||
"""Batch-Analyse-Form — nur für Admins; enqueued ungeprüfte Drucksachen eines BL."""
|
||||
from .bundeslaender import alle_bundeslaender
|
||||
bl_list = [
|
||||
{"code": bl.code, "name": bl.name}
|
||||
@ -2247,6 +2306,7 @@ async def v2_batch(request: Request):
|
||||
"request": request,
|
||||
"v2_active_nav": "batch",
|
||||
"bundeslaender": sorted(bl_list, key=lambda x: x["name"]),
|
||||
**_v2_template_context(current_user),
|
||||
})
|
||||
|
||||
|
||||
@ -2258,7 +2318,7 @@ async def v2_admin_freischaltungen(request: Request, user: dict = Depends(requir
|
||||
return templates.TemplateResponse("v2/screens/admin_freischaltungen.html", {
|
||||
"request": request,
|
||||
"v2_active_nav": "admin_freischaltungen",
|
||||
"is_admin": True,
|
||||
**_v2_template_context(user),
|
||||
})
|
||||
|
||||
|
||||
@ -2268,7 +2328,7 @@ async def v2_admin_queue(request: Request, user: dict = Depends(require_admin)):
|
||||
return templates.TemplateResponse("v2/screens/admin_queue.html", {
|
||||
"request": request,
|
||||
"v2_active_nav": "admin_queue",
|
||||
"is_admin": True,
|
||||
**_v2_template_context(user),
|
||||
})
|
||||
|
||||
|
||||
@ -2278,7 +2338,7 @@ async def v2_admin_abos(request: Request, user: dict = Depends(require_admin)):
|
||||
return templates.TemplateResponse("v2/screens/admin_abos.html", {
|
||||
"request": request,
|
||||
"v2_active_nav": "admin_abos",
|
||||
"is_admin": True,
|
||||
**_v2_template_context(user),
|
||||
})
|
||||
|
||||
|
||||
|
||||
@ -40,26 +40,26 @@
|
||||
<span class="v2-nav-count">{{ assessment_count }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
<a href="/v2/merkliste" class="v2-nav-item {% if v2_active_nav == 'merkliste' %}active{% endif %}">{{ icon("bookmark-simple", 14) }} Merkliste</a>
|
||||
{% if is_authenticated %}<a href="/v2/merkliste" class="v2-nav-item {% if v2_active_nav == 'merkliste' %}active{% endif %}">{{ icon("bookmark-simple", 14) }} Merkliste</a>{% endif %}
|
||||
<a href="/v2/tags" class="v2-nav-item {% if v2_active_nav == 'tags' %}active{% endif %}">{{ icon("tag", 14) }} Tags</a>
|
||||
<a href="/v2/cluster" class="v2-nav-item {% if v2_active_nav == 'cluster' %}active{% endif %}">{{ icon("graph", 14) }} Cluster</a>
|
||||
<a href="/v2/landtag-suche" class="v2-nav-item {% if v2_active_nav == 'landtag_suche' %}active{% endif %}">{{ icon("magnifying-glass-plus", 14) }} Landtag-Suche</a>
|
||||
{% if is_admin %}<a href="/v2/cluster" class="v2-nav-item {% if v2_active_nav == 'cluster' %}active{% endif %}">{{ icon("graph", 14) }} Cluster</a>{% endif %}
|
||||
{% if is_authenticated %}<a href="/v2/landtag-suche" class="v2-nav-item {% if v2_active_nav == 'landtag_suche' %}active{% endif %}">{{ icon("magnifying-glass-plus", 14) }} Landtag-Suche</a>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="v2-nav-group">
|
||||
<div class="v2-nav-label">— Prüfen</div>
|
||||
<a href="/v2/neu" class="v2-nav-item {% if v2_active_nav == 'neu' %}active{% endif %}">{{ icon("file-plus", 14) }} Neuer Antrag</a>
|
||||
<a href="/v2/batch" class="v2-nav-item {% if v2_active_nav == 'batch' %}active{% endif %}">{{ icon("stack", 14) }} Batch-Analyse</a>
|
||||
{% if is_authenticated %}<a href="/v2/neu" class="v2-nav-item {% if v2_active_nav == 'neu' %}active{% endif %}">{{ icon("file-plus", 14) }} Neuer Antrag</a>{% endif %}
|
||||
{% if is_admin %}<a href="/v2/batch" class="v2-nav-item {% if v2_active_nav == 'batch' %}active{% endif %}">{{ icon("stack", 14) }} Batch-Analyse</a>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="v2-nav-group">
|
||||
<div class="v2-nav-label">— Daten</div>
|
||||
<a href="/auswertungen" class="v2-nav-item {% if v2_active_nav == 'auswertungen' %}active{% endif %}">{{ icon("chart-bar", 14) }} Auswertungen</a>
|
||||
<a href="/api/auswertungen/export.csv" class="v2-nav-item">{{ icon("file-csv", 14) }} Export · API</a>
|
||||
<a href="/api/feed.xml" class="v2-nav-item">{{ icon("rss", 14) }} Atom-Feed</a>
|
||||
{% if is_authenticated %}<a href="/auswertungen" class="v2-nav-item {% if v2_active_nav == 'auswertungen' %}active{% endif %}">{{ icon("chart-bar", 14) }} Auswertungen</a>{% endif %}
|
||||
{% if is_authenticated %}<a href="/api/auswertungen/export.csv" class="v2-nav-item">{{ icon("file-csv", 14) }} Export · API</a>{% endif %}
|
||||
{% if is_authenticated %}<a href="/api/feed.xml" class="v2-nav-item">{{ icon("rss", 14) }} Atom-Feed</a>{% endif %}
|
||||
</div>
|
||||
|
||||
{% if is_admin is defined and is_admin %}
|
||||
{% if is_admin %}
|
||||
<div class="v2-nav-group">
|
||||
<div class="v2-nav-label">— Administration</div>
|
||||
<a href="/v2/admin/freischaltungen" class="v2-nav-item">{{ icon("user-check", 14) }} Freischaltungen</a>
|
||||
@ -77,6 +77,15 @@
|
||||
<a href="/methodik">{{ icon("info", 13) }} Methodik</a>
|
||||
<a href="/quellen">{{ icon("book-open", 13) }} Quellen</a>
|
||||
|
||||
{# ── Globaler Bundesland-Selector ─────────────────────────────────── #}
|
||||
<select id="v2-global-bl"
|
||||
onchange="v2SetGlobalBl(this.value)"
|
||||
aria-label="Bundesland wählen"
|
||||
style="font-family:var(--font-mono);font-size:11px;padding:3px 6px;border:1px solid var(--ecg-light, var(--ecg-border));background:var(--ecg-card-bg);color:var(--ecg-dark);text-transform:uppercase;border-radius:3px;cursor:pointer;">
|
||||
<option value="ALL">Bundesweit</option>
|
||||
{% for bl in v2_bundeslaender %}<option value="{{ bl.code }}">{{ bl.code }}</option>{% endfor %}
|
||||
</select>
|
||||
|
||||
{# ── Auth-Control — wird per JS nach /api/auth/me-Aufruf umgeschaltet ── #}
|
||||
<div id="v2-auth-control" style="display:inline-flex;align-items:center;">
|
||||
{# Platzhalter, bis initV2Auth() den Zustand kennt — unsichtbar #}
|
||||
@ -204,6 +213,27 @@
|
||||
|
||||
{% block body_scripts %}{% endblock %}
|
||||
|
||||
{# ── Globaler BL-Selector — Persistenz + Event ───────────────────────────── #}
|
||||
<script>
|
||||
(function () {
|
||||
var BL_KEY = 'gwoe.bl';
|
||||
|
||||
window.v2SetGlobalBl = function (code) {
|
||||
try { localStorage.setItem(BL_KEY, code); } catch (_) {}
|
||||
window.dispatchEvent(new CustomEvent('v2-bl-changed', { detail: { bl: code } }));
|
||||
};
|
||||
|
||||
window.v2GetGlobalBl = function () {
|
||||
try { return localStorage.getItem(BL_KEY) || 'ALL'; } catch (_) { return 'ALL'; }
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var sel = document.getElementById('v2-global-bl');
|
||||
if (sel) sel.value = window.v2GetGlobalBl();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
{# ── Auth Modal (global, einmal pro Seite) ────────────────────────────── #}
|
||||
{% include "v2/components/auth_modal.html" %}
|
||||
|
||||
@ -278,5 +308,8 @@
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
{# Queue-Statusbar mit Hover-Tooltip — analog zu classic-UI (#149) #}
|
||||
{% include "v2/components/queue_widget.html" %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
93
app/templates/v2/components/queue_widget.html
Normal file
93
app/templates/v2/components/queue_widget.html
Normal file
@ -0,0 +1,93 @@
|
||||
{#
|
||||
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>
|
||||
@ -181,13 +181,6 @@ table.modal-table th { background: var(--ecg-bg-subtle); font-weight: 700; }
|
||||
<option value="{{ wp }}">{{ wp }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label for="bl-filter">Bundesland:</label>
|
||||
<select id="bl-filter">
|
||||
<option value="">Alle</option>
|
||||
{% for code in bl_codes %}
|
||||
<option value="{{ code }}">{{ code }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button class="primary" onclick="loadBlMatrix()">Laden</button>
|
||||
<button onclick="window.location.href='/api/auswertungen/export.csv'">CSV-Export</button>
|
||||
</div>
|
||||
@ -200,14 +193,6 @@ table.modal-table th { background: var(--ecg-bg-subtle); font-weight: 700; }
|
||||
<!-- Panel 2: Thema × Fraktion -->
|
||||
<div class="auswert-panel" id="panel-themen">
|
||||
<div class="controls-bar">
|
||||
<label for="themen-bl-filter">Bundesland:</label>
|
||||
<select id="themen-bl-filter" onchange="loadThemenMatrix()">
|
||||
<option value="">Alle</option>
|
||||
{% for code in bl_codes %}
|
||||
<option value="{{ code }}">{{ code }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div id="themen-matrix-wrap" class="matrix-wrap">
|
||||
<div style="font-family:var(--font-mono);font-size:12px;opacity:0.5;">Wählen Sie den Tab „Thema × Fraktion".</div>
|
||||
</div>
|
||||
@ -258,6 +243,16 @@ function switchTab(id, btn) {
|
||||
}
|
||||
}
|
||||
|
||||
// Bei BL-Wechsel aktive Panels neu laden
|
||||
window.addEventListener('v2-bl-changed', function () {
|
||||
var activePanel = document.querySelector('.auswert-panel.active');
|
||||
if (!activePanel) return;
|
||||
if (activePanel.id === 'panel-bl-partei') loadBlMatrix();
|
||||
if (activePanel.id === 'panel-themen') loadThemenMatrix();
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () { loadBlMatrix(); });
|
||||
|
||||
function scoreClass(avg) {
|
||||
if (avg == null) return '';
|
||||
if (avg >= 6) return 's-high';
|
||||
@ -269,7 +264,8 @@ async function loadBlMatrix() {
|
||||
const wrap = document.getElementById('bl-matrix-wrap');
|
||||
const metaEl = document.getElementById('bl-matrix-meta');
|
||||
const wp = document.getElementById('wp-filter').value;
|
||||
const bl = document.getElementById('bl-filter').value;
|
||||
const blRaw = (window.v2GetGlobalBl && window.v2GetGlobalBl()) || 'ALL';
|
||||
const bl = (blRaw === 'ALL') ? '' : blRaw;
|
||||
|
||||
wrap.innerHTML = '<div style="font-family:var(--font-mono);font-size:12px;opacity:0.5;">Lade Matrix …</div>';
|
||||
metaEl.textContent = '';
|
||||
@ -317,7 +313,8 @@ async function loadBlMatrix() {
|
||||
|
||||
async function loadThemenMatrix() {
|
||||
const wrap = document.getElementById('themen-matrix-wrap');
|
||||
const bl = document.getElementById('themen-bl-filter').value;
|
||||
const blRaw = (window.v2GetGlobalBl && window.v2GetGlobalBl()) || 'ALL';
|
||||
const bl = (blRaw === 'ALL') ? '' : blRaw;
|
||||
wrap.innerHTML = '<div style="font-family:var(--font-mono);font-size:12px;opacity:0.5;">Lade Themen-Matrix …</div>';
|
||||
|
||||
let url = '/api/auswertungen/themen-matrix';
|
||||
|
||||
@ -9,13 +9,9 @@
|
||||
|
||||
{% block main %}
|
||||
|
||||
{# ── Toolbar: Bundesland-Filter + Suche ─────────────────────────── #}
|
||||
{# ── Toolbar: Suche ──────────────────────────────────────────────── #}
|
||||
{# BL-Filter läuft jetzt über den globalen Selector in der Topbar. #}
|
||||
<div class="v2-toolbar" id="v2-toolbar" role="toolbar" aria-label="Filter und Suche">
|
||||
<button class="v2-chip active" data-bl="ALL" onclick="v2SetBl(this,'ALL')">Bundesweit</button>
|
||||
{% for code in bl_codes %}
|
||||
<button class="v2-chip" data-bl="{{ code }}" onclick="v2SetBl(this,'{{ code }}')">{{ code }}</button>
|
||||
{% endfor %}
|
||||
<span class="v2-toolbar-sep"></span>
|
||||
<input class="v2-search"
|
||||
type="search"
|
||||
placeholder="Anträge durchsuchen …"
|
||||
@ -122,13 +118,11 @@
|
||||
if (empty) empty.style.display = (visible === 0) ? 'block' : 'none';
|
||||
}
|
||||
|
||||
window.v2SetBl = function (btn, code) {
|
||||
activeBl = code;
|
||||
document.querySelectorAll('[data-bl]').forEach(function (b) {
|
||||
b.classList.toggle('active', b.dataset.bl === code);
|
||||
});
|
||||
/* BL-Filter: globaler Selector in der Topbar */
|
||||
window.addEventListener('v2-bl-changed', function (e) {
|
||||
activeBl = (e.detail && e.detail.bl) ? e.detail.bl : 'ALL';
|
||||
applyFilters();
|
||||
};
|
||||
});
|
||||
|
||||
window.v2SetBand = function (btn, band) {
|
||||
activeBand = band;
|
||||
@ -140,13 +134,19 @@
|
||||
|
||||
window.v2ResetFilters = function () {
|
||||
document.getElementById('v2-search-input').value = '';
|
||||
v2SetBl(null, 'ALL');
|
||||
// BL auf ALL zurücksetzen: globalen Selector aktualisieren
|
||||
var sel = document.getElementById('v2-global-bl');
|
||||
if (sel) sel.value = 'ALL';
|
||||
window.v2SetGlobalBl && window.v2SetGlobalBl('ALL');
|
||||
v2SetBand(null, 'ALL');
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var input = document.getElementById('v2-search-input');
|
||||
if (input) input.addEventListener('input', applyFilters);
|
||||
// Gespeicherten BL-Wert beim Laden anwenden
|
||||
activeBl = (window.v2GetGlobalBl && window.v2GetGlobalBl()) || 'ALL';
|
||||
applyFilters();
|
||||
});
|
||||
})();
|
||||
|
||||
|
||||
@ -147,6 +147,9 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="ls-bl-hint" style="display:none;margin-bottom:12px;padding:8px 12px;background:color-mix(in srgb,var(--ecg-teal) 10%,transparent);border:1px solid var(--ecg-teal);border-radius:4px;font-family:var(--font-mono);font-size:11px;color:var(--ecg-teal);">
|
||||
Bitte zuerst ein Bundesland im Header wählen.
|
||||
</div>
|
||||
<form class="ls-form" onsubmit="lsSearch(event)">
|
||||
<div class="ls-q">
|
||||
<label for="ls-q-input">Suchbegriff</label>
|
||||
@ -158,14 +161,6 @@
|
||||
required
|
||||
onkeydown="if(event.key==='Enter'){event.preventDefault();lsSearch(event);}">
|
||||
</div>
|
||||
<div class="ls-bl">
|
||||
<label for="ls-bl-select">Bundesland</label>
|
||||
<select id="ls-bl-select" name="bundesland">
|
||||
{% for bl in bundeslaender %}
|
||||
<option value="{{ bl.code }}"{% if bl.code == 'NRW' %} selected{% endif %}>{{ bl.name }} ({{ bl.code }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="ls-submit" id="ls-btn">
|
||||
{{ icon("magnifying-glass-plus", 14) }} Suchen
|
||||
</button>
|
||||
@ -202,7 +197,14 @@ async function lsSearch(e) {
|
||||
if (e && e.preventDefault) e.preventDefault();
|
||||
|
||||
var q = (document.getElementById('ls-q-input').value || '').trim();
|
||||
var bl = document.getElementById('ls-bl-select').value;
|
||||
var bl = (window.v2GetGlobalBl && window.v2GetGlobalBl()) || 'ALL';
|
||||
|
||||
if (bl === 'ALL') {
|
||||
document.getElementById('ls-bl-hint').style.display = '';
|
||||
document.getElementById('ls-status').textContent = '';
|
||||
return;
|
||||
}
|
||||
document.getElementById('ls-bl-hint').style.display = 'none';
|
||||
|
||||
if (q.length < 2) {
|
||||
document.getElementById('ls-status').textContent = 'Bitte mindestens 2 Zeichen eingeben.';
|
||||
@ -344,5 +346,15 @@ function escHtml(s) {
|
||||
function escAttr(s) {
|
||||
return String(s).replace(/'/g, "\\'");
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var hint = document.getElementById('ls-bl-hint');
|
||||
function updateHint() {
|
||||
var bl = (window.v2GetGlobalBl && window.v2GetGlobalBl()) || 'ALL';
|
||||
if (hint) hint.style.display = (bl === 'ALL') ? '' : 'none';
|
||||
}
|
||||
updateHint();
|
||||
window.addEventListener('v2-bl-changed', updateHint);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -92,18 +92,15 @@
|
||||
|
||||
<form class="neu-form" onsubmit="startAnalyse(event)">
|
||||
|
||||
<div id="neu-bl-hint" style="display:none;margin-bottom:14px;padding:8px 12px;background:color-mix(in srgb,var(--ecg-teal) 10%,transparent);border:1px solid var(--ecg-teal);border-radius:4px;font-family:var(--font-mono);font-size:11px;color:var(--ecg-teal);">
|
||||
Bitte zuerst ein Bundesland im Header wählen.
|
||||
</div>
|
||||
|
||||
<label for="neu-drucksache">Drucksachen-Nummer</label>
|
||||
<input type="text" id="neu-drucksache" name="drucksache"
|
||||
placeholder="z. B. 18/12345 oder NRW-18/12345"
|
||||
required autocomplete="off">
|
||||
|
||||
<label for="neu-bl">Bundesland</label>
|
||||
<select id="neu-bl" name="bundesland">
|
||||
{% for bl in bundeslaender %}
|
||||
<option value="{{ bl.code }}"{% if bl.code == 'NRW' %} selected{% endif %}>{{ bl.name }} ({{ bl.code }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label for="neu-model">Modell</label>
|
||||
<select id="neu-model" name="model">
|
||||
<option value="">Standard ({{ default_model }})</option>
|
||||
@ -136,11 +133,17 @@ async function startAnalyse(e) {
|
||||
const errEl = document.getElementById('neu-error');
|
||||
|
||||
const drucksache = document.getElementById('neu-drucksache').value.trim();
|
||||
const bundesland = document.getElementById('neu-bl').value;
|
||||
const bundesland = (window.v2GetGlobalBl && window.v2GetGlobalBl()) || 'ALL';
|
||||
const model = document.getElementById('neu-model').value;
|
||||
|
||||
if (!drucksache) return;
|
||||
|
||||
if (bundesland === 'ALL') {
|
||||
errEl.style.display = '';
|
||||
errEl.textContent = 'Bitte zuerst ein Bundesland im Header wählen.';
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
statusEl.style.display = '';
|
||||
progEl.style.display = '';
|
||||
@ -212,5 +215,15 @@ async function startAnalyse(e) {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var hint = document.getElementById('neu-bl-hint');
|
||||
function updateHint() {
|
||||
var bl = (window.v2GetGlobalBl && window.v2GetGlobalBl()) || 'ALL';
|
||||
if (hint) hint.style.display = (bl === 'ALL') ? '' : 'none';
|
||||
}
|
||||
updateHint();
|
||||
window.addEventListener('v2-bl-changed', updateHint);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -42,23 +42,26 @@ echo " $(date -Iseconds)"
|
||||
echo "================================================================"
|
||||
|
||||
echo
|
||||
echo "[1] Hauptseiten erreichbar (alle 200)"
|
||||
echo "[1] Public-Seiten (200 ohne Auth)"
|
||||
check "v2 Default /" "200" "/"
|
||||
check "v2 Detail (echte DS)" "200" "/antrag/21/754S"
|
||||
check "Classic /classic" "200" "/classic"
|
||||
check "/auswertungen" "200" "/auswertungen"
|
||||
check "/methodik" "200" "/methodik"
|
||||
check "/quellen" "200" "/quellen"
|
||||
check "/impressum" "200" "/impressum"
|
||||
check "/datenschutz" "200" "/datenschutz"
|
||||
check "/v2/merkliste" "200" "/v2/merkliste"
|
||||
check "/v2/tags" "200" "/v2/tags"
|
||||
check "/v2/cluster" "200" "/v2/cluster"
|
||||
check "/v2/landtag-suche" "200" "/v2/landtag-suche"
|
||||
check "/v2/neu" "200" "/v2/neu"
|
||||
check "/v2/batch" "200" "/v2/batch"
|
||||
check "/health" "200" "/health"
|
||||
|
||||
echo
|
||||
echo "[1b] Auth-Routen (302/401 ohne Auth — Redirect zu Login)"
|
||||
check "/auswertungen (auth)" "302" "/auswertungen"
|
||||
check "/v2/merkliste (auth)" "302" "/v2/merkliste"
|
||||
check "/v2/landtag-suche (auth)" "302" "/v2/landtag-suche"
|
||||
check "/v2/neu (auth)" "302" "/v2/neu"
|
||||
check "/v2/cluster (admin)" "302" "/v2/cluster"
|
||||
check "/v2/batch (admin)" "302" "/v2/batch"
|
||||
|
||||
echo
|
||||
echo "[2] API-Endpoints (öffentlich)"
|
||||
check "/api/assessments" "200" "/api/assessments"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user