2026-03-28 22:30:24 +01:00
|
|
|
<!DOCTYPE html>
|
|
|
|
|
<html lang="de">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
<title>Quellen — {{ app_name }}</title>
|
|
|
|
|
<style>
|
|
|
|
|
:root {
|
|
|
|
|
--color-darkgray: #5a5a5a;
|
|
|
|
|
--color-green: #889e33;
|
|
|
|
|
--color-blue: #009da5;
|
|
|
|
|
--color-lightgray: #bfbfbf;
|
|
|
|
|
--color-bg: #f5f5f5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
|
|
|
|
|
|
body {
|
|
|
|
|
font-family: 'Avenir', 'Segoe UI', sans-serif;
|
|
|
|
|
color: var(--color-darkgray);
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
background: var(--color-bg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header {
|
|
|
|
|
background: white;
|
|
|
|
|
padding: 1rem 2rem;
|
|
|
|
|
border-bottom: 1px solid var(--color-lightgray);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header h1 {
|
|
|
|
|
color: var(--color-blue);
|
|
|
|
|
font-size: 1.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header a {
|
|
|
|
|
color: var(--color-blue);
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.container {
|
|
|
|
|
max-width: 1200px;
|
|
|
|
|
margin: 2rem auto;
|
|
|
|
|
padding: 0 2rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h2 {
|
|
|
|
|
color: var(--color-blue);
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
padding-bottom: 0.5rem;
|
|
|
|
|
border-bottom: 2px solid var(--color-blue);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.intro {
|
|
|
|
|
background: white;
|
|
|
|
|
padding: 1.5rem;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
margin-bottom: 2rem;
|
|
|
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.programme-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
|
|
|
|
gap: 1.5rem;
|
|
|
|
|
margin-bottom: 2rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.programme-card {
|
|
|
|
|
background: white;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
padding: 1.5rem;
|
|
|
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.programme-card h3 {
|
|
|
|
|
color: var(--color-darkgray);
|
|
|
|
|
margin-bottom: 0.5rem;
|
|
|
|
|
font-size: 1.1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.programme-meta {
|
|
|
|
|
color: #888;
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.programme-badge {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
padding: 0.2rem 0.5rem;
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
margin-right: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.badge-spd { background: #e3000f; color: white; }
|
|
|
|
|
.badge-cdu { background: #000000; color: white; }
|
|
|
|
|
.badge-gruene { background: #46962b; color: white; }
|
|
|
|
|
.badge-fdp { background: #ffed00; color: black; }
|
|
|
|
|
.badge-afd { background: #009ee0; color: white; }
|
|
|
|
|
|
|
|
|
|
.badge-wahlprogramm { background: var(--color-blue); color: white; }
|
|
|
|
|
.badge-parteiprogramm { background: var(--color-green); color: white; }
|
|
|
|
|
|
|
|
|
|
.btn {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
padding: 0.5rem 1rem;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
border: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-primary {
|
|
|
|
|
background: var(--color-blue);
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-primary:hover {
|
|
|
|
|
background: #007b82;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-secondary {
|
|
|
|
|
background: var(--color-lightgray);
|
|
|
|
|
color: var(--color-darkgray);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-box {
|
|
|
|
|
background: white;
|
|
|
|
|
padding: 1.5rem;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
margin-bottom: 2rem;
|
|
|
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
|
|
|
gap: 1rem;
|
|
|
|
|
margin-top: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-item {
|
|
|
|
|
text-align: center;
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
background: var(--color-bg);
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-value {
|
|
|
|
|
font-size: 2rem;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
color: var(--color-blue);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-label {
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
color: #888;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.indexed { color: var(--color-green); }
|
|
|
|
|
.not-indexed { color: #888; }
|
|
|
|
|
|
|
|
|
|
.back-link {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
color: var(--color-blue);
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.back-link:hover {
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
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
|
|
|
{% set page_title = 'Quellen' %}
|
|
|
|
|
{% include "_header.html" %}
|
2026-03-28 22:30:24 +01:00
|
|
|
|
|
|
|
|
<div class="container">
|
|
|
|
|
<a href="/" class="back-link">← Zurück zur Übersicht</a>
|
|
|
|
|
|
|
|
|
|
<div class="intro">
|
|
|
|
|
<h2>Quellen & Referenzdokumente</h2>
|
|
|
|
|
<p>
|
|
|
|
|
Der GWÖ-Antragsprüfer vergleicht parlamentarische Anträge mit den Wahl- und Grundsatzprogrammen
|
|
|
|
|
der Parteien. Hier finden Sie alle verwendeten Originaldokumente zum Download.
|
|
|
|
|
</p>
|
|
|
|
|
<p style="margin-top: 0.5rem; color: #888;">
|
|
|
|
|
Die Programme werden semantisch indexiert, um relevante Passagen für jeden Antrag zu finden.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="status-box">
|
|
|
|
|
<h3>Indexierungsstatus</h3>
|
|
|
|
|
<div class="status-grid">
|
|
|
|
|
<div class="status-item">
|
|
|
|
|
<div class="status-value">{{ status.indexed }}</div>
|
|
|
|
|
<div class="status-label">Indexiert</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="status-item">
|
|
|
|
|
<div class="status-value">{{ status.total }}</div>
|
|
|
|
|
<div class="status-label">Gesamt</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="margin-top: 1rem;">
|
2026-04-10 22:13:30 +02:00
|
|
|
<button class="btn btn-primary" onclick="indexAll()" title="Erfordert Anmeldung">
|
|
|
|
|
🔒 Alle Programme indexieren
|
2026-03-28 22:30:24 +01:00
|
|
|
</button>
|
|
|
|
|
<span id="index-status" style="margin-left: 1rem; color: #888;"></span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-04-10 19:10:18 +02:00
|
|
|
{% for bl_name, bl_progs in wahlprogramme_grouped %}
|
|
|
|
|
<h2>{{ bl_name }}</h2>
|
2026-03-28 22:30:24 +01:00
|
|
|
<div class="programme-grid">
|
2026-04-10 19:10:18 +02:00
|
|
|
{% for prog in bl_progs %}
|
2026-04-10 18:40:13 +02:00
|
|
|
<div class="programme-card" style="display: flex; gap: 1rem;">
|
|
|
|
|
<a href="{{ prog.pdf_url }}" target="_blank" style="flex-shrink: 0;">
|
|
|
|
|
<img src="/api/programme/thumbnail/{{ prog.id }}" alt="{{ prog.name }}"
|
|
|
|
|
style="width: 80px; border: 1px solid #ddd; border-radius: 4px;"
|
|
|
|
|
loading="lazy" onerror="this.style.display='none'">
|
|
|
|
|
</a>
|
|
|
|
|
<div>
|
2026-03-28 22:30:24 +01:00
|
|
|
<h3>
|
|
|
|
|
<span class="programme-badge badge-{{ prog.partei|lower }}">{{ prog.partei }}</span>
|
|
|
|
|
{{ prog.name }}
|
|
|
|
|
</h3>
|
|
|
|
|
<div class="programme-meta">
|
|
|
|
|
<span class="programme-badge badge-wahlprogramm">Wahlprogramm</span>
|
|
|
|
|
{% if prog.bundesland %}{{ prog.bundesland }}{% endif %}
|
|
|
|
|
</div>
|
2026-04-10 18:40:13 +02:00
|
|
|
<div style="margin-top: 0.5rem;">
|
2026-03-28 22:30:24 +01:00
|
|
|
<a href="{{ prog.pdf_url }}" target="_blank" class="btn btn-primary">
|
|
|
|
|
📄 PDF herunterladen
|
|
|
|
|
</a>
|
|
|
|
|
{% for s in status.programmes if s.id == prog.id %}
|
|
|
|
|
<span style="margin-left: 0.5rem; font-size: 0.85rem;" class="{% if s.indexed %}indexed{% else %}not-indexed{% endif %}">
|
|
|
|
|
{% if s.indexed %}✓ {{ s.chunks }} Chunks{% else %}○ Nicht indexiert{% endif %}
|
|
|
|
|
</span>
|
|
|
|
|
{% endfor %}
|
|
|
|
|
</div>
|
2026-04-10 18:40:13 +02:00
|
|
|
</div>
|
2026-03-28 22:30:24 +01:00
|
|
|
</div>
|
|
|
|
|
{% endfor %}
|
|
|
|
|
</div>
|
2026-04-10 19:10:18 +02:00
|
|
|
{% endfor %}
|
2026-04-10 18:40:13 +02:00
|
|
|
|
2026-04-10 19:10:18 +02:00
|
|
|
<h2>Grundsatzprogramme (Bundesebene)</h2>
|
2026-03-28 22:30:24 +01:00
|
|
|
<div class="programme-grid">
|
2026-04-10 19:10:18 +02:00
|
|
|
{% for prog in grundsatzprogramme %}
|
2026-04-10 18:40:13 +02:00
|
|
|
<div class="programme-card" style="display: flex; gap: 1rem;">
|
|
|
|
|
<a href="{{ prog.pdf_url }}" target="_blank" style="flex-shrink: 0;">
|
|
|
|
|
<img src="/api/programme/thumbnail/{{ prog.id }}" alt="{{ prog.name }}"
|
|
|
|
|
style="width: 80px; border: 1px solid #ddd; border-radius: 4px;"
|
|
|
|
|
loading="lazy" onerror="this.style.display='none'">
|
|
|
|
|
</a>
|
|
|
|
|
<div>
|
2026-03-28 22:30:24 +01:00
|
|
|
<h3>
|
|
|
|
|
<span class="programme-badge badge-{{ prog.partei|lower }}">{{ prog.partei }}</span>
|
|
|
|
|
{{ prog.name }}
|
|
|
|
|
</h3>
|
|
|
|
|
<div class="programme-meta">
|
|
|
|
|
<span class="programme-badge badge-parteiprogramm">Grundsatzprogramm</span>
|
|
|
|
|
</div>
|
2026-04-10 18:40:13 +02:00
|
|
|
<div style="margin-top: 0.5rem;">
|
2026-03-28 22:30:24 +01:00
|
|
|
<a href="{{ prog.pdf_url }}" target="_blank" class="btn btn-primary">
|
|
|
|
|
📄 PDF herunterladen
|
|
|
|
|
</a>
|
|
|
|
|
{% for s in status.programmes if s.id == prog.id %}
|
|
|
|
|
<span style="margin-left: 0.5rem; font-size: 0.85rem;" class="{% if s.indexed %}indexed{% else %}not-indexed{% endif %}">
|
|
|
|
|
{% if s.indexed %}✓ {{ s.chunks }} Chunks{% else %}○ Nicht indexiert{% endif %}
|
|
|
|
|
</span>
|
|
|
|
|
{% endfor %}
|
|
|
|
|
</div>
|
2026-04-10 18:40:13 +02:00
|
|
|
</div>
|
2026-03-28 22:30:24 +01:00
|
|
|
</div>
|
|
|
|
|
{% endfor %}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="intro" style="margin-top: 2rem;">
|
|
|
|
|
<h3>Hinweise zur Methodik</h3>
|
|
|
|
|
<ul style="margin-left: 1.5rem; margin-top: 0.5rem;">
|
|
|
|
|
<li>Die Programme werden in semantische Chunks aufgeteilt (~400 Wörter)</li>
|
|
|
|
|
<li>Jeder Chunk wird mit einem Embedding-Modell (Qwen) vektorisiert</li>
|
|
|
|
|
<li>Bei der Analyse wird der Antrag ebenfalls vektorisiert</li>
|
|
|
|
|
<li>Die ähnlichsten Passagen werden als Kontext an das LLM übergeben</li>
|
|
|
|
|
<li>Das LLM zitiert nur, wenn eine Passage wirklich zur Argumentation passt</li>
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
async function indexAll() {
|
|
|
|
|
const statusEl = document.getElementById('index-status');
|
|
|
|
|
statusEl.textContent = '⏳ Indexierung gestartet...';
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
formData.append('all_programmes', 'true');
|
|
|
|
|
|
|
|
|
|
const resp = await fetch('/api/programme/index', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: formData
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const data = await resp.json();
|
|
|
|
|
statusEl.textContent = `✓ Indexierung läuft für ${data.programmes.length} Programme`;
|
|
|
|
|
|
|
|
|
|
// Reload after a delay
|
|
|
|
|
setTimeout(() => location.reload(), 30000);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
statusEl.textContent = `✗ Fehler: ${e.message}`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|