feat: update history_overview_viz configuration and validation
All checks were successful
Deploy Development / deploy (push) Successful in 55s
Build Test / pytest-backend (push) Successful in 4s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 17s

- Replaced the `show_area_summaries` option with individual section visibility settings (`show_section_body`, `show_section_nutrition`, `show_section_fitness`, `show_section_recovery`) in the `history_overview_viz` widget configuration.
- Implemented migration logic to handle legacy `show_area_summaries` settings, ensuring backward compatibility.
- Updated validation logic to enforce visibility requirements for the new section keys.
- Enhanced tests to cover new configuration scenarios and validate the migration logic.
- Bumped application version to reflect these changes.
This commit is contained in:
Lars 2026-04-22 12:04:37 +02:00
parent 97dbb0f80b
commit 725e7ffe4b
7 changed files with 121 additions and 25 deletions

View File

@ -145,10 +145,17 @@ _RECOVERY_HISTORY_VIZ_DEFAULTS: dict[str, Any] = {
"show_vitals_extra_trends": False,
}
_HISTORY_OVERVIEW_VIZ_SECTION_KEYS: frozenset[str] = frozenset({
"show_section_body",
"show_section_nutrition",
"show_section_fitness",
"show_section_recovery",
})
_HISTORY_OVERVIEW_VIZ_BOOL_KEYS: frozenset[str] = frozenset({
"show_confidence_banner",
"show_intro_blurb",
"show_area_summaries",
*_HISTORY_OVERVIEW_VIZ_SECTION_KEYS,
"show_correlation_c1_c3",
"show_drivers_c4",
})
@ -157,7 +164,10 @@ _HISTORY_OVERVIEW_VIZ_DEFAULTS: dict[str, Any] = {
"chart_days": 30,
"show_confidence_banner": True,
"show_intro_blurb": True,
"show_area_summaries": True,
"show_section_body": True,
"show_section_nutrition": True,
"show_section_fitness": True,
"show_section_recovery": True,
"show_correlation_c1_c3": True,
"show_drivers_c4": True,
}
@ -457,36 +467,52 @@ def _validate_recovery_history_viz_config(raw: dict[str, Any]) -> dict[str, Any]
return out
def _migrate_history_overview_viz_raw(raw: dict[str, Any]) -> dict[str, Any]:
"""Alt: show_area_summaries → vier show_section_* (nur wo keine expliziten Section-Keys gesetzt)."""
r = dict(raw)
if "show_area_summaries" not in r:
return r
leg = r.pop("show_area_summaries")
if not isinstance(leg, bool):
raise ValueError("history_overview_viz: show_area_summaries muss boolean sein (veraltet — nutze show_section_*)")
for k in _HISTORY_OVERVIEW_VIZ_SECTION_KEYS:
if k not in r:
r[k] = leg
return r
def _validate_history_overview_viz_config(raw: dict[str, Any]) -> dict[str, Any]:
label = "history_overview_viz"
raw_m = _migrate_history_overview_viz_raw(raw)
allowed = _HISTORY_OVERVIEW_VIZ_BOOL_KEYS | frozenset({"chart_days"})
unknown = set(raw) - allowed
unknown = set(raw_m) - allowed
if unknown:
raise ValueError(f"{label}: unbekannte config-Felder: {sorted(unknown)}")
out: dict[str, Any] = dict(_HISTORY_OVERVIEW_VIZ_DEFAULTS)
for k in _HISTORY_OVERVIEW_VIZ_BOOL_KEYS:
if k not in raw:
if k not in raw_m:
continue
v = raw[k]
v = raw_m[k]
if not isinstance(v, bool):
raise ValueError(f"{label}: {k} muss boolean sein")
out[k] = v
if "chart_days" in raw:
v = _parse_chart_days(raw["chart_days"], label)
if "chart_days" in raw_m:
v = _parse_chart_days(raw_m["chart_days"], label)
if v < 7 or v > 90:
raise ValueError(f"{label}: chart_days muss zwischen 7 und 90 liegen")
out["chart_days"] = v
if not any(
has_section = any(out[k] for k in _HISTORY_OVERVIEW_VIZ_SECTION_KEYS)
has_other = any(
out[k]
for k in (
"show_confidence_banner",
"show_area_summaries",
"show_correlation_c1_c3",
"show_drivers_c4",
)
):
)
if not has_section and not has_other:
raise ValueError(
f"{label}: mindestens Datenlage-Banner, Bereichs-Kacheln, Lag-Korrelationen (C1C3) oder Treiber (C4) muss sichtbar sein"
f"{label}: mindestens eine Bereichs-Kachel, das Datenlage-Banner, Lag-Korrelationen (C1C3) oder Treiber (C4) muss sichtbar sein"
)
return out

