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>{{ 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);
|
2026-04-07 13:48:55 +02:00
|
|
|
min-height: 100vh;
|
|
|
|
|
min-height: 100dvh;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
2026-03-28 22:30:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Header */
|
|
|
|
|
.header {
|
|
|
|
|
background: white;
|
|
|
|
|
padding: 1rem 2rem;
|
|
|
|
|
border-bottom: 1px solid var(--color-lightgray);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 1rem;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header h1 {
|
|
|
|
|
color: var(--color-blue);
|
|
|
|
|
font-size: 1.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header .subtitle {
|
|
|
|
|
color: var(--color-darkgray);
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.bundesland-select {
|
|
|
|
|
padding: 0.5rem 1rem;
|
|
|
|
|
border: 1px solid var(--color-lightgray);
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
background: white;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.bundesland-select:focus {
|
|
|
|
|
outline: none;
|
|
|
|
|
border-color: var(--color-blue);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Main Layout */
|
|
|
|
|
.main-container {
|
|
|
|
|
display: flex;
|
2026-04-07 13:48:55 +02:00
|
|
|
flex: 1;
|
|
|
|
|
min-height: 0;
|
2026-03-28 22:30:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Left Panel - List */
|
|
|
|
|
.list-panel {
|
|
|
|
|
width: 400px;
|
|
|
|
|
min-width: 300px;
|
|
|
|
|
background: white;
|
|
|
|
|
border-right: 1px solid var(--color-lightgray);
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-header {
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
border-bottom: 1px solid var(--color-lightgray);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search-box {
|
|
|
|
|
flex: 1;
|
|
|
|
|
padding: 0.75rem;
|
|
|
|
|
border: 1px solid var(--color-lightgray);
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-landtag {
|
|
|
|
|
padding: 0.5rem 0.75rem;
|
|
|
|
|
background: var(--color-green);
|
|
|
|
|
color: white;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-landtag:hover {
|
|
|
|
|
background: #728a2b;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-landtag:disabled {
|
|
|
|
|
background: #ccc;
|
|
|
|
|
cursor: wait;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-filters {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
margin-top: 0.5rem;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.filter-btn {
|
|
|
|
|
padding: 0.25rem 0.75rem;
|
|
|
|
|
border: 1px solid var(--color-lightgray);
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
background: white;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.filter-btn.active {
|
|
|
|
|
background: var(--color-blue);
|
|
|
|
|
color: white;
|
|
|
|
|
border-color: var(--color-blue);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-content {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-item {
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
border-bottom: 1px solid var(--color-lightgray);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: background 0.2s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-item:hover {
|
|
|
|
|
background: var(--color-bg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-item.active {
|
|
|
|
|
background: #e8f4f5;
|
|
|
|
|
border-left: 3px solid var(--color-blue);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-item-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
margin-bottom: 0.25rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-item-id {
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
color: var(--color-blue);
|
|
|
|
|
}
|
Bundesland filter & transparency: stringent split + visible source (#8)
Brings the Bundesland-Dropdown from a cosmetic header widget to a real
filter that propagates through every layer (Listing, internal search,
statistics, party/tag filters, upload mode), and at the same time makes
the source parliament visible in every place where assessments from
multiple bundesländer can be mixed.
Backend
- database.get_all_assessments(bundesland=None) — new optional filter,
"ALL" treated as None.
- database.search_assessments — bug fix: previous `if bundesland:`
branch incorrectly added a `WHERE bundesland='ALL'` clause; now
guarded with `bundesland and bundesland != "ALL"`.
- main.list_assessments — accepts ?bundesland= query param, includes the
bundesland field in the response so the frontend can render badges.
- main.get_single_assessment — also includes bundesland in the response
so the detail header can show the source parlament.
- main.search_landtag — early HTTP 400 when bundesland is missing or
"ALL"; the live Landtag adapter cannot serve a synthetic Bundesweit
request.
- main.index() and main.list_bundeslaender — synthetic "🌍 Bundesweit"
entry prepended to the bundesländer list (kept out of bundeslaender.py
on purpose — ALL is not a real state). Both endpoints additionally
expose a parlament_names map so the frontend can render the source
parliament without an extra round-trip.
Report (PDF + HTML)
- generate_html_report / generate_pdf_report — new optional bundesland
parameter. When set, the report header carries the parliament name
("Landtag von Sachsen-Anhalt", "Landtag Nordrhein-Westfalen", …)
beside the title. Three call sites updated: run_analysis,
run_drucksache_analysis, download_assessment_pdf.
Frontend (templates/index.html)
- Header dropdown gets the synthetic ALL entry as first option;
initial currentBundesland is now 'ALL' (was 'NRW').
- localStorage persistence: changeBundesland writes, DOMContentLoaded
reads and validates against the visible options.
- changeBundesland resets the score / party / tag filter state, syncs
the upload-mode bundesland select, disables the Landtag-Suche button
+ tooltip when ALL, and toggles a data-mode attribute on
.list-content (used by CSS to show/hide the per-item bundesland
badge).
- loadAssessments now sends ?bundesland=… so the API does the actual
filtering. updateStats renders an additional per-bundesland average
block (Ø NRW: x · Ø LSA: y) when in ALL mode and the loaded list
spans more than one bundesland.
- renderList prepends a small "bl-badge" beside the Drucksachen-Nummer.
Hidden in single-bundesland mode via CSS selector to avoid clutter.
- showDetail header now shows the parliament name as its own line
(.detail-parlament).
- searchLandtag has an early-out alert if currentBundesland === 'ALL',
saving a network round-trip.
- Upload-Mode bundesland select now starts with a "— Bundesland wählen
—" placeholder (no auto-default), and startAnalysis validates that a
concrete bundesland was chosen.
CSS
- .bl-badge plus the .list-content[data-mode="single"] hide rule.
- .detail-parlament for the detail header line.
- .header-parlament for the PDF report header line.
Resolves #8.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 23:00:39 +02:00
|
|
|
|
|
|
|
|
/* Bundesland-Badge: Im Listen-Item links neben der Drucksachen-Nummer.
|
|
|
|
|
Im Bundesland-spezifischen Modus per data-mode="single" am Container
|
|
|
|
|
ausgeblendet (redundant, da alle Einträge demselben Land zugehören). */
|
|
|
|
|
.bl-badge {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
padding: 1px 6px;
|
|
|
|
|
margin-right: 0.4rem;
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
letter-spacing: 0.02em;
|
|
|
|
|
color: var(--color-blue);
|
|
|
|
|
border: 1px solid var(--color-blue);
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
vertical-align: middle;
|
|
|
|
|
}
|
|
|
|
|
.list-content[data-mode="single"] .bl-badge { display: none; }
|
|
|
|
|
|
|
|
|
|
/* Detail-Header: Parlament-Name unter dem Titel, vor der Drucksache-Zeile */
|
|
|
|
|
.detail-parlament {
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
color: var(--color-blue);
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
margin: 0.2rem 0 0.1rem;
|
|
|
|
|
}
|
2026-03-28 22:30:24 +01:00
|
|
|
|
|
|
|
|
.list-item-score {
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
padding: 0.1rem 0.5rem;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Neue Skala -5 bis +5 */
|
|
|
|
|
.score-high { background: #155724; color: white; } /* +4 bis +5: dunkelgrün */
|
|
|
|
|
.score-mid { background: #889e33; color: white; } /* +2 bis +3: GWÖ-grün */
|
|
|
|
|
.score-neutral { background: #6c757d; color: white; } /* 0 bis +1: grau */
|
|
|
|
|
.score-low { background: #fd7e14; color: white; } /* -1 bis -2: orange */
|
|
|
|
|
.score-negative { background: #dc3545; color: white; } /* -3 bis -5: rot */
|
|
|
|
|
.score-none { background: #e2e3e5; color: #383d41; }
|
|
|
|
|
|
|
|
|
|
.status-unchecked {
|
|
|
|
|
background: #fff3cd;
|
|
|
|
|
color: #856404;
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
padding: 0.2rem 0.5rem;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-checked {
|
|
|
|
|
background: #d4edda;
|
|
|
|
|
color: #155724;
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
padding: 0.2rem 0.5rem;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-check-now {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
padding: 0.3rem 0.75rem;
|
|
|
|
|
background: var(--color-blue);
|
|
|
|
|
color: white;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
margin-top: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-check-now:hover {
|
|
|
|
|
background: #007b82;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-check-now:disabled {
|
|
|
|
|
background: #ccc;
|
|
|
|
|
cursor: wait;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-item-title {
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
color: var(--color-darkgray);
|
|
|
|
|
margin-bottom: 0.25rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-item-meta {
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
color: #888;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-item-tags {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 0.25rem;
|
|
|
|
|
margin-top: 0.5rem;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tag {
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
padding: 0.1rem 0.4rem;
|
|
|
|
|
background: var(--color-bg);
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Right Panel - Detail */
|
|
|
|
|
.detail-panel {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
padding: 2rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-placeholder {
|
|
|
|
|
text-align: center;
|
|
|
|
|
color: #888;
|
|
|
|
|
padding: 4rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-card {
|
|
|
|
|
background: white;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
padding: 2rem;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-title {
|
|
|
|
|
font-size: 1.3rem;
|
|
|
|
|
color: var(--color-blue);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-id {
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
color: #888;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.score-display {
|
|
|
|
|
text-align: center;
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.score-big {
|
|
|
|
|
font-size: 3rem;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
color: var(--color-blue);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.score-label {
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
color: #888;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.matrix-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
margin: 1rem 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.matrix-item {
|
|
|
|
|
padding: 0.75rem;
|
|
|
|
|
background: var(--color-bg);
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.matrix-label {
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.matrix-rating {
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.rating-pos { color: var(--color-green); }
|
|
|
|
|
.rating-neg { color: #dc3545; }
|
|
|
|
|
.rating-neutral { color: #888; }
|
|
|
|
|
|
|
|
|
|
/* Matrix Table (5x5 Grid wie im PDF) */
|
|
|
|
|
.matrix-table {
|
|
|
|
|
width: 100%;
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
margin: 0.5rem 0;
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.matrix-table th, .matrix-table td {
|
|
|
|
|
border: 1px solid var(--color-lightgray);
|
|
|
|
|
padding: 0.4rem 0.5rem;
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.matrix-table thead th {
|
|
|
|
|
background: var(--color-blue);
|
|
|
|
|
color: white;
|
|
|
|
|
font-weight: normal;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.matrix-table tbody th {
|
|
|
|
|
background: var(--color-bg);
|
|
|
|
|
text-align: left;
|
|
|
|
|
font-weight: normal;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.matrix-table .positive {
|
|
|
|
|
background: var(--color-green);
|
|
|
|
|
color: white;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.matrix-table .negative {
|
|
|
|
|
background: #dc3545;
|
|
|
|
|
color: white;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.matrix-table .neutral {
|
|
|
|
|
background: #f0f0f0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Themen Tags in Detail */
|
|
|
|
|
.themen-tags .tag {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
margin-right: 0.5rem;
|
|
|
|
|
margin-bottom: 0.25rem;
|
|
|
|
|
padding: 0.25rem 0.5rem;
|
|
|
|
|
background: var(--color-blue);
|
|
|
|
|
color: white;
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Kernpunkte Liste */
|
|
|
|
|
.kernpunkte-list {
|
|
|
|
|
margin-left: 1.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.kernpunkte-list li {
|
|
|
|
|
margin-bottom: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.section-title {
|
|
|
|
|
font-size: 1.1rem;
|
|
|
|
|
color: var(--color-darkgray);
|
|
|
|
|
margin: 1.5rem 0 0.75rem;
|
|
|
|
|
padding-bottom: 0.5rem;
|
|
|
|
|
border-bottom: 1px solid var(--color-lightgray);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.text-block {
|
|
|
|
|
background: var(--color-bg);
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
margin: 0.5rem 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.strength-list, .weakness-list {
|
|
|
|
|
list-style: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.strength-list li::before {
|
|
|
|
|
content: "✓ ";
|
|
|
|
|
color: var(--color-green);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.weakness-list li::before {
|
|
|
|
|
content: "✗ ";
|
|
|
|
|
color: #dc3545;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-pdf {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
padding: 0.75rem 1.5rem;
|
|
|
|
|
background: var(--color-blue);
|
|
|
|
|
color: white;
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
margin-top: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-pdf:hover {
|
|
|
|
|
background: #007b82;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Upload Tab */
|
|
|
|
|
.upload-section {
|
|
|
|
|
background: white;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
padding: 2rem;
|
|
|
|
|
max-width: 600px;
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tabs {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 1rem;
|
|
|
|
|
margin-bottom: 1.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab-btn {
|
|
|
|
|
padding: 0.5rem 1rem;
|
|
|
|
|
border: none;
|
|
|
|
|
background: none;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
border-bottom: 2px solid transparent;
|
|
|
|
|
color: var(--color-darkgray);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab-btn.active {
|
|
|
|
|
border-bottom-color: var(--color-blue);
|
|
|
|
|
color: var(--color-blue);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
textarea {
|
|
|
|
|
width: 100%;
|
|
|
|
|
min-height: 200px;
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
border: 1px solid var(--color-lightgray);
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
resize: vertical;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.file-drop {
|
|
|
|
|
border: 2px dashed var(--color-lightgray);
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
padding: 3rem;
|
|
|
|
|
text-align: center;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.file-drop:hover {
|
|
|
|
|
border-color: var(--color-blue);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-analyze {
|
|
|
|
|
display: block;
|
|
|
|
|
width: 100%;
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
background: var(--color-green);
|
|
|
|
|
color: white;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 1rem;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
margin-top: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-analyze:hover {
|
|
|
|
|
background: #728a2b;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-analyze:disabled {
|
|
|
|
|
background: #ccc;
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Mode Toggle */
|
|
|
|
|
.mode-toggle {
|
|
|
|
|
display: flex;
|
|
|
|
|
background: var(--color-bg);
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
padding: 0.25rem;
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mode-btn {
|
|
|
|
|
padding: 0.5rem 1rem;
|
|
|
|
|
border: none;
|
|
|
|
|
background: none;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
color: var(--color-darkgray);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mode-btn.active {
|
|
|
|
|
background: white;
|
|
|
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Loading */
|
|
|
|
|
.loading {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 1rem;
|
|
|
|
|
padding: 2rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.spinner {
|
|
|
|
|
width: 30px;
|
|
|
|
|
height: 30px;
|
|
|
|
|
border: 3px solid var(--color-lightgray);
|
|
|
|
|
border-top-color: var(--color-blue);
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
animation: spin 1s linear infinite;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes spin {
|
|
|
|
|
to { transform: rotate(360deg); }
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 13:48:55 +02:00
|
|
|
/* Mobile Back-to-List Button (nur Mobile sichtbar) */
|
|
|
|
|
.btn-back-mobile {
|
|
|
|
|
display: none;
|
|
|
|
|
padding: 0.5rem 0.75rem;
|
|
|
|
|
background: var(--color-bg);
|
|
|
|
|
border: 1px solid var(--color-lightgray);
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
color: var(--color-darkgray);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 22:30:24 +01:00
|
|
|
/* Responsive */
|
|
|
|
|
@media (max-width: 900px) {
|
2026-04-07 13:48:55 +02:00
|
|
|
.header {
|
|
|
|
|
padding: 0.75rem 1rem;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header h1 {
|
|
|
|
|
font-size: 1.2rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header .subtitle {
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
flex-basis: 100%;
|
|
|
|
|
order: 10;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mode-toggle {
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mode-btn {
|
|
|
|
|
padding: 0.4rem 0.6rem;
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 22:30:24 +01:00
|
|
|
.main-container {
|
|
|
|
|
flex-direction: column;
|
2026-04-07 13:48:55 +02:00
|
|
|
flex: 1;
|
|
|
|
|
min-height: 0;
|
2026-03-28 22:30:24 +01:00
|
|
|
}
|
2026-04-07 13:48:55 +02:00
|
|
|
|
2026-03-28 22:30:24 +01:00
|
|
|
.list-panel {
|
|
|
|
|
width: 100%;
|
2026-04-07 13:48:55 +02:00
|
|
|
min-width: 0;
|
|
|
|
|
border-right: none;
|
|
|
|
|
border-bottom: 1px solid var(--color-lightgray);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-content {
|
2026-03-28 22:30:24 +01:00
|
|
|
max-height: 50vh;
|
|
|
|
|
}
|
2026-04-07 13:48:55 +02:00
|
|
|
|
2026-03-28 22:30:24 +01:00
|
|
|
.detail-panel {
|
|
|
|
|
padding: 1rem;
|
2026-04-07 13:48:55 +02:00
|
|
|
overflow: visible;
|
|
|
|
|
flex: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-back-mobile {
|
|
|
|
|
display: inline-block;
|
2026-03-28 22:30:24 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-07 13:48:55 +02:00
|
|
|
|
|
|
|
|
@media (max-width: 600px) {
|
|
|
|
|
.header h1 { font-size: 1.1rem; }
|
|
|
|
|
.header .subtitle { display: none; }
|
|
|
|
|
.detail-card { padding: 1rem; }
|
|
|
|
|
.matrix-table { font-size: 0.7rem; }
|
|
|
|
|
.matrix-table th, .matrix-table td { padding: 0.25rem 0.3rem; }
|
|
|
|
|
.matrix-grid { grid-template-columns: 1fr; }
|
|
|
|
|
.upload-section { padding: 1rem; }
|
|
|
|
|
.file-drop { padding: 1.5rem; }
|
|
|
|
|
.score-big { font-size: 2.2rem; }
|
|
|
|
|
.detail-header { flex-wrap: wrap; gap: 0.5rem; }
|
|
|
|
|
}
|
2026-03-28 22:30:24 +01:00
|
|
|
|
|
|
|
|
/* Stats Bar */
|
|
|
|
|
.stats-bar {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 2rem;
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
background: var(--color-bg);
|
|
|
|
|
border-bottom: 1px solid var(--color-lightgray);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat {
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-value {
|
|
|
|
|
font-size: 1.5rem;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
color: var(--color-blue);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-label {
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
color: #888;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<header class="header">
|
|
|
|
|
<h1>{{ app_name }}</h1>
|
|
|
|
|
<span class="subtitle">Gemeinwohl-Matrix 2.0 für Gemeinden</span>
|
|
|
|
|
<select class="bundesland-select" id="bundesland-select" onchange="changeBundesland(this.value)">
|
|
|
|
|
{% for bl in bundeslaender %}
|
|
|
|
|
<option value="{{ bl.code }}" {% if bl.active %}{% else %}disabled{% endif %}>
|
|
|
|
|
{{ bl.name }}{% if not bl.active %} (bald){% endif %}
|
|
|
|
|
</option>
|
|
|
|
|
{% endfor %}
|
|
|
|
|
</select>
|
|
|
|
|
<div class="mode-toggle">
|
|
|
|
|
<button class="mode-btn active" onclick="showMode('browse')">📋 Durchsuchen</button>
|
|
|
|
|
<button class="mode-btn" onclick="showMode('tags')">🏷️ Tags</button>
|
|
|
|
|
<button class="mode-btn" onclick="showMode('upload')">📤 Prüfen</button>
|
|
|
|
|
<a href="/quellen" class="mode-btn" style="text-decoration: none;">📚 Quellen</a>
|
2026-04-09 11:27:29 +02:00
|
|
|
<a href="/auswertungen" class="mode-btn" style="text-decoration: none;">📈 Auswertungen</a>
|
2026-03-28 22:30:24 +01:00
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<div class="main-container" id="browse-mode">
|
|
|
|
|
<!-- Left: List -->
|
|
|
|
|
<aside class="list-panel">
|
|
|
|
|
<div class="list-header">
|
2026-04-09 11:27:29 +02:00
|
|
|
<!--
|
|
|
|
|
#16: zwei klar getrennte Suchfelder. Das erste filtert
|
|
|
|
|
in der DB der bereits geprüften Anträge (Live, debounced).
|
|
|
|
|
Das zweite triggert per Enter oder Button eine Live-
|
|
|
|
|
Anfrage gegen den Landtag-Adapter. Beide schreiben in
|
|
|
|
|
dieselbe Liste, unterscheiden sich aber visuell und
|
|
|
|
|
semantisch klar.
|
|
|
|
|
-->
|
|
|
|
|
<div class="search-row" style="flex-direction: column; gap: 0.4rem;">
|
|
|
|
|
<div style="display: flex; gap: 0.4rem; width: 100%;">
|
|
|
|
|
<input type="text" class="search-box" id="search-input"
|
|
|
|
|
placeholder="📊 Suche in geprüften Anträgen (DB)…"
|
|
|
|
|
oninput="debounceSearch(this.value)"
|
|
|
|
|
style="flex: 1;">
|
|
|
|
|
</div>
|
|
|
|
|
<div style="display: flex; gap: 0.4rem; width: 100%;">
|
|
|
|
|
<input type="text" class="search-box" id="landtag-search-input"
|
|
|
|
|
placeholder="🏛️ Im Landtag suchen (live)…"
|
|
|
|
|
onkeydown="if(event.key==='Enter')searchLandtag()"
|
|
|
|
|
style="flex: 1;">
|
|
|
|
|
<button class="btn-landtag" id="btn-landtag" onclick="searchLandtag()">🔍 Suchen</button>
|
|
|
|
|
</div>
|
2026-03-28 22:30:24 +01:00
|
|
|
</div>
|
|
|
|
|
<div class="list-filters">
|
|
|
|
|
<button class="filter-btn active" data-filter="all" onclick="setScoreFilter('all', this)">Alle</button>
|
|
|
|
|
<button class="filter-btn" data-filter="high" onclick="setScoreFilter('high', this)">8-10</button>
|
|
|
|
|
<button class="filter-btn" data-filter="mid" onclick="setScoreFilter('mid', this)">5-7</button>
|
|
|
|
|
<button class="filter-btn" data-filter="low" onclick="setScoreFilter('low', this)">0-4</button>
|
|
|
|
|
<select id="partei-filter" onchange="setParteiFilter(this.value)" style="padding: 0.25rem 0.5rem; border-radius: 20px; border: 1px solid var(--color-lightgray); font-size: 0.8rem; cursor: pointer;">
|
|
|
|
|
<option value="">Alle Parteien</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stats-bar" style="padding: 0.5rem 1rem; gap: 1rem; flex-wrap: wrap; align-items: center;">
|
|
|
|
|
<span style="font-size: 0.8rem;"><strong id="stat-total">0</strong> geprüft · <strong id="stat-high">0</strong> vorbildlich · Ø <strong id="stat-avg">0</strong></span>
|
Bundesland filter & transparency: stringent split + visible source (#8)
Brings the Bundesland-Dropdown from a cosmetic header widget to a real
filter that propagates through every layer (Listing, internal search,
statistics, party/tag filters, upload mode), and at the same time makes
the source parliament visible in every place where assessments from
multiple bundesländer can be mixed.
Backend
- database.get_all_assessments(bundesland=None) — new optional filter,
"ALL" treated as None.
- database.search_assessments — bug fix: previous `if bundesland:`
branch incorrectly added a `WHERE bundesland='ALL'` clause; now
guarded with `bundesland and bundesland != "ALL"`.
- main.list_assessments — accepts ?bundesland= query param, includes the
bundesland field in the response so the frontend can render badges.
- main.get_single_assessment — also includes bundesland in the response
so the detail header can show the source parlament.
- main.search_landtag — early HTTP 400 when bundesland is missing or
"ALL"; the live Landtag adapter cannot serve a synthetic Bundesweit
request.
- main.index() and main.list_bundeslaender — synthetic "🌍 Bundesweit"
entry prepended to the bundesländer list (kept out of bundeslaender.py
on purpose — ALL is not a real state). Both endpoints additionally
expose a parlament_names map so the frontend can render the source
parliament without an extra round-trip.
Report (PDF + HTML)
- generate_html_report / generate_pdf_report — new optional bundesland
parameter. When set, the report header carries the parliament name
("Landtag von Sachsen-Anhalt", "Landtag Nordrhein-Westfalen", …)
beside the title. Three call sites updated: run_analysis,
run_drucksache_analysis, download_assessment_pdf.
Frontend (templates/index.html)
- Header dropdown gets the synthetic ALL entry as first option;
initial currentBundesland is now 'ALL' (was 'NRW').
- localStorage persistence: changeBundesland writes, DOMContentLoaded
reads and validates against the visible options.
- changeBundesland resets the score / party / tag filter state, syncs
the upload-mode bundesland select, disables the Landtag-Suche button
+ tooltip when ALL, and toggles a data-mode attribute on
.list-content (used by CSS to show/hide the per-item bundesland
badge).
- loadAssessments now sends ?bundesland=… so the API does the actual
filtering. updateStats renders an additional per-bundesland average
block (Ø NRW: x · Ø LSA: y) when in ALL mode and the loaded list
spans more than one bundesland.
- renderList prepends a small "bl-badge" beside the Drucksachen-Nummer.
Hidden in single-bundesland mode via CSS selector to avoid clutter.
- showDetail header now shows the parliament name as its own line
(.detail-parlament).
- searchLandtag has an early-out alert if currentBundesland === 'ALL',
saving a network round-trip.
- Upload-Mode bundesland select now starts with a "— Bundesland wählen
—" placeholder (no auto-default), and startAnalysis validates that a
concrete bundesland was chosen.
CSS
- .bl-badge plus the .list-content[data-mode="single"] hide rule.
- .detail-parlament for the detail header line.
- .header-parlament for the PDF report header line.
Resolves #8.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 23:00:39 +02:00
|
|
|
<span id="bundesland-stats" style="font-size: 0.8rem; color: var(--color-darkgray); display: none; gap: 0.6rem; flex-wrap: wrap;"></span>
|
2026-03-28 22:30:24 +01:00
|
|
|
<span style="color: var(--color-lightgray);">|</span>
|
|
|
|
|
<span id="partei-stats" style="font-size: 0.8rem; display: flex; gap: 0.75rem; flex-wrap: wrap;"></span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="list-content" id="list-content">
|
|
|
|
|
<div class="loading">
|
|
|
|
|
<div class="spinner"></div>
|
|
|
|
|
<span>Lade Bewertungen...</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
<!-- Right: Detail -->
|
|
|
|
|
<main class="detail-panel" id="detail-panel">
|
|
|
|
|
<div class="detail-placeholder">
|
|
|
|
|
<p>👈 Wähle einen Antrag aus der Liste</p>
|
|
|
|
|
</div>
|
|
|
|
|
</main>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Tags Mode -->
|
|
|
|
|
<div class="main-container" id="tags-mode" style="display: none;">
|
|
|
|
|
<aside class="list-panel" style="width: 100%; max-width: 400px;">
|
|
|
|
|
<div class="list-header">
|
|
|
|
|
<h3 style="margin-bottom: 0.5rem;">🏷️ Filter nach Tags</h3>
|
|
|
|
|
<p style="font-size: 0.85rem; color: #666; margin-bottom: 1rem;">Klicke auf Tags um zu filtern. Mehrfachauswahl zeigt Schnittmenge.</p>
|
|
|
|
|
<input type="text" class="search-box" id="tag-search-input" placeholder="Tags durchsuchen..." oninput="filterTagCloud(this.value)">
|
|
|
|
|
<div id="active-tags" style="margin-top: 0.75rem; display: flex; flex-wrap: wrap; gap: 0.5rem;"></div>
|
|
|
|
|
<button onclick="clearTagFilters()" style="margin-top: 0.75rem; padding: 0.5rem 1rem; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; display: none;" id="clear-tags-btn">✕ Filter zurücksetzen</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="list-content" id="tag-cloud" style="padding: 1rem;">
|
|
|
|
|
<div class="loading">
|
|
|
|
|
<div class="spinner"></div>
|
|
|
|
|
<span>Lade Tags...</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</aside>
|
|
|
|
|
<main class="detail-panel" id="tag-results-panel">
|
|
|
|
|
<div class="detail-card">
|
|
|
|
|
<h3 style="margin-bottom: 1rem;">📋 Gefilterte Anträge</h3>
|
|
|
|
|
<div id="tag-results-list">
|
|
|
|
|
<p style="color: #888;">Wähle Tags aus der Wolke um Anträge zu filtern.</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</main>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Upload Mode -->
|
|
|
|
|
<div class="main-container" id="upload-mode" style="display: none;">
|
|
|
|
|
<div class="detail-panel">
|
|
|
|
|
<div class="upload-section">
|
|
|
|
|
<h2 style="margin-bottom: 1rem; color: var(--color-blue);">Neuen Antrag prüfen</h2>
|
|
|
|
|
|
|
|
|
|
<div class="tabs">
|
|
|
|
|
<button class="tab-btn active" onclick="showTab('text')">Text eingeben</button>
|
|
|
|
|
<button class="tab-btn" onclick="showTab('file')">PDF hochladen</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div id="tab-text">
|
|
|
|
|
<textarea id="antrag-text" placeholder="Antragstext hier einfügen..."></textarea>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div id="tab-file" style="display: none;">
|
|
|
|
|
<div class="file-drop" onclick="document.getElementById('file-input').click()">
|
|
|
|
|
<p>📄 PDF hier ablegen oder klicken</p>
|
|
|
|
|
<input type="file" id="file-input" accept=".pdf" style="display: none" onchange="handleFile(this)">
|
|
|
|
|
<p id="file-name" style="margin-top: 0.5rem; color: var(--color-blue);"></p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style="margin-top: 1rem;">
|
|
|
|
|
<label>Bundesland:</label>
|
|
|
|
|
<select id="bundesland" style="padding: 0.5rem; margin-left: 0.5rem;">
|
Bundesland filter & transparency: stringent split + visible source (#8)
Brings the Bundesland-Dropdown from a cosmetic header widget to a real
filter that propagates through every layer (Listing, internal search,
statistics, party/tag filters, upload mode), and at the same time makes
the source parliament visible in every place where assessments from
multiple bundesländer can be mixed.
Backend
- database.get_all_assessments(bundesland=None) — new optional filter,
"ALL" treated as None.
- database.search_assessments — bug fix: previous `if bundesland:`
branch incorrectly added a `WHERE bundesland='ALL'` clause; now
guarded with `bundesland and bundesland != "ALL"`.
- main.list_assessments — accepts ?bundesland= query param, includes the
bundesland field in the response so the frontend can render badges.
- main.get_single_assessment — also includes bundesland in the response
so the detail header can show the source parlament.
- main.search_landtag — early HTTP 400 when bundesland is missing or
"ALL"; the live Landtag adapter cannot serve a synthetic Bundesweit
request.
- main.index() and main.list_bundeslaender — synthetic "🌍 Bundesweit"
entry prepended to the bundesländer list (kept out of bundeslaender.py
on purpose — ALL is not a real state). Both endpoints additionally
expose a parlament_names map so the frontend can render the source
parliament without an extra round-trip.
Report (PDF + HTML)
- generate_html_report / generate_pdf_report — new optional bundesland
parameter. When set, the report header carries the parliament name
("Landtag von Sachsen-Anhalt", "Landtag Nordrhein-Westfalen", …)
beside the title. Three call sites updated: run_analysis,
run_drucksache_analysis, download_assessment_pdf.
Frontend (templates/index.html)
- Header dropdown gets the synthetic ALL entry as first option;
initial currentBundesland is now 'ALL' (was 'NRW').
- localStorage persistence: changeBundesland writes, DOMContentLoaded
reads and validates against the visible options.
- changeBundesland resets the score / party / tag filter state, syncs
the upload-mode bundesland select, disables the Landtag-Suche button
+ tooltip when ALL, and toggles a data-mode attribute on
.list-content (used by CSS to show/hide the per-item bundesland
badge).
- loadAssessments now sends ?bundesland=… so the API does the actual
filtering. updateStats renders an additional per-bundesland average
block (Ø NRW: x · Ø LSA: y) when in ALL mode and the loaded list
spans more than one bundesland.
- renderList prepends a small "bl-badge" beside the Drucksachen-Nummer.
Hidden in single-bundesland mode via CSS selector to avoid clutter.
- showDetail header now shows the parliament name as its own line
(.detail-parlament).
- searchLandtag has an early-out alert if currentBundesland === 'ALL',
saving a network round-trip.
- Upload-Mode bundesland select now starts with a "— Bundesland wählen
—" placeholder (no auto-default), and startAnalysis validates that a
concrete bundesland was chosen.
CSS
- .bl-badge plus the .list-content[data-mode="single"] hide rule.
- .detail-parlament for the detail header line.
- .header-parlament for the PDF report header line.
Resolves #8.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 23:00:39 +02:00
|
|
|
<option value="" disabled selected>— Bundesland wählen —</option>
|
|
|
|
|
{% for bl in bundeslaender if bl.code != 'ALL' %}
|
|
|
|
|
<option value="{{ bl.code }}" {% if not bl.active %}disabled{% endif %}>
|
2026-03-28 22:30:24 +01:00
|
|
|
{{ bl.name }}{% if not bl.active %} (bald){% endif %}
|
|
|
|
|
</option>
|
|
|
|
|
{% endfor %}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<button class="btn-analyze" id="analyze-btn" onclick="startAnalysis()">
|
|
|
|
|
🔍 GWÖ-Analyse starten
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<div id="analysis-status" style="display: none;">
|
|
|
|
|
<div class="loading">
|
|
|
|
|
<div class="spinner"></div>
|
|
|
|
|
<span id="status-text">Analyse läuft...</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div id="analysis-result" style="display: none; margin-top: 1rem;"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
let allAssessments = [];
|
|
|
|
|
let currentScoreFilter = 'all';
|
|
|
|
|
let currentParteiFilter = '';
|
Bundesland filter & transparency: stringent split + visible source (#8)
Brings the Bundesland-Dropdown from a cosmetic header widget to a real
filter that propagates through every layer (Listing, internal search,
statistics, party/tag filters, upload mode), and at the same time makes
the source parliament visible in every place where assessments from
multiple bundesländer can be mixed.
Backend
- database.get_all_assessments(bundesland=None) — new optional filter,
"ALL" treated as None.
- database.search_assessments — bug fix: previous `if bundesland:`
branch incorrectly added a `WHERE bundesland='ALL'` clause; now
guarded with `bundesland and bundesland != "ALL"`.
- main.list_assessments — accepts ?bundesland= query param, includes the
bundesland field in the response so the frontend can render badges.
- main.get_single_assessment — also includes bundesland in the response
so the detail header can show the source parlament.
- main.search_landtag — early HTTP 400 when bundesland is missing or
"ALL"; the live Landtag adapter cannot serve a synthetic Bundesweit
request.
- main.index() and main.list_bundeslaender — synthetic "🌍 Bundesweit"
entry prepended to the bundesländer list (kept out of bundeslaender.py
on purpose — ALL is not a real state). Both endpoints additionally
expose a parlament_names map so the frontend can render the source
parliament without an extra round-trip.
Report (PDF + HTML)
- generate_html_report / generate_pdf_report — new optional bundesland
parameter. When set, the report header carries the parliament name
("Landtag von Sachsen-Anhalt", "Landtag Nordrhein-Westfalen", …)
beside the title. Three call sites updated: run_analysis,
run_drucksache_analysis, download_assessment_pdf.
Frontend (templates/index.html)
- Header dropdown gets the synthetic ALL entry as first option;
initial currentBundesland is now 'ALL' (was 'NRW').
- localStorage persistence: changeBundesland writes, DOMContentLoaded
reads and validates against the visible options.
- changeBundesland resets the score / party / tag filter state, syncs
the upload-mode bundesland select, disables the Landtag-Suche button
+ tooltip when ALL, and toggles a data-mode attribute on
.list-content (used by CSS to show/hide the per-item bundesland
badge).
- loadAssessments now sends ?bundesland=… so the API does the actual
filtering. updateStats renders an additional per-bundesland average
block (Ø NRW: x · Ø LSA: y) when in ALL mode and the loaded list
spans more than one bundesland.
- renderList prepends a small "bl-badge" beside the Drucksachen-Nummer.
Hidden in single-bundesland mode via CSS selector to avoid clutter.
- showDetail header now shows the parliament name as its own line
(.detail-parlament).
- searchLandtag has an early-out alert if currentBundesland === 'ALL',
saving a network round-trip.
- Upload-Mode bundesland select now starts with a "— Bundesland wählen
—" placeholder (no auto-default), and startAnalysis validates that a
concrete bundesland was chosen.
CSS
- .bl-badge plus the .list-content[data-mode="single"] hide rule.
- .detail-parlament for the detail header line.
- .header-parlament for the PDF report header line.
Resolves #8.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 23:00:39 +02:00
|
|
|
let currentBundesland = 'ALL';
|
2026-03-28 22:30:24 +01:00
|
|
|
let searchTimeout = null;
|
|
|
|
|
let isSearching = false;
|
|
|
|
|
let selectedTags = new Set();
|
|
|
|
|
let allTags = {};
|
Bundesland filter & transparency: stringent split + visible source (#8)
Brings the Bundesland-Dropdown from a cosmetic header widget to a real
filter that propagates through every layer (Listing, internal search,
statistics, party/tag filters, upload mode), and at the same time makes
the source parliament visible in every place where assessments from
multiple bundesländer can be mixed.
Backend
- database.get_all_assessments(bundesland=None) — new optional filter,
"ALL" treated as None.
- database.search_assessments — bug fix: previous `if bundesland:`
branch incorrectly added a `WHERE bundesland='ALL'` clause; now
guarded with `bundesland and bundesland != "ALL"`.
- main.list_assessments — accepts ?bundesland= query param, includes the
bundesland field in the response so the frontend can render badges.
- main.get_single_assessment — also includes bundesland in the response
so the detail header can show the source parlament.
- main.search_landtag — early HTTP 400 when bundesland is missing or
"ALL"; the live Landtag adapter cannot serve a synthetic Bundesweit
request.
- main.index() and main.list_bundeslaender — synthetic "🌍 Bundesweit"
entry prepended to the bundesländer list (kept out of bundeslaender.py
on purpose — ALL is not a real state). Both endpoints additionally
expose a parlament_names map so the frontend can render the source
parliament without an extra round-trip.
Report (PDF + HTML)
- generate_html_report / generate_pdf_report — new optional bundesland
parameter. When set, the report header carries the parliament name
("Landtag von Sachsen-Anhalt", "Landtag Nordrhein-Westfalen", …)
beside the title. Three call sites updated: run_analysis,
run_drucksache_analysis, download_assessment_pdf.
Frontend (templates/index.html)
- Header dropdown gets the synthetic ALL entry as first option;
initial currentBundesland is now 'ALL' (was 'NRW').
- localStorage persistence: changeBundesland writes, DOMContentLoaded
reads and validates against the visible options.
- changeBundesland resets the score / party / tag filter state, syncs
the upload-mode bundesland select, disables the Landtag-Suche button
+ tooltip when ALL, and toggles a data-mode attribute on
.list-content (used by CSS to show/hide the per-item bundesland
badge).
- loadAssessments now sends ?bundesland=… so the API does the actual
filtering. updateStats renders an additional per-bundesland average
block (Ø NRW: x · Ø LSA: y) when in ALL mode and the loaded list
spans more than one bundesland.
- renderList prepends a small "bl-badge" beside the Drucksachen-Nummer.
Hidden in single-bundesland mode via CSS selector to avoid clutter.
- showDetail header now shows the parliament name as its own line
(.detail-parlament).
- searchLandtag has an early-out alert if currentBundesland === 'ALL',
saving a network round-trip.
- Upload-Mode bundesland select now starts with a "— Bundesland wählen
—" placeholder (no auto-default), and startAnalysis validates that a
concrete bundesland was chosen.
CSS
- .bl-badge plus the .list-content[data-mode="single"] hide rule.
- .detail-parlament for the detail header line.
- .header-parlament for the PDF report header line.
Resolves #8.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 23:00:39 +02:00
|
|
|
|
|
|
|
|
// Map code → parlament_name, vom Backend mit dem Initial-Render geliefert.
|
|
|
|
|
// Wird im Detail-Header und im Listen-Item-Badge-Tooltip verwendet.
|
|
|
|
|
const PARLAMENT_NAMES = {{ parlament_names | tojson }};
|
|
|
|
|
|
|
|
|
|
// Load assessments on page load — localStorage-Auswahl wiederherstellen
|
2026-03-28 22:30:24 +01:00
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
Bundesland filter & transparency: stringent split + visible source (#8)
Brings the Bundesland-Dropdown from a cosmetic header widget to a real
filter that propagates through every layer (Listing, internal search,
statistics, party/tag filters, upload mode), and at the same time makes
the source parliament visible in every place where assessments from
multiple bundesländer can be mixed.
Backend
- database.get_all_assessments(bundesland=None) — new optional filter,
"ALL" treated as None.
- database.search_assessments — bug fix: previous `if bundesland:`
branch incorrectly added a `WHERE bundesland='ALL'` clause; now
guarded with `bundesland and bundesland != "ALL"`.
- main.list_assessments — accepts ?bundesland= query param, includes the
bundesland field in the response so the frontend can render badges.
- main.get_single_assessment — also includes bundesland in the response
so the detail header can show the source parlament.
- main.search_landtag — early HTTP 400 when bundesland is missing or
"ALL"; the live Landtag adapter cannot serve a synthetic Bundesweit
request.
- main.index() and main.list_bundeslaender — synthetic "🌍 Bundesweit"
entry prepended to the bundesländer list (kept out of bundeslaender.py
on purpose — ALL is not a real state). Both endpoints additionally
expose a parlament_names map so the frontend can render the source
parliament without an extra round-trip.
Report (PDF + HTML)
- generate_html_report / generate_pdf_report — new optional bundesland
parameter. When set, the report header carries the parliament name
("Landtag von Sachsen-Anhalt", "Landtag Nordrhein-Westfalen", …)
beside the title. Three call sites updated: run_analysis,
run_drucksache_analysis, download_assessment_pdf.
Frontend (templates/index.html)
- Header dropdown gets the synthetic ALL entry as first option;
initial currentBundesland is now 'ALL' (was 'NRW').
- localStorage persistence: changeBundesland writes, DOMContentLoaded
reads and validates against the visible options.
- changeBundesland resets the score / party / tag filter state, syncs
the upload-mode bundesland select, disables the Landtag-Suche button
+ tooltip when ALL, and toggles a data-mode attribute on
.list-content (used by CSS to show/hide the per-item bundesland
badge).
- loadAssessments now sends ?bundesland=… so the API does the actual
filtering. updateStats renders an additional per-bundesland average
block (Ø NRW: x · Ø LSA: y) when in ALL mode and the loaded list
spans more than one bundesland.
- renderList prepends a small "bl-badge" beside the Drucksachen-Nummer.
Hidden in single-bundesland mode via CSS selector to avoid clutter.
- showDetail header now shows the parliament name as its own line
(.detail-parlament).
- searchLandtag has an early-out alert if currentBundesland === 'ALL',
saving a network round-trip.
- Upload-Mode bundesland select now starts with a "— Bundesland wählen
—" placeholder (no auto-default), and startAnalysis validates that a
concrete bundesland was chosen.
CSS
- .bl-badge plus the .list-content[data-mode="single"] hide rule.
- .detail-parlament for the detail header line.
- .header-parlament for the PDF report header line.
Resolves #8.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 23:00:39 +02:00
|
|
|
const saved = localStorage.getItem('selectedBundesland');
|
|
|
|
|
const select = document.getElementById('bundesland-select');
|
|
|
|
|
if (saved) {
|
|
|
|
|
// Validieren: existiert die Option?
|
|
|
|
|
const exists = Array.from(select.options).some(
|
|
|
|
|
o => o.value === saved && !o.disabled
|
|
|
|
|
);
|
|
|
|
|
if (exists) {
|
|
|
|
|
currentBundesland = saved;
|
|
|
|
|
select.value = saved;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Modus-Klasse für CSS (Badges aus/an)
|
|
|
|
|
document.getElementById('list-content').dataset.mode =
|
|
|
|
|
(currentBundesland === 'ALL') ? 'all' : 'single';
|
|
|
|
|
// Landtag-Button-State für Initial-Auswahl
|
|
|
|
|
const btnLandtag = document.getElementById('btn-landtag');
|
|
|
|
|
if (currentBundesland === 'ALL') {
|
|
|
|
|
btnLandtag.disabled = true;
|
|
|
|
|
btnLandtag.title = 'Landtag-Suche nur mit konkretem Bundesland möglich';
|
|
|
|
|
}
|
2026-03-28 22:30:24 +01:00
|
|
|
loadAssessments();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function buildParteienFilter() {
|
|
|
|
|
// Alle Fraktionen sammeln
|
|
|
|
|
const parteien = new Set();
|
|
|
|
|
allAssessments.forEach(a => {
|
|
|
|
|
(a.fraktionen || []).forEach(f => parteien.add(f));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const select = document.getElementById('partei-filter');
|
|
|
|
|
select.innerHTML = '<option value="">Alle Parteien</option>';
|
|
|
|
|
Array.from(parteien).sort().forEach(p => {
|
|
|
|
|
select.innerHTML += `<option value="${p}">${p}</option>`;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildTagCloud() {
|
|
|
|
|
allTags = {};
|
|
|
|
|
allAssessments.forEach(a => {
|
|
|
|
|
(a.themen || []).forEach(tag => {
|
|
|
|
|
allTags[tag] = (allTags[tag] || 0) + 1;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
renderTagCloud();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderTagCloud(filter = '') {
|
|
|
|
|
const container = document.getElementById('tag-cloud');
|
|
|
|
|
const filterLower = filter.toLowerCase();
|
|
|
|
|
|
|
|
|
|
// Sortiert nach Häufigkeit
|
|
|
|
|
const sorted = Object.entries(allTags)
|
|
|
|
|
.filter(([tag]) => tag.toLowerCase().includes(filterLower))
|
|
|
|
|
.sort((a, b) => b[1] - a[1]);
|
|
|
|
|
|
|
|
|
|
if (sorted.length === 0) {
|
|
|
|
|
container.innerHTML = '<p style="color: #888;">Keine Tags gefunden</p>';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const maxCount = Math.max(...sorted.map(([, c]) => c));
|
|
|
|
|
|
|
|
|
|
container.innerHTML = '<div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">' +
|
|
|
|
|
sorted.map(([tag, count]) => {
|
|
|
|
|
const size = 0.75 + (count / maxCount) * 0.75;
|
|
|
|
|
const isActive = selectedTags.has(tag);
|
|
|
|
|
return `<button
|
|
|
|
|
onclick="toggleTag('${tag.replace(/'/g, "\\'")}')"
|
|
|
|
|
style="
|
|
|
|
|
font-size: ${size}rem;
|
|
|
|
|
padding: 0.3rem 0.6rem;
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
border: 1px solid ${isActive ? 'var(--color-blue)' : 'var(--color-lightgray)'};
|
|
|
|
|
background: ${isActive ? 'var(--color-blue)' : 'white'};
|
|
|
|
|
color: ${isActive ? 'white' : 'var(--color-darkgray)'};
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
"
|
|
|
|
|
title="${count} Anträge"
|
|
|
|
|
>${tag} <span style="font-size: 0.7rem; opacity: 0.7;">(${count})</span></button>`;
|
|
|
|
|
}).join('') +
|
|
|
|
|
'</div>';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function filterTagCloud(query) {
|
|
|
|
|
renderTagCloud(query);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleTag(tag) {
|
|
|
|
|
if (selectedTags.has(tag)) {
|
|
|
|
|
selectedTags.delete(tag);
|
|
|
|
|
} else {
|
|
|
|
|
selectedTags.add(tag);
|
|
|
|
|
}
|
|
|
|
|
updateTagFilterUI();
|
|
|
|
|
filterByTags();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateTagFilterUI() {
|
|
|
|
|
const container = document.getElementById('active-tags');
|
|
|
|
|
const clearBtn = document.getElementById('clear-tags-btn');
|
|
|
|
|
|
|
|
|
|
if (selectedTags.size === 0) {
|
|
|
|
|
container.innerHTML = '';
|
|
|
|
|
clearBtn.style.display = 'none';
|
|
|
|
|
} else {
|
|
|
|
|
container.innerHTML = Array.from(selectedTags).map(tag =>
|
|
|
|
|
`<span style="padding: 0.25rem 0.5rem; background: var(--color-blue); color: white; border-radius: 4px; font-size: 0.85rem;">
|
|
|
|
|
${tag} <span style="cursor: pointer; margin-left: 0.25rem;" onclick="toggleTag('${tag.replace(/'/g, "\\'")}')">×</span>
|
|
|
|
|
</span>`
|
|
|
|
|
).join('');
|
|
|
|
|
clearBtn.style.display = 'block';
|
|
|
|
|
}
|
|
|
|
|
renderTagCloud(document.getElementById('tag-search-input')?.value || '');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearTagFilters() {
|
|
|
|
|
selectedTags.clear();
|
|
|
|
|
updateTagFilterUI();
|
|
|
|
|
filterByTags();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function filterByTags() {
|
|
|
|
|
const resultsContainer = document.getElementById('tag-results-list');
|
|
|
|
|
|
|
|
|
|
if (selectedTags.size === 0) {
|
|
|
|
|
resultsContainer.innerHTML = '<p style="color: #888;">Wähle Tags aus der Wolke um Anträge zu filtern.</p>';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Schnittmenge: Anträge müssen ALLE ausgewählten Tags haben
|
|
|
|
|
const filtered = allAssessments.filter(a => {
|
|
|
|
|
const tags = new Set(a.themen || []);
|
|
|
|
|
return Array.from(selectedTags).every(t => tags.has(t));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (filtered.length === 0) {
|
|
|
|
|
resultsContainer.innerHTML = '<p style="color: #888;">Keine Anträge mit dieser Tag-Kombination gefunden.</p>';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resultsContainer.innerHTML = `
|
|
|
|
|
<p style="margin-bottom: 1rem; color: #666;">${filtered.length} Anträge gefunden</p>
|
|
|
|
|
${filtered.map(item => `
|
|
|
|
|
<div style="padding: 0.75rem; border-bottom: 1px solid var(--color-lightgray); cursor: pointer;" onclick="showMode('browse'); setTimeout(() => showDetail('${item.drucksache}'), 100);">
|
|
|
|
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
|
|
|
<span style="font-weight: bold; color: var(--color-blue);">${item.drucksache}</span>
|
|
|
|
|
<span style="padding: 0.1rem 0.5rem; border-radius: 4px; font-size: 0.85rem; ${item.gwoeScore >= 8 ? 'background: #155724; color: white;' : item.gwoeScore >= 5 ? 'background: #889e33; color: white;' : 'background: #dc3545; color: white;'}">${item.gwoeScore}/10</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="font-size: 0.9rem; margin-top: 0.25rem;">${item.title || 'Ohne Titel'}</div>
|
|
|
|
|
<div style="font-size: 0.8rem; color: #888;">${(item.fraktionen || []).join(', ')}</div>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('')}
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadAssessments() {
|
|
|
|
|
try {
|
Bundesland filter & transparency: stringent split + visible source (#8)
Brings the Bundesland-Dropdown from a cosmetic header widget to a real
filter that propagates through every layer (Listing, internal search,
statistics, party/tag filters, upload mode), and at the same time makes
the source parliament visible in every place where assessments from
multiple bundesländer can be mixed.
Backend
- database.get_all_assessments(bundesland=None) — new optional filter,
"ALL" treated as None.
- database.search_assessments — bug fix: previous `if bundesland:`
branch incorrectly added a `WHERE bundesland='ALL'` clause; now
guarded with `bundesland and bundesland != "ALL"`.
- main.list_assessments — accepts ?bundesland= query param, includes the
bundesland field in the response so the frontend can render badges.
- main.get_single_assessment — also includes bundesland in the response
so the detail header can show the source parlament.
- main.search_landtag — early HTTP 400 when bundesland is missing or
"ALL"; the live Landtag adapter cannot serve a synthetic Bundesweit
request.
- main.index() and main.list_bundeslaender — synthetic "🌍 Bundesweit"
entry prepended to the bundesländer list (kept out of bundeslaender.py
on purpose — ALL is not a real state). Both endpoints additionally
expose a parlament_names map so the frontend can render the source
parliament without an extra round-trip.
Report (PDF + HTML)
- generate_html_report / generate_pdf_report — new optional bundesland
parameter. When set, the report header carries the parliament name
("Landtag von Sachsen-Anhalt", "Landtag Nordrhein-Westfalen", …)
beside the title. Three call sites updated: run_analysis,
run_drucksache_analysis, download_assessment_pdf.
Frontend (templates/index.html)
- Header dropdown gets the synthetic ALL entry as first option;
initial currentBundesland is now 'ALL' (was 'NRW').
- localStorage persistence: changeBundesland writes, DOMContentLoaded
reads and validates against the visible options.
- changeBundesland resets the score / party / tag filter state, syncs
the upload-mode bundesland select, disables the Landtag-Suche button
+ tooltip when ALL, and toggles a data-mode attribute on
.list-content (used by CSS to show/hide the per-item bundesland
badge).
- loadAssessments now sends ?bundesland=… so the API does the actual
filtering. updateStats renders an additional per-bundesland average
block (Ø NRW: x · Ø LSA: y) when in ALL mode and the loaded list
spans more than one bundesland.
- renderList prepends a small "bl-badge" beside the Drucksachen-Nummer.
Hidden in single-bundesland mode via CSS selector to avoid clutter.
- showDetail header now shows the parliament name as its own line
(.detail-parlament).
- searchLandtag has an early-out alert if currentBundesland === 'ALL',
saving a network round-trip.
- Upload-Mode bundesland select now starts with a "— Bundesland wählen
—" placeholder (no auto-default), and startAnalysis validates that a
concrete bundesland was chosen.
CSS
- .bl-badge plus the .list-content[data-mode="single"] hide rule.
- .detail-parlament for the detail header line.
- .header-parlament for the PDF report header line.
Resolves #8.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 23:00:39 +02:00
|
|
|
const url = `/api/assessments?bundesland=${encodeURIComponent(currentBundesland)}`;
|
|
|
|
|
const resp = await fetch(url);
|
2026-03-28 22:30:24 +01:00
|
|
|
allAssessments = await resp.json();
|
|
|
|
|
updateStats();
|
|
|
|
|
renderList(allAssessments);
|
|
|
|
|
buildParteienFilter();
|
|
|
|
|
buildTagCloud();
|
|
|
|
|
} catch (e) {
|
Bundesland filter & transparency: stringent split + visible source (#8)
Brings the Bundesland-Dropdown from a cosmetic header widget to a real
filter that propagates through every layer (Listing, internal search,
statistics, party/tag filters, upload mode), and at the same time makes
the source parliament visible in every place where assessments from
multiple bundesländer can be mixed.
Backend
- database.get_all_assessments(bundesland=None) — new optional filter,
"ALL" treated as None.
- database.search_assessments — bug fix: previous `if bundesland:`
branch incorrectly added a `WHERE bundesland='ALL'` clause; now
guarded with `bundesland and bundesland != "ALL"`.
- main.list_assessments — accepts ?bundesland= query param, includes the
bundesland field in the response so the frontend can render badges.
- main.get_single_assessment — also includes bundesland in the response
so the detail header can show the source parlament.
- main.search_landtag — early HTTP 400 when bundesland is missing or
"ALL"; the live Landtag adapter cannot serve a synthetic Bundesweit
request.
- main.index() and main.list_bundeslaender — synthetic "🌍 Bundesweit"
entry prepended to the bundesländer list (kept out of bundeslaender.py
on purpose — ALL is not a real state). Both endpoints additionally
expose a parlament_names map so the frontend can render the source
parliament without an extra round-trip.
Report (PDF + HTML)
- generate_html_report / generate_pdf_report — new optional bundesland
parameter. When set, the report header carries the parliament name
("Landtag von Sachsen-Anhalt", "Landtag Nordrhein-Westfalen", …)
beside the title. Three call sites updated: run_analysis,
run_drucksache_analysis, download_assessment_pdf.
Frontend (templates/index.html)
- Header dropdown gets the synthetic ALL entry as first option;
initial currentBundesland is now 'ALL' (was 'NRW').
- localStorage persistence: changeBundesland writes, DOMContentLoaded
reads and validates against the visible options.
- changeBundesland resets the score / party / tag filter state, syncs
the upload-mode bundesland select, disables the Landtag-Suche button
+ tooltip when ALL, and toggles a data-mode attribute on
.list-content (used by CSS to show/hide the per-item bundesland
badge).
- loadAssessments now sends ?bundesland=… so the API does the actual
filtering. updateStats renders an additional per-bundesland average
block (Ø NRW: x · Ø LSA: y) when in ALL mode and the loaded list
spans more than one bundesland.
- renderList prepends a small "bl-badge" beside the Drucksachen-Nummer.
Hidden in single-bundesland mode via CSS selector to avoid clutter.
- showDetail header now shows the parliament name as its own line
(.detail-parlament).
- searchLandtag has an early-out alert if currentBundesland === 'ALL',
saving a network round-trip.
- Upload-Mode bundesland select now starts with a "— Bundesland wählen
—" placeholder (no auto-default), and startAnalysis validates that a
concrete bundesland was chosen.
CSS
- .bl-badge plus the .list-content[data-mode="single"] hide rule.
- .detail-parlament for the detail header line.
- .header-parlament for the PDF report header line.
Resolves #8.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 23:00:39 +02:00
|
|
|
document.getElementById('list-content').innerHTML =
|
2026-03-28 22:30:24 +01:00
|
|
|
'<p style="padding: 1rem; color: #888;">Fehler beim Laden</p>';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateStats() {
|
|
|
|
|
const checked = allAssessments.filter(a => a.status !== 'unchecked').length;
|
|
|
|
|
const high = allAssessments.filter(a => a.gwoeScore >= 8).length;
|
|
|
|
|
const avg = checked > 0 ? (allAssessments.filter(a => a.gwoeScore != null).reduce((s, a) => s + (a.gwoeScore || 0), 0) / checked).toFixed(1) : 0;
|
Bundesland filter & transparency: stringent split + visible source (#8)
Brings the Bundesland-Dropdown from a cosmetic header widget to a real
filter that propagates through every layer (Listing, internal search,
statistics, party/tag filters, upload mode), and at the same time makes
the source parliament visible in every place where assessments from
multiple bundesländer can be mixed.
Backend
- database.get_all_assessments(bundesland=None) — new optional filter,
"ALL" treated as None.
- database.search_assessments — bug fix: previous `if bundesland:`
branch incorrectly added a `WHERE bundesland='ALL'` clause; now
guarded with `bundesland and bundesland != "ALL"`.
- main.list_assessments — accepts ?bundesland= query param, includes the
bundesland field in the response so the frontend can render badges.
- main.get_single_assessment — also includes bundesland in the response
so the detail header can show the source parlament.
- main.search_landtag — early HTTP 400 when bundesland is missing or
"ALL"; the live Landtag adapter cannot serve a synthetic Bundesweit
request.
- main.index() and main.list_bundeslaender — synthetic "🌍 Bundesweit"
entry prepended to the bundesländer list (kept out of bundeslaender.py
on purpose — ALL is not a real state). Both endpoints additionally
expose a parlament_names map so the frontend can render the source
parliament without an extra round-trip.
Report (PDF + HTML)
- generate_html_report / generate_pdf_report — new optional bundesland
parameter. When set, the report header carries the parliament name
("Landtag von Sachsen-Anhalt", "Landtag Nordrhein-Westfalen", …)
beside the title. Three call sites updated: run_analysis,
run_drucksache_analysis, download_assessment_pdf.
Frontend (templates/index.html)
- Header dropdown gets the synthetic ALL entry as first option;
initial currentBundesland is now 'ALL' (was 'NRW').
- localStorage persistence: changeBundesland writes, DOMContentLoaded
reads and validates against the visible options.
- changeBundesland resets the score / party / tag filter state, syncs
the upload-mode bundesland select, disables the Landtag-Suche button
+ tooltip when ALL, and toggles a data-mode attribute on
.list-content (used by CSS to show/hide the per-item bundesland
badge).
- loadAssessments now sends ?bundesland=… so the API does the actual
filtering. updateStats renders an additional per-bundesland average
block (Ø NRW: x · Ø LSA: y) when in ALL mode and the loaded list
spans more than one bundesland.
- renderList prepends a small "bl-badge" beside the Drucksachen-Nummer.
Hidden in single-bundesland mode via CSS selector to avoid clutter.
- showDetail header now shows the parliament name as its own line
(.detail-parlament).
- searchLandtag has an early-out alert if currentBundesland === 'ALL',
saving a network round-trip.
- Upload-Mode bundesland select now starts with a "— Bundesland wählen
—" placeholder (no auto-default), and startAnalysis validates that a
concrete bundesland was chosen.
CSS
- .bl-badge plus the .list-content[data-mode="single"] hide rule.
- .detail-parlament for the detail header line.
- .header-parlament for the PDF report header line.
Resolves #8.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 23:00:39 +02:00
|
|
|
|
2026-03-28 22:30:24 +01:00
|
|
|
document.getElementById('stat-total').textContent = checked;
|
|
|
|
|
document.getElementById('stat-high').textContent = high;
|
|
|
|
|
document.getElementById('stat-avg').textContent = avg;
|
Bundesland filter & transparency: stringent split + visible source (#8)
Brings the Bundesland-Dropdown from a cosmetic header widget to a real
filter that propagates through every layer (Listing, internal search,
statistics, party/tag filters, upload mode), and at the same time makes
the source parliament visible in every place where assessments from
multiple bundesländer can be mixed.
Backend
- database.get_all_assessments(bundesland=None) — new optional filter,
"ALL" treated as None.
- database.search_assessments — bug fix: previous `if bundesland:`
branch incorrectly added a `WHERE bundesland='ALL'` clause; now
guarded with `bundesland and bundesland != "ALL"`.
- main.list_assessments — accepts ?bundesland= query param, includes the
bundesland field in the response so the frontend can render badges.
- main.get_single_assessment — also includes bundesland in the response
so the detail header can show the source parlament.
- main.search_landtag — early HTTP 400 when bundesland is missing or
"ALL"; the live Landtag adapter cannot serve a synthetic Bundesweit
request.
- main.index() and main.list_bundeslaender — synthetic "🌍 Bundesweit"
entry prepended to the bundesländer list (kept out of bundeslaender.py
on purpose — ALL is not a real state). Both endpoints additionally
expose a parlament_names map so the frontend can render the source
parliament without an extra round-trip.
Report (PDF + HTML)
- generate_html_report / generate_pdf_report — new optional bundesland
parameter. When set, the report header carries the parliament name
("Landtag von Sachsen-Anhalt", "Landtag Nordrhein-Westfalen", …)
beside the title. Three call sites updated: run_analysis,
run_drucksache_analysis, download_assessment_pdf.
Frontend (templates/index.html)
- Header dropdown gets the synthetic ALL entry as first option;
initial currentBundesland is now 'ALL' (was 'NRW').
- localStorage persistence: changeBundesland writes, DOMContentLoaded
reads and validates against the visible options.
- changeBundesland resets the score / party / tag filter state, syncs
the upload-mode bundesland select, disables the Landtag-Suche button
+ tooltip when ALL, and toggles a data-mode attribute on
.list-content (used by CSS to show/hide the per-item bundesland
badge).
- loadAssessments now sends ?bundesland=… so the API does the actual
filtering. updateStats renders an additional per-bundesland average
block (Ø NRW: x · Ø LSA: y) when in ALL mode and the loaded list
spans more than one bundesland.
- renderList prepends a small "bl-badge" beside the Drucksachen-Nummer.
Hidden in single-bundesland mode via CSS selector to avoid clutter.
- showDetail header now shows the parliament name as its own line
(.detail-parlament).
- searchLandtag has an early-out alert if currentBundesland === 'ALL',
saving a network round-trip.
- Upload-Mode bundesland select now starts with a "— Bundesland wählen
—" placeholder (no auto-default), and startAnalysis validates that a
concrete bundesland was chosen.
CSS
- .bl-badge plus the .list-content[data-mode="single"] hide rule.
- .detail-parlament for the detail header line.
- .header-parlament for the PDF report header line.
Resolves #8.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 23:00:39 +02:00
|
|
|
|
|
|
|
|
// Pro-Bundesland-Aufschlüsselung — nur im Bundesweit-Modus, und nur
|
|
|
|
|
// wenn tatsächlich mehr als ein Bundesland in der Liste vorkommt.
|
|
|
|
|
const blContainer = document.getElementById('bundesland-stats');
|
|
|
|
|
if (currentBundesland === 'ALL') {
|
|
|
|
|
const blStats = {};
|
|
|
|
|
allAssessments.forEach(a => {
|
|
|
|
|
if (a.gwoeScore == null || !a.bundesland) return;
|
|
|
|
|
if (!blStats[a.bundesland]) blStats[a.bundesland] = { sum: 0, count: 0 };
|
|
|
|
|
blStats[a.bundesland].sum += a.gwoeScore;
|
|
|
|
|
blStats[a.bundesland].count += 1;
|
|
|
|
|
});
|
|
|
|
|
const codes = Object.keys(blStats);
|
|
|
|
|
if (codes.length > 1) {
|
|
|
|
|
const sortedBl = codes
|
|
|
|
|
.map(c => ({ code: c, avg: blStats[c].sum / blStats[c].count, count: blStats[c].count }))
|
|
|
|
|
.sort((a, b) => b.avg - a.avg);
|
|
|
|
|
blContainer.innerHTML = sortedBl.map(b =>
|
|
|
|
|
`<span title="${PARLAMENT_NAMES[b.code] || b.code}">Ø <strong>${b.code}</strong> ${b.avg.toFixed(1)} <span style="color:#888">(n=${b.count})</span></span>`
|
|
|
|
|
).join(' · ');
|
|
|
|
|
blContainer.style.display = 'inline-flex';
|
|
|
|
|
} else {
|
|
|
|
|
blContainer.style.display = 'none';
|
|
|
|
|
blContainer.innerHTML = '';
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
blContainer.style.display = 'none';
|
|
|
|
|
blContainer.innerHTML = '';
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 22:30:24 +01:00
|
|
|
// Partei-Durchschnitte berechnen
|
|
|
|
|
const parteiStats = {};
|
|
|
|
|
allAssessments.forEach(a => {
|
|
|
|
|
if (a.gwoeScore == null) return;
|
|
|
|
|
(a.fraktionen || []).forEach(f => {
|
|
|
|
|
if (!parteiStats[f]) parteiStats[f] = { sum: 0, count: 0 };
|
|
|
|
|
parteiStats[f].sum += a.gwoeScore;
|
|
|
|
|
parteiStats[f].count += 1;
|
|
|
|
|
});
|
|
|
|
|
});
|
Bundesland filter & transparency: stringent split + visible source (#8)
Brings the Bundesland-Dropdown from a cosmetic header widget to a real
filter that propagates through every layer (Listing, internal search,
statistics, party/tag filters, upload mode), and at the same time makes
the source parliament visible in every place where assessments from
multiple bundesländer can be mixed.
Backend
- database.get_all_assessments(bundesland=None) — new optional filter,
"ALL" treated as None.
- database.search_assessments — bug fix: previous `if bundesland:`
branch incorrectly added a `WHERE bundesland='ALL'` clause; now
guarded with `bundesland and bundesland != "ALL"`.
- main.list_assessments — accepts ?bundesland= query param, includes the
bundesland field in the response so the frontend can render badges.
- main.get_single_assessment — also includes bundesland in the response
so the detail header can show the source parlament.
- main.search_landtag — early HTTP 400 when bundesland is missing or
"ALL"; the live Landtag adapter cannot serve a synthetic Bundesweit
request.
- main.index() and main.list_bundeslaender — synthetic "🌍 Bundesweit"
entry prepended to the bundesländer list (kept out of bundeslaender.py
on purpose — ALL is not a real state). Both endpoints additionally
expose a parlament_names map so the frontend can render the source
parliament without an extra round-trip.
Report (PDF + HTML)
- generate_html_report / generate_pdf_report — new optional bundesland
parameter. When set, the report header carries the parliament name
("Landtag von Sachsen-Anhalt", "Landtag Nordrhein-Westfalen", …)
beside the title. Three call sites updated: run_analysis,
run_drucksache_analysis, download_assessment_pdf.
Frontend (templates/index.html)
- Header dropdown gets the synthetic ALL entry as first option;
initial currentBundesland is now 'ALL' (was 'NRW').
- localStorage persistence: changeBundesland writes, DOMContentLoaded
reads and validates against the visible options.
- changeBundesland resets the score / party / tag filter state, syncs
the upload-mode bundesland select, disables the Landtag-Suche button
+ tooltip when ALL, and toggles a data-mode attribute on
.list-content (used by CSS to show/hide the per-item bundesland
badge).
- loadAssessments now sends ?bundesland=… so the API does the actual
filtering. updateStats renders an additional per-bundesland average
block (Ø NRW: x · Ø LSA: y) when in ALL mode and the loaded list
spans more than one bundesland.
- renderList prepends a small "bl-badge" beside the Drucksachen-Nummer.
Hidden in single-bundesland mode via CSS selector to avoid clutter.
- showDetail header now shows the parliament name as its own line
(.detail-parlament).
- searchLandtag has an early-out alert if currentBundesland === 'ALL',
saving a network round-trip.
- Upload-Mode bundesland select now starts with a "— Bundesland wählen
—" placeholder (no auto-default), and startAnalysis validates that a
concrete bundesland was chosen.
CSS
- .bl-badge plus the .list-content[data-mode="single"] hide rule.
- .detail-parlament for the detail header line.
- .header-parlament for the PDF report header line.
Resolves #8.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 23:00:39 +02:00
|
|
|
|
2026-03-28 22:30:24 +01:00
|
|
|
// Sortiert nach Durchschnitt (absteigend)
|
|
|
|
|
const sorted = Object.entries(parteiStats)
|
|
|
|
|
.map(([partei, data]) => ({ partei, avg: data.sum / data.count, count: data.count }))
|
|
|
|
|
.sort((a, b) => b.avg - a.avg);
|
Bundesland filter & transparency: stringent split + visible source (#8)
Brings the Bundesland-Dropdown from a cosmetic header widget to a real
filter that propagates through every layer (Listing, internal search,
statistics, party/tag filters, upload mode), and at the same time makes
the source parliament visible in every place where assessments from
multiple bundesländer can be mixed.
Backend
- database.get_all_assessments(bundesland=None) — new optional filter,
"ALL" treated as None.
- database.search_assessments — bug fix: previous `if bundesland:`
branch incorrectly added a `WHERE bundesland='ALL'` clause; now
guarded with `bundesland and bundesland != "ALL"`.
- main.list_assessments — accepts ?bundesland= query param, includes the
bundesland field in the response so the frontend can render badges.
- main.get_single_assessment — also includes bundesland in the response
so the detail header can show the source parlament.
- main.search_landtag — early HTTP 400 when bundesland is missing or
"ALL"; the live Landtag adapter cannot serve a synthetic Bundesweit
request.
- main.index() and main.list_bundeslaender — synthetic "🌍 Bundesweit"
entry prepended to the bundesländer list (kept out of bundeslaender.py
on purpose — ALL is not a real state). Both endpoints additionally
expose a parlament_names map so the frontend can render the source
parliament without an extra round-trip.
Report (PDF + HTML)
- generate_html_report / generate_pdf_report — new optional bundesland
parameter. When set, the report header carries the parliament name
("Landtag von Sachsen-Anhalt", "Landtag Nordrhein-Westfalen", …)
beside the title. Three call sites updated: run_analysis,
run_drucksache_analysis, download_assessment_pdf.
Frontend (templates/index.html)
- Header dropdown gets the synthetic ALL entry as first option;
initial currentBundesland is now 'ALL' (was 'NRW').
- localStorage persistence: changeBundesland writes, DOMContentLoaded
reads and validates against the visible options.
- changeBundesland resets the score / party / tag filter state, syncs
the upload-mode bundesland select, disables the Landtag-Suche button
+ tooltip when ALL, and toggles a data-mode attribute on
.list-content (used by CSS to show/hide the per-item bundesland
badge).
- loadAssessments now sends ?bundesland=… so the API does the actual
filtering. updateStats renders an additional per-bundesland average
block (Ø NRW: x · Ø LSA: y) when in ALL mode and the loaded list
spans more than one bundesland.
- renderList prepends a small "bl-badge" beside the Drucksachen-Nummer.
Hidden in single-bundesland mode via CSS selector to avoid clutter.
- showDetail header now shows the parliament name as its own line
(.detail-parlament).
- searchLandtag has an early-out alert if currentBundesland === 'ALL',
saving a network round-trip.
- Upload-Mode bundesland select now starts with a "— Bundesland wählen
—" placeholder (no auto-default), and startAnalysis validates that a
concrete bundesland was chosen.
CSS
- .bl-badge plus the .list-content[data-mode="single"] hide rule.
- .detail-parlament for the detail header line.
- .header-parlament for the PDF report header line.
Resolves #8.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 23:00:39 +02:00
|
|
|
|
2026-03-28 22:30:24 +01:00
|
|
|
const container = document.getElementById('partei-stats');
|
|
|
|
|
container.innerHTML = sorted.map(p => {
|
|
|
|
|
const color = p.avg >= 7 ? '#889e33' : p.avg >= 4 ? '#fd7e14' : '#dc3545';
|
|
|
|
|
return `<span style="color: ${color};"><strong>${p.partei}</strong> ${p.avg.toFixed(1)}</span>`;
|
|
|
|
|
}).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderList(items) {
|
|
|
|
|
const container = document.getElementById('list-content');
|
|
|
|
|
if (items.length === 0) {
|
|
|
|
|
container.innerHTML = '<p style="padding: 1rem; color: #888;">Keine Ergebnisse</p>';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
container.innerHTML = items.map(item => {
|
|
|
|
|
const isUnchecked = item.status === 'unchecked';
|
|
|
|
|
// Skala 0-10
|
|
|
|
|
const scoreClass = isUnchecked ? 'status-unchecked' :
|
|
|
|
|
item.gwoeScore >= 8 ? 'score-high' :
|
|
|
|
|
item.gwoeScore >= 5 ? 'score-mid' :
|
|
|
|
|
item.gwoeScore >= 3 ? 'score-low' : 'score-negative';
|
|
|
|
|
const fraktionen = (item.fraktionen || []).join(', ') || 'k.A.';
|
|
|
|
|
const themen = (item.themen || []).slice(0, 3);
|
|
|
|
|
const scoreText = isUnchecked ? '⏳' : `${item.gwoeScore}/10`;
|
|
|
|
|
|
Bundesland filter & transparency: stringent split + visible source (#8)
Brings the Bundesland-Dropdown from a cosmetic header widget to a real
filter that propagates through every layer (Listing, internal search,
statistics, party/tag filters, upload mode), and at the same time makes
the source parliament visible in every place where assessments from
multiple bundesländer can be mixed.
Backend
- database.get_all_assessments(bundesland=None) — new optional filter,
"ALL" treated as None.
- database.search_assessments — bug fix: previous `if bundesland:`
branch incorrectly added a `WHERE bundesland='ALL'` clause; now
guarded with `bundesland and bundesland != "ALL"`.
- main.list_assessments — accepts ?bundesland= query param, includes the
bundesland field in the response so the frontend can render badges.
- main.get_single_assessment — also includes bundesland in the response
so the detail header can show the source parlament.
- main.search_landtag — early HTTP 400 when bundesland is missing or
"ALL"; the live Landtag adapter cannot serve a synthetic Bundesweit
request.
- main.index() and main.list_bundeslaender — synthetic "🌍 Bundesweit"
entry prepended to the bundesländer list (kept out of bundeslaender.py
on purpose — ALL is not a real state). Both endpoints additionally
expose a parlament_names map so the frontend can render the source
parliament without an extra round-trip.
Report (PDF + HTML)
- generate_html_report / generate_pdf_report — new optional bundesland
parameter. When set, the report header carries the parliament name
("Landtag von Sachsen-Anhalt", "Landtag Nordrhein-Westfalen", …)
beside the title. Three call sites updated: run_analysis,
run_drucksache_analysis, download_assessment_pdf.
Frontend (templates/index.html)
- Header dropdown gets the synthetic ALL entry as first option;
initial currentBundesland is now 'ALL' (was 'NRW').
- localStorage persistence: changeBundesland writes, DOMContentLoaded
reads and validates against the visible options.
- changeBundesland resets the score / party / tag filter state, syncs
the upload-mode bundesland select, disables the Landtag-Suche button
+ tooltip when ALL, and toggles a data-mode attribute on
.list-content (used by CSS to show/hide the per-item bundesland
badge).
- loadAssessments now sends ?bundesland=… so the API does the actual
filtering. updateStats renders an additional per-bundesland average
block (Ø NRW: x · Ø LSA: y) when in ALL mode and the loaded list
spans more than one bundesland.
- renderList prepends a small "bl-badge" beside the Drucksachen-Nummer.
Hidden in single-bundesland mode via CSS selector to avoid clutter.
- showDetail header now shows the parliament name as its own line
(.detail-parlament).
- searchLandtag has an early-out alert if currentBundesland === 'ALL',
saving a network round-trip.
- Upload-Mode bundesland select now starts with a "— Bundesland wählen
—" placeholder (no auto-default), and startAnalysis validates that a
concrete bundesland was chosen.
CSS
- .bl-badge plus the .list-content[data-mode="single"] hide rule.
- .detail-parlament for the detail header line.
- .header-parlament for the PDF report header line.
Resolves #8.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 23:00:39 +02:00
|
|
|
const blBadge = item.bundesland
|
|
|
|
|
? `<span class="bl-badge" title="${PARLAMENT_NAMES[item.bundesland] || item.bundesland}">${item.bundesland}</span>`
|
|
|
|
|
: '';
|
2026-03-28 22:30:24 +01:00
|
|
|
return `
|
|
|
|
|
<div class="list-item ${isUnchecked ? 'unchecked' : ''}" data-drucksache="${item.drucksache}" onclick="${isUnchecked ? '' : `showDetail('${item.drucksache}')`}">
|
|
|
|
|
<div class="list-item-header">
|
Bundesland filter & transparency: stringent split + visible source (#8)
Brings the Bundesland-Dropdown from a cosmetic header widget to a real
filter that propagates through every layer (Listing, internal search,
statistics, party/tag filters, upload mode), and at the same time makes
the source parliament visible in every place where assessments from
multiple bundesländer can be mixed.
Backend
- database.get_all_assessments(bundesland=None) — new optional filter,
"ALL" treated as None.
- database.search_assessments — bug fix: previous `if bundesland:`
branch incorrectly added a `WHERE bundesland='ALL'` clause; now
guarded with `bundesland and bundesland != "ALL"`.
- main.list_assessments — accepts ?bundesland= query param, includes the
bundesland field in the response so the frontend can render badges.
- main.get_single_assessment — also includes bundesland in the response
so the detail header can show the source parlament.
- main.search_landtag — early HTTP 400 when bundesland is missing or
"ALL"; the live Landtag adapter cannot serve a synthetic Bundesweit
request.
- main.index() and main.list_bundeslaender — synthetic "🌍 Bundesweit"
entry prepended to the bundesländer list (kept out of bundeslaender.py
on purpose — ALL is not a real state). Both endpoints additionally
expose a parlament_names map so the frontend can render the source
parliament without an extra round-trip.
Report (PDF + HTML)
- generate_html_report / generate_pdf_report — new optional bundesland
parameter. When set, the report header carries the parliament name
("Landtag von Sachsen-Anhalt", "Landtag Nordrhein-Westfalen", …)
beside the title. Three call sites updated: run_analysis,
run_drucksache_analysis, download_assessment_pdf.
Frontend (templates/index.html)
- Header dropdown gets the synthetic ALL entry as first option;
initial currentBundesland is now 'ALL' (was 'NRW').
- localStorage persistence: changeBundesland writes, DOMContentLoaded
reads and validates against the visible options.
- changeBundesland resets the score / party / tag filter state, syncs
the upload-mode bundesland select, disables the Landtag-Suche button
+ tooltip when ALL, and toggles a data-mode attribute on
.list-content (used by CSS to show/hide the per-item bundesland
badge).
- loadAssessments now sends ?bundesland=… so the API does the actual
filtering. updateStats renders an additional per-bundesland average
block (Ø NRW: x · Ø LSA: y) when in ALL mode and the loaded list
spans more than one bundesland.
- renderList prepends a small "bl-badge" beside the Drucksachen-Nummer.
Hidden in single-bundesland mode via CSS selector to avoid clutter.
- showDetail header now shows the parliament name as its own line
(.detail-parlament).
- searchLandtag has an early-out alert if currentBundesland === 'ALL',
saving a network round-trip.
- Upload-Mode bundesland select now starts with a "— Bundesland wählen
—" placeholder (no auto-default), and startAnalysis validates that a
concrete bundesland was chosen.
CSS
- .bl-badge plus the .list-content[data-mode="single"] hide rule.
- .detail-parlament for the detail header line.
- .header-parlament for the PDF report header line.
Resolves #8.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 23:00:39 +02:00
|
|
|
<span class="list-item-id">${blBadge}${item.drucksache}</span>
|
2026-03-28 22:30:24 +01:00
|
|
|
<span class="list-item-score ${scoreClass}">${scoreText}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="list-item-title">${item.title || 'Ohne Titel'}</div>
|
|
|
|
|
<div class="list-item-meta">${fraktionen} · ${item.datum || ''}</div>
|
|
|
|
|
${isUnchecked ? `
|
|
|
|
|
<button class="btn-check-now" onclick="event.stopPropagation(); checkNow('${item.drucksache}', this)">
|
|
|
|
|
🔍 Jetzt prüfen
|
|
|
|
|
</button>
|
|
|
|
|
` : `
|
|
|
|
|
<div class="list-item-tags">
|
|
|
|
|
${themen.map(t => `<span class="tag">${t}</span>`).join('')}
|
|
|
|
|
</div>
|
|
|
|
|
`}
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function debounceSearch(query) {
|
|
|
|
|
clearTimeout(searchTimeout);
|
|
|
|
|
searchTimeout = setTimeout(() => performSearch(query), 300);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function performSearch(query) {
|
|
|
|
|
if (query.length < 2) {
|
|
|
|
|
// Show all from DB with current filters
|
|
|
|
|
applyAllFilters();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isSearching = true;
|
|
|
|
|
document.getElementById('list-content').innerHTML = `
|
|
|
|
|
<div class="loading">
|
|
|
|
|
<div class="spinner"></div>
|
|
|
|
|
<span>Suche...</span>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const resp = await fetch(`/api/search?q=${encodeURIComponent(query)}&bundesland=${currentBundesland}`);
|
|
|
|
|
let results = await resp.json();
|
|
|
|
|
|
|
|
|
|
// Score-Filter anwenden
|
|
|
|
|
if (currentScoreFilter !== 'all') {
|
|
|
|
|
results = applyScoreFilter(results, currentScoreFilter);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Partei-Filter anwenden
|
|
|
|
|
if (currentParteiFilter) {
|
|
|
|
|
results = results.filter(a =>
|
|
|
|
|
(a.fraktionen || []).includes(currentParteiFilter)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderList(results);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
document.getElementById('list-content').innerHTML =
|
|
|
|
|
'<p style="padding: 1rem; color: #888;">Suchfehler</p>';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isSearching = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function checkNow(drucksache, btn) {
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
btn.textContent = '⏳ Prüfe...';
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const resp = await fetch('/api/analyze-drucksache', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
|
|
|
|
body: `drucksache=${encodeURIComponent(drucksache)}&bundesland=${currentBundesland}`
|
|
|
|
|
});
|
|
|
|
|
const data = await resp.json();
|
|
|
|
|
|
|
|
|
|
if (data.status === 'queued') {
|
|
|
|
|
btn.textContent = '⏳ Analysiere...';
|
|
|
|
|
pollAnalysis(data.job_id, drucksache, btn);
|
|
|
|
|
} else if (data.status === 'already_checked') {
|
|
|
|
|
btn.textContent = '✓ Bereits geprüft';
|
|
|
|
|
setTimeout(() => loadAssessments(), 500);
|
|
|
|
|
} else {
|
|
|
|
|
btn.textContent = '✗ Fehler';
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
btn.textContent = '✗ Fehler';
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function pollAnalysis(jobId, drucksache, btn) {
|
|
|
|
|
try {
|
|
|
|
|
const resp = await fetch(`/status/${jobId}`);
|
|
|
|
|
const data = await resp.json();
|
|
|
|
|
|
|
|
|
|
if (data.status === 'completed') {
|
|
|
|
|
btn.textContent = '✓ Geprüft';
|
|
|
|
|
btn.style.background = '#889e33'; // Green
|
|
|
|
|
// Update this item in current list to show as checked
|
|
|
|
|
const listItem = btn.closest('.list-item');
|
|
|
|
|
if (listItem) {
|
|
|
|
|
listItem.classList.remove('unchecked');
|
|
|
|
|
listItem.onclick = () => showDetail(drucksache);
|
|
|
|
|
}
|
|
|
|
|
// Reload assessments in background (for internal list)
|
|
|
|
|
loadAssessments();
|
|
|
|
|
// Show detail for this item
|
|
|
|
|
setTimeout(() => showDetail(drucksache), 500);
|
|
|
|
|
} else if (data.status === 'failed') {
|
|
|
|
|
btn.textContent = '✗ Fehlgeschlagen';
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
} else {
|
|
|
|
|
setTimeout(() => pollAnalysis(jobId, drucksache, btn), 2000);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
btn.textContent = '✗ Fehler';
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function changeBundesland(code) {
|
|
|
|
|
currentBundesland = code;
|
Bundesland filter & transparency: stringent split + visible source (#8)
Brings the Bundesland-Dropdown from a cosmetic header widget to a real
filter that propagates through every layer (Listing, internal search,
statistics, party/tag filters, upload mode), and at the same time makes
the source parliament visible in every place where assessments from
multiple bundesländer can be mixed.
Backend
- database.get_all_assessments(bundesland=None) — new optional filter,
"ALL" treated as None.
- database.search_assessments — bug fix: previous `if bundesland:`
branch incorrectly added a `WHERE bundesland='ALL'` clause; now
guarded with `bundesland and bundesland != "ALL"`.
- main.list_assessments — accepts ?bundesland= query param, includes the
bundesland field in the response so the frontend can render badges.
- main.get_single_assessment — also includes bundesland in the response
so the detail header can show the source parlament.
- main.search_landtag — early HTTP 400 when bundesland is missing or
"ALL"; the live Landtag adapter cannot serve a synthetic Bundesweit
request.
- main.index() and main.list_bundeslaender — synthetic "🌍 Bundesweit"
entry prepended to the bundesländer list (kept out of bundeslaender.py
on purpose — ALL is not a real state). Both endpoints additionally
expose a parlament_names map so the frontend can render the source
parliament without an extra round-trip.
Report (PDF + HTML)
- generate_html_report / generate_pdf_report — new optional bundesland
parameter. When set, the report header carries the parliament name
("Landtag von Sachsen-Anhalt", "Landtag Nordrhein-Westfalen", …)
beside the title. Three call sites updated: run_analysis,
run_drucksache_analysis, download_assessment_pdf.
Frontend (templates/index.html)
- Header dropdown gets the synthetic ALL entry as first option;
initial currentBundesland is now 'ALL' (was 'NRW').
- localStorage persistence: changeBundesland writes, DOMContentLoaded
reads and validates against the visible options.
- changeBundesland resets the score / party / tag filter state, syncs
the upload-mode bundesland select, disables the Landtag-Suche button
+ tooltip when ALL, and toggles a data-mode attribute on
.list-content (used by CSS to show/hide the per-item bundesland
badge).
- loadAssessments now sends ?bundesland=… so the API does the actual
filtering. updateStats renders an additional per-bundesland average
block (Ø NRW: x · Ø LSA: y) when in ALL mode and the loaded list
spans more than one bundesland.
- renderList prepends a small "bl-badge" beside the Drucksachen-Nummer.
Hidden in single-bundesland mode via CSS selector to avoid clutter.
- showDetail header now shows the parliament name as its own line
(.detail-parlament).
- searchLandtag has an early-out alert if currentBundesland === 'ALL',
saving a network round-trip.
- Upload-Mode bundesland select now starts with a "— Bundesland wählen
—" placeholder (no auto-default), and startAnalysis validates that a
concrete bundesland was chosen.
CSS
- .bl-badge plus the .list-content[data-mode="single"] hide rule.
- .detail-parlament for the detail header line.
- .header-parlament for the PDF report header line.
Resolves #8.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 23:00:39 +02:00
|
|
|
localStorage.setItem('selectedBundesland', code);
|
|
|
|
|
|
|
|
|
|
// Filter zurücksetzen — Parteien & Tags pro Bundesland unterschiedlich,
|
|
|
|
|
// ein "LINKE"-Filter aus LSA würde in NRW eine leere Liste zeigen.
|
|
|
|
|
currentScoreFilter = 'all';
|
|
|
|
|
currentParteiFilter = '';
|
|
|
|
|
selectedTags.clear();
|
2026-03-28 22:30:24 +01:00
|
|
|
document.getElementById('search-input').value = '';
|
2026-04-09 11:27:29 +02:00
|
|
|
const landtagInput = document.getElementById('landtag-search-input');
|
|
|
|
|
if (landtagInput) landtagInput.value = '';
|
Bundesland filter & transparency: stringent split + visible source (#8)
Brings the Bundesland-Dropdown from a cosmetic header widget to a real
filter that propagates through every layer (Listing, internal search,
statistics, party/tag filters, upload mode), and at the same time makes
the source parliament visible in every place where assessments from
multiple bundesländer can be mixed.
Backend
- database.get_all_assessments(bundesland=None) — new optional filter,
"ALL" treated as None.
- database.search_assessments — bug fix: previous `if bundesland:`
branch incorrectly added a `WHERE bundesland='ALL'` clause; now
guarded with `bundesland and bundesland != "ALL"`.
- main.list_assessments — accepts ?bundesland= query param, includes the
bundesland field in the response so the frontend can render badges.
- main.get_single_assessment — also includes bundesland in the response
so the detail header can show the source parlament.
- main.search_landtag — early HTTP 400 when bundesland is missing or
"ALL"; the live Landtag adapter cannot serve a synthetic Bundesweit
request.
- main.index() and main.list_bundeslaender — synthetic "🌍 Bundesweit"
entry prepended to the bundesländer list (kept out of bundeslaender.py
on purpose — ALL is not a real state). Both endpoints additionally
expose a parlament_names map so the frontend can render the source
parliament without an extra round-trip.
Report (PDF + HTML)
- generate_html_report / generate_pdf_report — new optional bundesland
parameter. When set, the report header carries the parliament name
("Landtag von Sachsen-Anhalt", "Landtag Nordrhein-Westfalen", …)
beside the title. Three call sites updated: run_analysis,
run_drucksache_analysis, download_assessment_pdf.
Frontend (templates/index.html)
- Header dropdown gets the synthetic ALL entry as first option;
initial currentBundesland is now 'ALL' (was 'NRW').
- localStorage persistence: changeBundesland writes, DOMContentLoaded
reads and validates against the visible options.
- changeBundesland resets the score / party / tag filter state, syncs
the upload-mode bundesland select, disables the Landtag-Suche button
+ tooltip when ALL, and toggles a data-mode attribute on
.list-content (used by CSS to show/hide the per-item bundesland
badge).
- loadAssessments now sends ?bundesland=… so the API does the actual
filtering. updateStats renders an additional per-bundesland average
block (Ø NRW: x · Ø LSA: y) when in ALL mode and the loaded list
spans more than one bundesland.
- renderList prepends a small "bl-badge" beside the Drucksachen-Nummer.
Hidden in single-bundesland mode via CSS selector to avoid clutter.
- showDetail header now shows the parliament name as its own line
(.detail-parlament).
- searchLandtag has an early-out alert if currentBundesland === 'ALL',
saving a network round-trip.
- Upload-Mode bundesland select now starts with a "— Bundesland wählen
—" placeholder (no auto-default), and startAnalysis validates that a
concrete bundesland was chosen.
CSS
- .bl-badge plus the .list-content[data-mode="single"] hide rule.
- .detail-parlament for the detail header line.
- .header-parlament for the PDF report header line.
Resolves #8.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 23:00:39 +02:00
|
|
|
document.querySelectorAll('.filter-btn').forEach(b => {
|
|
|
|
|
b.classList.toggle('active', b.dataset.filter === 'all');
|
|
|
|
|
});
|
|
|
|
|
const parteiSelect = document.getElementById('partei-filter');
|
|
|
|
|
if (parteiSelect) parteiSelect.value = '';
|
|
|
|
|
|
|
|
|
|
// Upload-Mode-Dropdown synchronisieren. Bei "ALL" KEIN automatischer
|
|
|
|
|
// Default — der User soll im Upload-Form bewusst ein Bundesland wählen.
|
|
|
|
|
const uploadDropdown = document.getElementById('bundesland');
|
|
|
|
|
if (uploadDropdown) {
|
|
|
|
|
if (code === 'ALL') {
|
|
|
|
|
uploadDropdown.value = '';
|
|
|
|
|
} else {
|
|
|
|
|
uploadDropdown.value = code;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Landtag-Suche-Button im Bundesweit-Modus deaktivieren
|
|
|
|
|
const btnLandtag = document.getElementById('btn-landtag');
|
|
|
|
|
if (code === 'ALL') {
|
|
|
|
|
btnLandtag.disabled = true;
|
|
|
|
|
btnLandtag.title = 'Landtag-Suche nur mit konkretem Bundesland möglich';
|
|
|
|
|
} else {
|
|
|
|
|
btnLandtag.disabled = false;
|
|
|
|
|
btnLandtag.title = '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Modus-Klasse für CSS (Badges aus/an im Single-Modus)
|
|
|
|
|
document.getElementById('list-content').dataset.mode =
|
|
|
|
|
(code === 'ALL') ? 'all' : 'single';
|
|
|
|
|
|
2026-03-28 22:30:24 +01:00
|
|
|
loadAssessments();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function searchLandtag() {
|
Bundesland filter & transparency: stringent split + visible source (#8)
Brings the Bundesland-Dropdown from a cosmetic header widget to a real
filter that propagates through every layer (Listing, internal search,
statistics, party/tag filters, upload mode), and at the same time makes
the source parliament visible in every place where assessments from
multiple bundesländer can be mixed.
Backend
- database.get_all_assessments(bundesland=None) — new optional filter,
"ALL" treated as None.
- database.search_assessments — bug fix: previous `if bundesland:`
branch incorrectly added a `WHERE bundesland='ALL'` clause; now
guarded with `bundesland and bundesland != "ALL"`.
- main.list_assessments — accepts ?bundesland= query param, includes the
bundesland field in the response so the frontend can render badges.
- main.get_single_assessment — also includes bundesland in the response
so the detail header can show the source parlament.
- main.search_landtag — early HTTP 400 when bundesland is missing or
"ALL"; the live Landtag adapter cannot serve a synthetic Bundesweit
request.
- main.index() and main.list_bundeslaender — synthetic "🌍 Bundesweit"
entry prepended to the bundesländer list (kept out of bundeslaender.py
on purpose — ALL is not a real state). Both endpoints additionally
expose a parlament_names map so the frontend can render the source
parliament without an extra round-trip.
Report (PDF + HTML)
- generate_html_report / generate_pdf_report — new optional bundesland
parameter. When set, the report header carries the parliament name
("Landtag von Sachsen-Anhalt", "Landtag Nordrhein-Westfalen", …)
beside the title. Three call sites updated: run_analysis,
run_drucksache_analysis, download_assessment_pdf.
Frontend (templates/index.html)
- Header dropdown gets the synthetic ALL entry as first option;
initial currentBundesland is now 'ALL' (was 'NRW').
- localStorage persistence: changeBundesland writes, DOMContentLoaded
reads and validates against the visible options.
- changeBundesland resets the score / party / tag filter state, syncs
the upload-mode bundesland select, disables the Landtag-Suche button
+ tooltip when ALL, and toggles a data-mode attribute on
.list-content (used by CSS to show/hide the per-item bundesland
badge).
- loadAssessments now sends ?bundesland=… so the API does the actual
filtering. updateStats renders an additional per-bundesland average
block (Ø NRW: x · Ø LSA: y) when in ALL mode and the loaded list
spans more than one bundesland.
- renderList prepends a small "bl-badge" beside the Drucksachen-Nummer.
Hidden in single-bundesland mode via CSS selector to avoid clutter.
- showDetail header now shows the parliament name as its own line
(.detail-parlament).
- searchLandtag has an early-out alert if currentBundesland === 'ALL',
saving a network round-trip.
- Upload-Mode bundesland select now starts with a "— Bundesland wählen
—" placeholder (no auto-default), and startAnalysis validates that a
concrete bundesland was chosen.
CSS
- .bl-badge plus the .list-content[data-mode="single"] hide rule.
- .detail-parlament for the detail header line.
- .header-parlament for the PDF report header line.
Resolves #8.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 23:00:39 +02:00
|
|
|
if (currentBundesland === 'ALL') {
|
|
|
|
|
alert('Landtag-Suche ist nur mit Auswahl eines konkreten Bundeslands möglich.\nBitte oben ein Bundesland auswählen.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-04-09 11:27:29 +02:00
|
|
|
// #16: Landtag-Suche zieht aus dem dedizierten Landtag-Input,
|
|
|
|
|
// nicht mehr aus dem DB-Suchfeld.
|
|
|
|
|
const query = document.getElementById('landtag-search-input').value.trim();
|
2026-03-28 22:30:24 +01:00
|
|
|
if (query.length < 2) {
|
2026-04-09 11:27:29 +02:00
|
|
|
alert('Bitte mindestens 2 Zeichen ins Landtag-Suchfeld eingeben');
|
2026-03-28 22:30:24 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const btn = document.getElementById('btn-landtag');
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
btn.textContent = '⏳ Suche...';
|
|
|
|
|
|
|
|
|
|
document.getElementById('list-content').innerHTML = `
|
|
|
|
|
<div class="loading">
|
|
|
|
|
<div class="spinner"></div>
|
|
|
|
|
<span>Suche im Landtag ${currentBundesland}...</span>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const resp = await fetch(`/api/search-landtag?q=${encodeURIComponent(query)}&bundesland=${currentBundesland}`);
|
|
|
|
|
const results = await resp.json();
|
|
|
|
|
|
|
|
|
|
if (results.error) {
|
|
|
|
|
document.getElementById('list-content').innerHTML = `
|
|
|
|
|
<p style="padding: 1rem; color: #888;">${results.error}</p>
|
|
|
|
|
`;
|
|
|
|
|
} else if (results.length === 0) {
|
|
|
|
|
document.getElementById('list-content').innerHTML = `
|
|
|
|
|
<p style="padding: 1rem; color: #888;">Keine Treffer im Landtag für "${query}"</p>
|
|
|
|
|
`;
|
|
|
|
|
} else {
|
|
|
|
|
// Merge with checked status from DB
|
|
|
|
|
const checkedIds = new Set(allAssessments.map(a => a.drucksache));
|
|
|
|
|
const merged = results.map(r => ({
|
|
|
|
|
...r,
|
|
|
|
|
gwoeScore: checkedIds.has(r.drucksache)
|
|
|
|
|
? allAssessments.find(a => a.drucksache === r.drucksache)?.gwoeScore
|
|
|
|
|
: null,
|
|
|
|
|
status: checkedIds.has(r.drucksache) ? 'checked' : 'unchecked'
|
|
|
|
|
}));
|
|
|
|
|
renderList(merged);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
document.getElementById('list-content').innerHTML = `
|
|
|
|
|
<p style="padding: 1rem; color: #888;">Suchfehler: ${e.message}</p>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
btn.textContent = '🔍 Im Landtag';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function filterList(query) {
|
|
|
|
|
const q = query.toLowerCase();
|
|
|
|
|
let filtered = allAssessments.filter(a =>
|
|
|
|
|
(a.title || '').toLowerCase().includes(q) ||
|
|
|
|
|
(a.drucksache || '').toLowerCase().includes(q) ||
|
|
|
|
|
(a.fraktionen || []).join(' ').toLowerCase().includes(q) ||
|
|
|
|
|
(a.themen || []).join(' ').toLowerCase().includes(q)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Score-Filter anwenden
|
|
|
|
|
if (currentScoreFilter !== 'all') {
|
|
|
|
|
filtered = applyScoreFilter(filtered, currentScoreFilter);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Partei-Filter anwenden
|
|
|
|
|
if (currentParteiFilter) {
|
|
|
|
|
filtered = filtered.filter(a =>
|
|
|
|
|
(a.fraktionen || []).includes(currentParteiFilter)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderList(filtered);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setScoreFilter(filter, btn) {
|
|
|
|
|
currentScoreFilter = filter;
|
|
|
|
|
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
|
|
|
|
btn.classList.add('active');
|
|
|
|
|
applyAllFilters();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setParteiFilter(partei) {
|
|
|
|
|
currentParteiFilter = partei;
|
|
|
|
|
applyAllFilters();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applyAllFilters() {
|
|
|
|
|
let filtered = allAssessments;
|
|
|
|
|
|
|
|
|
|
// Score-Filter
|
|
|
|
|
if (currentScoreFilter !== 'all') {
|
|
|
|
|
filtered = applyScoreFilter(filtered, currentScoreFilter);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Partei-Filter
|
|
|
|
|
if (currentParteiFilter) {
|
|
|
|
|
filtered = filtered.filter(a =>
|
|
|
|
|
(a.fraktionen || []).includes(currentParteiFilter)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderList(filtered);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applyScoreFilter(items, filter) {
|
|
|
|
|
switch (filter) {
|
|
|
|
|
case 'high': return items.filter(a => a.gwoeScore >= 8);
|
|
|
|
|
case 'mid': return items.filter(a => a.gwoeScore >= 5 && a.gwoeScore < 8);
|
|
|
|
|
case 'low': return items.filter(a => a.gwoeScore < 5);
|
|
|
|
|
default: return items;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showDetail(drucksache) {
|
|
|
|
|
const item = allAssessments.find(a => a.drucksache === drucksache);
|
|
|
|
|
if (!item) return;
|
|
|
|
|
|
|
|
|
|
// Highlight active item
|
|
|
|
|
document.querySelectorAll('.list-item').forEach(el => el.classList.remove('active'));
|
|
|
|
|
// Find and highlight the list item by drucksache
|
|
|
|
|
const listItem = document.querySelector(`.list-item[data-drucksache="${drucksache}"]`);
|
|
|
|
|
if (listItem) listItem.classList.add('active');
|
|
|
|
|
|
|
|
|
|
// Skala 0-10
|
|
|
|
|
const scoreClass = item.gwoeScore >= 8 ? 'score-high' :
|
|
|
|
|
item.gwoeScore >= 5 ? 'score-mid' :
|
|
|
|
|
item.gwoeScore >= 3 ? 'score-low' : 'score-negative';
|
|
|
|
|
|
|
|
|
|
// Matrix als 5x5-Tabelle wie im PDF
|
|
|
|
|
const matrixData = {};
|
|
|
|
|
(item.gwoeMatrix || []).forEach(m => { matrixData[m.field] = m; });
|
|
|
|
|
|
|
|
|
|
const rowLabels = {
|
|
|
|
|
'A': 'Lieferant:innen',
|
|
|
|
|
'B': 'Finanzen',
|
|
|
|
|
'C': 'Führung/Verwaltung',
|
|
|
|
|
'D': 'Bürger:innen',
|
|
|
|
|
'E': 'Gesellschaft/Natur'
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Spaltenüberschriften der GWÖ-Matrix (5 Werte)
|
|
|
|
|
const colLabels = {
|
|
|
|
|
1: 'Menschen-würde',
|
|
|
|
|
2: 'Solidarität',
|
|
|
|
|
3: 'Ökol. Nachh.',
|
|
|
|
|
4: 'Soz. Gerecht.',
|
|
|
|
|
5: 'Transparenz'
|
|
|
|
|
};
|
|
|
|
|
const colFull = {
|
|
|
|
|
1: 'Menschenwürde',
|
|
|
|
|
2: 'Solidarität',
|
|
|
|
|
3: 'Ökologische Nachhaltigkeit',
|
|
|
|
|
4: 'Soziale Gerechtigkeit',
|
|
|
|
|
5: 'Transparenz & Mitbestimmung'
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let matrixTableHtml = '<table class="matrix-table"><thead><tr><th></th>';
|
|
|
|
|
for (let col = 1; col <= 5; col++) matrixTableHtml += `<th title="${colFull[col]}">${colLabels[col]}</th>`;
|
|
|
|
|
matrixTableHtml += '</tr></thead><tbody>';
|
|
|
|
|
|
|
|
|
|
['A', 'B', 'C', 'D', 'E'].forEach(row => {
|
|
|
|
|
matrixTableHtml += `<tr><th>${row}: ${rowLabels[row]}</th>`;
|
|
|
|
|
for (let col = 1; col <= 5; col++) {
|
|
|
|
|
const field = `${row}${col}`;
|
|
|
|
|
const entry = matrixData[field];
|
|
|
|
|
if (entry) {
|
|
|
|
|
const cssClass = entry.rating > 0 ? 'positive' : (entry.rating < 0 ? 'negative' : 'neutral');
|
|
|
|
|
matrixTableHtml += `<td class="${cssClass}" title="${entry.aspect || entry.label}">${entry.symbol}</td>`;
|
|
|
|
|
} else {
|
|
|
|
|
matrixTableHtml += '<td></td>';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
matrixTableHtml += '</tr>';
|
|
|
|
|
});
|
|
|
|
|
matrixTableHtml += '</tbody></table>';
|
|
|
|
|
|
|
|
|
|
// Zusätzlich die Detail-Liste der bewerteten Felder
|
|
|
|
|
const matrixDetailHtml = (item.gwoeMatrix || []).map(m => `
|
|
|
|
|
<div class="matrix-item">
|
|
|
|
|
<span class="matrix-label">${m.field}: ${m.label}</span>
|
|
|
|
|
<span class="matrix-rating ${m.rating > 0 ? 'rating-pos' : m.rating < 0 ? 'rating-neg' : 'rating-neutral'}">${m.symbol}</span>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('');
|
|
|
|
|
|
|
|
|
|
const stärkenHtml = (item.stärken || []).map(s => `<li>${s}</li>`).join('');
|
|
|
|
|
const schwächenHtml = (item.schwächen || []).map(s => `<li>${s}</li>`).join('');
|
|
|
|
|
|
|
|
|
|
// Verbesserungsvorschläge formatieren
|
|
|
|
|
const verbesserungenHtml = (item.verbesserungen || []).map(v => {
|
|
|
|
|
// Redline-Format: **fett** = grün/neu, ~~durchgestrichen~~ = rot/gelöscht
|
|
|
|
|
let vorschlag = v.vorschlag || '';
|
|
|
|
|
vorschlag = vorschlag.replace(/\*\*([^*]+)\*\*/g, '<span style="color: #889e33; font-weight: bold;">$1</span>');
|
|
|
|
|
vorschlag = vorschlag.replace(/~~([^~]+)~~/g, '<span style="color: #d00000; text-decoration: line-through;">$1</span>');
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
<div style="margin: 0.75rem 0; padding: 0.75rem; border: 1px solid var(--color-lightgray); border-radius: 4px;">
|
|
|
|
|
<div style="background: #f5f5f5; padding: 0.5rem; margin-bottom: 0.5rem; font-size: 0.9rem;">
|
|
|
|
|
<strong>Original:</strong><br>${v.original || '-'}
|
|
|
|
|
</div>
|
|
|
|
|
<div style="background: rgba(136, 158, 51, 0.1); border-left: 3px solid #889e33; padding: 0.5rem; font-size: 0.9rem;">
|
|
|
|
|
<strong>Vorschlag:</strong><br>${vorschlag}
|
|
|
|
|
</div>
|
|
|
|
|
<div style="font-size: 0.85rem; color: #666; margin-top: 0.5rem; font-style: italic;">
|
|
|
|
|
${v.begruendung || ''}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`}).join('');
|
|
|
|
|
|
|
|
|
|
const wahlprogrammHtml = (item.wahlprogrammScores || []).map(wp => {
|
|
|
|
|
// Zitate formatieren mit klickbaren Links
|
|
|
|
|
const zitateHtml = (wp.wahlprogramm?.zitate || []).map(z => `
|
|
|
|
|
<div style="margin: 0.5rem 0; padding: 0.5rem; background: #f8f9fa; border-left: 3px solid #889e33; font-size: 0.85rem;">
|
|
|
|
|
<em>"${z.text}"</em><br>
|
|
|
|
|
<a href="${z.url || '#'}" target="_blank" style="color: #009da5; font-size: 0.8rem;">
|
|
|
|
|
📄 ${z.quelle}
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('');
|
#63 B+C: Force-Honesty + UI-Warning bei Score ohne Zitate
Problem: BUND 21/3660 zeigt Score 10/10 für Linke und Grüne, aber null
Zitate — der Report sieht aus als sei die Bewertung fundiert, obwohl das
LLM mangels indexierter Quellen (linke-grundsatz fehlt) aus
Trainingswissen geraten hat. User-Feedback: "Da muss stehen warum."
Fix C — Force-Honesty im Prompt:
- format_quotes_for_prompt akzeptiert neuen Parameter searched_parties.
Parteien, für die kein Chunk retrievt wurde, werden explizit als
"KEINE QUELLEN VORHANDEN" markiert, mit der Anweisung "score: 0,
zitate: [], Begründung: keine Quellen im Index".
- Neue ZITATEREGEL Punkt 5: "Wenn KEINE QUELLEN VORHANDEN → score 0."
Das ist die strukturelle Lösung — das LLM darf nicht mehr raten.
- analyzer.py: fraktionen-Liste wird an format_quotes_for_prompt als
searched_parties durchgereicht.
Fix B — UI-Transparenz:
- index.html: gelbe Warn-Box (amber, border-left #ffc107) wenn
wp.wahlprogramm.score > 0 aber wp.wahlprogramm.zitate.length === 0:
"Keine belegbaren Quellen im Index gefunden — Score basiert auf
LLM-Einschätzung, nicht auf verifizierten Programm-Stellen."
- Wird für bestehende Assessments sofort sichtbar (JS-seitig berechnet),
keine DB-Migration nötig. Neue Assessments nach Force-Honesty sollten
idealerweise Score=0 haben, aber die Warning ist ein Fallback für
den Fall dass das LLM die Prompt-Regel nicht immer 100% befolgt.
Fix A (Linke/AfD-Grundsatzprogramme) folgt als separater Commit —
sind öffentlich downloadbar, brauchen manuellen Sichtbarkeitscheck.
Tests: 194/194 grün (keine Schema-Änderung, nur Prompt + Template).
Refs: #63, ADR 0001
2026-04-10 09:32:31 +02:00
|
|
|
|
|
|
|
|
// Issue #63: Transparenz-Warnung bei Score > 0 ohne Zitate.
|
|
|
|
|
// Differenziert zwischen "Score 0 = keine Quellen" (LLM hat
|
|
|
|
|
// Force-Honesty befolgt) und "Score > 0 aber 0 Zitate" (LLM
|
|
|
|
|
// hat trotzdem geratet oder Zitate wurden von reconstruct_zitate
|
|
|
|
|
// verworfen). Nur bei Scores > 0 warnen, weil Score 0 schon
|
|
|
|
|
// selbsterklärend ist.
|
|
|
|
|
const wpScore = wp.wahlprogramm?.score ?? 0;
|
|
|
|
|
const wpZitateCount = (wp.wahlprogramm?.zitate || []).length;
|
|
|
|
|
const noQuotesWarning = (wpScore > 0 && wpZitateCount === 0) ? `
|
|
|
|
|
<div style="margin: 0.5rem 0; padding: 0.5rem; background: #fff3cd; border-left: 3px solid #ffc107; font-size: 0.8rem; color: #856404;">
|
|
|
|
|
⚠ Keine belegbaren Quellen im Index gefunden — Score basiert auf LLM-Einschätzung, nicht auf verifizierten Programm-Stellen.
|
|
|
|
|
</div>
|
|
|
|
|
` : '';
|
|
|
|
|
|
2026-03-28 22:30:24 +01:00
|
|
|
return `
|
|
|
|
|
<div style="margin: 0.75rem 0; padding: 0.75rem; background: var(--color-bg); border-radius: 4px;">
|
|
|
|
|
<strong>${wp.fraktion}</strong>
|
|
|
|
|
${wp.istAntragsteller ? ' <span style="color:#889e33">(Antragsteller)</span>' : ''}
|
|
|
|
|
${wp.istRegierung ? ' <span style="color:#009da5">(Regierung)</span>' : ''}<br>
|
|
|
|
|
<div style="margin: 0.5rem 0;">
|
#63 B+C: Force-Honesty + UI-Warning bei Score ohne Zitate
Problem: BUND 21/3660 zeigt Score 10/10 für Linke und Grüne, aber null
Zitate — der Report sieht aus als sei die Bewertung fundiert, obwohl das
LLM mangels indexierter Quellen (linke-grundsatz fehlt) aus
Trainingswissen geraten hat. User-Feedback: "Da muss stehen warum."
Fix C — Force-Honesty im Prompt:
- format_quotes_for_prompt akzeptiert neuen Parameter searched_parties.
Parteien, für die kein Chunk retrievt wurde, werden explizit als
"KEINE QUELLEN VORHANDEN" markiert, mit der Anweisung "score: 0,
zitate: [], Begründung: keine Quellen im Index".
- Neue ZITATEREGEL Punkt 5: "Wenn KEINE QUELLEN VORHANDEN → score 0."
Das ist die strukturelle Lösung — das LLM darf nicht mehr raten.
- analyzer.py: fraktionen-Liste wird an format_quotes_for_prompt als
searched_parties durchgereicht.
Fix B — UI-Transparenz:
- index.html: gelbe Warn-Box (amber, border-left #ffc107) wenn
wp.wahlprogramm.score > 0 aber wp.wahlprogramm.zitate.length === 0:
"Keine belegbaren Quellen im Index gefunden — Score basiert auf
LLM-Einschätzung, nicht auf verifizierten Programm-Stellen."
- Wird für bestehende Assessments sofort sichtbar (JS-seitig berechnet),
keine DB-Migration nötig. Neue Assessments nach Force-Honesty sollten
idealerweise Score=0 haben, aber die Warning ist ein Fallback für
den Fall dass das LLM die Prompt-Regel nicht immer 100% befolgt.
Fix A (Linke/AfD-Grundsatzprogramme) folgt als separater Commit —
sind öffentlich downloadbar, brauchen manuellen Sichtbarkeitscheck.
Tests: 194/194 grün (keine Schema-Änderung, nur Prompt + Template).
Refs: #63, ADR 0001
2026-04-10 09:32:31 +02:00
|
|
|
Wahlprogramm: <strong>${wp.wahlprogramm?.score || '-'}/10</strong> ·
|
2026-03-28 22:30:24 +01:00
|
|
|
Parteiprogramm: <strong>${wp.parteiprogramm?.score || '-'}/10</strong>
|
|
|
|
|
</div>
|
|
|
|
|
${wp.wahlprogramm?.begründung ? `<div style="font-size: 0.9rem; color: #555; margin-bottom: 0.5rem;">${wp.wahlprogramm.begründung}</div>` : ''}
|
#63 B+C: Force-Honesty + UI-Warning bei Score ohne Zitate
Problem: BUND 21/3660 zeigt Score 10/10 für Linke und Grüne, aber null
Zitate — der Report sieht aus als sei die Bewertung fundiert, obwohl das
LLM mangels indexierter Quellen (linke-grundsatz fehlt) aus
Trainingswissen geraten hat. User-Feedback: "Da muss stehen warum."
Fix C — Force-Honesty im Prompt:
- format_quotes_for_prompt akzeptiert neuen Parameter searched_parties.
Parteien, für die kein Chunk retrievt wurde, werden explizit als
"KEINE QUELLEN VORHANDEN" markiert, mit der Anweisung "score: 0,
zitate: [], Begründung: keine Quellen im Index".
- Neue ZITATEREGEL Punkt 5: "Wenn KEINE QUELLEN VORHANDEN → score 0."
Das ist die strukturelle Lösung — das LLM darf nicht mehr raten.
- analyzer.py: fraktionen-Liste wird an format_quotes_for_prompt als
searched_parties durchgereicht.
Fix B — UI-Transparenz:
- index.html: gelbe Warn-Box (amber, border-left #ffc107) wenn
wp.wahlprogramm.score > 0 aber wp.wahlprogramm.zitate.length === 0:
"Keine belegbaren Quellen im Index gefunden — Score basiert auf
LLM-Einschätzung, nicht auf verifizierten Programm-Stellen."
- Wird für bestehende Assessments sofort sichtbar (JS-seitig berechnet),
keine DB-Migration nötig. Neue Assessments nach Force-Honesty sollten
idealerweise Score=0 haben, aber die Warning ist ein Fallback für
den Fall dass das LLM die Prompt-Regel nicht immer 100% befolgt.
Fix A (Linke/AfD-Grundsatzprogramme) folgt als separater Commit —
sind öffentlich downloadbar, brauchen manuellen Sichtbarkeitscheck.
Tests: 194/194 grün (keine Schema-Änderung, nur Prompt + Template).
Refs: #63, ADR 0001
2026-04-10 09:32:31 +02:00
|
|
|
${noQuotesWarning}
|
2026-03-28 22:30:24 +01:00
|
|
|
${zitateHtml}
|
|
|
|
|
</div>
|
|
|
|
|
`}).join('');
|
|
|
|
|
|
|
|
|
|
document.getElementById('detail-panel').innerHTML = `
|
|
|
|
|
<div class="detail-card">
|
2026-04-07 13:48:55 +02:00
|
|
|
<button class="btn-back-mobile" onclick="document.querySelector('.list-panel').scrollIntoView({behavior:'smooth', block:'start'})">← Zur Liste</button>
|
2026-03-28 22:30:24 +01:00
|
|
|
<div class="detail-header">
|
|
|
|
|
<div>
|
|
|
|
|
<div class="detail-title">${item.title || 'Ohne Titel'}</div>
|
Bundesland filter & transparency: stringent split + visible source (#8)
Brings the Bundesland-Dropdown from a cosmetic header widget to a real
filter that propagates through every layer (Listing, internal search,
statistics, party/tag filters, upload mode), and at the same time makes
the source parliament visible in every place where assessments from
multiple bundesländer can be mixed.
Backend
- database.get_all_assessments(bundesland=None) — new optional filter,
"ALL" treated as None.
- database.search_assessments — bug fix: previous `if bundesland:`
branch incorrectly added a `WHERE bundesland='ALL'` clause; now
guarded with `bundesland and bundesland != "ALL"`.
- main.list_assessments — accepts ?bundesland= query param, includes the
bundesland field in the response so the frontend can render badges.
- main.get_single_assessment — also includes bundesland in the response
so the detail header can show the source parlament.
- main.search_landtag — early HTTP 400 when bundesland is missing or
"ALL"; the live Landtag adapter cannot serve a synthetic Bundesweit
request.
- main.index() and main.list_bundeslaender — synthetic "🌍 Bundesweit"
entry prepended to the bundesländer list (kept out of bundeslaender.py
on purpose — ALL is not a real state). Both endpoints additionally
expose a parlament_names map so the frontend can render the source
parliament without an extra round-trip.
Report (PDF + HTML)
- generate_html_report / generate_pdf_report — new optional bundesland
parameter. When set, the report header carries the parliament name
("Landtag von Sachsen-Anhalt", "Landtag Nordrhein-Westfalen", …)
beside the title. Three call sites updated: run_analysis,
run_drucksache_analysis, download_assessment_pdf.
Frontend (templates/index.html)
- Header dropdown gets the synthetic ALL entry as first option;
initial currentBundesland is now 'ALL' (was 'NRW').
- localStorage persistence: changeBundesland writes, DOMContentLoaded
reads and validates against the visible options.
- changeBundesland resets the score / party / tag filter state, syncs
the upload-mode bundesland select, disables the Landtag-Suche button
+ tooltip when ALL, and toggles a data-mode attribute on
.list-content (used by CSS to show/hide the per-item bundesland
badge).
- loadAssessments now sends ?bundesland=… so the API does the actual
filtering. updateStats renders an additional per-bundesland average
block (Ø NRW: x · Ø LSA: y) when in ALL mode and the loaded list
spans more than one bundesland.
- renderList prepends a small "bl-badge" beside the Drucksachen-Nummer.
Hidden in single-bundesland mode via CSS selector to avoid clutter.
- showDetail header now shows the parliament name as its own line
(.detail-parlament).
- searchLandtag has an early-out alert if currentBundesland === 'ALL',
saving a network round-trip.
- Upload-Mode bundesland select now starts with a "— Bundesland wählen
—" placeholder (no auto-default), and startAnalysis validates that a
concrete bundesland was chosen.
CSS
- .bl-badge plus the .list-content[data-mode="single"] hide rule.
- .detail-parlament for the detail header line.
- .header-parlament for the PDF report header line.
Resolves #8.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 23:00:39 +02:00
|
|
|
${item.bundesland && PARLAMENT_NAMES[item.bundesland] ? `<div class="detail-parlament">${PARLAMENT_NAMES[item.bundesland]}</div>` : ''}
|
2026-03-28 22:30:24 +01:00
|
|
|
<div class="detail-id">${item.drucksache} · ${(item.fraktionen || []).join(', ')} · ${item.datum || ''}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="score-display">
|
|
|
|
|
<div class="score-big ${scoreClass}">${item.gwoeScore}</div>
|
|
|
|
|
<div class="score-label">GWÖ-Score</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
${item.themen && item.themen.length > 0 ? `
|
|
|
|
|
<div class="themen-tags" style="margin-bottom: 1rem;">
|
|
|
|
|
${item.themen.map(t => `<span class="tag">${t}</span>`).join(' ')}
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
|
|
|
|
|
<h3 class="section-title">Zusammenfassung</h3>
|
|
|
|
|
<div class="text-block">${item.antragZusammenfassung || '-'}</div>
|
|
|
|
|
|
|
|
|
|
${item.antragKernpunkte && item.antragKernpunkte.length > 0 ? `
|
|
|
|
|
<h3 class="section-title">Kernpunkte</h3>
|
|
|
|
|
<ul class="kernpunkte-list">${item.antragKernpunkte.map(k => `<li>${k}</li>`).join('')}</ul>
|
|
|
|
|
` : ''}
|
|
|
|
|
|
|
|
|
|
<h3 class="section-title">GWÖ-Begründung</h3>
|
|
|
|
|
<div class="text-block">${item.gwoeBegründung || '-'}</div>
|
|
|
|
|
|
|
|
|
|
${item.gwoeSchwerpunkt ? `
|
|
|
|
|
<h3 class="section-title">GWÖ-Schwerpunkt</h3>
|
|
|
|
|
<div class="text-block">${item.gwoeSchwerpunkt}</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
|
|
|
|
|
${item.gwoeMatrix && item.gwoeMatrix.length > 0 ? `
|
|
|
|
|
<h3 class="section-title">GWÖ-Matrix</h3>
|
|
|
|
|
${matrixTableHtml}
|
|
|
|
|
<div class="matrix-grid" style="margin-top: 0.75rem;">${matrixDetailHtml}</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
|
|
|
|
|
${wahlprogrammHtml ? `
|
|
|
|
|
<h3 class="section-title">Programmtreue</h3>
|
|
|
|
|
${wahlprogrammHtml}
|
|
|
|
|
` : ''}
|
|
|
|
|
|
|
|
|
|
${stärkenHtml ? `
|
|
|
|
|
<h3 class="section-title">Stärken</h3>
|
|
|
|
|
<ul class="strength-list">${stärkenHtml}</ul>
|
|
|
|
|
` : ''}
|
|
|
|
|
|
|
|
|
|
${schwächenHtml ? `
|
|
|
|
|
<h3 class="section-title">Schwächen</h3>
|
|
|
|
|
<ul class="weakness-list">${schwächenHtml}</ul>
|
|
|
|
|
` : ''}
|
|
|
|
|
|
|
|
|
|
${verbesserungenHtml ? `
|
|
|
|
|
<h3 class="section-title">Verbesserungsvorschläge</h3>
|
|
|
|
|
${verbesserungenHtml}
|
|
|
|
|
` : ''}
|
|
|
|
|
|
|
|
|
|
${item.verbesserungspotenzial ? `
|
|
|
|
|
<h3 class="section-title">Verbesserungspotenzial</h3>
|
|
|
|
|
<div class="text-block">${item.verbesserungspotenzial}</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
|
|
|
|
|
<h3 class="section-title">Empfehlung</h3>
|
|
|
|
|
<div class="text-block">
|
|
|
|
|
<strong>${item.empfehlungSymbol || ''} ${item.empfehlung || '-'}</strong>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<a href="${item.link}" target="_blank" class="btn-pdf">📄 Original-PDF öffnen</a>
|
|
|
|
|
<a href="/api/assessment/pdf?drucksache=${encodeURIComponent(item.drucksache)}" class="btn-pdf" style="background: var(--color-green); margin-left: 0.5rem;">📥 GWÖ-Report als PDF</a>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
2026-04-07 13:48:55 +02:00
|
|
|
|
|
|
|
|
// Auf Mobile: zum Detail-Panel scrollen, damit der gewählte Antrag sichtbar wird
|
|
|
|
|
if (window.matchMedia('(max-width: 900px)').matches) {
|
|
|
|
|
document.getElementById('detail-panel').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
|
|
|
}
|
2026-03-28 22:30:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mode Toggle
|
|
|
|
|
function showMode(mode) {
|
|
|
|
|
document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
|
|
|
|
|
if (event && event.currentTarget) {
|
|
|
|
|
event.currentTarget.classList.add('active');
|
|
|
|
|
} else {
|
|
|
|
|
// Find and activate the right button
|
|
|
|
|
document.querySelectorAll('.mode-btn').forEach(b => {
|
|
|
|
|
if (b.textContent.includes(mode === 'browse' ? 'Durchsuchen' : mode === 'tags' ? 'Tags' : 'Prüfen')) {
|
|
|
|
|
b.classList.add('active');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.getElementById('browse-mode').style.display = mode === 'browse' ? 'flex' : 'none';
|
|
|
|
|
document.getElementById('tags-mode').style.display = mode === 'tags' ? 'flex' : 'none';
|
|
|
|
|
document.getElementById('upload-mode').style.display = mode === 'upload' ? 'flex' : 'none';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Upload Tab Toggle
|
|
|
|
|
function showTab(tab) {
|
|
|
|
|
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
|
|
|
|
event.currentTarget.classList.add('active');
|
|
|
|
|
|
|
|
|
|
document.getElementById('tab-text').style.display = tab === 'text' ? 'block' : 'none';
|
|
|
|
|
document.getElementById('tab-file').style.display = tab === 'file' ? 'block' : 'none';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleFile(input) {
|
|
|
|
|
if (input.files[0]) {
|
|
|
|
|
document.getElementById('file-name').textContent = input.files[0].name;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function startAnalysis() {
|
|
|
|
|
const btn = document.getElementById('analyze-btn');
|
|
|
|
|
const statusDiv = document.getElementById('analysis-status');
|
|
|
|
|
const resultDiv = document.getElementById('analysis-result');
|
|
|
|
|
|
|
|
|
|
const text = document.getElementById('antrag-text').value;
|
|
|
|
|
const file = document.getElementById('file-input').files[0];
|
|
|
|
|
const bundesland = document.getElementById('bundesland').value;
|
Bundesland filter & transparency: stringent split + visible source (#8)
Brings the Bundesland-Dropdown from a cosmetic header widget to a real
filter that propagates through every layer (Listing, internal search,
statistics, party/tag filters, upload mode), and at the same time makes
the source parliament visible in every place where assessments from
multiple bundesländer can be mixed.
Backend
- database.get_all_assessments(bundesland=None) — new optional filter,
"ALL" treated as None.
- database.search_assessments — bug fix: previous `if bundesland:`
branch incorrectly added a `WHERE bundesland='ALL'` clause; now
guarded with `bundesland and bundesland != "ALL"`.
- main.list_assessments — accepts ?bundesland= query param, includes the
bundesland field in the response so the frontend can render badges.
- main.get_single_assessment — also includes bundesland in the response
so the detail header can show the source parlament.
- main.search_landtag — early HTTP 400 when bundesland is missing or
"ALL"; the live Landtag adapter cannot serve a synthetic Bundesweit
request.
- main.index() and main.list_bundeslaender — synthetic "🌍 Bundesweit"
entry prepended to the bundesländer list (kept out of bundeslaender.py
on purpose — ALL is not a real state). Both endpoints additionally
expose a parlament_names map so the frontend can render the source
parliament without an extra round-trip.
Report (PDF + HTML)
- generate_html_report / generate_pdf_report — new optional bundesland
parameter. When set, the report header carries the parliament name
("Landtag von Sachsen-Anhalt", "Landtag Nordrhein-Westfalen", …)
beside the title. Three call sites updated: run_analysis,
run_drucksache_analysis, download_assessment_pdf.
Frontend (templates/index.html)
- Header dropdown gets the synthetic ALL entry as first option;
initial currentBundesland is now 'ALL' (was 'NRW').
- localStorage persistence: changeBundesland writes, DOMContentLoaded
reads and validates against the visible options.
- changeBundesland resets the score / party / tag filter state, syncs
the upload-mode bundesland select, disables the Landtag-Suche button
+ tooltip when ALL, and toggles a data-mode attribute on
.list-content (used by CSS to show/hide the per-item bundesland
badge).
- loadAssessments now sends ?bundesland=… so the API does the actual
filtering. updateStats renders an additional per-bundesland average
block (Ø NRW: x · Ø LSA: y) when in ALL mode and the loaded list
spans more than one bundesland.
- renderList prepends a small "bl-badge" beside the Drucksachen-Nummer.
Hidden in single-bundesland mode via CSS selector to avoid clutter.
- showDetail header now shows the parliament name as its own line
(.detail-parlament).
- searchLandtag has an early-out alert if currentBundesland === 'ALL',
saving a network round-trip.
- Upload-Mode bundesland select now starts with a "— Bundesland wählen
—" placeholder (no auto-default), and startAnalysis validates that a
concrete bundesland was chosen.
CSS
- .bl-badge plus the .list-content[data-mode="single"] hide rule.
- .detail-parlament for the detail header line.
- .header-parlament for the PDF report header line.
Resolves #8.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 23:00:39 +02:00
|
|
|
|
2026-03-28 22:30:24 +01:00
|
|
|
if (!text && !file) {
|
|
|
|
|
alert('Bitte Text eingeben oder PDF hochladen');
|
|
|
|
|
return;
|
|
|
|
|
}
|
Bundesland filter & transparency: stringent split + visible source (#8)
Brings the Bundesland-Dropdown from a cosmetic header widget to a real
filter that propagates through every layer (Listing, internal search,
statistics, party/tag filters, upload mode), and at the same time makes
the source parliament visible in every place where assessments from
multiple bundesländer can be mixed.
Backend
- database.get_all_assessments(bundesland=None) — new optional filter,
"ALL" treated as None.
- database.search_assessments — bug fix: previous `if bundesland:`
branch incorrectly added a `WHERE bundesland='ALL'` clause; now
guarded with `bundesland and bundesland != "ALL"`.
- main.list_assessments — accepts ?bundesland= query param, includes the
bundesland field in the response so the frontend can render badges.
- main.get_single_assessment — also includes bundesland in the response
so the detail header can show the source parlament.
- main.search_landtag — early HTTP 400 when bundesland is missing or
"ALL"; the live Landtag adapter cannot serve a synthetic Bundesweit
request.
- main.index() and main.list_bundeslaender — synthetic "🌍 Bundesweit"
entry prepended to the bundesländer list (kept out of bundeslaender.py
on purpose — ALL is not a real state). Both endpoints additionally
expose a parlament_names map so the frontend can render the source
parliament without an extra round-trip.
Report (PDF + HTML)
- generate_html_report / generate_pdf_report — new optional bundesland
parameter. When set, the report header carries the parliament name
("Landtag von Sachsen-Anhalt", "Landtag Nordrhein-Westfalen", …)
beside the title. Three call sites updated: run_analysis,
run_drucksache_analysis, download_assessment_pdf.
Frontend (templates/index.html)
- Header dropdown gets the synthetic ALL entry as first option;
initial currentBundesland is now 'ALL' (was 'NRW').
- localStorage persistence: changeBundesland writes, DOMContentLoaded
reads and validates against the visible options.
- changeBundesland resets the score / party / tag filter state, syncs
the upload-mode bundesland select, disables the Landtag-Suche button
+ tooltip when ALL, and toggles a data-mode attribute on
.list-content (used by CSS to show/hide the per-item bundesland
badge).
- loadAssessments now sends ?bundesland=… so the API does the actual
filtering. updateStats renders an additional per-bundesland average
block (Ø NRW: x · Ø LSA: y) when in ALL mode and the loaded list
spans more than one bundesland.
- renderList prepends a small "bl-badge" beside the Drucksachen-Nummer.
Hidden in single-bundesland mode via CSS selector to avoid clutter.
- showDetail header now shows the parliament name as its own line
(.detail-parlament).
- searchLandtag has an early-out alert if currentBundesland === 'ALL',
saving a network round-trip.
- Upload-Mode bundesland select now starts with a "— Bundesland wählen
—" placeholder (no auto-default), and startAnalysis validates that a
concrete bundesland was chosen.
CSS
- .bl-badge plus the .list-content[data-mode="single"] hide rule.
- .detail-parlament for the detail header line.
- .header-parlament for the PDF report header line.
Resolves #8.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 23:00:39 +02:00
|
|
|
if (!bundesland) {
|
|
|
|
|
alert('Bitte ein Bundesland wählen.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-28 22:30:24 +01:00
|
|
|
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
statusDiv.style.display = 'block';
|
|
|
|
|
resultDiv.style.display = 'none';
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
if (text) formData.append('text', text);
|
|
|
|
|
if (file) formData.append('file', file);
|
|
|
|
|
formData.append('bundesland', bundesland);
|
|
|
|
|
formData.append('model', 'qwen-plus');
|
|
|
|
|
|
|
|
|
|
const resp = await fetch('/analyze', { method: 'POST', body: formData });
|
|
|
|
|
const data = await resp.json();
|
|
|
|
|
|
|
|
|
|
if (data.job_id) {
|
|
|
|
|
pollStatus(data.job_id);
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error(data.detail || 'Fehler');
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
statusDiv.innerHTML = `<p style="color: #dc3545;">✗ Fehler: ${e.message}</p>`;
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function pollStatus(jobId) {
|
|
|
|
|
const statusText = document.getElementById('status-text');
|
|
|
|
|
const statusDiv = document.getElementById('analysis-status');
|
|
|
|
|
const resultDiv = document.getElementById('analysis-result');
|
|
|
|
|
const btn = document.getElementById('analyze-btn');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const resp = await fetch(`/status/${jobId}`);
|
|
|
|
|
const data = await resp.json();
|
|
|
|
|
|
|
|
|
|
if (data.status === 'completed') {
|
|
|
|
|
statusDiv.style.display = 'none';
|
|
|
|
|
resultDiv.innerHTML = `
|
|
|
|
|
<p style="color: var(--color-green);">✓ Analyse abgeschlossen!</p>
|
|
|
|
|
<a href="/result/${jobId}" class="btn-pdf" style="background: var(--color-green);">📊 Ergebnis ansehen</a>
|
|
|
|
|
<a href="/result/${jobId}/pdf" class="btn-pdf">📄 PDF herunterladen</a>
|
|
|
|
|
`;
|
|
|
|
|
resultDiv.style.display = 'block';
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
} else if (data.status === 'failed') {
|
|
|
|
|
statusDiv.innerHTML = `<p style="color: #dc3545;">✗ Fehler: Analyse fehlgeschlagen. Bitte erneut versuchen.</p>`;
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
} else {
|
|
|
|
|
statusText.textContent = `Analysiere... (${data.status})`;
|
|
|
|
|
setTimeout(() => pollStatus(jobId), 2000);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
statusDiv.innerHTML = `<p style="color: #dc3545;">✗ Fehler: ${e.message}</p>`;
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|