766 lines
20 KiB
Markdown
766 lines
20 KiB
Markdown
# Issue #54: Dynamic Placeholder System
|
|
|
|
**Status:** 📋 Planned (Post Phase 0c)
|
|
**Priorität:** Medium
|
|
**Aufwand:** 6-8h
|
|
**Erstellt:** 28. März 2026
|
|
**Abhängigkeiten:** Phase 0c ✅
|
|
|
|
---
|
|
|
|
## Problem
|
|
|
|
**Aktuell (Phase 0b/0c):**
|
|
```python
|
|
# backend/placeholder_resolver.py
|
|
|
|
PLACEHOLDER_FUNCTIONS = {
|
|
"weight_aktuell": resolve_weight_aktuell,
|
|
"weight_trend": resolve_weight_trend,
|
|
# ... 50+ manual entries ...
|
|
}
|
|
|
|
def get_placeholder_catalog(profile_id: str):
|
|
placeholders = {
|
|
'Körper': [
|
|
('weight_aktuell', 'Aktuelles Gewicht in kg'),
|
|
('weight_trend', 'Gewichtstrend (7d/30d)'),
|
|
# ... 50+ manual entries ...
|
|
],
|
|
}
|
|
```
|
|
|
|
**Probleme:**
|
|
- ❌ Neue Platzhalter erfordern 3 Code-Änderungen:
|
|
1. Funktion implementieren
|
|
2. In `PLACEHOLDER_FUNCTIONS` registrieren
|
|
3. In `get_placeholder_catalog()` dokumentieren
|
|
- ❌ Fehleranfällig (vergisst man einen Schritt → Bug)
|
|
- ❌ Katalog kann out-of-sync mit tatsächlich verfügbaren Platzhaltern sein
|
|
- ❌ Keine Introspection möglich (welche Platzhalter gibt es?)
|
|
|
|
---
|
|
|
|
## Lösung: Auto-Discovery mit Decorators
|
|
|
|
### Konzept
|
|
|
|
```python
|
|
# 1. Decorator registriert Funktionen automatisch
|
|
@placeholder(
|
|
name="weight_aktuell",
|
|
category="Körper",
|
|
description="Aktuelles Gewicht in kg"
|
|
)
|
|
def resolve_weight_aktuell(profile_id: str) -> str:
|
|
...
|
|
|
|
# 2. Registry sammelt alle registrierten Platzhalter
|
|
PLACEHOLDER_REGISTRY = {} # Wird automatisch gefüllt
|
|
|
|
# 3. Katalog wird aus Registry generiert
|
|
def get_placeholder_catalog():
|
|
return generate_catalog_from_registry()
|
|
```
|
|
|
|
**Vorteile:**
|
|
- ✅ Nur 1 Stelle zu ändern (Decorator über Funktion)
|
|
- ✅ Auto-Sync: Katalog immer aktuell
|
|
- ✅ Introspection: Alle verfügbaren Platzhalter abrufbar
|
|
- ✅ Metadata direkt bei Funktion (Single Source of Truth)
|
|
|
|
---
|
|
|
|
## Implementierung
|
|
|
|
### Step 1: Decorator + Registry erstellen (2h)
|
|
|
|
**Datei:** `backend/placeholder_resolver.py`
|
|
|
|
```python
|
|
from functools import wraps
|
|
from typing import Dict, List, Callable
|
|
|
|
# ── REGISTRY ─────────────────────────────────────────────────────
|
|
|
|
PLACEHOLDER_REGISTRY: Dict[str, dict] = {}
|
|
|
|
def placeholder(
|
|
name: str,
|
|
category: str,
|
|
description: str,
|
|
example: str = None
|
|
):
|
|
"""
|
|
Decorator to register a placeholder function.
|
|
|
|
Usage:
|
|
@placeholder(
|
|
name="weight_aktuell",
|
|
category="Körper",
|
|
description="Aktuelles Gewicht in kg",
|
|
example="85.3 kg"
|
|
)
|
|
def resolve_weight_aktuell(profile_id: str) -> str:
|
|
...
|
|
|
|
Args:
|
|
name: Placeholder key (used in templates as {{name}})
|
|
category: Category for grouping (e.g., "Körper", "Ernährung")
|
|
description: Human-readable description
|
|
example: Optional example output
|
|
|
|
Returns:
|
|
Decorated function (registered in PLACEHOLDER_REGISTRY)
|
|
"""
|
|
def decorator(func: Callable[[str], str]) -> Callable[[str], str]:
|
|
# Validate function signature
|
|
import inspect
|
|
sig = inspect.signature(func)
|
|
params = list(sig.parameters.keys())
|
|
|
|
if len(params) != 1 or params[0] != 'profile_id':
|
|
raise ValueError(
|
|
f"Placeholder function {func.__name__} must have signature: "
|
|
f"(profile_id: str) -> str"
|
|
)
|
|
|
|
if sig.return_annotation != str:
|
|
raise ValueError(
|
|
f"Placeholder function {func.__name__} must return str"
|
|
)
|
|
|
|
# Register in global registry
|
|
PLACEHOLDER_REGISTRY[name] = {
|
|
'function': func,
|
|
'category': category,
|
|
'description': description,
|
|
'example': example or "N/A",
|
|
'function_name': func.__name__
|
|
}
|
|
|
|
@wraps(func)
|
|
def wrapper(profile_id: str) -> str:
|
|
return func(profile_id)
|
|
|
|
return wrapper
|
|
|
|
return decorator
|
|
|
|
|
|
# ── CATALOG GENERATION ───────────────────────────────────────────
|
|
|
|
def get_placeholder_catalog(profile_id: str = None) -> Dict[str, List[Dict[str, str]]]:
|
|
"""
|
|
Generate placeholder catalog from registry.
|
|
|
|
Args:
|
|
profile_id: Optional - if provided, generates example values
|
|
|
|
Returns:
|
|
{
|
|
"category": [
|
|
{
|
|
"key": "placeholder_name",
|
|
"description": "...",
|
|
"example": "..." or computed value
|
|
},
|
|
...
|
|
],
|
|
...
|
|
}
|
|
"""
|
|
catalog = {}
|
|
|
|
for name, meta in PLACEHOLDER_REGISTRY.items():
|
|
category = meta['category']
|
|
|
|
if category not in catalog:
|
|
catalog[category] = []
|
|
|
|
# Generate example value if profile_id provided
|
|
example = meta['example']
|
|
if profile_id and example == "N/A":
|
|
try:
|
|
example = meta['function'](profile_id)
|
|
except Exception as e:
|
|
example = f"Error: {str(e)}"
|
|
|
|
catalog[category].append({
|
|
'key': name,
|
|
'description': meta['description'],
|
|
'example': example,
|
|
'placeholder': f'{{{{{name}}}}}' # {{name}}
|
|
})
|
|
|
|
# Sort categories
|
|
sorted_catalog = {}
|
|
category_order = [
|
|
'Profil', 'Körper', 'Ernährung', 'Training',
|
|
'Schlaf & Erholung', 'Vitalwerte', 'Scores', 'Focus Areas', 'Zeitraum'
|
|
]
|
|
|
|
for cat in category_order:
|
|
if cat in catalog:
|
|
sorted_catalog[cat] = sorted(catalog[cat], key=lambda x: x['key'])
|
|
|
|
# Add any remaining categories not in order
|
|
for cat, items in catalog.items():
|
|
if cat not in sorted_catalog:
|
|
sorted_catalog[cat] = sorted(items, key=lambda x: x['key'])
|
|
|
|
return sorted_catalog
|
|
|
|
|
|
# ── PLACEHOLDER RESOLUTION ───────────────────────────────────────
|
|
|
|
def resolve_placeholders(template: str, profile_id: str) -> str:
|
|
"""
|
|
Resolve all placeholders in template.
|
|
|
|
Uses PLACEHOLDER_REGISTRY (auto-populated by decorators).
|
|
"""
|
|
result = template
|
|
|
|
for name, meta in PLACEHOLDER_REGISTRY.items():
|
|
placeholder = f'{{{{{name}}}}}'
|
|
|
|
if placeholder in result:
|
|
try:
|
|
value = meta['function'](profile_id)
|
|
result = result.replace(placeholder, str(value))
|
|
except Exception as e:
|
|
# Log error but don't crash
|
|
import traceback
|
|
print(f"Error resolving {{{{{{name}}}}}}: {e}")
|
|
traceback.print_exc()
|
|
result = result.replace(placeholder, f"[Error: {name}]")
|
|
|
|
return result
|
|
|
|
|
|
# ── API ENDPOINT ─────────────────────────────────────────────────
|
|
|
|
def list_available_placeholders() -> List[str]:
|
|
"""
|
|
List all available placeholder names.
|
|
|
|
Returns:
|
|
["weight_aktuell", "weight_trend", ...]
|
|
"""
|
|
return sorted(PLACEHOLDER_REGISTRY.keys())
|
|
|
|
|
|
def get_placeholder_metadata(name: str) -> dict:
|
|
"""
|
|
Get metadata for a specific placeholder.
|
|
|
|
Args:
|
|
name: Placeholder key
|
|
|
|
Returns:
|
|
{
|
|
"name": "weight_aktuell",
|
|
"category": "Körper",
|
|
"description": "...",
|
|
"example": "...",
|
|
"function_name": "resolve_weight_aktuell"
|
|
}
|
|
|
|
Raises:
|
|
KeyError: If placeholder doesn't exist
|
|
"""
|
|
if name not in PLACEHOLDER_REGISTRY:
|
|
raise KeyError(f"Placeholder '{name}' not found")
|
|
|
|
meta = PLACEHOLDER_REGISTRY[name].copy()
|
|
del meta['function'] # Don't expose function reference in API
|
|
meta['name'] = name
|
|
return meta
|
|
```
|
|
|
|
### Step 2: Platzhalter mit Decorator versehen (3-4h)
|
|
|
|
**Migration-Strategie:**
|
|
|
|
```python
|
|
# ALT (Phase 0b/0c):
|
|
def resolve_weight_aktuell(profile_id: str) -> str:
|
|
"""Returns current weight"""
|
|
...
|
|
|
|
PLACEHOLDER_FUNCTIONS = {
|
|
"weight_aktuell": resolve_weight_aktuell,
|
|
}
|
|
|
|
|
|
# NEU (Issue #54):
|
|
@placeholder(
|
|
name="weight_aktuell",
|
|
category="Körper",
|
|
description="Aktuelles Gewicht in kg",
|
|
example="85.3 kg"
|
|
)
|
|
def resolve_weight_aktuell(profile_id: str) -> str:
|
|
"""Returns current weight"""
|
|
...
|
|
|
|
# PLACEHOLDER_FUNCTIONS wird nicht mehr benötigt!
|
|
```
|
|
|
|
**Alle ~50 Platzhalter konvertieren:**
|
|
```python
|
|
# Profil
|
|
@placeholder(name="name", category="Profil", description="Name des Nutzers")
|
|
def resolve_name(profile_id: str) -> str: ...
|
|
|
|
@placeholder(name="age", category="Profil", description="Alter in Jahren")
|
|
def resolve_age(profile_id: str) -> str: ...
|
|
|
|
# Körper
|
|
@placeholder(name="weight_aktuell", category="Körper", description="Aktuelles Gewicht in kg")
|
|
def resolve_weight_aktuell(profile_id: str) -> str: ...
|
|
|
|
@placeholder(name="weight_7d_median", category="Körper", description="Gewicht 7d Median (kg)")
|
|
def resolve_weight_7d_median(profile_id: str) -> str: ...
|
|
|
|
# ... etc. für alle 50+ Platzhalter
|
|
```
|
|
|
|
### Step 3: API Endpoints erstellen (1h)
|
|
|
|
**Datei:** `backend/routers/placeholders.py` (NEU)
|
|
|
|
```python
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from auth import require_auth
|
|
from placeholder_resolver import (
|
|
get_placeholder_catalog,
|
|
list_available_placeholders,
|
|
get_placeholder_metadata,
|
|
resolve_placeholders
|
|
)
|
|
|
|
router = APIRouter(prefix="/api/placeholders", tags=["placeholders"])
|
|
|
|
|
|
@router.get("/catalog")
|
|
def get_catalog(
|
|
with_examples: bool = False,
|
|
session: dict = Depends(require_auth)
|
|
):
|
|
"""
|
|
Get grouped placeholder catalog.
|
|
|
|
Args:
|
|
with_examples: If true, generates example values using user's data
|
|
|
|
Returns:
|
|
{
|
|
"category": [
|
|
{
|
|
"key": "placeholder_name",
|
|
"description": "...",
|
|
"example": "...",
|
|
"placeholder": "{{placeholder_name}}"
|
|
},
|
|
...
|
|
],
|
|
...
|
|
}
|
|
"""
|
|
profile_id = session['profile_id'] if with_examples else None
|
|
return get_placeholder_catalog(profile_id)
|
|
|
|
|
|
@router.get("/list")
|
|
def list_placeholders():
|
|
"""
|
|
List all available placeholder names (no auth required).
|
|
|
|
Returns:
|
|
["weight_aktuell", "weight_trend", ...]
|
|
"""
|
|
return list_available_placeholders()
|
|
|
|
|
|
@router.get("/metadata/{name}")
|
|
def get_metadata(name: str):
|
|
"""
|
|
Get metadata for a specific placeholder (no auth required).
|
|
|
|
Returns:
|
|
{
|
|
"name": "weight_aktuell",
|
|
"category": "Körper",
|
|
"description": "...",
|
|
"example": "...",
|
|
"function_name": "resolve_weight_aktuell"
|
|
}
|
|
"""
|
|
try:
|
|
return get_placeholder_metadata(name)
|
|
except KeyError:
|
|
raise HTTPException(status_code=404, detail=f"Placeholder '{name}' not found")
|
|
|
|
|
|
@router.post("/resolve")
|
|
def resolve_template(
|
|
template: str,
|
|
session: dict = Depends(require_auth)
|
|
):
|
|
"""
|
|
Resolve all placeholders in template.
|
|
|
|
Args:
|
|
template: String with placeholders (e.g., "Dein Gewicht ist {{weight_aktuell}}")
|
|
|
|
Returns:
|
|
{
|
|
"original": "...",
|
|
"resolved": "...",
|
|
"placeholders_found": ["weight_aktuell", ...],
|
|
"placeholders_resolved": ["weight_aktuell", ...],
|
|
"placeholders_failed": []
|
|
}
|
|
"""
|
|
profile_id = session['profile_id']
|
|
|
|
# Find all placeholders in template
|
|
import re
|
|
found = re.findall(r'\{\{([^}]+)\}\}', template)
|
|
|
|
# Resolve template
|
|
resolved = resolve_placeholders(template, profile_id)
|
|
|
|
# Check which placeholders were resolved
|
|
resolved_list = [p for p in found if f'{{{{{p}}}}}' not in resolved]
|
|
failed_list = [p for p in found if f'{{{{{p}}}}}' in resolved]
|
|
|
|
return {
|
|
"original": template,
|
|
"resolved": resolved,
|
|
"placeholders_found": found,
|
|
"placeholders_resolved": resolved_list,
|
|
"placeholders_failed": failed_list
|
|
}
|
|
```
|
|
|
|
**Router in main.py registrieren:**
|
|
```python
|
|
# backend/main.py
|
|
|
|
from routers import placeholders # NEU
|
|
|
|
app.include_router(placeholders.router)
|
|
```
|
|
|
|
### Step 4: Frontend Integration (1-2h)
|
|
|
|
**Placeholder Browser Komponente:**
|
|
|
|
```javascript
|
|
// frontend/src/components/PlaceholderBrowser.jsx
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { api } from '../utils/api'
|
|
|
|
export default function PlaceholderBrowser({ onSelect }) {
|
|
const [catalog, setCatalog] = useState({})
|
|
const [loading, setLoading] = useState(true)
|
|
const [searchTerm, setSearchTerm] = useState('')
|
|
|
|
useEffect(() => {
|
|
loadCatalog()
|
|
}, [])
|
|
|
|
async function loadCatalog() {
|
|
try {
|
|
const data = await api.getPlaceholderCatalog(true) // with examples
|
|
setCatalog(data)
|
|
} catch (err) {
|
|
console.error('Failed to load catalog:', err)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
function filterPlaceholders() {
|
|
if (!searchTerm) return catalog
|
|
|
|
const filtered = {}
|
|
for (const [category, items] of Object.entries(catalog)) {
|
|
const matching = items.filter(p =>
|
|
p.key.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
p.description.toLowerCase().includes(searchTerm.toLowerCase())
|
|
)
|
|
if (matching.length > 0) {
|
|
filtered[category] = matching
|
|
}
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
if (loading) return <div className="spinner" />
|
|
|
|
const filteredCatalog = filterPlaceholders()
|
|
|
|
return (
|
|
<div className="placeholder-browser">
|
|
<input
|
|
type="text"
|
|
placeholder="Platzhalter suchen..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="form-input"
|
|
/>
|
|
|
|
{Object.entries(filteredCatalog).map(([category, items]) => (
|
|
<div key={category} className="category-section">
|
|
<h3>{category}</h3>
|
|
<div className="placeholder-grid">
|
|
{items.map(p => (
|
|
<div
|
|
key={p.key}
|
|
className="placeholder-card"
|
|
onClick={() => onSelect && onSelect(p.placeholder)}
|
|
>
|
|
<div className="placeholder-key">{p.placeholder}</div>
|
|
<div className="placeholder-desc">{p.description}</div>
|
|
{p.example !== 'N/A' && (
|
|
<div className="placeholder-example">
|
|
Beispiel: {p.example}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
**API Functions hinzufügen:**
|
|
|
|
```javascript
|
|
// frontend/src/utils/api.js
|
|
|
|
export const api = {
|
|
// ... existing functions ...
|
|
|
|
// Placeholder System
|
|
getPlaceholderCatalog: async (withExamples = false) => {
|
|
return await apiFetch(`/api/placeholders/catalog?with_examples=${withExamples}`)
|
|
},
|
|
|
|
listPlaceholders: async () => {
|
|
return await apiFetch('/api/placeholders/list')
|
|
},
|
|
|
|
getPlaceholderMetadata: async (name) => {
|
|
return await apiFetch(`/api/placeholders/metadata/${name}`)
|
|
},
|
|
|
|
resolvePlaceholders: async (template) => {
|
|
return await apiFetch('/api/placeholders/resolve', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ template })
|
|
})
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Vorteile nach Implementierung
|
|
|
|
### Developer Experience
|
|
- ✅ Nur 1 Stelle ändern (Decorator)
|
|
- ✅ Automatische Validierung (Signatur-Check)
|
|
- ✅ IDE Auto-Complete für Decorator-Parameter
|
|
- ✅ Weniger Fehler (kein out-of-sync)
|
|
|
|
### API Features
|
|
- ✅ `GET /api/placeholders/list` - Alle verfügbaren Platzhalter
|
|
- ✅ `GET /api/placeholders/catalog` - Gruppierter Katalog
|
|
- ✅ `GET /api/placeholders/metadata/{name}` - Details zu Platzhalter
|
|
- ✅ `POST /api/placeholders/resolve` - Template auflösen
|
|
|
|
### Frontend Features
|
|
- ✅ Placeholder Browser mit Suche
|
|
- ✅ Live-Beispielwerte aus User-Daten
|
|
- ✅ Click-to-Insert in Prompt-Editor
|
|
- ✅ Auto-Complete beim Tippen
|
|
|
|
---
|
|
|
|
## Migration-Plan
|
|
|
|
### Phase 1: Backwards Compatible (2h)
|
|
```python
|
|
# Beide Systeme parallel unterstützen
|
|
|
|
# 1. Decorator-System implementieren
|
|
@placeholder(...)
|
|
def resolve_weight_aktuell(profile_id: str) -> str: ...
|
|
|
|
# 2. Legacy PLACEHOLDER_FUNCTIONS weiter unterstützen
|
|
PLACEHOLDER_FUNCTIONS = PLACEHOLDER_REGISTRY # Alias
|
|
|
|
# 3. get_placeholder_catalog() nutzt Registry
|
|
```
|
|
|
|
### Phase 2: Migration (3h)
|
|
```python
|
|
# Alle 50+ Platzhalter mit Decorator versehen
|
|
# Ein Commit pro Kategorie:
|
|
# - commit 1: Profil (5 Platzhalter)
|
|
# - commit 2: Körper (12 Platzhalter)
|
|
# - commit 3: Ernährung (10 Platzhalter)
|
|
# - commit 4: Training (10 Platzhalter)
|
|
# - commit 5: Schlaf & Erholung (8 Platzhalter)
|
|
# - commit 6: Vitalwerte (6 Platzhalter)
|
|
# - commit 7: Rest (Scores, Focus Areas, Zeitraum)
|
|
```
|
|
|
|
### Phase 3: Cleanup (1h)
|
|
```python
|
|
# Legacy Code entfernen
|
|
# - PLACEHOLDER_FUNCTIONS Dictionary löschen
|
|
# - Alte get_placeholder_catalog() Logik löschen
|
|
```
|
|
|
|
---
|
|
|
|
## Testing
|
|
|
|
### Unit Tests
|
|
```python
|
|
# backend/tests/test_placeholder_system.py
|
|
|
|
def test_decorator_registration():
|
|
"""Test that decorator registers placeholder"""
|
|
@placeholder(name="test_ph", category="Test", description="Test")
|
|
def resolve_test(profile_id: str) -> str:
|
|
return "test_value"
|
|
|
|
assert "test_ph" in PLACEHOLDER_REGISTRY
|
|
assert PLACEHOLDER_REGISTRY["test_ph"]["category"] == "Test"
|
|
|
|
def test_invalid_signature():
|
|
"""Test that decorator validates function signature"""
|
|
with pytest.raises(ValueError):
|
|
@placeholder(name="bad", category="Test", description="Test")
|
|
def resolve_bad(profile_id: str, extra: str) -> str: # Wrong signature!
|
|
return "bad"
|
|
|
|
def test_catalog_generation():
|
|
"""Test catalog generation from registry"""
|
|
catalog = get_placeholder_catalog()
|
|
|
|
assert isinstance(catalog, dict)
|
|
assert "Körper" in catalog
|
|
assert len(catalog["Körper"]) > 0
|
|
|
|
def test_placeholder_resolution():
|
|
"""Test resolving placeholders in template"""
|
|
template = "Gewicht: {{weight_aktuell}}"
|
|
resolved = resolve_placeholders(template, "test_profile")
|
|
|
|
assert "{{weight_aktuell}}" not in resolved
|
|
assert "kg" in resolved or "Nicht genug Daten" in resolved
|
|
```
|
|
|
|
### Integration Tests
|
|
```python
|
|
def test_api_catalog_endpoint(client, auth_token):
|
|
"""Test /api/placeholders/catalog endpoint"""
|
|
response = client.get(
|
|
"/api/placeholders/catalog",
|
|
headers={"X-Auth-Token": auth_token}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "Körper" in data
|
|
assert len(data["Körper"]) > 0
|
|
|
|
def test_api_resolve_endpoint(client, auth_token):
|
|
"""Test /api/placeholders/resolve endpoint"""
|
|
response = client.post(
|
|
"/api/placeholders/resolve",
|
|
headers={"X-Auth-Token": auth_token},
|
|
json={"template": "Gewicht: {{weight_aktuell}}"}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "resolved" in data
|
|
assert "{{weight_aktuell}}" not in data["resolved"]
|
|
```
|
|
|
|
---
|
|
|
|
## Acceptance Criteria
|
|
|
|
✅ **Issue #54 ist abgeschlossen, wenn:**
|
|
|
|
### Backend
|
|
- ✅ `@placeholder` Decorator implementiert
|
|
- ✅ `PLACEHOLDER_REGISTRY` automatisch gefüllt
|
|
- ✅ `get_placeholder_catalog()` nutzt Registry
|
|
- ✅ Alle 50+ Platzhalter mit Decorator versehen
|
|
- ✅ Legacy `PLACEHOLDER_FUNCTIONS` entfernt
|
|
- ✅ API Endpoints implementiert (/list, /catalog, /metadata, /resolve)
|
|
- ✅ Unit Tests geschrieben (>80% coverage)
|
|
|
|
### Frontend
|
|
- ✅ `PlaceholderBrowser` Komponente erstellt
|
|
- ✅ Suche funktioniert
|
|
- ✅ Click-to-Insert funktioniert
|
|
- ✅ Live-Beispielwerte werden angezeigt
|
|
- ✅ Integration in Prompt-Editor
|
|
|
|
### Dokumentation
|
|
- ✅ `PLACEHOLDER_DEVELOPMENT_GUIDE.md` aktualisiert
|
|
- ✅ API-Dokumentation erstellt
|
|
- ✅ CLAUDE.md aktualisiert
|
|
|
|
---
|
|
|
|
## Ausblick: Future Enhancements
|
|
|
|
### Auto-Discovery von Data Layer Funktionen
|
|
|
|
**Nach Phase 0c:** Data Layer Funktionen könnten automatisch als Platzhalter erkannt werden:
|
|
|
|
```python
|
|
# backend/data_layer/body_metrics.py
|
|
|
|
@data_function(
|
|
provides_placeholders=[
|
|
("weight_7d_median", "Gewicht 7d Median (kg)"),
|
|
("weight_28d_slope", "Gewichtstrend 28d (kg/Tag)"),
|
|
]
|
|
)
|
|
def get_weight_trend_data(profile_id: str, days: int = 90) -> dict:
|
|
...
|
|
|
|
# Automatisch generierte Platzhalter:
|
|
@placeholder(name="weight_7d_median", category="Körper", description="...")
|
|
def resolve_weight_7d_median(profile_id: str) -> str:
|
|
data = get_weight_trend_data(profile_id, days=7)
|
|
return f"{data['rolling_median_7d'][-1][1]:.1f} kg"
|
|
```
|
|
|
|
**Vorteil:** Data Layer Funktionen automatisch als Platzhalter verfügbar.
|
|
|
|
---
|
|
|
|
**Erstellt:** 28. März 2026
|
|
**Autor:** Claude Sonnet 4.5
|
|
**Status:** Planned (Post Phase 0c)
|
|
**Geschätzter Aufwand:** 6-8h
|