View File

@ -157,7 +157,10 @@ def test_history_overview_viz_empty_expands_defaults():
d = validate_widget_entry_config("history_overview_viz", {})
assert d["chart_days"] == 30
assert d["show_confidence_banner"] is True
assert d["show_area_summaries"] is True
assert d["show_section_body"] is True
assert d["show_section_nutrition"] is True
assert d["show_section_fitness"] is True
assert d["show_section_recovery"] is True
assert d["show_correlation_c1_c3"] is True
assert d["show_drivers_c4"] is True
@ -176,13 +179,26 @@ def test_history_overview_viz_requires_visible_block():
"history_overview_viz",
{
"show_confidence_banner": False,
"show_area_summaries": False,
"show_section_body": False,
"show_section_nutrition": False,
"show_section_fitness": False,
"show_section_recovery": False,
"show_correlation_c1_c3": False,
"show_drivers_c4": False,
},
)
def test_history_overview_viz_legacy_show_area_summaries_maps_sections():
d = validate_widget_entry_config(
"history_overview_viz",
{"show_area_summaries": False, "show_correlation_c1_c3": True},
)
assert d["show_section_body"] is False
assert d["show_section_fitness"] is False
assert d["show_correlation_c1_c3"] is True
def test_history_overview_viz_unknown_key():
with pytest.raises(ValueError):
validate_widget_entry_config("history_overview_viz", {"evil": True})

View File

@ -30,7 +30,7 @@ MODULE_VERSIONS = {
"importdata": "1.0.0",
"membership": "2.1.0",
"workflow": "0.7.0", # Part 3: Inline Prompts (reference + inline mode)
"app_dashboard": "1.17.0", # history_overview_viz Widget + chart_payloads im Overview-Bundle
"app_dashboard": "1.17.1", # history_overview_viz: Bereichs-Kacheln einzeln per show_section_*
"csv_import": "0.3.2", # Import-Fehler: enrich_row_error / freundlichere 500-Hinweise
"admin_csv_templates": "0.3.0", # POST /validate + Speichern nur bei valid (422 + warnings in Response)
}

View File

