feat: Update reference values and introduce pilot visualization module
All checks were successful
Deploy Development / deploy (push) Successful in 47s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 23s

- Bumped version of reference_values module to 1.3.0.
- Added new imports and functionality for reference values in the backend, enhancing data retrieval.
- Introduced a new PilotVizPage in the frontend for visualizing data, linked from the SettingsPage for easy access.
- Updated routing in App.jsx to include the new pilot visualization route.
This commit is contained in:
Lars 2026-04-07 10:15:13 +02:00
parent 3e916c082c
commit 932bceb1e1
10 changed files with 428 additions and 146 deletions

View File

@ -21,6 +21,8 @@ Modules:
- utils: Shared functions (confidence, baseline, outliers) - utils: Shared functions (confidence, baseline, outliers)
- training_profile: Template-based training evaluation scaffold (Layer 1) - training_profile: Template-based training evaluation scaffold (Layer 1)
Import: ``from data_layer.training_profile import resolve_training_evaluation`` Import: ``from data_layer.training_profile import resolve_training_evaluation``
- reference_values: Profile reference value reads + summary (Layer 1)
Import: ``from data_layer import reference_values`` or submodule imports
Phase 0c: Multi-Layer Architecture Phase 0c: Multi-Layer Architecture
Version: 1.0 Version: 1.0

View File

