feat: Introduce admin dashboard product standard management
All checks were successful
Deploy Development / deploy (push) Successful in 57s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 17s

- Added new API endpoints for managing the product dashboard standard, including retrieval, update, and deletion functionalities.
- Enhanced the DashboardConfigurePage to support admin mode for configuring the product dashboard standard.
- Updated the admin navigation to include a link for the product dashboard standard configuration.
- Refactored the dashboard layout logic to utilize the new product standard management features.
- Bumped app_dashboard version to 1.10.0 to reflect these enhancements and changes.
This commit is contained in:
Lars 2026-04-08 10:32:18 +02:00
parent ff95ef63c7
commit 365ce49c6a
12 changed files with 299 additions and 32 deletions

View File

@ -39,7 +39,7 @@ def lab_default_layout_dict() -> dict[str, Any]:
def product_default_layout_dict() -> dict[str, Any]: def product_default_layout_dict() -> dict[str, Any]:
"""System-Standard für die Produkt-Übersicht (kein DB-Override).""" """Code-Fallback für die Produkt-Übersicht; live-Standard ggf. system_config (siehe get_product_default_base_dict)."""
on = DEFAULT_PRODUCT_DASHBOARD_WIDGET_IDS on = DEFAULT_PRODUCT_DASHBOARD_WIDGET_IDS
return { return {
"version": 1, "version": 1,

View File

@ -58,6 +58,14 @@ def widgets_catalog_payload(profile_id: str, conn) -> dict[str, Any]:
} }
def widgets_catalog_admin_payload() -> dict[str, Any]:
"""Admin: alle Widgets als auswählbar (ohne Feature-Filter)."""
return {
"catalog_version": 1,
"widgets": [_public_row(e, allowed=True) for e in WIDGET_CATALOG],
}
def apply_entitlements_to_layout_dict(layout: dict[str, Any], profile_id: str, conn) -> dict[str, Any]: def apply_entitlements_to_layout_dict(layout: dict[str, Any], profile_id: str, conn) -> dict[str, Any]:
""" """
Setzt enabled=False für Widgets ohne Berechtigung. Mindestens ein Widget bleibt aktiv (welcome). Setzt enabled=False für Widgets ohne Berechtigung. Mindestens ein Widget bleibt aktiv (welcome).

View File

