feat: update history_overview_viz configuration and validation
- 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:
parent
97dbb0f80b
commit
725e7ffe4b
|
|
@ -145,10 +145,17 @@ _RECOVERY_HISTORY_VIZ_DEFAULTS: dict[str, Any] = {
|
||||||
"show_vitals_extra_trends": False,
|
"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({
|
_HISTORY_OVERVIEW_VIZ_BOOL_KEYS: frozenset[str] = frozenset({
|
||||||
"show_confidence_banner",
|
"show_confidence_banner",
|
||||||
"show_intro_blurb",
|
"show_intro_blurb",
|
||||||
"show_area_summaries",
|
*_HISTORY_OVERVIEW_VIZ_SECTION_KEYS,
|
||||||
"show_correlation_c1_c3",
|
"show_correlation_c1_c3",
|
||||||
"show_drivers_c4",
|
"show_drivers_c4",
|
||||||
})
|
})
|
||||||
|
|
@ -157,7 +164,10 @@ _HISTORY_OVERVIEW_VIZ_DEFAULTS: dict[str, Any] = {
|
||||||
"chart_days": 30,
|
"chart_days": 30,
|
||||||
"show_confidence_banner": True,
|
"show_confidence_banner": True,
|
||||||
"show_intro_blurb": 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_correlation_c1_c3": True,
|
||||||
"show_drivers_c4": True,
|
"show_drivers_c4": True,
|
||||||
}
|
}
|
||||||
|
|
@ -457,36 +467,52 @@ def _validate_recovery_history_viz_config(raw: dict[str, Any]) -> dict[str, Any]
|
||||||
return out
|
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]:
|
def _validate_history_overview_viz_config(raw: dict[str, Any]) -> dict[str, Any]:
|
||||||
label = "history_overview_viz"
|
label = "history_overview_viz"
|
||||||
|
raw_m = _migrate_history_overview_viz_raw(raw)
|
||||||
allowed = _HISTORY_OVERVIEW_VIZ_BOOL_KEYS | frozenset({"chart_days"})
|
allowed = _HISTORY_OVERVIEW_VIZ_BOOL_KEYS | frozenset({"chart_days"})
|
||||||
unknown = set(raw) - allowed
|
unknown = set(raw_m) - allowed
|
||||||
if unknown:
|
if unknown:
|
||||||
raise ValueError(f"{label}: unbekannte config-Felder: {sorted(unknown)}")
|
raise ValueError(f"{label}: unbekannte config-Felder: {sorted(unknown)}")
|
||||||
out: dict[str, Any] = dict(_HISTORY_OVERVIEW_VIZ_DEFAULTS)
|
out: dict[str, Any] = dict(_HISTORY_OVERVIEW_VIZ_DEFAULTS)
|
||||||
for k in _HISTORY_OVERVIEW_VIZ_BOOL_KEYS:
|
for k in _HISTORY_OVERVIEW_VIZ_BOOL_KEYS:
|
||||||
if k not in raw:
|
if k not in raw_m:
|
||||||
continue
|
continue
|
||||||
v = raw[k]
|
v = raw_m[k]
|
||||||
if not isinstance(v, bool):
|
if not isinstance(v, bool):
|
||||||
raise ValueError(f"{label}: {k} muss boolean sein")
|
raise ValueError(f"{label}: {k} muss boolean sein")
|
||||||
out[k] = v
|
out[k] = v
|
||||||
if "chart_days" in raw:
|
if "chart_days" in raw_m:
|
||||||
v = _parse_chart_days(raw["chart_days"], label)
|
v = _parse_chart_days(raw_m["chart_days"], label)
|
||||||
if v < 7 or v > 90:
|
if v < 7 or v > 90:
|
||||||
raise ValueError(f"{label}: chart_days muss zwischen 7 und 90 liegen")
|
raise ValueError(f"{label}: chart_days muss zwischen 7 und 90 liegen")
|
||||||
out["chart_days"] = v
|
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]
|
out[k]
|
||||||
for k in (
|
for k in (
|
||||||
"show_confidence_banner",
|
"show_confidence_banner",
|
||||||
"show_area_summaries",
|
|
||||||
"show_correlation_c1_c3",
|
"show_correlation_c1_c3",
|
||||||
"show_drivers_c4",
|
"show_drivers_c4",
|
||||||
)
|
)
|
||||||
):
|
)
|
||||||
|
if not has_section and not has_other:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"{label}: mindestens Datenlage-Banner, Bereichs-Kacheln, Lag-Korrelationen (C1–C3) oder Treiber (C4) muss sichtbar sein"
|
f"{label}: mindestens eine Bereichs-Kachel, das Datenlage-Banner, Lag-Korrelationen (C1–C3) oder Treiber (C4) muss sichtbar sein"
|
||||||
)
|
)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,10 @@ def test_history_overview_viz_empty_expands_defaults():
|
||||||
d = validate_widget_entry_config("history_overview_viz", {})
|
d = validate_widget_entry_config("history_overview_viz", {})
|
||||||
assert d["chart_days"] == 30
|
assert d["chart_days"] == 30
|
||||||
assert d["show_confidence_banner"] is True
|
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_correlation_c1_c3"] is True
|
||||||
assert d["show_drivers_c4"] is True
|
assert d["show_drivers_c4"] is True
|
||||||
|
|
||||||
|
|
@ -176,13 +179,26 @@ def test_history_overview_viz_requires_visible_block():
|
||||||
"history_overview_viz",
|
"history_overview_viz",
|
||||||
{
|
{
|
||||||
"show_confidence_banner": False,
|
"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_correlation_c1_c3": False,
|
||||||
"show_drivers_c4": 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():
|
def test_history_overview_viz_unknown_key():
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
validate_widget_entry_config("history_overview_viz", {"evil": True})
|
validate_widget_entry_config("history_overview_viz", {"evil": True})
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ MODULE_VERSIONS = {
|
||||||
"importdata": "1.0.0",
|
"importdata": "1.0.0",
|
||||||
"membership": "2.1.0",
|
"membership": "2.1.0",
|
||||||
"workflow": "0.7.0", # Part 3: Inline Prompts (reference + inline mode)
|
"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
|
"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)
|
"admin_csv_templates": "0.3.0", # POST /validate + Speichern nur bei valid (422 + warnings in Response)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@ WIDGET_CATALOG: list[WidgetCatalogEntry] = [
|
||||||
{
|
{
|
||||||
"id": "history_overview_viz",
|
"id": "history_overview_viz",
|
||||||
"title": "Verlauf — Gesamtübersicht",
|
"title": "Verlauf — Gesamtübersicht",
|
||||||
"description": "Layer-2b history-overview-viz: konsolidierte Kurzinfos (Körper/Ernährung/Fitness/Erholung) + C1–C4; chart_payloads im Bundle; chart_days 7–90; Blöcke per show_*",
|
"description": "Layer-2b history-overview-viz: Kurzinfos pro Bereich (show_section_body/nutrition/fitness/recovery) + C1–C4; chart_payloads; chart_days 7–90",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "recovery_charts_panel",
|
"id": "recovery_charts_panel",
|
||||||
|
|
|
||||||
|
|
@ -363,6 +363,20 @@ export default function HistoryOverviewVizSection({
|
||||||
const vis =
|
const vis =
|
||||||
visibilityProp != null ? normalizeHistoryOverviewVizConfig(visibilityProp) : HISTORY_OVERVIEW_VIZ_PAGE_FULL
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{!embedded && <SectionHeader title="📊 Gesamtansicht" />}
|
{!embedded && <SectionHeader title="📊 Gesamtansicht" />}
|
||||||
|
|
@ -402,11 +416,11 @@ export default function HistoryOverviewVizSection({
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{vis.show_area_summaries && (sections.length === 0 ? (
|
{wantsAnySectionTile && (visibleSections.length === 0 ? (
|
||||||
<EmptySection text="Keine Bereichsdaten." />
|
<EmptySection text="Keine Bereichsdaten." />
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: 10 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: 10 }}>
|
||||||
{sections.map((sec) => {
|
{visibleSections.map((sec) => {
|
||||||
const tone = overviewSectionTone(sec)
|
const tone = overviewSectionTone(sec)
|
||||||
const stripe = getStatusColor(tone)
|
const stripe = getStatusColor(tone)
|
||||||
const badgeBg = getStatusBg(tone)
|
const badgeBg = getStatusBg(tone)
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,16 @@ import {
|
||||||
normalizeHistoryOverviewVizConfig,
|
normalizeHistoryOverviewVizConfig,
|
||||||
} from './historyOverviewVizConfig'
|
} 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_confidence_banner', label: 'Banner «Datenlage»' },
|
||||||
{ key: 'show_intro_blurb', label: 'Hinweistext (Ernährung / API)' },
|
{ 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 C1–C3 (Charts)' },
|
{ key: 'show_correlation_c1_c3', label: 'Lag-Korrelationen C1–C3 (Charts)' },
|
||||||
{ key: 'show_drivers_c4', label: 'Einflussfaktoren C4' },
|
{ key: 'show_drivers_c4', label: 'Einflussfaktoren C4' },
|
||||||
]
|
]
|
||||||
|
|
@ -34,11 +40,20 @@ export default function HistoryOverviewVizConfigEditor({ config, onChange }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ marginTop: 10, marginLeft: 28 }}>
|
<div style={{ marginTop: 10, marginLeft: 28 }}>
|
||||||
<div style={{ fontSize: 12, color: 'var(--text2)', marginBottom: 8, lineHeight: 1.5 }}>
|
<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>
|
||||||
<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 }}>
|
<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' }}>
|
<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)} />
|
<input type="checkbox" checked={merged[key]} onChange={(e) => setBool(key, e.target.checked)} />
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,21 @@
|
||||||
* `visibility === undefined` → Verlauf-Tab: volle Gesamtübersicht (wie bisher).
|
* `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 = {
|
export const HISTORY_OVERVIEW_VIZ_PAGE_FULL = {
|
||||||
chart_days: 30,
|
chart_days: 30,
|
||||||
show_confidence_banner: true,
|
show_confidence_banner: true,
|
||||||
show_intro_blurb: 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_correlation_c1_c3: true,
|
||||||
show_drivers_c4: true,
|
show_drivers_c4: true,
|
||||||
}
|
}
|
||||||
|
|
@ -16,7 +26,10 @@ export const HISTORY_OVERVIEW_VIZ_WIDGET_DEFAULTS = {
|
||||||
chart_days: 30,
|
chart_days: 30,
|
||||||
show_confidence_banner: true,
|
show_confidence_banner: true,
|
||||||
show_intro_blurb: 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_correlation_c1_c3: true,
|
||||||
show_drivers_c4: true,
|
show_drivers_c4: true,
|
||||||
}
|
}
|
||||||
|
|
@ -24,17 +37,29 @@ export const HISTORY_OVERVIEW_VIZ_WIDGET_DEFAULTS = {
|
||||||
const BOOL_KEYS = [
|
const BOOL_KEYS = [
|
||||||
'show_confidence_banner',
|
'show_confidence_banner',
|
||||||
'show_intro_blurb',
|
'show_intro_blurb',
|
||||||
'show_area_summaries',
|
...HISTORY_OVERVIEW_VIZ_SECTION_KEYS,
|
||||||
'show_correlation_c1_c3',
|
'show_correlation_c1_c3',
|
||||||
'show_drivers_c4',
|
'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
|
* @param {Record<string, unknown>|null|undefined} raw
|
||||||
*/
|
*/
|
||||||
export function normalizeHistoryOverviewVizConfig(raw) {
|
export function normalizeHistoryOverviewVizConfig(raw) {
|
||||||
const base = { ...HISTORY_OVERVIEW_VIZ_WIDGET_DEFAULTS }
|
const base = { ...HISTORY_OVERVIEW_VIZ_WIDGET_DEFAULTS }
|
||||||
if (!raw || typeof raw !== 'object') return base
|
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) {
|
for (const k of BOOL_KEYS) {
|
||||||
if (Object.prototype.hasOwnProperty.call(raw, k)) {
|
if (Object.prototype.hasOwnProperty.call(raw, k)) {
|
||||||
base[k] = raw[k] === true
|
base[k] = raw[k] === true
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user