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))
|
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")
|
@app.on_event("startup")
|
||||||
async def startup():
|
async def startup():
|
||||||
import asyncio
|
import asyncio
|
||||||
@ -209,7 +231,7 @@ async def classic_index(request: Request):
|
|||||||
# ─── Default: / → v2 (Default-Flip #139 Phase 2) ────────────────────────────
|
# ─── Default: / → v2 (Default-Flip #139 Phase 2) ────────────────────────────
|
||||||
|
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@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).
|
"""Startseite — rendert v2-Listenansicht (Default-Flip Phase 2).
|
||||||
|
|
||||||
Alte /?drucksache=XX-YYYY Deep-Links werden per 301-Redirect auf
|
Alte /?drucksache=XX-YYYY Deep-Links werden per 301-Redirect auf
|
||||||
@ -230,11 +252,12 @@ async def index(request: Request):
|
|||||||
"assessments": assessments,
|
"assessments": assessments,
|
||||||
"bl_codes": bl_codes,
|
"bl_codes": bl_codes,
|
||||||
"assessment_count": len(assessments),
|
"assessment_count": len(assessments),
|
||||||
|
**_v2_template_context(current_user),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.get("/antrag/{drucksache:path}", response_class=HTMLResponse)
|
@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."""
|
"""v2-Antragsdetail-Route — Server-Side-Rendering mit echten DB-Daten."""
|
||||||
try:
|
try:
|
||||||
drucksache = validate_drucksache(drucksache)
|
drucksache = validate_drucksache(drucksache)
|
||||||
@ -243,6 +266,7 @@ async def antrag_detail(request: Request, drucksache: str):
|
|||||||
"request": request,
|
"request": request,
|
||||||
"v2_active_nav": "durchsuchen",
|
"v2_active_nav": "durchsuchen",
|
||||||
"error": f"Ungültige Drucksachen-ID: {drucksache}",
|
"error": f"Ungültige Drucksachen-ID: {drucksache}",
|
||||||
|
**_v2_template_context(current_user),
|
||||||
}, status_code=400)
|
}, status_code=400)
|
||||||
|
|
||||||
row = await get_assessment(drucksache)
|
row = await get_assessment(drucksache)
|
||||||
@ -251,6 +275,7 @@ async def antrag_detail(request: Request, drucksache: str):
|
|||||||
"request": request,
|
"request": request,
|
||||||
"v2_active_nav": "durchsuchen",
|
"v2_active_nav": "durchsuchen",
|
||||||
"error": f"Antrag {drucksache} wurde nicht gefunden.",
|
"error": f"Antrag {drucksache} wurde nicht gefunden.",
|
||||||
|
**_v2_template_context(current_user),
|
||||||
}, status_code=404)
|
}, status_code=404)
|
||||||
|
|
||||||
antrag = _row_to_detail(row)
|
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?",
|
"E5": "Setzt sich Ihre Kommune für mehr Demokratie ein — auch auf Landes- und Bundesebene? Werden internationale Abkommen unterstützt?",
|
||||||
},
|
},
|
||||||
"matrix_labels": MATRIX_LABELS,
|
"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):
|
def _rows_to_list(rows):
|
||||||
"""Konvertiert DB-Rows in das flache Dict-Format für die v2-Listenansicht."""
|
"""Konvertiert DB-Rows in das flache Dict-Format für die v2-Listenansicht."""
|
||||||
result = []
|
result = []
|
||||||
@ -1588,25 +1637,27 @@ async def list_bundeslaender():
|
|||||||
# === Impressum / Datenschutz ===
|
# === Impressum / Datenschutz ===
|
||||||
|
|
||||||
@app.get("/impressum", response_class=HTMLResponse)
|
@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", {
|
return templates.TemplateResponse("v2/screens/legal.html", {
|
||||||
"request": request, "app_name": settings.app_name,
|
"request": request, "app_name": settings.app_name,
|
||||||
"title": "Impressum", "section": "impressum",
|
"title": "Impressum", "section": "impressum",
|
||||||
|
**_v2_template_context(current_user),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.get("/datenschutz", response_class=HTMLResponse)
|
@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", {
|
return templates.TemplateResponse("v2/screens/legal.html", {
|
||||||
"request": request, "app_name": settings.app_name,
|
"request": request, "app_name": settings.app_name,
|
||||||
"title": "Datenschutzerklärung", "section": "datenschutz",
|
"title": "Datenschutzerklärung", "section": "datenschutz",
|
||||||
|
**_v2_template_context(current_user),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
# === Quellen / Programme ===
|
# === Quellen / Programme ===
|
||||||
|
|
||||||
@app.get("/methodik", response_class=HTMLResponse)
|
@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)."""
|
"""Transparenz-/Methodik-Seite (#96)."""
|
||||||
from .bundeslaender import aktive_bundeslaender, BUNDESLAENDER
|
from .bundeslaender import aktive_bundeslaender, BUNDESLAENDER
|
||||||
from .embeddings import get_indexing_status
|
from .embeddings import get_indexing_status
|
||||||
@ -1630,11 +1681,12 @@ async def methodik_page(request: Request):
|
|||||||
"programme_count": status.get("total", 0),
|
"programme_count": status.get("total", 0),
|
||||||
"chunk_count": sum(p.get("chunks", 0) for p in status.get("programmes", [])),
|
"chunk_count": sum(p.get("chunks", 0) for p in status.get("programmes", [])),
|
||||||
"bundeslaender": sorted(bl_list, key=lambda x: x["name"]),
|
"bundeslaender": sorted(bl_list, key=lambda x: x["name"]),
|
||||||
|
**_v2_template_context(current_user),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.get("/quellen", response_class=HTMLResponse)
|
@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."""
|
"""Quellen-Seite mit allen Wahl- und Parteiprogrammen, nach BL gruppiert."""
|
||||||
from .bundeslaender import BUNDESLAENDER
|
from .bundeslaender import BUNDESLAENDER
|
||||||
programmes = get_programme_info()
|
programmes = get_programme_info()
|
||||||
@ -1661,6 +1713,7 @@ async def quellen_page(request: Request):
|
|||||||
"wahlprogramme_grouped": wahlprogramme_grouped,
|
"wahlprogramme_grouped": wahlprogramme_grouped,
|
||||||
"grundsatzprogramme": grundsatz,
|
"grundsatzprogramme": grundsatz,
|
||||||
"status": status,
|
"status": status,
|
||||||
|
**_v2_template_context(current_user),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -1850,7 +1903,7 @@ async def index_programme(
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/auswertungen", response_class=HTMLResponse)
|
@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)."""
|
"""Auswertungs-Dashboard in v2 (Phase 3 Migration aus Classic)."""
|
||||||
from .auswertungen import get_wahlperioden
|
from .auswertungen import get_wahlperioden
|
||||||
from .bundeslaender import alle_bundeslaender
|
from .bundeslaender import alle_bundeslaender
|
||||||
@ -1864,6 +1917,7 @@ async def auswertungen_page(request: Request):
|
|||||||
"v2_active_nav": "auswertungen",
|
"v2_active_nav": "auswertungen",
|
||||||
"wahlperioden": wahlperioden,
|
"wahlperioden": wahlperioden,
|
||||||
"bl_codes": bl_codes,
|
"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)
|
@app.get("/v2/merkliste", response_class=HTMLResponse)
|
||||||
async def v2_merkliste(request: Request):
|
async def v2_merkliste(request: Request, current_user: dict = Depends(require_auth)):
|
||||||
"""Merkliste (Bookmarks) — lädt Daten via /api/bookmarks client-seitig."""
|
"""Merkliste (Bookmarks) — nur für eingeloggte User; lädt Daten via /api/bookmarks client-seitig."""
|
||||||
return templates.TemplateResponse("v2/screens/merkliste.html", {
|
return templates.TemplateResponse("v2/screens/merkliste.html", {
|
||||||
"request": request,
|
"request": request,
|
||||||
"v2_active_nav": "merkliste",
|
"v2_active_nav": "merkliste",
|
||||||
|
**_v2_template_context(current_user),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.get("/v2/tags", response_class=HTMLResponse)
|
@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."""
|
"""Tag-Cloud-Seite — Themen-Filter über alle Assessments."""
|
||||||
return templates.TemplateResponse("v2/screens/tags.html", {
|
return templates.TemplateResponse("v2/screens/tags.html", {
|
||||||
"request": request,
|
"request": request,
|
||||||
"v2_active_nav": "tags",
|
"v2_active_nav": "tags",
|
||||||
|
**_v2_template_context(current_user),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.get("/v2/cluster", response_class=HTMLResponse)
|
@app.get("/v2/cluster", response_class=HTMLResponse)
|
||||||
async def v2_cluster(request: Request):
|
async def v2_cluster(request: Request, current_user: dict = Depends(require_admin)):
|
||||||
"""Cluster-Liste — Top-10 Cluster als redaktionelle Liste."""
|
"""Cluster-Liste — nur für Admins."""
|
||||||
rows = await get_all_assessments(None)
|
rows = await get_all_assessments(None)
|
||||||
assessments = _rows_to_list(rows)
|
assessments = _rows_to_list(rows)
|
||||||
bl_codes = sorted({a["bundesland"] for a in assessments if a.get("bundesland")})
|
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,
|
"request": request,
|
||||||
"v2_active_nav": "cluster",
|
"v2_active_nav": "cluster",
|
||||||
"bl_codes": bl_codes,
|
"bl_codes": bl_codes,
|
||||||
|
**_v2_template_context(current_user),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.get("/v2/neu", response_class=HTMLResponse)
|
@app.get("/v2/neu", response_class=HTMLResponse)
|
||||||
async def v2_neu(request: Request):
|
async def v2_neu(request: Request, current_user: dict = Depends(require_auth)):
|
||||||
"""Neuer-Antrag-Form — startet Analyse via /api/analyze-drucksache."""
|
"""Neuer-Antrag-Form — nur für eingeloggte User; startet Analyse via /api/analyze-drucksache."""
|
||||||
from .bundeslaender import alle_bundeslaender
|
from .bundeslaender import alle_bundeslaender
|
||||||
bl_list = [
|
bl_list = [
|
||||||
{"code": bl.code, "name": bl.name}
|
{"code": bl.code, "name": bl.name}
|
||||||
@ -2215,12 +2272,13 @@ async def v2_neu(request: Request):
|
|||||||
"v2_active_nav": "neu",
|
"v2_active_nav": "neu",
|
||||||
"bundeslaender": sorted(bl_list, key=lambda x: x["name"]),
|
"bundeslaender": sorted(bl_list, key=lambda x: x["name"]),
|
||||||
"default_model": settings.llm_model_default,
|
"default_model": settings.llm_model_default,
|
||||||
|
**_v2_template_context(current_user),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.get("/v2/landtag-suche", response_class=HTMLResponse)
|
@app.get("/v2/landtag-suche", response_class=HTMLResponse)
|
||||||
async def v2_landtag_suche(request: Request):
|
async def v2_landtag_suche(request: Request, current_user: dict = Depends(require_auth)):
|
||||||
"""Landtag-Suche — sucht Drucksachen live im Landtags-Portal (nicht nur DB)."""
|
"""Landtag-Suche — nur für eingeloggte User; sucht Drucksachen live im Landtags-Portal."""
|
||||||
from .bundeslaender import alle_bundeslaender
|
from .bundeslaender import alle_bundeslaender
|
||||||
bl_list = [
|
bl_list = [
|
||||||
{"code": bl.code, "name": bl.name}
|
{"code": bl.code, "name": bl.name}
|
||||||
@ -2231,12 +2289,13 @@ async def v2_landtag_suche(request: Request):
|
|||||||
"request": request,
|
"request": request,
|
||||||
"v2_active_nav": "landtag_suche",
|
"v2_active_nav": "landtag_suche",
|
||||||
"bundeslaender": sorted(bl_list, key=lambda x: x["name"]),
|
"bundeslaender": sorted(bl_list, key=lambda x: x["name"]),
|
||||||
|
**_v2_template_context(current_user),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.get("/v2/batch", response_class=HTMLResponse)
|
@app.get("/v2/batch", response_class=HTMLResponse)
|
||||||
async def v2_batch(request: Request):
|
async def v2_batch(request: Request, current_user: dict = Depends(require_admin)):
|
||||||
"""Batch-Analyse-Form (Admin) — enqueued ungeprüfte Drucksachen eines BL."""
|
"""Batch-Analyse-Form — nur für Admins; enqueued ungeprüfte Drucksachen eines BL."""
|
||||||
from .bundeslaender import alle_bundeslaender
|
from .bundeslaender import alle_bundeslaender
|
||||||
bl_list = [
|
bl_list = [
|
||||||
{"code": bl.code, "name": bl.name}
|
{"code": bl.code, "name": bl.name}
|
||||||
@ -2247,6 +2306,7 @@ async def v2_batch(request: Request):
|
|||||||
"request": request,
|
"request": request,
|
||||||
"v2_active_nav": "batch",
|
"v2_active_nav": "batch",
|
||||||
"bundeslaender": sorted(bl_list, key=lambda x: x["name"]),
|
"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", {
|
return templates.TemplateResponse("v2/screens/admin_freischaltungen.html", {
|
||||||
"request": request,
|
"request": request,
|
||||||
"v2_active_nav": "admin_freischaltungen",
|
"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", {
|
return templates.TemplateResponse("v2/screens/admin_queue.html", {
|
||||||
"request": request,
|
"request": request,
|
||||||
"v2_active_nav": "admin_queue",
|
"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", {
|
return templates.TemplateResponse("v2/screens/admin_abos.html", {
|
||||||
"request": request,
|
"request": request,
|
||||||
"v2_active_nav": "admin_abos",
|
"v2_active_nav": "admin_abos",
|
||||||
"is_admin": True,
|
**_v2_template_context(user),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -40,26 +40,26 @@
|
|||||||
<span class="v2-nav-count">{{ assessment_count }}</span>
|
<span class="v2-nav-count">{{ assessment_count }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</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/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>
|
{% if is_admin %}<a href="/v2/cluster" class="v2-nav-item {% if v2_active_nav == 'cluster' %}active{% endif %}">{{ icon("graph", 14) }} Cluster</a>{% endif %}
|
||||||
<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_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>
|
||||||
|
|
||||||
<div class="v2-nav-group">
|
<div class="v2-nav-group">
|
||||||
<div class="v2-nav-label">— Prüfen</div>
|
<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>
|
{% 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 %}
|
||||||
<a href="/v2/batch" class="v2-nav-item {% if v2_active_nav == 'batch' %}active{% endif %}">{{ icon("stack", 14) }} Batch-Analyse</a>
|
{% 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>
|
||||||
|
|
||||||
<div class="v2-nav-group">
|
<div class="v2-nav-group">
|
||||||
<div class="v2-nav-label">— Daten</div>
|
<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>
|
{% if is_authenticated %}<a href="/auswertungen" class="v2-nav-item {% if v2_active_nav == 'auswertungen' %}active{% endif %}">{{ icon("chart-bar", 14) }} Auswertungen</a>{% endif %}
|
||||||
<a href="/api/auswertungen/export.csv" class="v2-nav-item">{{ icon("file-csv", 14) }} Export · API</a>
|
{% if is_authenticated %}<a href="/api/auswertungen/export.csv" class="v2-nav-item">{{ icon("file-csv", 14) }} Export · API</a>{% endif %}
|
||||||
<a href="/api/feed.xml" class="v2-nav-item">{{ icon("rss", 14) }} Atom-Feed</a>
|
{% if is_authenticated %}<a href="/api/feed.xml" class="v2-nav-item">{{ icon("rss", 14) }} Atom-Feed</a>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if is_admin is defined and is_admin %}
|
{% if is_admin %}
|
||||||
<div class="v2-nav-group">
|
<div class="v2-nav-group">
|
||||||
<div class="v2-nav-label">— Administration</div>
|
<div class="v2-nav-label">— Administration</div>
|
||||||
<a href="/v2/admin/freischaltungen" class="v2-nav-item">{{ icon("user-check", 14) }} Freischaltungen</a>
|
<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="/methodik">{{ icon("info", 13) }} Methodik</a>
|
||||||
<a href="/quellen">{{ icon("book-open", 13) }} Quellen</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 ── #}
|
{# ── Auth-Control — wird per JS nach /api/auth/me-Aufruf umgeschaltet ── #}
|
||||||
<div id="v2-auth-control" style="display:inline-flex;align-items:center;">
|
<div id="v2-auth-control" style="display:inline-flex;align-items:center;">
|
||||||
{# Platzhalter, bis initV2Auth() den Zustand kennt — unsichtbar #}
|
{# Platzhalter, bis initV2Auth() den Zustand kennt — unsichtbar #}
|
||||||
@ -204,6 +213,27 @@
|
|||||||
|
|
||||||
{% block body_scripts %}{% endblock %}
|
{% 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) ────────────────────────────── #}
|
{# ── Auth Modal (global, einmal pro Seite) ────────────────────────────── #}
|
||||||
{% include "v2/components/auth_modal.html" %}
|
{% include "v2/components/auth_modal.html" %}
|
||||||
|
|
||||||
@ -278,5 +308,8 @@
|
|||||||
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{# Queue-Statusbar mit Hover-Tooltip — analog zu classic-UI (#149) #}
|
||||||
|
{% include "v2/components/queue_widget.html" %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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>
|
<option value="{{ wp }}">{{ wp }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</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 class="primary" onclick="loadBlMatrix()">Laden</button>
|
||||||
<button onclick="window.location.href='/api/auswertungen/export.csv'">CSV-Export</button>
|
<button onclick="window.location.href='/api/auswertungen/export.csv'">CSV-Export</button>
|
||||||
</div>
|
</div>
|
||||||
@ -200,14 +193,6 @@ table.modal-table th { background: var(--ecg-bg-subtle); font-weight: 700; }
|
|||||||
<!-- Panel 2: Thema × Fraktion -->
|
<!-- Panel 2: Thema × Fraktion -->
|
||||||
<div class="auswert-panel" id="panel-themen">
|
<div class="auswert-panel" id="panel-themen">
|
||||||
<div class="controls-bar">
|
<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 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 style="font-family:var(--font-mono);font-size:12px;opacity:0.5;">Wählen Sie den Tab „Thema × Fraktion".</div>
|
||||||
</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) {
|
function scoreClass(avg) {
|
||||||
if (avg == null) return '';
|
if (avg == null) return '';
|
||||||
if (avg >= 6) return 's-high';
|
if (avg >= 6) return 's-high';
|
||||||
@ -269,7 +264,8 @@ async function loadBlMatrix() {
|
|||||||
const wrap = document.getElementById('bl-matrix-wrap');
|
const wrap = document.getElementById('bl-matrix-wrap');
|
||||||
const metaEl = document.getElementById('bl-matrix-meta');
|
const metaEl = document.getElementById('bl-matrix-meta');
|
||||||
const wp = document.getElementById('wp-filter').value;
|
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>';
|
wrap.innerHTML = '<div style="font-family:var(--font-mono);font-size:12px;opacity:0.5;">Lade Matrix …</div>';
|
||||||
metaEl.textContent = '';
|
metaEl.textContent = '';
|
||||||
@ -317,7 +313,8 @@ async function loadBlMatrix() {
|
|||||||
|
|
||||||
async function loadThemenMatrix() {
|
async function loadThemenMatrix() {
|
||||||
const wrap = document.getElementById('themen-matrix-wrap');
|
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>';
|
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';
|
let url = '/api/auswertungen/themen-matrix';
|
||||||
|
|||||||
@ -9,13 +9,9 @@
|
|||||||
|
|
||||||
{% block main %}
|
{% 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">
|
<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"
|
<input class="v2-search"
|
||||||
type="search"
|
type="search"
|
||||||
placeholder="Anträge durchsuchen …"
|
placeholder="Anträge durchsuchen …"
|
||||||
@ -122,13 +118,11 @@
|
|||||||
if (empty) empty.style.display = (visible === 0) ? 'block' : 'none';
|
if (empty) empty.style.display = (visible === 0) ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
window.v2SetBl = function (btn, code) {
|
/* BL-Filter: globaler Selector in der Topbar */
|
||||||
activeBl = code;
|
window.addEventListener('v2-bl-changed', function (e) {
|
||||||
document.querySelectorAll('[data-bl]').forEach(function (b) {
|
activeBl = (e.detail && e.detail.bl) ? e.detail.bl : 'ALL';
|
||||||
b.classList.toggle('active', b.dataset.bl === code);
|
|
||||||
});
|
|
||||||
applyFilters();
|
applyFilters();
|
||||||
};
|
});
|
||||||
|
|
||||||
window.v2SetBand = function (btn, band) {
|
window.v2SetBand = function (btn, band) {
|
||||||
activeBand = band;
|
activeBand = band;
|
||||||
@ -140,13 +134,19 @@
|
|||||||
|
|
||||||
window.v2ResetFilters = function () {
|
window.v2ResetFilters = function () {
|
||||||
document.getElementById('v2-search-input').value = '';
|
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');
|
v2SetBand(null, 'ALL');
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
var input = document.getElementById('v2-search-input');
|
var input = document.getElementById('v2-search-input');
|
||||||
if (input) input.addEventListener('input', applyFilters);
|
if (input) input.addEventListener('input', applyFilters);
|
||||||
|
// Gespeicherten BL-Wert beim Laden anwenden
|
||||||
|
activeBl = (window.v2GetGlobalBl && window.v2GetGlobalBl()) || 'ALL';
|
||||||
|
applyFilters();
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
@ -147,6 +147,9 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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)">
|
<form class="ls-form" onsubmit="lsSearch(event)">
|
||||||
<div class="ls-q">
|
<div class="ls-q">
|
||||||
<label for="ls-q-input">Suchbegriff</label>
|
<label for="ls-q-input">Suchbegriff</label>
|
||||||
@ -158,14 +161,6 @@
|
|||||||
required
|
required
|
||||||
onkeydown="if(event.key==='Enter'){event.preventDefault();lsSearch(event);}">
|
onkeydown="if(event.key==='Enter'){event.preventDefault();lsSearch(event);}">
|
||||||
</div>
|
</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">
|
<button type="submit" class="ls-submit" id="ls-btn">
|
||||||
{{ icon("magnifying-glass-plus", 14) }} Suchen
|
{{ icon("magnifying-glass-plus", 14) }} Suchen
|
||||||
</button>
|
</button>
|
||||||
@ -202,7 +197,14 @@ async function lsSearch(e) {
|
|||||||
if (e && e.preventDefault) e.preventDefault();
|
if (e && e.preventDefault) e.preventDefault();
|
||||||
|
|
||||||
var q = (document.getElementById('ls-q-input').value || '').trim();
|
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) {
|
if (q.length < 2) {
|
||||||
document.getElementById('ls-status').textContent = 'Bitte mindestens 2 Zeichen eingeben.';
|
document.getElementById('ls-status').textContent = 'Bitte mindestens 2 Zeichen eingeben.';
|
||||||
@ -344,5 +346,15 @@ function escHtml(s) {
|
|||||||
function escAttr(s) {
|
function escAttr(s) {
|
||||||
return String(s).replace(/'/g, "\\'");
|
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>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -92,18 +92,15 @@
|
|||||||
|
|
||||||
<form class="neu-form" onsubmit="startAnalyse(event)">
|
<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>
|
<label for="neu-drucksache">Drucksachen-Nummer</label>
|
||||||
<input type="text" id="neu-drucksache" name="drucksache"
|
<input type="text" id="neu-drucksache" name="drucksache"
|
||||||
placeholder="z. B. 18/12345 oder NRW-18/12345"
|
placeholder="z. B. 18/12345 oder NRW-18/12345"
|
||||||
required autocomplete="off">
|
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>
|
<label for="neu-model">Modell</label>
|
||||||
<select id="neu-model" name="model">
|
<select id="neu-model" name="model">
|
||||||
<option value="">Standard ({{ default_model }})</option>
|
<option value="">Standard ({{ default_model }})</option>
|
||||||
@ -136,11 +133,17 @@ async function startAnalyse(e) {
|
|||||||
const errEl = document.getElementById('neu-error');
|
const errEl = document.getElementById('neu-error');
|
||||||
|
|
||||||
const drucksache = document.getElementById('neu-drucksache').value.trim();
|
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;
|
const model = document.getElementById('neu-model').value;
|
||||||
|
|
||||||
if (!drucksache) return;
|
if (!drucksache) return;
|
||||||
|
|
||||||
|
if (bundesland === 'ALL') {
|
||||||
|
errEl.style.display = '';
|
||||||
|
errEl.textContent = 'Bitte zuerst ein Bundesland im Header wählen.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
statusEl.style.display = '';
|
statusEl.style.display = '';
|
||||||
progEl.style.display = '';
|
progEl.style.display = '';
|
||||||
@ -212,5 +215,15 @@ async function startAnalyse(e) {
|
|||||||
btn.disabled = false;
|
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>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -42,23 +42,26 @@ echo " $(date -Iseconds)"
|
|||||||
echo "================================================================"
|
echo "================================================================"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "[1] Hauptseiten erreichbar (alle 200)"
|
echo "[1] Public-Seiten (200 ohne Auth)"
|
||||||
check "v2 Default /" "200" "/"
|
check "v2 Default /" "200" "/"
|
||||||
check "v2 Detail (echte DS)" "200" "/antrag/21/754S"
|
check "v2 Detail (echte DS)" "200" "/antrag/21/754S"
|
||||||
check "Classic /classic" "200" "/classic"
|
check "Classic /classic" "200" "/classic"
|
||||||
check "/auswertungen" "200" "/auswertungen"
|
|
||||||
check "/methodik" "200" "/methodik"
|
check "/methodik" "200" "/methodik"
|
||||||
check "/quellen" "200" "/quellen"
|
check "/quellen" "200" "/quellen"
|
||||||
check "/impressum" "200" "/impressum"
|
check "/impressum" "200" "/impressum"
|
||||||
check "/datenschutz" "200" "/datenschutz"
|
check "/datenschutz" "200" "/datenschutz"
|
||||||
check "/v2/merkliste" "200" "/v2/merkliste"
|
|
||||||
check "/v2/tags" "200" "/v2/tags"
|
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"
|
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
|
||||||
echo "[2] API-Endpoints (öffentlich)"
|
echo "[2] API-Endpoints (öffentlich)"
|
||||||
check "/api/assessments" "200" "/api/assessments"
|
check "/api/assessments" "200" "/api/assessments"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user