feat: Stand-Dashboard, Score-Histogram, PM-Markdown, Live-Polling, Cluster-Indicator
Sechs zusammengehoerige UX/Performance-Erweiterungen:
**1. /v2/admin/stand — System-Stand-Dashboard**
KPI-Kacheln (Bewertungen, Plenum-Votes, Match, Vote-Orphans, News, PM-
Drafts, Bookmarks) + GWÖ-Score-Histogram + Per-BL-Tabelle + News-Source-
Tabelle. Auto-Refresh 30 s. Endpoint /api/admin/stand liefert alles in
einem Roundtrip. Nav-Eintrag "Stand" in der Admin-Sektion.
**2. /auswertungen Score-Histogram-Tab**
4. Tab "Score-Verteilung" mit Bar-Chart 0–10. Endpoint
/api/auswertungen/score-histogram liefert Buckets, optional gefiltert
nach Bundesland + Wahlperiode. Reagiert auf den globalen BL-Filter.
**3. PM-Body Markdown-Rendering**
Mini-Renderer im Modal: **bold** / __bold__ / *italic* / _italic_ /
- list-bullets / Doppel-Newline-Paragraphen. Kein externer Markdown-
Parser, keine neue Dependency. Body wird HTML-escaped, Patterns dann
zu Tags umgesetzt.
**4. Performance-Cache fuer themen_matching**
TTL-Cache (60 s) fuer aggregate_top_themen und aggregate_news_cluster.
Cache-Key inkl. aller Filter-Parameter. Automatische Invalidation in
news_aggregator.run_aggregator nach erfolgreichem Insert/Embed.
4 neue Tests fuer cache_get/set/clear-Verhalten.
**5. Stimmverhalten Banner Live-Update**
Statt setTimeout(800) jetzt pollQueueUntilDrained: alle 4 s
GET /api/queue/status, Banner zeigt pending + elapsed live. Bei
pending=0 zwei Polls in Folge: Banner + Stimmverhalten-Charts neu
laden. Max 5 Min Polling-Timeout. Bricht ab wenn Tab gewechselt wird.
**6. Antrag-Detail Cluster-Indicator**
News-Match-Box im Antrag-Detail laedt parallel /aktuelle-themen/cluster
und mappt URL → Cluster. Pro News-Card ein "🔗 Cluster (N News)"-Badge
mit Hover-Tooltip der anderen Cluster-Members. Macht thematische
Bündel sichtbar, ohne Pop-Out auf den Cluster-Tab.
Suite: 1088 → 1092 grün (4 Cache-Tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 02:49:06 +02:00
|
|
|
|
{% 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>
|
|
|
|
|
|
|
feat(#173): Vote-Orphans-Auto-Bewertung als Cron-Job + Tracking
Phase 3 (Vote-Orphans-Auto-Bewertung):
- Neue Tabelle `auto_rate_runs` (additiv) mit started_at, source,
bundesland, limit_requested, n_attempted/succeeded/failed/skipped,
error_summary.
- Neue DB-Helper: record_auto_rate_run, list_auto_rate_runs,
auto_rate_today_total.
- POST /api/auswertungen/vote-orphans/auto-rate erweitert um source,
daily_cap und Run-Persistenz. Throttled gegen Tagessumme.
- Neuer Endpoint GET /api/auto-rate-runs (admin) — letzte N Runs +
Tagessumme.
- scripts/auto-rate-orphans.sh: Cron-Wrapper (analog auto-fetch-news.sh)
mit MAX_PER_RUN=30 / MAX_PER_DAY=200 Defaults, BUNDESLAND-Filter
optional, ruft direkt die Python-Worker-Funktion via docker exec.
- Admin-Stand-Dashboard: KPI-Zeile "heute X Runs / Y versucht" + Tabelle
der letzten 5 Runs mit BL/Counts/Notiz.
Refs: #173, ADR 0010
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:02:33 +02:00
|
|
|
|
<!-- 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>
|
|
|
|
|
|
|
feat: Stand-Dashboard, Score-Histogram, PM-Markdown, Live-Polling, Cluster-Indicator
Sechs zusammengehoerige UX/Performance-Erweiterungen:
**1. /v2/admin/stand — System-Stand-Dashboard**
KPI-Kacheln (Bewertungen, Plenum-Votes, Match, Vote-Orphans, News, PM-
Drafts, Bookmarks) + GWÖ-Score-Histogram + Per-BL-Tabelle + News-Source-
Tabelle. Auto-Refresh 30 s. Endpoint /api/admin/stand liefert alles in
einem Roundtrip. Nav-Eintrag "Stand" in der Admin-Sektion.
**2. /auswertungen Score-Histogram-Tab**
4. Tab "Score-Verteilung" mit Bar-Chart 0–10. Endpoint
/api/auswertungen/score-histogram liefert Buckets, optional gefiltert
nach Bundesland + Wahlperiode. Reagiert auf den globalen BL-Filter.
**3. PM-Body Markdown-Rendering**
Mini-Renderer im Modal: **bold** / __bold__ / *italic* / _italic_ /
- list-bullets / Doppel-Newline-Paragraphen. Kein externer Markdown-
Parser, keine neue Dependency. Body wird HTML-escaped, Patterns dann
zu Tags umgesetzt.
**4. Performance-Cache fuer themen_matching**
TTL-Cache (60 s) fuer aggregate_top_themen und aggregate_news_cluster.
Cache-Key inkl. aller Filter-Parameter. Automatische Invalidation in
news_aggregator.run_aggregator nach erfolgreichem Insert/Embed.
4 neue Tests fuer cache_get/set/clear-Verhalten.
**5. Stimmverhalten Banner Live-Update**
Statt setTimeout(800) jetzt pollQueueUntilDrained: alle 4 s
GET /api/queue/status, Banner zeigt pending + elapsed live. Bei
pending=0 zwei Polls in Folge: Banner + Stimmverhalten-Charts neu
laden. Max 5 Min Polling-Timeout. Bricht ab wenn Tab gewechselt wird.
**6. Antrag-Detail Cluster-Indicator**
News-Match-Box im Antrag-Detail laedt parallel /aktuelle-themen/cluster
und mappt URL → Cluster. Pro News-Card ein "🔗 Cluster (N News)"-Badge
mit Hover-Tooltip der anderen Cluster-Members. Macht thematische
Bündel sichtbar, ohne Pop-Out auf den Cluster-Tab.
Suite: 1088 → 1092 grün (4 Cache-Tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 02:49:06 +02:00
|
|
|
|
<div id="stand-meta" style="font-family:var(--font-mono);font-size:11px;opacity:0.5;margin-top:1.5rem;"></div>
|
2026-05-07 11:29:06 +02:00
|
|
|
|
|
|
|
|
|
|
{# 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/<Drucksache></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>
|
feat: Stand-Dashboard, Score-Histogram, PM-Markdown, Live-Polling, Cluster-Indicator
Sechs zusammengehoerige UX/Performance-Erweiterungen:
**1. /v2/admin/stand — System-Stand-Dashboard**
KPI-Kacheln (Bewertungen, Plenum-Votes, Match, Vote-Orphans, News, PM-
Drafts, Bookmarks) + GWÖ-Score-Histogram + Per-BL-Tabelle + News-Source-
Tabelle. Auto-Refresh 30 s. Endpoint /api/admin/stand liefert alles in
einem Roundtrip. Nav-Eintrag "Stand" in der Admin-Sektion.
**2. /auswertungen Score-Histogram-Tab**
4. Tab "Score-Verteilung" mit Bar-Chart 0–10. Endpoint
/api/auswertungen/score-histogram liefert Buckets, optional gefiltert
nach Bundesland + Wahlperiode. Reagiert auf den globalen BL-Filter.
**3. PM-Body Markdown-Rendering**
Mini-Renderer im Modal: **bold** / __bold__ / *italic* / _italic_ /
- list-bullets / Doppel-Newline-Paragraphen. Kein externer Markdown-
Parser, keine neue Dependency. Body wird HTML-escaped, Patterns dann
zu Tags umgesetzt.
**4. Performance-Cache fuer themen_matching**
TTL-Cache (60 s) fuer aggregate_top_themen und aggregate_news_cluster.
Cache-Key inkl. aller Filter-Parameter. Automatische Invalidation in
news_aggregator.run_aggregator nach erfolgreichem Insert/Embed.
4 neue Tests fuer cache_get/set/clear-Verhalten.
**5. Stimmverhalten Banner Live-Update**
Statt setTimeout(800) jetzt pollQueueUntilDrained: alle 4 s
GET /api/queue/status, Banner zeigt pending + elapsed live. Bei
pending=0 zwei Polls in Folge: Banner + Stimmverhalten-Charts neu
laden. Max 5 Min Polling-Timeout. Bricht ab wenn Tab gewechselt wird.
**6. Antrag-Detail Cluster-Indicator**
News-Match-Box im Antrag-Detail laedt parallel /aktuelle-themen/cluster
und mappt URL → Cluster. Pro News-Card ein "🔗 Cluster (N News)"-Badge
mit Hover-Tooltip der anderen Cluster-Members. Macht thematische
Bündel sichtbar, ohne Pop-Out auf den Cluster-Tab.
Suite: 1088 → 1092 grün (4 Cache-Tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 02:49:06 +02:00
|
|
|
|
</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('');
|
|
|
|
|
|
|
feat(#173): Vote-Orphans-Auto-Bewertung als Cron-Job + Tracking
Phase 3 (Vote-Orphans-Auto-Bewertung):
- Neue Tabelle `auto_rate_runs` (additiv) mit started_at, source,
bundesland, limit_requested, n_attempted/succeeded/failed/skipped,
error_summary.
- Neue DB-Helper: record_auto_rate_run, list_auto_rate_runs,
auto_rate_today_total.
- POST /api/auswertungen/vote-orphans/auto-rate erweitert um source,
daily_cap und Run-Persistenz. Throttled gegen Tagessumme.
- Neuer Endpoint GET /api/auto-rate-runs (admin) — letzte N Runs +
Tagessumme.
- scripts/auto-rate-orphans.sh: Cron-Wrapper (analog auto-fetch-news.sh)
mit MAX_PER_RUN=30 / MAX_PER_DAY=200 Defaults, BUNDESLAND-Filter
optional, ruft direkt die Python-Worker-Funktion via docker exec.
- Admin-Stand-Dashboard: KPI-Zeile "heute X Runs / Y versucht" + Tabelle
der letzten 5 Runs mit BL/Counts/Notiz.
Refs: #173, ADR 0010
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:02:33 +02:00
|
|
|
|
// 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>';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: Stand-Dashboard, Score-Histogram, PM-Markdown, Live-Polling, Cluster-Indicator
Sechs zusammengehoerige UX/Performance-Erweiterungen:
**1. /v2/admin/stand — System-Stand-Dashboard**
KPI-Kacheln (Bewertungen, Plenum-Votes, Match, Vote-Orphans, News, PM-
Drafts, Bookmarks) + GWÖ-Score-Histogram + Per-BL-Tabelle + News-Source-
Tabelle. Auto-Refresh 30 s. Endpoint /api/admin/stand liefert alles in
einem Roundtrip. Nav-Eintrag "Stand" in der Admin-Sektion.
**2. /auswertungen Score-Histogram-Tab**
4. Tab "Score-Verteilung" mit Bar-Chart 0–10. Endpoint
/api/auswertungen/score-histogram liefert Buckets, optional gefiltert
nach Bundesland + Wahlperiode. Reagiert auf den globalen BL-Filter.
**3. PM-Body Markdown-Rendering**
Mini-Renderer im Modal: **bold** / __bold__ / *italic* / _italic_ /
- list-bullets / Doppel-Newline-Paragraphen. Kein externer Markdown-
Parser, keine neue Dependency. Body wird HTML-escaped, Patterns dann
zu Tags umgesetzt.
**4. Performance-Cache fuer themen_matching**
TTL-Cache (60 s) fuer aggregate_top_themen und aggregate_news_cluster.
Cache-Key inkl. aller Filter-Parameter. Automatische Invalidation in
news_aggregator.run_aggregator nach erfolgreichem Insert/Embed.
4 neue Tests fuer cache_get/set/clear-Verhalten.
**5. Stimmverhalten Banner Live-Update**
Statt setTimeout(800) jetzt pollQueueUntilDrained: alle 4 s
GET /api/queue/status, Banner zeigt pending + elapsed live. Bei
pending=0 zwei Polls in Folge: Banner + Stimmverhalten-Charts neu
laden. Max 5 Min Polling-Timeout. Bricht ab wenn Tab gewechselt wird.
**6. Antrag-Detail Cluster-Indicator**
News-Match-Box im Antrag-Detail laedt parallel /aktuelle-themen/cluster
und mappt URL → Cluster. Pro News-Card ein "🔗 Cluster (N News)"-Badge
mit Hover-Tooltip der anderen Cluster-Members. Macht thematische
Bündel sichtbar, ohne Pop-Out auf den Cluster-Tab.
Suite: 1088 → 1092 grün (4 Cache-Tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 02:49:06 +02:00
|
|
|
|
document.getElementById('stand-meta').textContent =
|
|
|
|
|
|
'Aktualisiert: ' + new Date().toLocaleTimeString('de-DE');
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
document.getElementById('stand-loading').textContent = 'Fehler: ' + e;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-07 11:29:06 +02:00
|
|
|
|
/* 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.';
|
|
|
|
|
|
}
|
|
|
|
|
|
})();
|
|
|
|
|
|
|
feat: Stand-Dashboard, Score-Histogram, PM-Markdown, Live-Polling, Cluster-Indicator
Sechs zusammengehoerige UX/Performance-Erweiterungen:
**1. /v2/admin/stand — System-Stand-Dashboard**
KPI-Kacheln (Bewertungen, Plenum-Votes, Match, Vote-Orphans, News, PM-
Drafts, Bookmarks) + GWÖ-Score-Histogram + Per-BL-Tabelle + News-Source-
Tabelle. Auto-Refresh 30 s. Endpoint /api/admin/stand liefert alles in
einem Roundtrip. Nav-Eintrag "Stand" in der Admin-Sektion.
**2. /auswertungen Score-Histogram-Tab**
4. Tab "Score-Verteilung" mit Bar-Chart 0–10. Endpoint
/api/auswertungen/score-histogram liefert Buckets, optional gefiltert
nach Bundesland + Wahlperiode. Reagiert auf den globalen BL-Filter.
**3. PM-Body Markdown-Rendering**
Mini-Renderer im Modal: **bold** / __bold__ / *italic* / _italic_ /
- list-bullets / Doppel-Newline-Paragraphen. Kein externer Markdown-
Parser, keine neue Dependency. Body wird HTML-escaped, Patterns dann
zu Tags umgesetzt.
**4. Performance-Cache fuer themen_matching**
TTL-Cache (60 s) fuer aggregate_top_themen und aggregate_news_cluster.
Cache-Key inkl. aller Filter-Parameter. Automatische Invalidation in
news_aggregator.run_aggregator nach erfolgreichem Insert/Embed.
4 neue Tests fuer cache_get/set/clear-Verhalten.
**5. Stimmverhalten Banner Live-Update**
Statt setTimeout(800) jetzt pollQueueUntilDrained: alle 4 s
GET /api/queue/status, Banner zeigt pending + elapsed live. Bei
pending=0 zwei Polls in Folge: Banner + Stimmverhalten-Charts neu
laden. Max 5 Min Polling-Timeout. Bricht ab wenn Tab gewechselt wird.
**6. Antrag-Detail Cluster-Indicator**
News-Match-Box im Antrag-Detail laedt parallel /aktuelle-themen/cluster
und mappt URL → Cluster. Pro News-Card ein "🔗 Cluster (N News)"-Badge
mit Hover-Tooltip der anderen Cluster-Members. Macht thematische
Bündel sichtbar, ohne Pop-Out auf den Cluster-Tab.
Suite: 1088 → 1092 grün (4 Cache-Tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 02:49:06 +02:00
|
|
|
|
loadStand();
|
2026-05-07 09:01:48 +02:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
feat: Stand-Dashboard, Score-Histogram, PM-Markdown, Live-Polling, Cluster-Indicator
Sechs zusammengehoerige UX/Performance-Erweiterungen:
**1. /v2/admin/stand — System-Stand-Dashboard**
KPI-Kacheln (Bewertungen, Plenum-Votes, Match, Vote-Orphans, News, PM-
Drafts, Bookmarks) + GWÖ-Score-Histogram + Per-BL-Tabelle + News-Source-
Tabelle. Auto-Refresh 30 s. Endpoint /api/admin/stand liefert alles in
einem Roundtrip. Nav-Eintrag "Stand" in der Admin-Sektion.
**2. /auswertungen Score-Histogram-Tab**
4. Tab "Score-Verteilung" mit Bar-Chart 0–10. Endpoint
/api/auswertungen/score-histogram liefert Buckets, optional gefiltert
nach Bundesland + Wahlperiode. Reagiert auf den globalen BL-Filter.
**3. PM-Body Markdown-Rendering**
Mini-Renderer im Modal: **bold** / __bold__ / *italic* / _italic_ /
- list-bullets / Doppel-Newline-Paragraphen. Kein externer Markdown-
Parser, keine neue Dependency. Body wird HTML-escaped, Patterns dann
zu Tags umgesetzt.
**4. Performance-Cache fuer themen_matching**
TTL-Cache (60 s) fuer aggregate_top_themen und aggregate_news_cluster.
Cache-Key inkl. aller Filter-Parameter. Automatische Invalidation in
news_aggregator.run_aggregator nach erfolgreichem Insert/Embed.
4 neue Tests fuer cache_get/set/clear-Verhalten.
**5. Stimmverhalten Banner Live-Update**
Statt setTimeout(800) jetzt pollQueueUntilDrained: alle 4 s
GET /api/queue/status, Banner zeigt pending + elapsed live. Bei
pending=0 zwei Polls in Folge: Banner + Stimmverhalten-Charts neu
laden. Max 5 Min Polling-Timeout. Bricht ab wenn Tab gewechselt wird.
**6. Antrag-Detail Cluster-Indicator**
News-Match-Box im Antrag-Detail laedt parallel /aktuelle-themen/cluster
und mappt URL → Cluster. Pro News-Card ein "🔗 Cluster (N News)"-Badge
mit Hover-Tooltip der anderen Cluster-Members. Macht thematische
Bündel sichtbar, ohne Pop-Out auf den Cluster-Tab.
Suite: 1088 → 1092 grün (4 Cache-Tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 02:49:06 +02:00
|
|
|
|
</script>
|
|
|
|
|
|
{% endblock %}
|