gwoe-antragspruefer/app/templates/v2/screens/admin_stand.html
Dotty Dotter 79e7937d51 feat: PDF-Generierung auf v3-Layout, Werkstatt-Link im Admin, Share nur bei Login
Drei Aufgaben in einem Schwung:

1. Werkstatt-Link im Admin
   admin_stand bekommt eine Sektion 'Design-Werkstaetten' mit Link auf
   /v2/scorecard-werkstatt — damit Admins den Live-Editor finden ohne
   die URL kennen zu muessen.

2. Share-Block nur fuer angemeldete User
   Der ganze Share-Block (Kopieren, Threads, Mastodon, LinkedIn,
   Instagram, E-Mail, Scorecard, Stock-Bild) bekommt id=v2-share-block
   und wird per initAuth() display:none/block geschaltet — analog zum
   Comment-Form. Default im Markup: display:none, damit Gaeste ihn
   nicht waehrend des Auth-Roundtrips kurz sehen. Funktioniert in v2
   und v3 (gleicher JS-Handler via super-Inheritance).

3. PDF-Layout = v3-Layout
   Neues Template v3/pdf/antrag_pdf.html (single column, A4) reused die
   Visuallogik aus der Online-Detailseite:
   - Score-Hero-Block mit Farb-Tint
   - Matrix 5×5 mit Achsen-Labels (Werte oben, Berührungsgruppen links)
   - Programm-Treue pro Fraktion mit Begruendung + Zitaten + Fallback-
     Hinweis bei fehlenden Zitaten
   - Verbesserungsvorschlaege mit Redline-Format
   - Abstimmungsergebnis (best-effort via get_plenum_votes) inkl.
     Konsistenz-Hinweis

   Online-spezifisches gestrichen: Merken-Button, Vote-treffend, Share,
   Kommentare, News-Box, Reanalyze, Historie, Modals.

   NEU im PDF: 'Schwerpunkte erklaert'-Sektion direkt unter der Matrix.
   Listet die Top-4 positiven und Top-4 negativen Matrix-Felder mit
   ihrem LLM-generierten label + aspect — Ersatz fuer den interaktiven
   Klick, der im PDF nicht funktioniert.

   report.generate_html_report_v3() neu, generate_pdf_report() ruft
   diese statt der alten Inline-HTML-Variante. Alte generate_html_report
   bleibt als Fallback erhalten.

   WeasyPrint rendert mit @page A4, Footer mit Drucksache + Branding +
   Seitenzahl 'Seite X von Y'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 13:48:19 +02:00