@ -0,0 +1,8 @@
-- Globale System-Konfiguration (Key/Value, JSONB)
CREATE TABLE IF NOT EXISTS system_config (
key VARCHAR(64) PRIMARY KEY,
value JSONB NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_system_config_updated_at ON system_config (updated_at);

View File

@ -7,12 +7,21 @@ import os
import smtplib import smtplib
from email.mime.text import MIMEText from email.mime.text import MIMEText
from datetime import datetime from datetime import datetime
from typing import Any
from fastapi import APIRouter, HTTPException, Depends from fastapi import APIRouter, HTTPException, Depends
from db import get_db, get_cursor, r2d from db import get_db, get_cursor, r2d
from auth import require_admin, hash_pin from auth import require_admin, hash_pin
from models import AdminProfileUpdate from models import AdminProfileUpdate
from dashboard_layout_schema import DashboardLayoutPayload, product_default_layout_dict
from dashboard_widget_entitlements import widgets_catalog_admin_payload
from system_dashboard_product_default import (
delete_product_default_override,
get_product_default_base_dict,
get_stored_product_default_validated,
upsert_product_default_base,
)
router = APIRouter(prefix="/api/admin", tags=["admin"]) router = APIRouter(prefix="/api/admin", tags=["admin"])
@ -155,3 +164,52 @@ def admin_test_email(data: dict, session: dict=Depends(require_admin)):
return {"ok": True, "message": f"Test-E-Mail an {email} gesendet"} return {"ok": True, "message": f"Test-E-Mail an {email} gesendet"}
except Exception as e: except Exception as e:
raise HTTPException(500, f"Fehler beim Senden: {str(e)}") raise HTTPException(500, f"Fehler beim Senden: {str(e)}")
@router.get("/widgets/catalog-full")
def admin_widgets_catalog_full(session: dict = Depends(require_admin)):
"""Dashboard-Widget-Katalog ohne Feature-Filter (Konfiguration des Produkt-Standards)."""
_ = session
return widgets_catalog_admin_payload()
@router.get("/dashboard-product-default")
def admin_get_dashboard_product_default(session: dict = Depends(require_admin)):
"""Aktueller Produkt-Dashboard-Standard (DB oder Code)."""
_ = session
with get_db() as conn:
layout = get_product_default_base_dict(conn)
from_database = get_stored_product_default_validated(conn) is not None
code_ref = product_default_layout_dict()
return {
"from_database": from_database,
"layout": layout,
"code_reference": code_ref,
}
@router.put("/dashboard-product-default")
def admin_put_dashboard_product_default(
body: dict[str, Any],
session: dict = Depends(require_admin),
):
"""System-Standard persistieren (JSON wie Nutzer-Layout v1)."""
_ = session
try:
payload = DashboardLayoutPayload.model_validate(body)
except Exception as e:
raise HTTPException(422, str(e)) from e
stored = payload.to_stored_dict()
with get_db() as conn:
upsert_product_default_base(conn, stored)
return {"ok": True, "layout": stored, "from_database": True}
@router.delete("/dashboard-product-default")
def admin_delete_dashboard_product_default(session: dict = Depends(require_admin)):
"""DB-Override entfernen; App fällt auf Code-Standard zurück."""
_ = session
with get_db() as conn:
delete_product_default_override(conn)
layout = get_product_default_base_dict(conn)
return {"ok": True, "layout": layout, "from_database": False}

View File

@ -13,11 +13,11 @@ from dashboard_layout_schema import (
DashboardLayoutPayload, DashboardLayoutPayload,
coalesce_effective_layout, coalesce_effective_layout,
lab_default_layout_dict, lab_default_layout_dict,
product_default_layout_dict,
) )
from dashboard_widget_entitlements import apply_entitlements_to_layout_dict, widgets_catalog_payload from dashboard_widget_entitlements import apply_entitlements_to_layout_dict, widgets_catalog_payload
from db import get_cursor, get_db from db import get_cursor, get_db
from routers.profiles import get_pid from routers.profiles import get_pid
from system_dashboard_product_default import get_product_default_base_dict
router = APIRouter(prefix="/api/app", tags=["app-dashboard-lab"]) router = APIRouter(prefix="/api/app", tags=["app-dashboard-lab"])
@ -51,8 +51,11 @@ def get_dashboard_layout(
raw = row["dashboard_layout"] if row else None raw = row["dashboard_layout"] if row else None
custom, effective = coalesce_effective_layout(raw) custom, effective = coalesce_effective_layout(raw)
with get_db() as conn: with get_db() as conn:
base_product = get_product_default_base_dict(conn)
if not custom:
effective = base_product
effective = apply_entitlements_to_layout_dict(effective, pid, conn) effective = apply_entitlements_to_layout_dict(effective, pid, conn)
product_adj = apply_entitlements_to_layout_dict(product_default_layout_dict(), pid, conn) product_adj = apply_entitlements_to_layout_dict(base_product, pid, conn)
lab_adj = apply_entitlements_to_layout_dict(lab_default_layout_dict(), pid, conn) lab_adj = apply_entitlements_to_layout_dict(lab_default_layout_dict(), pid, conn)
return { return {
"custom": custom, "custom": custom,
@ -107,5 +110,6 @@ def reset_dashboard_layout(
) )
if cur.rowcount == 0: if cur.rowcount == 0:
raise HTTPException(404, "Profil nicht gefunden") raise HTTPException(404, "Profil nicht gefunden")
cleared = apply_entitlements_to_layout_dict(product_default_layout_dict(), pid, conn) base = get_product_default_base_dict(conn)
cleared = apply_entitlements_to_layout_dict(base, pid, conn)
return {"ok": True, "layout": cleared} return {"ok": True, "layout": cleared}

View File

@ -0,0 +1,77 @@
"""
Persistenter System-Standard für die Produkt-Übersicht (Dashboard).
Key in system_config: dashboard_product_default gültiges DashboardLayoutPayload (JSON).
"""
from __future__ import annotations
from typing import Any
from psycopg2.extras import Json
from dashboard_layout_schema import DashboardLayoutPayload, product_default_layout_dict
from db import get_cursor
SYSTEM_CONFIG_KEY_DASHBOARD_PRODUCT_DEFAULT = "dashboard_product_default"
def get_stored_product_default_validated(conn) -> dict[str, Any] | None:
"""Gültiges Layout aus DB oder None (fehlt/ungültig)."""
cur = get_cursor(conn)
cur.execute(
"SELECT value FROM system_config WHERE key = %s",
(SYSTEM_CONFIG_KEY_DASHBOARD_PRODUCT_DEFAULT,),
)
row = cur.fetchone()
if not row or row.get("value") is None:
return None
raw = row["value"]
if isinstance(raw, str):
import json
try:
raw = json.loads(raw)
except json.JSONDecodeError:
return None
if not isinstance(raw, dict):
return None
try:
payload = DashboardLayoutPayload.model_validate(
{"version": raw.get("version", 1), "widgets": raw.get("widgets", [])}
)
return payload.to_stored_dict()
except Exception:
return None
def get_product_default_base_dict(conn) -> dict[str, Any]:
"""Basis-Layout (ohne Entitlements): DB-Override oder Code-Standard."""
stored = get_stored_product_default_validated(conn)
if stored is not None:
return stored
return product_default_layout_dict()
def upsert_product_default_base(conn, layout: dict[str, Any]) -> dict[str, Any]:
payload = DashboardLayoutPayload.model_validate(layout)
stored = payload.to_stored_dict()
cur = get_cursor(conn)
cur.execute(
"""
INSERT INTO system_config (key, value, updated_at)
VALUES (%s, %s, CURRENT_TIMESTAMP)
ON CONFLICT (key) DO UPDATE SET
value = EXCLUDED.value,
updated_at = CURRENT_TIMESTAMP
""",
(SYSTEM_CONFIG_KEY_DASHBOARD_PRODUCT_DEFAULT, Json(stored)),
)
return stored
def delete_product_default_override(conn) -> None:
cur = get_cursor(conn)
cur.execute(
"DELETE FROM system_config WHERE key = %s",
(SYSTEM_CONFIG_KEY_DASHBOARD_PRODUCT_DEFAULT,),
)

View File

@ -0,0 +1,45 @@
from dashboard_layout_schema import DashboardLayoutPayload, product_default_layout_dict
from dashboard_widget_entitlements import widgets_catalog_admin_payload
def test_widgets_catalog_admin_all_allowed():
p = widgets_catalog_admin_payload()
assert p["catalog_version"] == 1
assert len(p["widgets"]) >= 1
assert all(w["allowed"] is True for w in p["widgets"])
def test_get_product_default_base_uses_code_when_no_row(monkeypatch):
from system_dashboard_product_default import get_product_default_base_dict
class _Cur:
def execute(self, *a, **k):
pass
def fetchone(self):
return None
monkeypatch.setattr("system_dashboard_product_default.get_cursor", lambda _c: _Cur())
assert get_product_default_base_dict(object()) == product_default_layout_dict()
def test_get_product_default_base_uses_db_when_valid(monkeypatch):
from system_dashboard_product_default import get_product_default_base_dict
from widget_catalog import ALLOWED_WIDGET_IDS
small = {
"version": 1,
"widgets": [{"id": wid, "enabled": wid == "welcome"} for wid in sorted(ALLOWED_WIDGET_IDS)],
}
DashboardLayoutPayload.model_validate(small)
class _Cur:
def execute(self, *a, **k):
pass
def fetchone(self):
return {"value": small}
monkeypatch.setattr("system_dashboard_product_default.get_cursor", lambda _c: _Cur())
assert get_product_default_base_dict(object()) == small

View File

@ -9,7 +9,7 @@ Semantic Versioning: MAJOR.MINOR.PATCH
APP_VERSION = "0.9n" APP_VERSION = "0.9n"
BUILD_DATE = "2026-04-05" BUILD_DATE = "2026-04-05"
DB_SCHEMA_VERSION = "20260406c" # Migration 039 DB_SCHEMA_VERSION = "20260406d" # Migration 040
MODULE_VERSIONS = { MODULE_VERSIONS = {
"auth": "1.2.0", "auth": "1.2.0",
@ -24,20 +24,21 @@ MODULE_VERSIONS = {
"photos": "1.0.0", "photos": "1.0.0",
"insights": "1.3.0", "insights": "1.3.0",
"prompts": "1.1.0", "prompts": "1.1.0",
"admin": "1.2.0", "admin": "1.3.0", # Dashboard Produkt-Standard (system_config) + catalog-full
"stats": "1.0.1", "stats": "1.0.1",
"exportdata": "1.1.0", "exportdata": "1.1.0",
"importdata": "1.0.0", "importdata": "1.0.0",
"membership": "2.1.0", "membership": "2.1.0",
"workflow": "0.6.0", # Phase 4: End Node Template Engine "workflow": "0.6.0", # Phase 4: End Node Template Engine
"app_dashboard": "1.9.0", # product vs lab layout defaults; user dashboard configure page API fields "app_dashboard": "1.10.0", # Produkt-Standard aus system_config; Response-Form unverändert
} }
CHANGELOG = [ CHANGELOG = [
{ {
"version": "0.9n", "version": "0.9n",
"date": "2026-04-05", "date": "2026-04-06",
"changes": [ "changes": [
"Admin: Produkt-Dashboard-Systemstandard (Migration 040 system_config, API, UI)",
"Phase 4: End Node Template Engine", "Phase 4: End Node Template Engine",
"workflow_models.py: EndNodeOutputMode enum (AUTO, TEMPLATE)", "workflow_models.py: EndNodeOutputMode enum (AUTO, TEMPLATE)",
"workflow_executor.py: execute_end_node() with Jinja2 template rendering", "workflow_executor.py: execute_end_node() with Jinja2 template rendering",

View File

@ -241,6 +241,7 @@ function AppShell() {
<Route path="g/:groupId" element={<AdminGroupHubPage />} /> <Route path="g/:groupId" element={<AdminGroupHubPage />} />
<Route path="users" element={<AdminUsersPage />} /> <Route path="users" element={<AdminUsersPage />} />
<Route path="system" element={<AdminSystemPage />} /> <Route path="system" element={<AdminSystemPage />} />
<Route path="dashboard-product-default" element={<DashboardConfigurePage adminMode />} />
<Route path="tier-limits" element={<AdminTierLimitsPage/>}/> <Route path="tier-limits" element={<AdminTierLimitsPage/>}/>
<Route path="features" element={<AdminFeaturesPage/>}/> <Route path="features" element={<AdminFeaturesPage/>}/>
<Route path="tiers" element={<AdminTiersPage/>}/> <Route path="tiers" element={<AdminTiersPage/>}/>

View File

@ -115,6 +115,11 @@ export const ADMIN_GROUPS = [
label: 'SMTP & Metadaten-Export', label: 'SMTP & Metadaten-Export',
description: 'SMTP-Status, Test-Mail und Placeholder-Katalog (JSON/ZIP).', description: 'SMTP-Status, Test-Mail und Placeholder-Katalog (JSON/ZIP).',
}, },
{
to: '/admin/dashboard-product-default',
label: 'Produkt-Dashboard (Standard)',
description: 'Globales Standard-Layout der Startseite (DB oder Code-Fallback).',
},
], ],
}, },
] ]

View File

@ -32,10 +32,14 @@ function catalogMetaById(catalog) {
return Object.fromEntries(catalog.widgets.map((w) => [w.id, w])) return Object.fromEntries(catalog.widgets.map((w) => [w.id, w]))
} }
export default function DashboardConfigurePage() { /**
* @param {{ adminMode?: boolean }} [props]
*/
export default function DashboardConfigurePage({ adminMode = false } = {}) {
ensurePilotLabWidgetsRegistered() ensurePilotLabWidgetsRegistered()
const [bundle, setBundle] = useState(null) const [bundle, setBundle] = useState(null)
const [adminFromDatabase, setAdminFromDatabase] = useState(null)
const [catalog, setCatalog] = useState(null) const [catalog, setCatalog] = useState(null)
const [layout, setLayout] = useState(null) const [layout, setLayout] = useState(null)
const [addPanelOpen, setAddPanelOpen] = useState(false) const [addPanelOpen, setAddPanelOpen] = useState(false)
@ -103,16 +107,29 @@ export default function DashboardConfigurePage() {
const load = useCallback(async () => { const load = useCallback(async () => {
setErr(null) setErr(null)
try { try {
if (adminMode) {
const [cat, d] = await Promise.all([
api.adminGetWidgetsCatalogFull(),
api.adminGetDashboardProductDefault(),
])
setCatalog(cat)
setBundle({ custom: false, product_default_layout: d.layout })
setAdminFromDatabase(!!d.from_database)
setChartDaysDraftByWidgetId({})
setLayout(normalizeLayoutForEditor(structuredClone(d.layout)))
} else {
const [cat, b] = await Promise.all([api.getAppWidgetsCatalog(), api.getAppDashboardLayout()]) const [cat, b] = await Promise.all([api.getAppWidgetsCatalog(), api.getAppDashboardLayout()])
setCatalog(cat) setCatalog(cat)
setBundle(b) setBundle(b)
setAdminFromDatabase(null)
setChartDaysDraftByWidgetId({}) setChartDaysDraftByWidgetId({})
const base = b.custom ? b.layout : structuredClone(b.product_default_layout) const base = b.custom ? b.layout : structuredClone(b.product_default_layout)
setLayout(normalizeLayoutForEditor(base)) setLayout(normalizeLayoutForEditor(base))
}
} catch (e) { } catch (e) {
setErr(formatFastApiDetail(null, e.message)) setErr(formatFastApiDetail(null, e.message))
} }
}, []) }, [adminMode])
useEffect(() => { useEffect(() => {
load() load()
@ -138,8 +155,13 @@ export default function DashboardConfigurePage() {
setMsg(null) setMsg(null)
setErr(null) setErr(null)
try { try {
if (adminMode) {
await api.adminPutDashboardProductDefault(toSave)
setMsg('System-Standard für die Übersicht wurde gespeichert.')
} else {
await api.putAppDashboardLayout(toSave) await api.putAppDashboardLayout(toSave)
setMsg('Dein Dashboard wurde gespeichert.') setMsg('Dein Dashboard wurde gespeichert.')
}
await load() await load()
} catch (e) { } catch (e) {
setErr(formatFastApiDetail(null, e.message)) setErr(formatFastApiDetail(null, e.message))
@ -149,15 +171,27 @@ export default function DashboardConfigurePage() {
} }
const resetToSystem = async () => { const resetToSystem = async () => {
if (!window.confirm('Dein individuelles Layout löschen und System-Standard wiederherstellen?')) return const ok = adminMode
? window.confirm(
'Eintrag in der Datenbank löschen und Layout aus dem Code (widget_catalog) wiederherstellen?'
)
: window.confirm('Dein individuelles Layout löschen und System-Standard wiederherstellen?')
if (!ok) return
setBusy(true) setBusy(true)
setMsg(null) setMsg(null)
setErr(null) setErr(null)
try { try {
if (adminMode) {
const r = await api.adminDeleteDashboardProductDefault()
setChartDaysDraftByWidgetId({})
setLayout(normalizeLayoutForEditor(r.layout))
setMsg('Code-Standard wiederhergestellt (kein DB-Override mehr).')
} else {
const r = await api.resetAppDashboardLayout() const r = await api.resetAppDashboardLayout()
setChartDaysDraftByWidgetId({}) setChartDaysDraftByWidgetId({})
setLayout(normalizeLayoutForEditor(r.layout)) setLayout(normalizeLayoutForEditor(r.layout))
setMsg('Auf System-Standard zurückgesetzt.') setMsg('Auf System-Standard zurückgesetzt.')
}
await load() await load()
} catch (e) { } catch (e) {
setErr(formatFastApiDetail(null, e.message)) setErr(formatFastApiDetail(null, e.message))
@ -253,21 +287,36 @@ export default function DashboardConfigurePage() {
<div style={{ paddingBottom: 96, textAlign: 'left', maxWidth: 720, margin: '0 auto' }}> <div style={{ paddingBottom: 96, textAlign: 'left', maxWidth: 720, margin: '0 auto' }}>
<div style={{ marginBottom: 20 }}> <div style={{ marginBottom: 20 }}>
<Link <Link
to="/settings" to={adminMode ? '/admin/g/system' : '/settings'}
className="btn btn-secondary" className="btn btn-secondary"
style={{ display: 'inline-flex', marginBottom: 12, textDecoration: 'none' }} style={{ display: 'inline-flex', marginBottom: 12, textDecoration: 'none' }}
> >
Einstellungen {adminMode ? '← Basiseinstellungen (Admin)' : '← Einstellungen'}
</Link> </Link>
<h1 className="page-title" style={{ display: 'flex', alignItems: 'center', gap: 10 }}> <h1 className="page-title" style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<LayoutDashboard size={26} color="var(--accent)" /> <LayoutDashboard size={26} color="var(--accent)" />
Übersicht anpassen {adminMode ? 'Produkt-Übersicht: Systemstandard' : 'Übersicht anpassen'}
</h1> </h1>
<p style={{ fontSize: 13, color: 'var(--text2)', lineHeight: 1.6, marginTop: 8 }}> <p style={{ fontSize: 13, color: 'var(--text2)', lineHeight: 1.6, marginTop: 8 }}>
Kacheln für die Startseite sortieren und entfernen. Neue Kacheln über Kachel hinzufügen mit Suche direkt {adminMode
im eigenen Fenster, ohne langes Scrollen. ? 'Globales Standard-Dashboard für alle Nutzer ohne eigenes Layout. Gespeichert in der Datenbank; mit „Code-Standard wiederherstellen“ wird der Eintrag entfernt und der Fallback aus dem Code genutzt.'
: 'Kacheln für die Startseite sortieren und entfernen. Neue Kacheln über „Kachel hinzufügen“ mit Suche direkt im eigenen Fenster, ohne langes Scrollen.'}
</p> </p>
{!bundle?.custom && ( {adminMode && adminFromDatabase != null && (
<p style={{ fontSize: 12, color: 'var(--accent)', marginTop: 10, lineHeight: 1.5 }}>
{adminFromDatabase ? (
<>
Aktuell gilt ein <strong>gespeicherter Systemstandard</strong> (Datenbank).
</>
) : (
<>
Es liegt <strong>kein DB-Override</strong> vor es wird der Code-Standard aus dem Widget-Katalog
verwendet.
</>
)}
</p>
)}
{!adminMode && !bundle?.custom && (
<p style={{ fontSize: 12, color: 'var(--accent)', marginTop: 10, lineHeight: 1.5 }}> <p style={{ fontSize: 12, color: 'var(--accent)', marginTop: 10, lineHeight: 1.5 }}>
Du bearbeitest gerade das <strong>System-Standardlayout</strong>. Mit Speichern legst du deine persönliche Du bearbeitest gerade das <strong>System-Standardlayout</strong>. Mit Speichern legst du deine persönliche
Version ab. Version ab.
@ -457,10 +506,14 @@ export default function DashboardConfigurePage() {
Speichern Speichern
</button> </button>
<button type="button" className="btn btn-secondary" disabled={busy} onClick={resetToSystem}> <button type="button" className="btn btn-secondary" disabled={busy} onClick={resetToSystem}>
System-Standard wiederherstellen {adminMode ? 'Code-Standard wiederherstellen' : 'System-Standard wiederherstellen'}
</button> </button>
<Link to="/" className="btn btn-secondary" style={{ textDecoration: 'none', textAlign: 'center' }}> <Link
Zur Übersicht to={adminMode ? '/admin' : '/'}
className="btn btn-secondary"
style={{ textDecoration: 'none', textAlign: 'center' }}
>
{adminMode ? 'Admin-Übersicht' : 'Zur Übersicht'}
</Link> </Link>
</div> </div>

View File

@ -76,6 +76,13 @@ export const api = {
putAppDashboardLayout: (layout) => req('/app/dashboard-layout', jput(layout)), putAppDashboardLayout: (layout) => req('/app/dashboard-layout', jput(layout)),
resetAppDashboardLayout: () => req('/app/dashboard-layout/reset', { method: 'POST' }), resetAppDashboardLayout: () => req('/app/dashboard-layout/reset', { method: 'POST' }),
adminGetWidgetsCatalogFull: () => req('/admin/widgets/catalog-full'),
adminGetDashboardProductDefault: () => req('/admin/dashboard-product-default'),
adminPutDashboardProductDefault: (layout) =>
req('/admin/dashboard-product-default', jput(layout)),
adminDeleteDashboardProductDefault: () =>
req('/admin/dashboard-product-default', { method: 'DELETE' }),
// Persönliche Referenzwerte (Profil, historisch) // Persönliche Referenzwerte (Profil, historisch)
listReferenceValueTypes: () => req('/reference-value-types'), listReferenceValueTypes: () => req('/reference-value-types'),
listReferenceValueMetaEnums: () => req('/reference-value-meta/enums'), listReferenceValueMetaEnums: () => req('/reference-value-meta/enums'),