diff --git a/generative/eval_dashboard.py b/generative/eval_dashboard.py
index 59090b5..310f1dd 100644
--- a/generative/eval_dashboard.py
+++ b/generative/eval_dashboard.py
@@ -974,7 +974,12 @@ def _chart_longitudinal(log_data: dict) -> dict:
n_pts = [len(vm[v]) if vm.get(v) else 0 for v in versions]
datasets.append(
{
- "label": _PDF_LABELS.get(key, key),
+ # Multi-Perspektiven-Review 2026-07-15 U4: fuer PDFs ausserhalb der
+ # 3 registrierten _PDF_LABELS-Eintraege zeigte das Trade-off-Chart-2
+ # (Akzeptanzrate ueber Versionen) den rohen, kleingeschriebenen Key
+ # (z.B. "cobaltite-paper") -- ueber DB-Fallback-Runs inzwischen der
+ # Regelfall statt die Ausnahme. Fallback prettified statt roh.
+ "label": _PDF_LABELS.get(key) or re.sub(r"[-_]+", " ", key).strip().title(),
"data": data_pts,
"n": n_pts,
"color": _pdf_color(key),
diff --git a/generative/eval_dashboard_server.py b/generative/eval_dashboard_server.py
index 1efdd3a..a50e415 100644
--- a/generative/eval_dashboard_server.py
+++ b/generative/eval_dashboard_server.py
@@ -604,11 +604,16 @@ def build_data(
# all_log_runs — so erscheinen nur PDFs mit echten Daten im View (sonst
# listet das Dropdown z. B. foss-only evaluierte PDFs, die „0 Notes" ergeben,
# solange kein foss-Modell gewählt ist). Volltitel, dedupliziert.
+ # D6 (Multi-Perspektiven-Review 2026-07-15): Sprachfilter fehlte hier — das
+ # Versions-Dropdown direkt darunter berücksichtigt ihn bereits (`if
+ # language: _pver_rows = ...`), dieselbe Einschränkung fehlte am
+ # PDF-Dropdown, das dadurch auch PDFs der jeweils anderen Sprache listete.
_all_pdfs_opts = D._dedupe_pdf_options(
r.get("pdf")
for r in all_quality_rows
if r.get("eval_version") == eval_version
and r.get("pdf")
+ and (not language or r.get("language") == language)
and not (_exclude_foss and D.is_foss_version(r.get("version") or r.get("pipeline_version") or ""))
)
# Versions-Dropdown: nur Pipeline-Versionen MIT Eval-Daten in der aktiven
diff --git a/generative/tests/test_dashboard_ch1_zero_n_marker.py b/generative/tests/test_dashboard_ch1_zero_n_marker.py
new file mode 100644
index 0000000..314ed23
--- /dev/null
+++ b/generative/tests/test_dashboard_ch1_zero_n_marker.py
@@ -0,0 +1,22 @@
+"""Test für #U5 (Multi-Perspektiven-Review 2026-07-15): n=0-Balken im
+Akzeptanz-Chart (ch1) — das aufgedruckte Wert-Label ("100,0 %") wirkte wie ein
+echter Wert, obwohl 0 Notes evaluiert wurden (nur Routing-Daten, siehe #249).
+Die Balkenfarbe war für diesen Fall bereits grau (barColor), das Wert-Label
+selbst trug aber kein eigenes Signal.
+
+Fix: bei nMap[label]==0 (dieselbe Bedingung wie barColor) wird das Wert-Label
+zusätzlich per globalAlpha gedimmt UND mit einem Sternchen ("100,0 %*")
+gekennzeichnet.
+"""
+
+from __future__ import annotations
+
+from generative.eval_dashboard_server import _build_live_html
+
+
+def test_ch1_value_label_marks_zero_n_bars_with_asterisk_and_dimming():
+ html = _build_live_html()
+ assert "isEmpty?.55:1" in html or "isEmpty ? .55 : 1" in html
+ assert "isEmpty?'*':''" in html or "isEmpty ? '*' : ''" in html
+ # Dieselbe n===0-Bedingung wie barColor -- kein zweiter Datenpfad.
+ assert "const isEmpty = (nMap[rawPairs[i]?.l]||0)===0;" in html
diff --git a/generative/tests/test_dashboard_filter_badges.py b/generative/tests/test_dashboard_filter_badges.py
index f400df4..4cddf3c 100644
--- a/generative/tests/test_dashboard_filter_badges.py
+++ b/generative/tests/test_dashboard_filter_badges.py
@@ -27,3 +27,17 @@ def test_filter_badges_have_always_visible_reset_button():
html = _build_live_html()
assert "fbadge-reset" in html
assert "resetAllFilters()" in html
+
+
+def test_run_filter_badge_uses_option_text_not_raw_id():
+ """U3 (Multi-Perspektiven-Review 2026-07-15): Der Lauf-Filter-Badge zeigte
+ die rohe Run-ID aus _globalFilters.run (select-value, z.B.
+ "20260713-084724") statt des sichtbaren Options-Texts ("13.07. 08:47 ·
+ Schlebbe…"). Fix: für den run-Filter wird der Options-Text
+ (selectedOptions[0].text) ins Badge übernommen."""
+ html = _build_live_html()
+ assert "selectedOptions" in html
+ assert "opt.text" in html
+ # Regressions-Waechter: das alte Verhalten (immer roher select-value `v`
+ # ohne Sonderfall fuer 'run') darf nicht wiederkehren.
+ assert "${_escHTML(labels[k])}: ${_escHTML(v)}" not in html
diff --git a/generative/tests/test_dashboard_followups.py b/generative/tests/test_dashboard_followups.py
index 67b4dc6..539b02e 100644
--- a/generative/tests/test_dashboard_followups.py
+++ b/generative/tests/test_dashboard_followups.py
@@ -178,6 +178,31 @@ def test_chart_longitudinal_n_zero_for_missing_version():
assert ds["n"] == [1]
+# ------------------------------------------ Multi-Perspektiven-Review 2026-07-15 U4
+def test_chart_longitudinal_prettifies_unregistered_pdf_key():
+ """Trade-off-Chart-2 (Akzeptanzrate ueber Versionen, ch4/leg4) zeigte fuer PDFs
+ ausserhalb der 3 registrierten _PDF_LABELS-Eintraege (bates/kuhlthau/schlebbe)
+ den rohen, kleingeschriebenen Key im Chart-Label (z.B. "cobaltite-paper" statt
+ "Cobaltite Paper") -- inzwischen werden ueber die DB-Fallback-Runs zahlreiche
+ weitere PDFs evaluiert, fuer die dieser Fallback der Regelfall ist."""
+ runs = [{"key": "cobaltite-paper", "label": "irrelevant", "ver": "v1", "accept_pct": 50.0}]
+ from generative.eval_dashboard import _build_log_data
+
+ log_data = _build_log_data(runs)
+ out = _chart_longitudinal(log_data)
+ assert out["datasets"][0]["label"] == "Cobaltite Paper"
+
+
+def test_chart_longitudinal_keeps_registered_pdf_label():
+ """Regression: registrierte Kurzschluessel (_PDF_LABELS) bleiben unveraendert."""
+ runs = [{"key": "bates", "label": "irrelevant", "ver": "v1", "accept_pct": 50.0}]
+ from generative.eval_dashboard import _build_log_data
+
+ log_data = _build_log_data(runs)
+ out = _chart_longitudinal(log_data)
+ assert out["datasets"][0]["label"] == "Bates 2017"
+
+
# ----------------------------------------------------- foss/generative-Trennung
def test_is_foss_version_detects_foss_prefix():
assert is_foss_version("foss-v0.1.1") is True
diff --git a/generative/tests/test_dashboard_pdf_dropdown_language_filter.py b/generative/tests/test_dashboard_pdf_dropdown_language_filter.py
new file mode 100644
index 0000000..a074a8e
--- /dev/null
+++ b/generative/tests/test_dashboard_pdf_dropdown_language_filter.py
@@ -0,0 +1,68 @@
+"""Test für #D6 (Multi-Perspektiven-Review 2026-07-15): PDF-Dropdown ignoriert
+aktiven Sprachfilter.
+
+Befund: Das Versions-Dropdown (`_all_pvers_opts`, eval_dashboard_server.py)
+schränkt sich bei aktivem Sprachfilter bereits auf die passenden Zeilen ein
+(`if language: _pver_rows = [...]`). Das PDF-Dropdown direkt darüber
+(`_all_pdfs_opts`) tat das nicht — bei z.B. Sprachfilter "DE→DE" erschienen
+weiterhin PDFs, die nur in anderen Sprachen evaluiert wurden.
+"""
+
+from __future__ import annotations
+
+
+def _eval_row(run_id, ver, pdf, language, eval_version="4.1"):
+ return {
+ "run_id": run_id,
+ "note_path": f"notes/{run_id}.md",
+ "acceptance_status": "vault",
+ "hallucination_rate": 0.05,
+ "coverage_factual": 0.8,
+ "pipeline_version": ver,
+ "version": ver,
+ "pdf": pdf,
+ "language": language,
+ "eval_version": eval_version,
+ "anchors_total": 10,
+ "anchors_hallucinated": 1,
+ }
+
+
+def test_pdf_dropdown_respects_active_language_filter(monkeypatch):
+ from generative import db as _gdb
+ from generative import eval_dashboard as D
+ from generative import eval_dashboard_server as S
+
+ evals = [
+ _eval_row("r-de", "v0.3.140", "Deutsches-Paper.pdf", "DE→DE"),
+ _eval_row("r-en", "v0.3.140", "English-Paper.pdf", "EN→DE"),
+ ]
+ monkeypatch.setattr(_gdb, "query_pipeline_runs", lambda *a, **k: [])
+ monkeypatch.setattr(_gdb, "query_note_evals", lambda *a, **k: [dict(r) for r in evals])
+ monkeypatch.setattr(D, "_read_all_log_runs", lambda: [])
+ monkeypatch.setattr(D, "_read_token_runs", lambda: [])
+
+ data = S.build_data(language="DE→DE")
+
+ assert any("Deutsches-Paper" in o for o in data["all_pdfs"])
+ assert not any("English-Paper" in o for o in data["all_pdfs"])
+
+
+def test_pdf_dropdown_shows_all_pdfs_without_language_filter(monkeypatch):
+ from generative import db as _gdb
+ from generative import eval_dashboard as D
+ from generative import eval_dashboard_server as S
+
+ evals = [
+ _eval_row("r-de", "v0.3.140", "Deutsches-Paper.pdf", "DE→DE"),
+ _eval_row("r-en", "v0.3.140", "English-Paper.pdf", "EN→DE"),
+ ]
+ monkeypatch.setattr(_gdb, "query_pipeline_runs", lambda *a, **k: [])
+ monkeypatch.setattr(_gdb, "query_note_evals", lambda *a, **k: [dict(r) for r in evals])
+ monkeypatch.setattr(D, "_read_all_log_runs", lambda: [])
+ monkeypatch.setattr(D, "_read_token_runs", lambda: [])
+
+ data = S.build_data()
+
+ assert any("Deutsches-Paper" in o for o in data["all_pdfs"])
+ assert any("English-Paper" in o for o in data["all_pdfs"])
diff --git a/generative/tests/test_dashboard_version_label_case.py b/generative/tests/test_dashboard_version_label_case.py
new file mode 100644
index 0000000..8194fcd
--- /dev/null
+++ b/generative/tests/test_dashboard_version_label_case.py
@@ -0,0 +1,29 @@
+"""Test für #U7 (Multi-Perspektiven-Review 2026-07-15): kpi-ver-label wird per
+CSS uppercase gerendert.
+
+Befund: `.sub-label` hat `text-transform: uppercase` (Sub-Head-Beschriftungen
+wie "QUALITÄT" sollen groß sein). Das trifft aber auch die eingebettete
+Versionsnummer `#kpi-ver-label` ("V0.3.143" statt "v0.3.143") -- Pipeline-
+Versionen sind kleingeschrieben (`v0.3.143`), Großschreibung verfälscht sie.
+Fix: `text-transform: none` auf dem Versions-Span, damit `.sub-label`s
+Uppercase-Regel es nicht mehr trifft.
+"""
+
+from __future__ import annotations
+
+from generative.eval_dashboard_server import _build_live_html
+
+
+def test_kpi_ver_label_excluded_from_uppercase_transform():
+ html = _build_live_html()
+ assert "#kpi-ver-label" in html
+ assert "text-transform: none" in html
+
+
+def test_kpi_ver_label_perf_also_excluded_for_future_pr291_label():
+ # #291 (offen, Stand dieser Änderung) fügt ein zweites Versions-Label
+ # id="kpi-ver-label-perf" am "Kosten & Performance"-Header hinzu -- der
+ # CSS-Selektor deckt es bereits ab, damit es denselben Fix erbt sobald
+ # #291 gemergt ist (kein Nacharbeits-Bedarf).
+ html = _build_live_html()
+ assert "#kpi-ver-label-perf" in html
diff --git a/internal/dashboard/eval_dashboard.html b/internal/dashboard/eval_dashboard.html
index 43cbdab..b1a9c26 100644
--- a/internal/dashboard/eval_dashboard.html
+++ b/internal/dashboard/eval_dashboard.html
@@ -244,6 +244,10 @@
font-family: var(--f-mono); font-size: 11px; font-weight: 600;
color: var(--ink-2); text-transform: uppercase; letter-spacing: .10em;
}
+/* U7 (Multi-Perspektiven-Review 2026-07-15): .sub-label ist uppercase, trifft
+ aber auch die eingebettete Versionsnummer ("V0.3.143" statt "v0.3.143") --
+ Versions-Spans von der Transformation ausnehmen. */
+#kpi-ver-label, #kpi-ver-label-perf { text-transform: none; }
.sub-meta {
font-family: var(--f-body); font-size: 11px;
color: var(--ink-4); font-style: italic;
@@ -1192,14 +1196,23 @@
const labels = { pver: 'Version', pdf: 'PDF', lang: 'Sprache', model: 'Modell', run: 'Lauf' };
const active = Object.entries(_globalFilters)
.filter(([k, v]) => v && v !== '__all__' && labels[k]);
- const badges = active.map(([k, v]) =>
- `
- ${_escHTML(labels[k])}: ${_escHTML(v)}
+ const badges = active.map(([k, v]) => {
+ // U3 (Multi-Perspektiven-Review 2026-07-15): Lauf-Filter speichert im
+ // select-value die rohe Run-ID ("20260713-084724") -- im Badge soll der
+ // sichtbare Options-Text stehen ("13.07. 08:47 · ..."), nicht die ID.
+ let display = v;
+ if (k === 'run') {
+ const runSel = document.getElementById('global-run');
+ const opt = runSel && runSel.selectedOptions && runSel.selectedOptions[0];
+ if (opt && opt.value === v) display = opt.text;
+ }
+ return `
+ ${_escHTML(labels[k])}: ${_escHTML(display)}
- `
- ).join('');
+ `;
+ }).join('');
// #236: Reset auch außerhalb des Empty-State (0 Treffer) erreichbar, sobald
// mindestens ein Filter aktiv ist — vorher steckte der einzige Reset im
// Empty-Banner (nur bei 0 Treffern sichtbar).
@@ -1780,7 +1793,15 @@
const {ctx}=chart; ctx.save(); ctx.font='600 10.5px "IBM Plex Sans"'; ctx.textAlign='left'; ctx.textBaseline='middle';
chart.getDatasetMeta(0).data.forEach((bar,i)=>{
const val=rawPairs[i]?.v??0; const col=barColor(rawPairs[i]);
- ctx.fillStyle=col; ctx.fillText(_fmtDE(val)+' %', bar.x+6, bar.y);
+ // U5 (Multi-Perspektiven-Review 2026-07-15): bei n=0 (keine Eval-
+ // Basis) wirkte das aufgedruckte "100,0 %" wie ein echter Wert —
+ // dieselbe Bedingung wie barColor (n===0), zusätzlich per Sternchen
+ // im Text UND per globalAlpha gedimmt gekennzeichnet (die graue
+ // Balkenfarbe allein reicht als Signal nicht).
+ const isEmpty = (nMap[rawPairs[i]?.l]||0)===0;
+ ctx.fillStyle=col; ctx.globalAlpha=isEmpty?.55:1;
+ ctx.fillText(_fmtDE(val)+' %'+(isEmpty?'*':''), bar.x+6, bar.y);
+ ctx.globalAlpha=1;
}); ctx.restore();
}}]
});