344 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "v2/base.html" %}
{% block title %}System-Stand — GWÖ-Antragsprüfer{% endblock %}
{% set v2_active_nav = "admin_stand" %}
{% block head_extra %}
<script src="/static/chart.umd.min.js"></script>
<style>
.stand-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 14px;
margin-bottom: 1.5rem;
}
.stand-kpi {
background: var(--ecg-card-bg);
border: 1px solid var(--ecg-border);
border-radius: 6px;
padding: 16px;
text-align: center;
}
.stand-kpi-value {
font-family: var(--font-mono);
font-size: 30px;
font-weight: 700;
color: var(--ecg-teal);
line-height: 1.1;
}
.stand-kpi-label {
font-family: var(--font-mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
opacity: 0.6;
margin-top: 4px;
}
.stand-kpi-sub {
font-family: var(--font-mono);
font-size: 11px;
opacity: 0.55;
margin-top: 6px;
}
.stand-section {
margin-top: 2rem;
}
.stand-section h2 {
font-family: var(--font-display);
font-size: 16px;
color: var(--ecg-teal);
margin: 0 0 10px;
}
.stand-table {
width: 100%;
border-collapse: collapse;
font-family: var(--font-mono);
font-size: 12px;
}
.stand-table td, .stand-table th {
border-bottom: 1px solid var(--ecg-border);
padding: 4px 8px;
text-align: left;
}
.stand-table th {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.05em;
opacity: 0.6;
font-weight: 700;
}
.stand-table td:nth-child(2) {
text-align: right;
}
</style>
{% endblock %}
{% block main %}
<div style="padding:0 0 1.5rem;">
<h1 style="font-family:var(--font-display);font-size:22px;color:var(--ecg-teal);margin:0 0 4px;">System-Stand</h1>
<p style="font-size:12px;font-family:var(--font-mono);color:var(--ecg-dark);opacity:0.6;">
Datenüberblick · automatische Aktualisierung alle 30 s
</p>
</div>
<div id="stand-loading" style="font-family:var(--font-mono);font-size:12px;opacity:0.5;padding:16px 0;">Lade Stand …</div>
<div id="stand-content" style="display:none;">
<!-- KPI-Kacheln -->
<div class="stand-grid">
<div class="stand-kpi">
<div class="stand-kpi-value" id="kpi-ass"></div>
<div class="stand-kpi-label">Bewertungen</div>
<div class="stand-kpi-sub" id="kpi-ass-7d">— in 7 Tagen</div>
</div>
<div class="stand-kpi">
<div class="stand-kpi-value" id="kpi-votes"></div>
<div class="stand-kpi-label">Plenum-Votes</div>
<div class="stand-kpi-sub">aus Plenarprotokollen</div>
</div>
<div class="stand-kpi">
<div class="stand-kpi-value" id="kpi-match"></div>
<div class="stand-kpi-label">Bewertung ∩ Vote</div>
<div class="stand-kpi-sub" id="kpi-orphans">— Vote-Orphans</div>
</div>
<div class="stand-kpi">
<div class="stand-kpi-value" id="kpi-news"></div>
<div class="stand-kpi-label">News</div>
<div class="stand-kpi-sub" id="kpi-news-7d">— in 7 Tagen</div>
</div>
<div class="stand-kpi">
<div class="stand-kpi-value" id="kpi-drafts"></div>
<div class="stand-kpi-label">PM-Entwürfe</div>
<div class="stand-kpi-sub" id="kpi-drafts-7d">— in 7 Tagen</div>
</div>
<div class="stand-kpi">
<div class="stand-kpi-value" id="kpi-bookmarks"></div>
<div class="stand-kpi-label">Merklisten-Einträge</div>
<div class="stand-kpi-sub">user-übergreifend</div>
</div>
</div>
<!-- Score-Histogram -->
<div class="stand-section">
<h2>GWÖ-Score-Verteilung</h2>
<div style="background:var(--ecg-card-bg);border:1px solid var(--ecg-border);border-radius:6px;padding:14px;">
<canvas id="stand-score-chart" style="max-height:240px;"></canvas>
</div>
</div>
<!-- Per-Bundesland -->
<div class="stand-section">
<h2>Bewertungen + Votes pro Bundesland</h2>
<table class="stand-table">
<thead><tr><th>BL</th><th>Bewertungen</th><th>Plenum-Votes</th></tr></thead>
<tbody id="stand-bl-rows"></tbody>
</table>
</div>
<!-- News-Quellen -->
<div class="stand-section">
<h2>News pro Quelle</h2>
<table class="stand-table">
<thead><tr><th>Quelle</th><th>Anzahl</th></tr></thead>
<tbody id="stand-news-rows"></tbody>
</table>
</div>
<!-- Vote-Orphans-Auto-Bewertung (#173) -->
<div class="stand-section">
<h2>Vote-Orphans-Auto-Bewertung</h2>
<p style="font-size:12px;opacity:0.65;margin:-4px 0 8px;">
Heute: <strong id="auto-rate-today"></strong>.
Cron läuft alle 6h und enqueued bis zu 30 Orphans pro Lauf, max. 200 Anträge/Tag.
</p>
<table class="stand-table">
<thead>
<tr>
<th>Zeitpunkt</th><th>Quelle</th><th>BL</th>
<th style="text-align:right;">Versucht</th>
<th style="text-align:right;">Enqueued</th>
<th style="text-align:right;">Skipped</th>
<th>Notiz</th>
</tr>
</thead>
<tbody id="stand-autorate-rows"></tbody>
</table>
</div>
<div id="stand-meta" style="font-family:var(--font-mono);font-size:11px;opacity:0.5;margin-top:1.5rem;"></div>
{# Alternative Detail-Ansichten — nur im Admin sichtbar.
Default ist v3 (`/antrag/{drs}`), der frühere Profi-Modus läuft
unter /v2/antrag/. Hier ein Sample-Link, damit Admins ihn finden. #}
<h3 class="v2-h3" style="margin-top:32px;">Alternative Ansichten</h3>
<p style="font-family:var(--font-mono);font-size:12px;opacity:0.75;line-height:1.6;">
Standard-Detailansicht ist v3 (Bürger:innen-Modus, single column).
Die frühere v2-Profi-Ansicht (zwei Spalten, alle Felder gleichzeitig)
bleibt unter <code>/v2/antrag/&lt;Drucksache&gt;</code> erreichbar.
</p>
<p id="alt-views-sample" style="font-family:var(--font-mono);font-size:12px;line-height:1.8;">
Lade aktuelles Beispiel …
</p>
<h3 class="v2-h3" style="margin-top:24px;">Design-Werkstätten</h3>
<p style="font-family:var(--font-mono);font-size:12px;opacity:0.75;line-height:1.6;">
Live-Editoren für Layout-Iteration ohne Server-Redeploy:
</p>
<ul style="font-family:var(--font-mono);font-size:12px;line-height:1.8;">
<li><a href="/v2/scorecard-werkstatt">Scorecard-Werkstatt</a>
Live-Vorschau aller Card-Formate (Portrait / Square / OG) mit
CSS-Editor und Embed-Link-Generator.</li>
</ul>
</div>
{% endblock %}
{% block body_scripts %}
<script>
let _scoreChart = null;
function fmtN(n) {
return (n == null) ? '—' : n.toLocaleString('de-DE');
}
async function loadStand() {
try {
const r = await fetch('/api/admin/stand');
if (!r.ok) {
document.getElementById('stand-loading').textContent = 'Fehler ' + r.status;
return;
}
const d = await r.json();
document.getElementById('stand-loading').style.display = 'none';
document.getElementById('stand-content').style.display = '';
// KPIs
document.getElementById('kpi-ass').textContent = fmtN(d.assessments.total);
document.getElementById('kpi-ass-7d').textContent = '+' + fmtN(d.assessments.last_7_days) + ' in 7 Tagen';
document.getElementById('kpi-votes').textContent = fmtN(d.plenum_votes.total);
document.getElementById('kpi-match').textContent = fmtN(d.match.with_assessment_and_vote);
document.getElementById('kpi-orphans').textContent = fmtN(d.match.vote_orphans) + ' Vote-Orphans';
document.getElementById('kpi-news').textContent = fmtN(d.news.total);
document.getElementById('kpi-news-7d').textContent =
'+' + fmtN(d.news.last_7_days) + ' in 7 Tagen, ' + fmtN(d.news.embedded) + ' embedded';
document.getElementById('kpi-drafts').textContent = fmtN(d.presse_drafts.total);
document.getElementById('kpi-drafts-7d').textContent = '+' + fmtN(d.presse_drafts.last_7_days) + ' in 7 Tagen';
document.getElementById('kpi-bookmarks').textContent = fmtN(d.bookmarks);
// Score-Histogram
const dist = d.assessments.score_distribution || {};
const buckets = [0,1,2,3,4,5,6,7,8,9,10];
const values = buckets.map(b => dist[String(b)] || 0);
const colors = buckets.map(b => {
if (b <= 2) return 'rgba(200,30,30,0.6)';
if (b <= 4) return 'rgba(247,148,29,0.6)';
if (b <= 6) return 'rgba(150,150,150,0.5)';
if (b <= 8) return 'rgba(136,158,51,0.6)';
return 'rgba(0,157,165,0.7)';
});
if (_scoreChart) _scoreChart.destroy();
_scoreChart = new Chart(document.getElementById('stand-score-chart'), {
type: 'bar',
data: { labels: buckets.map(b => b + '' + (b+1)), datasets: [{
label: 'Bewertungen',
data: values,
backgroundColor: colors,
}]},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: {
y: { beginAtZero: true, title: { display: true, text: 'Anzahl' } },
x: { title: { display: true, text: 'GWÖ-Score-Bucket' } },
},
},
});
// Per-BL-Tabelle
const ass_bl = d.assessments.by_bundesland || {};
const vote_bl = d.plenum_votes.by_bundesland || {};
const bl_set = new Set([...Object.keys(ass_bl), ...Object.keys(vote_bl)]);
const bl_rows = [...bl_set].sort();
document.getElementById('stand-bl-rows').innerHTML = bl_rows.map(bl => `
<tr>
<td>${bl}</td>
<td>${fmtN(ass_bl[bl] || 0)}</td>
<td>${fmtN(vote_bl[bl] || 0)}</td>
</tr>`).join('');
// News-Source-Tabelle
const ns = d.news.by_source || {};
document.getElementById('stand-news-rows').innerHTML =
Object.entries(ns).sort((a, b) => b[1] - a[1]).map(([s, n]) => `
<tr><td>${s}</td><td>${fmtN(n)}</td></tr>`).join('');
// Auto-Rate-Run-Tabelle (#173)
const ar = d.auto_rate || {};
const elToday = document.getElementById('auto-rate-today');
if (elToday) {
elToday.textContent =
`${fmtN(ar.today_runs || 0)} Runs · ${fmtN(ar.today_attempted || 0)} Anträge versucht · ${fmtN(ar.today_succeeded || 0)} enqueued`;
}
const arRecent = ar.recent || [];
const arRowsEl = document.getElementById('stand-autorate-rows');
if (arRowsEl) {
arRowsEl.innerHTML = arRecent.length
? arRecent.map(r => `
<tr>
<td style="font-family:var(--font-mono);font-size:11px;">${(r.started_at || '').slice(0, 16)}</td>
<td style="font-family:var(--font-mono);font-size:11px;">${r.source || '—'}</td>
<td>${r.bundesland || 'ALL'}</td>
<td style="text-align:right;">${fmtN(r.n_attempted)}</td>
<td style="text-align:right;">${fmtN(r.n_succeeded)}</td>
<td style="text-align:right;">${fmtN(r.n_skipped)}</td>
<td style="font-family:var(--font-mono);font-size:11px;opacity:0.65;">${r.error_summary || ''}</td>
</tr>`).join('')
: '<tr><td colspan="7" style="opacity:0.5;font-style:italic;">Noch kein Run heute oder in den letzten 5 Läufen.</td></tr>';
}
document.getElementById('stand-meta').textContent =
'Aktualisiert: ' + new Date().toLocaleTimeString('de-DE');
} catch (e) {
document.getElementById('stand-loading').textContent = 'Fehler: ' + e;
}
}
/* Sample-Link für Alternative Ansichten: aktuellster Antrag */
(async function () {
var el = document.getElementById('alt-views-sample');
if (!el) return;
try {
var r = await fetch('/api/assessments?limit=1');
var data = await r.json();
var a = (data && data[0]) || (data.results && data.results[0]) || null;
if (!a || !a.drucksache) {
el.textContent = 'Kein Beispiel-Antrag verfügbar.';
return;
}
var drs = encodeURIComponent(a.drucksache);
var labelFor = function (s) { return s.length > 70 ? s.slice(0,68) + '…' : s; };
el.innerHTML =
'<strong>Beispiel:</strong> ' + (a.bundesland || '?') + ' · Drs. ' + a.drucksache +
' — „' + labelFor((a.title || '')) + '"<br>' +
'· <a href="/antrag/' + drs + '">Standard (v3, Bürger:innen-Modus)</a><br>' +
'· <a href="/v2/antrag/' + drs + '">v2 — Profi-Modus, zwei Spalten</a>';
} catch (e) {
el.textContent = 'Konnte Beispiel-Link nicht laden.';
}
})();
loadStand();
let _standInterval = setInterval(loadStand, 30000);
// Pause-Polling wenn Tab versteckt (#183).
document.addEventListener('visibilitychange', function () {
if (document.hidden) {
if (_standInterval) { clearInterval(_standInterval); _standInterval = null; }
} else if (!_standInterval) {
loadStand();
_standInterval = setInterval(loadStand, 30000);
}
});
</script>
{% endblock %}