gwoe-antragspruefer/app/templates/v2/screens/admin_wahlprogramme.html
Dotty Dotter 565849bd84 feat(#139,#129,#138,#141): v2-Frontend (ECOnGOOD-CD), Login-Modal, Auto-DL, OG-Cards
v2-Frontend (#139, ECOnGOOD CD Manual Juni 2024):
- app/static/v2/: tokens.css, fonts.css, v2.css, Nunito-Sans woff2, Phosphor-Icons (21 SVGs)
- app/templates/v2/: base.html + 11 Screens + 8 Component-Macros
- AppShell mit Sidebar (Lesen/Pruefen/Daten/Admin), v2-Detail mit allen Features
  (ScoreHero, MatrixMini, QuoteCard, Redline, Fraktions-Scores)
- v2 ist jetzt Default unter / — classic unter /classic
- Login-Modal in v2-Topbar mit Tabs Anmelden/Registrieren (#129)
- Phosphor-Icons in Sidebar + Topbar mit dynamischem Theme-Toggle
- Keyboard-Shortcuts (j/k/Enter/Esc/?/path), Landtag-Suche, Antrag-Historie,
  Sort-Dropdown, Matrix-Feld-Info-Modal, Bookmarks/Comments/Voting/Share/Re-Analyze

Backend-Erweiterungen:
- main.py: ~30 neue Routes (/v2/*, /antrag/{ds}, /api/auth/{login,refresh,logout},
  /api/me/merkliste/*, /api/admin/*, /v2/admin/*, OG-Cards, etc.)
- og_card.py + og_template: Open-Graph-Bilder via Playwright (#141)
- wahlprogramm_fetch.py + wahlprogramm-links.yaml: SHA-Gate Auto-DL (#138)
- auswertungen.py: BL-Filter + get_wahlperioden Helper (#137)
- auth.py: Direct-Access-Grant + Refresh-Token-Cookie

Classic-Updates:
- Header-DRY via _header.html, Auswertungen redirected, Batch-Inline raus

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:55:57 +02:00

123 lines
4.2 KiB
HTML

{% extends "v2/base.html" %}
{% block title %}Wahlprogramme — GWÖ-Antragsprüfer{% endblock %}
{% set v2_active_nav = "admin_wahlprogramme" %}
{% 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;">
Wahlprogramm-Beschaffung
</h1>
<p style="font-size:12px;font-family:var(--font-mono);color:var(--ecg-dark);opacity:0.6;">
Halbautomatisch (#138) · nur Lücken mit Kandidaten-URL aus wahlprogramm-links.yaml
</p>
</div>
<div id="flash" class="v2-kasten" style="display:none;margin-bottom:16px;"></div>
{% if not missing %}
<div class="v2-kasten" style="opacity:0.7;">
<p style="font-family:var(--font-mono);font-size:13px;">Keine Lücken gefunden — alle registrierten Einträge haben eine Datei.</p>
</div>
{% else %}
<table style="width:100%;border-collapse:collapse;font-size:13px;font-family:var(--font-mono);">
<thead>
<tr style="border-bottom:1px solid var(--ecg-border, rgba(255,255,255,0.1));opacity:0.5;">
<th style="text-align:left;padding:8px 12px;">BL</th>
<th style="text-align:left;padding:8px 12px;">Partei</th>
<th style="text-align:left;padding:8px 12px;">Dateiname</th>
<th style="text-align:left;padding:8px 12px;">Kandidat-URL</th>
<th style="padding:8px 12px;"></th>
</tr>
</thead>
<tbody>
{% for entry in missing %}
{% set first_url = entry.kandidaten[0].url if entry.kandidaten else "" %}
<tr style="border-bottom:1px solid rgba(255,255,255,0.06);" data-bl="{{ entry.bl }}" data-partei="{{ entry.partei }}">
<td style="padding:10px 12px;font-weight:700;color:var(--ecg-teal);">{{ entry.bl }}</td>
<td style="padding:10px 12px;">{{ entry.partei }}</td>
<td style="padding:10px 12px;opacity:0.6;">
{{ entry.dateiname or "— noch nicht registriert —" }}
</td>
<td style="padding:10px 12px;max-width:380px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
<input type="text"
class="url-input"
value="{{ first_url }}"
style="width:100%;background:rgba(0,0,0,0.3);border:1px solid rgba(255,255,255,0.12);
border-radius:4px;padding:4px 8px;color:inherit;font-family:inherit;font-size:12px;"
>
</td>
<td style="padding:10px 12px;">
<button class="fetch-btn v2-btn"
style="font-size:11px;padding:4px 14px;cursor:pointer;"
onclick="fetchProgramm(this)">
Laden
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<p style="margin-top:24px;font-size:11px;font-family:var(--font-mono);opacity:0.4;">
Nach einem erfolgreichen Download müssen die Embeddings neu indexiert werden:
<code>python -m app.reindex_embeddings</code>
</p>
<script>
async function fetchProgramm(btn) {
const row = btn.closest("tr");
const bl = row.dataset.bl;
const partei = row.dataset.partei;
const url = row.querySelector(".url-input").value.trim();
const flash = document.getElementById("flash");
if (!url) {
showFlash("Keine URL eingetragen.", "error");
return;
}
btn.disabled = true;
btn.textContent = "…";
try {
const resp = await fetch("/api/admin/wahlprogramm-fetch", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({bl, partei, url}),
});
const data = await resp.json();
if (resp.ok) {
const note = data.changed ? "gespeichert" : "unverändert";
showFlash(
`${bl}/${partei}: ${note} — SHA ${data.sha256.slice(0,12)}`,
"ok",
);
if (data.changed) row.style.opacity = "0.4";
} else {
showFlash(`Fehler: ${data.detail || resp.status}`, "error");
}
} catch (err) {
showFlash(`Netzwerkfehler: ${err}`, "error");
} finally {
btn.disabled = false;
btn.textContent = "Laden";
}
}
function showFlash(msg, type) {
const el = document.getElementById("flash");
el.textContent = msg;
el.style.display = "block";
el.style.borderColor = type === "ok"
? "var(--ecg-green, #2d9e5f)"
: "var(--redline-contra, #e05252)";
clearTimeout(el._t);
el._t = setTimeout(() => { el.style.display = "none"; }, 8000);
}
</script>
{% endblock %}