mitai-jinkendo/.claude/docs/architecture/FEATURE_ENFORCEMENT.md
Lars 7940dc7560 docs: Struktur .claude/docs versionieren, working/, Gitea-Index, Regeln
- .gitignore: .claude/docs, rules, commands tracken; settings.local weiter ignorieren
- DOCUMENTATION.md: verbindliche Ablage functional/technical/working/issues
- .claude/README.md: Agent-Einstieg; GITEA_ISSUES_INDEX aus MCP (Stand 2026-04-08)
- Arbeitspapiere von docs/ nach .claude/docs/working/ verschoben
- docs/MEMBERSHIP_SYSTEM.md als Stub; kanonisch technical/MEMBERSHIP_SYSTEM.md
- CLAUDE.md Pflichtlektüre und Links angepasst; docs/README.md vereinfacht

Made-with: Cursor
2026-04-08 13:01:49 +02:00

320 lines
9.7 KiB
Markdown

# Feature Enforcement Pattern
## Übersicht
Das Membership-System verwendet ein **4-Phasen-Modell** für Feature-Limits:
1. **Phase 1: Cleanup** - Feature-Konsolidierung, DB-Migration
2. **Phase 2: Non-blocking Monitoring** - JSON-Logging, keine Blockierung
3. **Phase 3: Frontend Display** - Usage-Badges, Quota-Übersicht
4. **Phase 4: Enforcement** - Tatsächliche Blockierung bei Limit-Überschreitung
**Status (2026-03-21):** ✅ Alle 11 Features sind in Phase 4 (komplett implementiert)
## Wie neue Features hinzufügen
### 1. Feature in Datenbank registrieren
```sql
INSERT INTO features (id, name, description, category, limit_type, reset_period, default_limit)
VALUES (
'new_feature_id', -- eindeutige ID
'Feature Name', -- Anzeigename
'Beschreibung', -- Beschreibung
'data', -- Kategorie: data, ai, export, integration
'count', -- limit_type: 'count' oder 'boolean'
'monthly', -- reset_period: 'never', 'daily', 'monthly'
50 -- default_limit: INT (NULL = unlimited, 0 = disabled)
);
```
**limit_type:**
- `count`: Zählbare Features (z.B. 50 Einträge pro Monat)
- `boolean`: An/Aus-Features (0 = disabled, 1 = enabled)
**reset_period:**
- `never`: Counter reset sich nie (z.B. Lifetime-Limits)
- `daily`: Reset um Mitternacht
- `monthly`: Reset am 1. des Monats
### 2. Backend-Implementierung
#### a) Router-Endpoint anpassen
```python
from auth import require_auth, check_feature_access, increment_feature_usage
from feature_logger import log_feature_usage
@router.post("/api/resource")
def create_resource(data: dict, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
"""Create new resource entry."""
pid = get_pid(x_profile_id)
# Phase 4: Check feature access and ENFORCE
access = check_feature_access(pid, 'new_feature_id')
log_feature_usage(pid, 'new_feature_id', access, 'create')
if not access['allowed']:
logger.warning(
f"[FEATURE-LIMIT] User {pid} blocked: "
f"new_feature_id {access['reason']} (used: {access['used']}, limit: {access['limit']})"
)
raise HTTPException(
status_code=403,
detail=f"Limit erreicht: Du hast das Kontingent für [Feature-Name] überschritten ({access['used']}/{access['limit']}). "
f"Bitte kontaktiere den Admin oder warte bis zum nächsten Reset."
)
# ... Business Logic: INSERT into DB ...
# Phase 2: Increment usage counter (only for NEW entries, not updates!)
increment_feature_usage(pid, 'new_feature_id')
return {"id": resource_id}
```
#### b) Wichtige Regeln
**Counter nur bei INSERT, nicht bei UPDATE:**
```python
cur.execute("SELECT id FROM resource WHERE profile_id=%s AND date=%s", (pid, date))
existing = cur.fetchone()
if existing:
# UPDATE - NICHT incrementieren
cur.execute("UPDATE resource SET ... WHERE id=%s", (..., existing['id']))
else:
# INSERT - incrementieren
cur.execute("INSERT INTO resource (...) VALUES (...)", (...))
increment_feature_usage(pid, 'new_feature_id') # ← Nur hier!
```
**Für Bulk-Operationen (CSV-Import):**
```python
new_entries = 0
for row in csv_data:
cur.execute("SELECT id FROM resource WHERE profile_id=%s AND date=%s", (pid, row['date']))
if not cur.fetchone():
# INSERT
cur.execute("INSERT INTO resource (...) VALUES (...)", (...))
new_entries += 1
# Increment counter für alle neuen Einträge
for _ in range(new_entries):
increment_feature_usage(pid, 'new_feature_id')
```
### 3. Frontend-Implementierung
#### a) Import UsageBadge
```javascript
import UsageBadge from '../components/UsageBadge'
```
#### b) State Management
```javascript
export default function ResourcePage() {
const [saving, setSaving] = useState(false)
const [saved, setSaved] = useState(false)
const [error, setError] = useState(null)
const [resourceUsage, setResourceUsage] = useState(null) // Phase 4
const load = () => api.listResources().then(setResources)
const loadUsage = () => {
api.getFeatureUsage().then(features => {
const feature = features.find(f => f.feature_id === 'new_feature_id')
setResourceUsage(feature)
}).catch(err => console.error('Failed to load usage:', err))
}
useEffect(() => {
load()
loadUsage()
}, [])
const handleSave = async () => {
setSaving(true)
setError(null)
try {
await api.createResource(data)
setSaved(true)
await load()
await loadUsage() // Reload usage nach save
setTimeout(() => setSaved(false), 2000)
} catch (err) {
console.error('Save failed:', err)
setError(err.message || 'Fehler beim Speichern')
setTimeout(() => setError(null), 5000)
} finally {
setSaving(false)
}
}
```
#### c) UI mit Badge und deaktiviertem Button
```javascript
return (
<div className="card">
{/* Titel mit Badge */}
<div className="card-title badge-container-right">
<span>Neuer Eintrag</span>
{resourceUsage && <UsageBadge {...resourceUsage} />}
</div>
{/* Error-Meldung */}
{error && (
<div style={{
padding:'10px',
background:'var(--danger-bg)',
border:'1px solid var(--danger)',
borderRadius:8,
fontSize:13,
color:'var(--danger)',
marginBottom:12
}}>
{error}
</div>
)}
{/* Button mit Tooltip-Wrapper (disabled buttons zeigen keine nativen tooltips!) */}
<div
title={resourceUsage && !resourceUsage.allowed
? `Limit erreicht (${resourceUsage.used}/${resourceUsage.limit}). Kontaktiere den Admin oder warte bis zum nächsten Reset.`
: ''}
style={{display:'inline-block', width:'100%'}}
>
<button
className="btn btn-primary btn-full"
onClick={handleSave}
disabled={saving || !data || (resourceUsage && !resourceUsage.allowed)}
style={{cursor: (resourceUsage && !resourceUsage.allowed) ? 'not-allowed' : 'pointer'}}
>
{saved ? <><Check size={15}/> Gespeichert!</>
: saving ? <><div className="spinner" style={{width:14,height:14}}/> </>
: (resourceUsage && !resourceUsage.allowed) ? '🔒 Limit erreicht'
: 'Speichern'}
</button>
</div>
</div>
)
```
#### d) CSS-Styling (bereits vorhanden)
Die Badge-Styles sind zentral in `frontend/src/components/UsageBadge.css`:
```css
/* Badge container rechts-aligniert */
.badge-container-right {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
/* Badge selbst */
.usage-badge {
font-size: 0.65rem;
font-weight: 600;
padding: 2px 6px;
border-radius: 4px;
opacity: 0.6;
white-space: nowrap;
}
.usage-badge--ok { color: #888; background: #f0f0f0; }
.usage-badge--warning { color: #EF9F27; background: #FFF4E5; }
.usage-badge--exceeded { color: #D85A30; background: #FCEBEB; }
```
### 4. Admin-UI (optional)
Wenn das Feature tier-spezifische Limits braucht, in Admin-UI ergänzen:
```javascript
// frontend/src/pages/admin/TierLimitsPage.jsx
const FEATURE_OPTIONS = [
{ value: 'new_feature_id', label: 'Feature Name' },
// ... existing features
]
```
## Checkliste für neue Features
- [ ] Feature in `features` Tabelle registriert
- [ ] Backend: `check_feature_access()` + `log_feature_usage()` vor Business Logic
- [ ] Backend: `raise HTTPException(403, ...)` wenn `!access['allowed']`
- [ ] Backend: `increment_feature_usage()` nach INSERT (nicht bei UPDATE!)
- [ ] Frontend: `UsageBadge` importiert
- [ ] Frontend: `resourceUsage` State mit `loadUsage()`
- [ ] Frontend: Badge im Titel (`badge-container-right`)
- [ ] Frontend: Button deaktiviert bei `!resourceUsage.allowed`
- [ ] Frontend: Tooltip-Wrapper um Button (`<div title="...">`)
- [ ] Frontend: Error-Handling in `handleSave()` mit `catch`
- [ ] Frontend: `loadUsage()` nach erfolgreichem Speichern
- [ ] Feature-ID in CLAUDE.md dokumentiert (falls relevant)
## Bestehende Features (Referenz)
| Feature ID | Typ | Reset | Beschreibung |
|-----------|-----|-------|--------------|
| weight_entries | count | never | Gewichtseinträge |
| circumference_entries | count | never | Umfangseinträge |
| caliper_entries | count | never | Caliper-Messungen |
| activity_entries | count | monthly | Aktivitätseinträge |
| nutrition_entries | count | monthly | Ernährungseinträge (meist CSV-Import) |
| photos | count | monthly | Progress-Fotos |
| ai_calls | count | monthly | KI-Analysen (Einzelanalysen) |
| ai_pipeline | boolean | - | KI-Pipeline (An/Aus) |
| data_export | count | monthly | Daten-Exporte (CSV/JSON/ZIP) |
| data_import | count | monthly | Daten-Importe (ZIP) |
## Debugging
**Log-Datei prüfen:**
```bash
tail -f backend/logs/feature-usage.log | jq .
```
**Struktur:**
```json
{
"timestamp": "2026-03-21T14:23:45.123456",
"profile_id": "uuid",
"feature_id": "weight_entries",
"action": "create",
"allowed": true,
"used": 7,
"limit": 50,
"remaining": 43,
"reason": "within_limit"
}
```
**Datenbank prüfen:**
```sql
-- Aktuelle Usage
SELECT * FROM user_feature_usage WHERE profile_id = 'xxx';
-- Feature-Definition
SELECT * FROM features WHERE id = 'new_feature_id';
-- Tier-Limits
SELECT * FROM tier_limits WHERE feature_id = 'new_feature_id';
-- User-Overrides
SELECT * FROM user_feature_restrictions
WHERE profile_id = 'xxx' AND feature_id = 'new_feature_id';
```
## Weiterführende Dokumentation
- **Membership-System:** `.claude/docs/technical/MEMBERSHIP_SYSTEM.md`
- **Backend-Architektur:** `.claude/docs/architecture/BACKEND.md`
- **Frontend-Architektur:** `.claude/docs/architecture/FRONTEND.md`
- **Coding Rules:** `.claude/docs/rules/CODING_RULES.md`