@ -0,0 +1,179 @@
"""
Reference values Layer 1 (read paths + structured rows)
Structured retrieval for profile reference values and the active type catalog.
Mutations (INSERT/UPDATE/DELETE) stay in routers/reference_values.py with validation.
Dates are normalized to ISO strings; Decimals to float suitable for JSON/API layers.
"""
from __future__ import annotations
from decimal import Decimal
from typing import Any, Optional
from db import get_cursor, get_db, r2d
def normalize_reference_row(d: Optional[dict[str, Any]]) -> dict[str, Any]:
"""Normalize DB row dict for JSON (dates → ISO, Decimal → float)."""
if not d:
return d
out = dict(d)
for k in ("effective_date", "created_at", "updated_at"):
v = out.get(k)
if v is not None and hasattr(v, "isoformat"):
out[k] = v.isoformat()
vn = out.get("value_numeric")
if vn is not None and isinstance(vn, Decimal):
out["value_numeric"] = float(vn)
return out
def fetch_reference_type_by_key(cur, key: str, require_active: bool = True) -> Optional[dict[str, Any]]:
"""Single type row by key; for use inside router transactions (shared cursor)."""
q = (
"SELECT id, key, label, description, default_unit, active, category, "
"value_data_type, validation_rules, metadata "
"FROM reference_value_types WHERE key = %s "
)
if require_active:
q += "AND active = TRUE "
cur.execute(q, (key,))
row = cur.fetchone()
return r2d(row) if row else None
def list_active_reference_value_types_data() -> list[dict[str, Any]]:
"""All active reference_value_types rows, catalog sort order."""
with get_db() as conn:
cur = get_cursor(conn)
cur.execute(
"""
SELECT
id, key, label, description, default_unit, sort_order, active,
category, value_data_type, validation_rules, metadata, created_at
FROM reference_value_types
WHERE active = TRUE
ORDER BY sort_order ASC, id ASC
"""
)
return [normalize_reference_row(r2d(r)) for r in cur.fetchall()]
def list_profile_reference_values_for_type(
profile_id: str, type_key: str
) -> Optional[list[dict[str, Any]]]:
"""
Historical entries for one type, newest first.
Returns None if type_key does not resolve to an active type.
"""
with get_db() as conn:
cur = get_cursor(conn)
t = fetch_reference_type_by_key(cur, type_key, require_active=True)
if not t:
return None
cur.execute(
"""
SELECT
v.id,
v.profile_id,
v.reference_value_type_id,
v.effective_date,
v.value_numeric,
v.value_text,
v.unit,
v.source,
v.confidence,
v.method,
v.notes,
v.extra,
v.created_at,
v.updated_at,
rt.key AS type_key,
rt.label AS type_label
FROM profile_reference_values v
JOIN reference_value_types rt ON rt.id = v.reference_value_type_id
WHERE v.profile_id = %s AND rt.key = %s
ORDER BY v.effective_date DESC, v.created_at DESC
""",
(profile_id, t["key"]),
)
return [normalize_reference_row(r2d(r)) for r in cur.fetchall()]
def build_summary_tiles_from_ranked_rows(raw_rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""
Build /summary.tile payloads from SQL rows (rn 1..2 per type).
Each row still contains rn, type_sort_order, value_data_type before stripping.
"""
by_key: dict[str, dict[str, Any]] = {}
skip_cols = frozenset({"rn", "type_sort_order", "value_data_type"})
for row in raw_rows:
rn = int(row.get("rn") or 0)
key = row["type_key"]
if key not in by_key:
by_key[key] = {
"type_key": key,
"type_label": row.get("type_label") or key,
"value_data_type": (row.get("value_data_type") or "decimal").strip().lower(),
"sort_key": (row.get("type_sort_order") or 0, key),
"latest": None,
"previous": None,
}
entry = {k: v for k, v in row.items() if k not in skip_cols}
api_entry = normalize_reference_row(entry)
if rn == 1:
by_key[key]["latest"] = api_entry
elif rn == 2:
by_key[key]["previous"] = api_entry
tiles = sorted(by_key.values(), key=lambda t: t["sort_key"])
for t in tiles:
t.pop("sort_key", None)
return tiles
def get_profile_reference_values_summary(profile_id: str) -> dict[str, Any]:
"""latest + previous entry per type (active types only), tiles sorted like catalog."""
with get_db() as conn:
cur = get_cursor(conn)
cur.execute(
"""
WITH ranked AS (
SELECT
v.id,
v.profile_id,
v.reference_value_type_id,
v.effective_date,
v.value_numeric,
v.value_text,
v.unit,
v.source,
v.confidence,
v.method,
v.notes,
v.extra,
v.created_at,
v.updated_at,
rt.key AS type_key,
rt.label AS type_label,
rt.sort_order AS type_sort_order,
rt.value_data_type,
ROW_NUMBER() OVER (
PARTITION BY v.reference_value_type_id
ORDER BY v.effective_date DESC, v.created_at DESC
) AS rn
FROM profile_reference_values v
JOIN reference_value_types rt ON rt.id = v.reference_value_type_id
WHERE v.profile_id = %s AND rt.active = TRUE
)
SELECT * FROM ranked WHERE rn <= 2
ORDER BY type_sort_order ASC, type_key ASC, rn ASC
""",
(profile_id,),
)
raw_rows = [r2d(r) for r in cur.fetchall()]
tiles = build_summary_tiles_from_ranked_rows(raw_rows)
return {"tiles": tiles}

View File

@ -3,6 +3,8 @@ Persönliche Referenzwerte (profilorientiert)
Typkatalog system-seeded; Nutzer pflegt historische Einträge pro aktivem Profil. Typkatalog system-seeded; Nutzer pflegt historische Einträge pro aktivem Profil.
Einheit immer aus dem Typ; Wert je value_data_type validiert. Einheit immer aus dem Typ; Wert je value_data_type validiert.
Reads (Liste, Summary, Katalog) data_layer.reference_values (Layer 1).
""" """
from __future__ import annotations from __future__ import annotations
@ -15,6 +17,13 @@ from pydantic import BaseModel, Field
from psycopg2.extras import Json from psycopg2.extras import Json
from auth import require_auth from auth import require_auth
from data_layer.reference_values import (
fetch_reference_type_by_key,
get_profile_reference_values_summary,
list_active_reference_value_types_data,
list_profile_reference_values_for_type,
normalize_reference_row,
)
from db import get_db, get_cursor, r2d from db import get_db, get_cursor, r2d
from reference_value_validation import ( from reference_value_validation import (
REF_VALUE_CONFIDENCE, REF_VALUE_CONFIDENCE,
@ -32,40 +41,6 @@ from routers.profiles import get_pid
router = APIRouter(prefix="/api", tags=["reference-values"]) router = APIRouter(prefix="/api", tags=["reference-values"])
def _row_to_api(d: dict[str, Any]) -> dict[str, Any]:
if not d:
return d
out = dict(d)
ed = out.get("effective_date")
if ed is not None and hasattr(ed, "isoformat"):
out["effective_date"] = ed.isoformat()
ca = out.get("created_at")
if ca is not None and hasattr(ca, "isoformat"):
out["created_at"] = ca.isoformat()
ua = out.get("updated_at")
if ua is not None and hasattr(ua, "isoformat"):
out["updated_at"] = ua.isoformat()
vn = out.get("value_numeric")
if vn is not None and isinstance(vn, Decimal):
out["value_numeric"] = float(vn)
return out
def _get_type_by_key(cur, key: str, require_active: bool = True) -> dict:
q = (
"SELECT id, key, label, description, default_unit, active, category, "
"value_data_type, validation_rules, metadata "
"FROM reference_value_types WHERE key = %s "
)
if require_active:
q += "AND active = TRUE "
cur.execute(q, (key,))
row = cur.fetchone()
if not row:
raise HTTPException(404, "Referenztyp nicht gefunden")
return r2d(row)
class ProfileReferenceValueCreate(BaseModel): class ProfileReferenceValueCreate(BaseModel):
reference_value_type_key: str = Field(..., min_length=1, max_length=64) reference_value_type_key: str = Field(..., min_length=1, max_length=64)
effective_date: str effective_date: str
@ -92,19 +67,7 @@ class ProfileReferenceValueUpdate(BaseModel):
@router.get("/reference-value-types") @router.get("/reference-value-types")
def list_reference_value_types(session: dict = Depends(require_auth)): def list_reference_value_types(session: dict = Depends(require_auth)):
"""Alle aktiven Referenztyp-Definitionen (dynamische UI inkl. Validierungsmetadaten).""" """Alle aktiven Referenztyp-Definitionen (dynamische UI inkl. Validierungsmetadaten)."""
with get_db() as conn: return list_active_reference_value_types_data()
cur = get_cursor(conn)
cur.execute(
"""
SELECT
id, key, label, description, default_unit, sort_order, active,
category, value_data_type, validation_rules, metadata, created_at
FROM reference_value_types
WHERE active = TRUE
ORDER BY sort_order ASC, id ASC
"""
)
return [_row_to_api(r2d(r)) for r in cur.fetchall()]
@router.get("/reference-value-meta/enums") @router.get("/reference-value-meta/enums")
@ -127,71 +90,7 @@ def profile_reference_values_summary(
plus der unmittelbar vorherige (gleiche Sortierung wie Liste), für Tendenz-Anzeigen. plus der unmittelbar vorherige (gleiche Sortierung wie Liste), für Tendenz-Anzeigen.
""" """
pid = get_pid(x_profile_id) pid = get_pid(x_profile_id)
with get_db() as conn: return get_profile_reference_values_summary(pid)
cur = get_cursor(conn)
cur.execute(
"""
WITH ranked AS (
SELECT
v.id,
v.profile_id,
v.reference_value_type_id,
v.effective_date,
v.value_numeric,
v.value_text,
v.unit,
v.source,
v.confidence,
v.method,
v.notes,
v.extra,
v.created_at,
v.updated_at,
rt.key AS type_key,
rt.label AS type_label,
rt.sort_order AS type_sort_order,
rt.value_data_type,
ROW_NUMBER() OVER (
PARTITION BY v.reference_value_type_id
ORDER BY v.effective_date DESC, v.created_at DESC
) AS rn
FROM profile_reference_values v
JOIN reference_value_types rt ON rt.id = v.reference_value_type_id
WHERE v.profile_id = %s AND rt.active = TRUE
)
SELECT * FROM ranked WHERE rn <= 2
ORDER BY type_sort_order ASC, type_key ASC, rn ASC
""",
(pid,),
)
raw_rows = [r2d(r) for r in cur.fetchall()]
by_key: dict[str, dict[str, Any]] = {}
skip_cols = frozenset({"rn", "type_sort_order", "value_data_type"})
for row in raw_rows:
rn = int(row.get("rn") or 0)
key = row["type_key"]
if key not in by_key:
by_key[key] = {
"type_key": key,
"type_label": row.get("type_label") or key,
"value_data_type": (row.get("value_data_type") or "decimal").strip().lower(),
"sort_key": (row.get("type_sort_order") or 0, key),
"latest": None,
"previous": None,
}
entry = {k: v for k, v in row.items() if k not in skip_cols}
api_entry = _row_to_api(entry)
if rn == 1:
by_key[key]["latest"] = api_entry
elif rn == 2:
by_key[key]["previous"] = api_entry
tiles = sorted(by_key.values(), key=lambda t: t["sort_key"])
for t in tiles:
t.pop("sort_key", None)
return {"tiles": tiles}
@router.get("/profile-reference-values") @router.get("/profile-reference-values")
@ -202,36 +101,10 @@ def list_profile_reference_values(
): ):
"""Historische Einträge eines Typs für das aktive Profil (neueste zuerst).""" """Historische Einträge eines Typs für das aktive Profil (neueste zuerst)."""
pid = get_pid(x_profile_id) pid = get_pid(x_profile_id)
with get_db() as conn: rows = list_profile_reference_values_for_type(pid, type_key)
cur = get_cursor(conn) if rows is None:
t = _get_type_by_key(cur, type_key, require_active=True) raise HTTPException(404, "Referenztyp nicht gefunden")
cur.execute( return rows
"""
SELECT
v.id,
v.profile_id,
v.reference_value_type_id,
v.effective_date,
v.value_numeric,
v.value_text,
v.unit,
v.source,
v.confidence,
v.method,
v.notes,
v.extra,
v.created_at,
v.updated_at,
rt.key AS type_key,
rt.label AS type_label
FROM profile_reference_values v
JOIN reference_value_types rt ON rt.id = v.reference_value_type_id
WHERE v.profile_id = %s AND rt.key = %s
ORDER BY v.effective_date DESC, v.created_at DESC
""",
(pid, t["key"]),
)
return [_row_to_api(r2d(r)) for r in cur.fetchall()]
@router.post("/profile-reference-values") @router.post("/profile-reference-values")
@ -252,7 +125,9 @@ def create_profile_reference_value(
with get_db() as conn: with get_db() as conn:
cur = get_cursor(conn) cur = get_cursor(conn)
t = _get_type_by_key(cur, body.reference_value_type_key.strip(), require_active=True) t = fetch_reference_type_by_key(cur, body.reference_value_type_key.strip(), require_active=True)
if not t:
raise HTTPException(404, "Referenztyp nicht gefunden")
vdt = (t.get("value_data_type") or "decimal").strip().lower() vdt = (t.get("value_data_type") or "decimal").strip().lower()
rules = t.get("validation_rules") or {} rules = t.get("validation_rules") or {}
vnum, vtxt = validate_value_for_data_type(vdt, rules, body.value_numeric, body.value_text) vnum, vtxt = validate_value_for_data_type(vdt, rules, body.value_numeric, body.value_text)
@ -309,7 +184,7 @@ def create_profile_reference_value(
""", """,
(new_id, pid), (new_id, pid),
) )
return _row_to_api(r2d(cur.fetchone())) return normalize_reference_row(r2d(cur.fetchone()))
@router.put("/profile-reference-values/{entry_id}") @router.put("/profile-reference-values/{entry_id}")
@ -424,7 +299,7 @@ def update_profile_reference_value(
""", """,
(entry_id, pid), (entry_id, pid),
) )
return _row_to_api(r2d(cur.fetchone())) return normalize_reference_row(r2d(cur.fetchone()))
@router.delete("/profile-reference-values/{entry_id}") @router.delete("/profile-reference-values/{entry_id}")

View File

@ -0,0 +1,48 @@
"""Unit tests for data_layer.reference_values (summary assembly, no DB)."""
from data_layer.reference_values import build_summary_tiles_from_ranked_rows
def test_build_summary_tiles_single_type_two_rows():
raw = [
{
"type_key": "hr_max",
"type_label": "HF max",
"type_sort_order": 1,
"value_data_type": "decimal",
"rn": 1,
"id": 2,
"effective_date": "2026-04-01",
"value_numeric": 180.0,
"value_text": None,
"unit": "bpm",
},
{
"type_key": "hr_max",
"type_label": "HF max",
"type_sort_order": 1,
"value_data_type": "decimal",
"rn": 2,
"id": 1,
"effective_date": "2026-03-01",
"value_numeric": 175.0,
"value_text": None,
"unit": "bpm",
},
]
tiles = build_summary_tiles_from_ranked_rows(raw)
assert len(tiles) == 1
t = tiles[0]
assert t["type_key"] == "hr_max"
assert t["latest"]["value_numeric"] == 180.0
assert t["previous"]["value_numeric"] == 175.0
assert "sort_key" not in t
def test_build_summary_tiles_multi_type_order():
raw = [
{"type_key": "b", "type_label": "B", "type_sort_order": 2, "value_data_type": "decimal", "rn": 1, "id": 1},
{"type_key": "a", "type_label": "A", "type_sort_order": 1, "value_data_type": "decimal", "rn": 1, "id": 2},
]
tiles = build_summary_tiles_from_ranked_rows(raw)
assert [x["type_key"] for x in tiles] == ["a", "b"]

View File

@ -14,7 +14,7 @@ DB_SCHEMA_VERSION = "20260406b" # Migration 038
MODULE_VERSIONS = { MODULE_VERSIONS = {
"auth": "1.2.0", "auth": "1.2.0",
"profiles": "1.1.0", "profiles": "1.1.0",
"reference_values": "1.2.0", "reference_values": "1.3.0",
"admin_reference_value_types": "1.0.0", "admin_reference_value_types": "1.0.0",
"weight": "1.0.3", "weight": "1.0.3",
"circumference": "1.0.1", "circumference": "1.0.1",

View File

@ -24,6 +24,7 @@ import Analysis from './pages/Analysis'
import SettingsPage from './pages/SettingsPage' import SettingsPage from './pages/SettingsPage'
import SettingsShell from './layouts/SettingsShell' import SettingsShell from './layouts/SettingsShell'
import ProfileReferenceValuesPage from './pages/ProfileReferenceValuesPage' import ProfileReferenceValuesPage from './pages/ProfileReferenceValuesPage'
import PilotVizPage from './pages/PilotVizPage'
import GuidePage from './pages/GuidePage' import GuidePage from './pages/GuidePage'
import AdminTierLimitsPage from './pages/AdminTierLimitsPage' import AdminTierLimitsPage from './pages/AdminTierLimitsPage'
import AdminFeaturesPage from './pages/AdminFeaturesPage' import AdminFeaturesPage from './pages/AdminFeaturesPage'
@ -253,6 +254,7 @@ function AppShell() {
</Route> </Route>
<Route path="/workflow-editor/:id" element={<WorkflowEditorPage/>}/> <Route path="/workflow-editor/:id" element={<WorkflowEditorPage/>}/>
<Route path="/subscription" element={<SubscriptionPage/>}/> <Route path="/subscription" element={<SubscriptionPage/>}/>
<Route path="/pilot/viz" element={<PilotVizPage />} />
</Routes> </Routes>
</main> </main>
</div> </div>

View File

@ -0,0 +1,88 @@
import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { api } from '../../utils/api'
function formatEntryValue(row) {
if (row.value_numeric != null && row.value_numeric !== '') {
const n = Number(row.value_numeric)
return Number.isFinite(n) ? String(n) : String(row.value_numeric)
}
return row.value_text != null ? String(row.value_text) : ''
}
export default function ReferenceValuesSummaryWidget() {
const [tiles, setTiles] = useState([])
const [loading, setLoading] = useState(true)
const [err, setErr] = useState(null)
useEffect(() => {
let cancelled = false
;(async () => {
try {
const data = await api.listProfileReferenceValuesSummary()
if (!cancelled) {
setTiles(Array.isArray(data?.tiles) ? data.tiles : [])
setErr(null)
}
} catch (e) {
if (!cancelled) {
setErr(e.message || 'Laden fehlgeschlagen')
setTiles([])
}
} finally {
if (!cancelled) setLoading(false)
}
})()
return () => {
cancelled = true
}
}, [])
if (loading) {
return (
<div style={{ padding: 20, textAlign: 'center' }}>
<div className="spinner" />
</div>
)
}
if (err) {
return <div style={{ fontSize: 13, color: 'var(--danger)' }}>{err}</div>
}
if (tiles.length === 0) {
return (
<p style={{ fontSize: 13, color: 'var(--text2)', margin: 0 }}>
Noch keine Referenzwerte erfasst.{' '}
<Link to="/settings/reference-values" style={{ color: 'var(--accent)' }}>
Zur Erfassung
</Link>
</p>
)
}
return (
<div className="ref-value-tiles-grid">
{tiles
.filter((t) => t?.latest)
.map((tile) => (
<div
key={tile.type_key}
className="card"
style={{ margin: 0, padding: 14, boxSizing: 'border-box' }}
>
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text2)' }}>{tile.type_label}</div>
<div style={{ fontSize: 20, fontWeight: 700, marginTop: 6 }}>
{formatEntryValue(tile.latest)}
{tile.latest.unit ? (
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--text2)', marginLeft: 6 }}>
{tile.latest.unit}
</span>
) : null}
</div>
<div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 6 }}>
Stand {String(tile.latest.effective_date || '').slice(0, 10)}
</div>
</div>
))}
</div>
)
}

View File

@ -0,0 +1,49 @@
import { FlaskConical } from 'lucide-react'
import { Link } from 'react-router-dom'
import { getPilotWidgetsInOrder } from '../pilot/widgetRegistry'
/**
* Pilot: Widget-Schicht (Layer 3b) parallel zum produktiven Dashboard.
* Daten für Referenzwerte: Layer 1 (backend/data_layer/reference_values.py) API.
*/
export default function PilotVizPage() {
const widgets = getPilotWidgetsInOrder()
return (
<div style={{ paddingBottom: 96, textAlign: 'left', maxWidth: 900, margin: '0 auto' }}>
<div style={{ marginBottom: 20 }}>
<Link
to="/settings"
className="btn btn-secondary"
style={{ display: 'inline-flex', marginBottom: 12, textDecoration: 'none' }}
>
Einstellungen
</Link>
<h1 className="page-title" style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<FlaskConical size={26} color="var(--accent)" />
Pilot: Visualisierungs-Module
</h1>
<p style={{ fontSize: 14, color: 'var(--text2)', lineHeight: 1.6, marginTop: 8 }}>
Testumgebung für die zukünftige Widget-Registry. Die produktive Übersicht und der Verlauf bleiben
unverändert. Referenzwerte-Summary wird über die gleiche API geladen wie in der Erfassungs-UI, angeboten
durch den Data Layer (Layer 1).
</p>
</div>
{widgets.map((def) => {
const { Component } = def
return (
<section key={def.id} className="card section-gap" style={{ overflow: 'hidden' }}>
<div className="card-title">{def.title}</div>
{def.description && (
<p style={{ fontSize: 13, color: 'var(--text2)', marginTop: 4, marginBottom: 14, lineHeight: 1.5 }}>
{def.description}
</p>
)}
<Component />
</section>
)
})}
</div>
)
}

View File

@ -441,6 +441,25 @@ export default function SettingsPage() {
</Link> </Link>
</div> </div>
<div
className="card section-gap"
style={{ borderStyle: 'dashed', borderColor: 'var(--border2)', background: 'var(--surface2)' }}
>
<div className="card-title" style={{ fontSize: 14 }}>
Pilot: Visualisierungs-Module
</div>
<p style={{ fontSize: 12, color: 'var(--text2)', marginBottom: 12, lineHeight: 1.5 }}>
Testumgebung für die Widget-Schicht (Layer 3b). Die Übersicht und der Verlauf bleiben unverändert.
</p>
<Link
to="/pilot/viz"
className="btn btn-secondary btn-full"
style={{ textAlign: 'center', textDecoration: 'none', boxSizing: 'border-box' }}
>
Pilot öffnen
</Link>
</div>
{/* Auth actions */} {/* Auth actions */}
<div className="card section-gap"> <div className="card section-gap">
<div className="card-title">🔐 Konto</div> <div className="card-title">🔐 Konto</div>

View File

@ -0,0 +1,20 @@
import ReferenceValuesSummaryWidget from '../components/pilot/ReferenceValuesSummaryWidget'
/**
* Pilot: konfigurierbare Widget-Reihenfolge (Layer 3b-Vorspiel).
* Nur für /pilot/viz produktives Dashboard bleibt unverändert.
*/
export const PILOT_WIDGET_ORDER = ['reference_values_summary']
export const PILOT_WIDGET_DEFS = {
reference_values_summary: {
id: 'reference_values_summary',
title: 'Referenzwerte',
description: 'Aktuellste Kennwerte pro Typ (Daten via Layer 1 → bestehende API).',
Component: ReferenceValuesSummaryWidget,
},
}
export function getPilotWidgetsInOrder() {
return PILOT_WIDGET_ORDER.map((id) => PILOT_WIDGET_DEFS[id]).filter(Boolean)
}