@ -120,7 +120,7 @@ WIDGET_CATALOG: list[WidgetCatalogEntry] = [
{
"id": "history_overview_viz",
"title": "Verlauf — Gesamtübersicht",
"description": "Layer-2b history-overview-viz: konsolidierte Kurzinfos (Körper/Ernährung/Fitness/Erholung) + C1C4; chart_payloads im Bundle; chart_days 790; Blöcke per show_*",
"description": "Layer-2b history-overview-viz: Kurzinfos pro Bereich (show_section_body/nutrition/fitness/recovery) + C1C4; chart_payloads; chart_days 790",
},
{
"id": "recovery_charts_panel",

View File

@ -363,6 +363,20 @@ export default function HistoryOverviewVizSection({
const vis =
visibilityProp != null ? normalizeHistoryOverviewVizConfig(visibilityProp) : HISTORY_OVERVIEW_VIZ_PAGE_FULL
const sectionTileEnabled = (id) => {
if (id === 'body') return vis.show_section_body
if (id === 'nutrition') return vis.show_section_nutrition
if (id === 'fitness') return vis.show_section_fitness
if (id === 'recovery') return vis.show_section_recovery
return true
}
const wantsAnySectionTile =
vis.show_section_body ||
vis.show_section_nutrition ||
vis.show_section_fitness ||
vis.show_section_recovery
const visibleSections = wantsAnySectionTile ? sections.filter((s) => sectionTileEnabled(s.id)) : []
return (
<div>
{!embedded && <SectionHeader title="📊 Gesamtansicht" />}
@ -402,11 +416,11 @@ export default function HistoryOverviewVizSection({
</p>
)}
{vis.show_area_summaries && (sections.length === 0 ? (
{wantsAnySectionTile && (visibleSections.length === 0 ? (
<EmptySection text="Keine Bereichsdaten." />
) : (
<div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: 10 }}>
{sections.map((sec) => {
{visibleSections.map((sec) => {
const tone = overviewSectionTone(sec)
const stripe = getStatusColor(tone)
const badgeBg = getStatusBg(tone)

View File

@ -3,10 +3,16 @@ import {
normalizeHistoryOverviewVizConfig,
} from './historyOverviewVizConfig'
const TOGGLES = [
const SECTION_TOGGLES = [
{ key: 'show_section_body', label: 'Körper' },
{ key: 'show_section_nutrition', label: 'Ernährung' },
{ key: 'show_section_fitness', label: 'Fitness' },
{ key: 'show_section_recovery', label: 'Erholung' },
]
const OTHER_TOGGLES = [
{ key: 'show_confidence_banner', label: 'Banner «Datenlage»' },
{ key: 'show_intro_blurb', label: 'Hinweistext (Ernährung / API)' },
{ key: 'show_area_summaries', label: 'Kacheln Körper · Ernährung · Fitness · Erholung' },
{ key: 'show_correlation_c1_c3', label: 'Lag-Korrelationen C1C3 (Charts)' },
{ key: 'show_drivers_c4', label: 'Einflussfaktoren C4' },
]
@ -34,11 +40,20 @@ export default function HistoryOverviewVizConfigEditor({ config, onChange }) {
return (
<div style={{ marginTop: 10, marginLeft: 28 }}>
<div style={{ fontSize: 12, color: 'var(--text2)', marginBottom: 8, lineHeight: 1.5 }}>
<strong>Gesamtübersicht (Verlauf-Bundle):</strong> konsolidierte Kurzinfos und Korrelations-Kacheln wie im Verlauf-Reiter «Gesamt».
<strong>Gesamtübersicht (Verlauf-Bundle):</strong> welche Bereichs-Kacheln und weitere Blöcke erscheinen.
</div>
<div style={{ fontSize: 12, color: 'var(--text2)', marginBottom: 6 }}>Bereiche</div>
<div style={{ fontSize: 12, color: 'var(--text2)', marginBottom: 6 }}>Bereichs-Kacheln</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 12 }}>
{SECTION_TOGGLES.map(({ key, label }) => (
<label key={key} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, cursor: 'pointer' }}>
<input type="checkbox" checked={merged[key]} onChange={(e) => setBool(key, e.target.checked)} />
<span>{label}</span>
</label>
))}
</div>
<div style={{ fontSize: 12, color: 'var(--text2)', marginBottom: 6 }}>Weitere Bereiche</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{TOGGLES.map(({ key, label }) => (
{OTHER_TOGGLES.map(({ key, label }) => (
<label key={key} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, cursor: 'pointer' }}>
<input type="checkbox" checked={merged[key]} onChange={(e) => setBool(key, e.target.checked)} />
<span>{label}</span>

View File

@ -3,11 +3,21 @@
* `visibility === undefined` Verlauf-Tab: volle Gesamtübersicht (wie bisher).
*/
export const HISTORY_OVERVIEW_VIZ_SECTION_KEYS = [
'show_section_body',
'show_section_nutrition',
'show_section_fitness',
'show_section_recovery',
]
export const HISTORY_OVERVIEW_VIZ_PAGE_FULL = {
chart_days: 30,
show_confidence_banner: true,
show_intro_blurb: true,
show_area_summaries: true,
show_section_body: true,
show_section_nutrition: true,
show_section_fitness: true,
show_section_recovery: true,
show_correlation_c1_c3: true,
show_drivers_c4: true,
}
@ -16,7 +26,10 @@ export const HISTORY_OVERVIEW_VIZ_WIDGET_DEFAULTS = {
chart_days: 30,
show_confidence_banner: true,
show_intro_blurb: true,
show_area_summaries: true,
show_section_body: true,
show_section_nutrition: true,
show_section_fitness: true,
show_section_recovery: true,
show_correlation_c1_c3: true,
show_drivers_c4: true,
}
@ -24,17 +37,29 @@ export const HISTORY_OVERVIEW_VIZ_WIDGET_DEFAULTS = {
const BOOL_KEYS = [
'show_confidence_banner',
'show_intro_blurb',
'show_area_summaries',
...HISTORY_OVERVIEW_VIZ_SECTION_KEYS,
'show_correlation_c1_c3',
'show_drivers_c4',
]
function hasExplicitSectionKeys(raw) {
return HISTORY_OVERVIEW_VIZ_SECTION_KEYS.some((k) => Object.prototype.hasOwnProperty.call(raw, k))
}
/**
* @param {Record<string, unknown>|null|undefined} raw
*/
export function normalizeHistoryOverviewVizConfig(raw) {
const base = { ...HISTORY_OVERVIEW_VIZ_WIDGET_DEFAULTS }
if (!raw || typeof raw !== 'object') return base
if (Object.prototype.hasOwnProperty.call(raw, 'show_area_summaries') && !hasExplicitSectionKeys(raw)) {
const v = raw.show_area_summaries === true
for (const k of HISTORY_OVERVIEW_VIZ_SECTION_KEYS) {
base[k] = v
}
}
for (const k of BOOL_KEYS) {
if (Object.prototype.hasOwnProperty.call(raw, k)) {
base[k] = raw[k] === true