diff --git a/app/report.py b/app/report.py
index 72a3587..b8d3570 100644
--- a/app/report.py
+++ b/app/report.py
@@ -14,11 +14,21 @@ from html import escape as _e
from pathlib import Path
from typing import Optional
+from jinja2 import Environment, FileSystemLoader, select_autoescape
+
logger = logging.getLogger(__name__)
from .models import Assessment, MATRIX_LABELS, EMPFEHLUNG_CONFIG
from .bundeslaender import BUNDESLAENDER
+# Eigene Jinja-Env fuer PDF-Templates (separat von Starlette templates,
+# weil report.py auch von Hintergrund-Jobs ohne FastAPI-Request laufen muss).
+_TEMPLATE_DIR = Path(__file__).parent / "templates"
+_pdf_jinja = Environment(
+ loader=FileSystemLoader(str(_TEMPLATE_DIR)),
+ autoescape=select_autoescape(["html"]),
+)
+
# ECOnGOOD Colors
COLORS = {
"darkgray": "#5a5a5a",
@@ -512,6 +522,90 @@ async def generate_html_report(
output_path.write_text(html)
+async def generate_html_report_v3(
+ assessment: Assessment,
+ output_path: Path,
+ bundesland: Optional[str] = None,
+) -> None:
+ """Render Antrags-PDF im neuen v3-Layout (single column, A4 portrait).
+
+ Reuses die v3-Layout-Logik (Score-Hero, Matrix mit Achsen-Labels,
+ Programm-Treue, Verbesserungen) und ergaenzt sie um die im PDF
+ notwendigen Adaptionen:
+
+ - Kein interaktiver Matrix-Klick → "Schwerpunkte erklaert"-Sektion
+ listet die Top-3 positiven und Top-3 negativen Felder mit ihren
+ LLM-generierten label/aspect-Texten unter der Matrix.
+ - Plenum-Votes werden best-effort geladen, inkl. Konsistenz-Hinweis
+ (Mehrheit deckt sich / gegen GWOE-Empfehlung).
+ - Online-Elemente (Share, Vote-treffend, Kommentare, News, Modals)
+ sind im Template gar nicht erst angelegt.
+
+ Template: app/templates/v3/pdf/antrag_pdf.html
+ """
+ matrix_lookup = {e.field: {"rating": e.rating} for e in assessment.gwoe_matrix}
+
+ # Schwerpunkt-Felder mit Erklaerung: Top + Bottom Ratings.
+ sorted_matrix = sorted(
+ assessment.gwoe_matrix, key=lambda e: e.rating, reverse=True
+ )
+ matrix_top = [
+ {"field": e.field, "label": e.label, "aspect": e.aspect, "rating": e.rating}
+ for e in sorted_matrix if e.rating > 0
+ ][:4]
+ matrix_bottom = [
+ {"field": e.field, "label": e.label, "aspect": e.aspect, "rating": e.rating}
+ for e in sorted(sorted_matrix, key=lambda e: e.rating) if e.rating < 0
+ ][:4]
+
+ # Score-Color (gleich wie Scorecard)
+ s = assessment.gwoe_score
+ if s >= 8: score_color = "#1a7f37"
+ elif s >= 5: score_color = "#bf6c10"
+ else: score_color = "#9a2a2a"
+
+ parlament_name = ""
+ if bundesland and bundesland in BUNDESLAENDER:
+ parlament_name = BUNDESLAENDER[bundesland].parlament_name
+
+ # Plenum-Votes best-effort (Hintergrund-Job kann ohne DB-Pfad laufen,
+ # in dem Fall einfach keine Votes anzeigen).
+ plenum_votes: list[dict] = []
+ konsistenz_state: Optional[str] = None
+ konsistenz_decisive: Optional[str] = None
+ try:
+ from .database import get_plenum_votes
+ plenum_votes = await get_plenum_votes(
+ bundesland or "NRW", assessment.drucksache,
+ )
+ if plenum_votes:
+ from .marker import consistency_state, decisive_outcome
+ konsistenz_state = consistency_state(
+ assessment.empfehlung.value, plenum_votes,
+ )
+ konsistenz_decisive = decisive_outcome(plenum_votes)
+ except Exception as exc:
+ logger.warning(
+ "Plenum-Votes fuer PDF nicht ladbar (drucksache=%s): %s",
+ assessment.drucksache, exc,
+ )
+
+ template = _pdf_jinja.get_template("v3/pdf/antrag_pdf.html")
+ html = template.render(
+ assessment=assessment,
+ matrix_lookup=matrix_lookup,
+ matrix_top=matrix_top,
+ matrix_bottom=matrix_bottom,
+ score_color=score_color,
+ parlament_name=parlament_name,
+ bundesland=bundesland or "",
+ plenum_votes=plenum_votes,
+ konsistenz_state=konsistenz_state,
+ konsistenz_decisive=konsistenz_decisive,
+ )
+ output_path.write_text(html)
+
+
async def generate_pdf_report(
assessment: Assessment,
output_path: Path,
@@ -535,9 +629,10 @@ async def generate_pdf_report(
``bundesland`` is forwarded to ``generate_html_report`` so the source
parlament name appears in the report header.
"""
- # Step 1 — render the report itself
+ # Step 1 — render the report itself, neues v3-Layout (single column,
+ # Score-Hero, Matrix mit Achsen-Labels, Schwerpunkte-erklaert).
html_path = output_path.with_suffix('.tmp.html')
- await generate_html_report(assessment, html_path, bundesland=bundesland)
+ await generate_html_report_v3(assessment, html_path, bundesland=bundesland)
try:
from weasyprint import HTML
diff --git a/app/templates/v2/screens/admin_stand.html b/app/templates/v2/screens/admin_stand.html
index 3a908fd..c3fcb98 100644
--- a/app/templates/v2/screens/admin_stand.html
+++ b/app/templates/v2/screens/admin_stand.html
@@ -180,6 +180,16 @@
Lade aktuelles Beispiel …
+
+ Design-Werkstätten
+
+ Live-Editoren für Layout-Iteration ohne Server-Redeploy:
+
+
+ - Scorecard-Werkstatt —
+ Live-Vorschau aller Card-Formate (Portrait / Square / OG) mit
+ CSS-Editor und Embed-Link-Generator.
+
{% endblock %}
diff --git a/app/templates/v2/screens/antrag_detail.html b/app/templates/v2/screens/antrag_detail.html
index bd6138a..ec0a649 100644
--- a/app/templates/v2/screens/antrag_detail.html
+++ b/app/templates/v2/screens/antrag_detail.html
@@ -484,8 +484,9 @@
- {# ── Share-Block (analog v1) ───────────────────────────────────── #}
-
+ {# ── Share-Block (analog v1) — nur fuer angemeldete User sichtbar,
+ wird in initAuth() ein-/ausgeblendet (display:none default). ── #}
+
Teilen
- {# Teilen #}
-
+ {# Teilen — nur fuer angemeldete User; initAuth() blendet via #v2-share-block ein. #}
+