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>
123 lines
4.2 KiB
HTML
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 %}
|