# 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 (
{/* Titel mit Badge */}
Neuer Eintrag {resourceUsage && }
{/* Error-Meldung */} {error && (
{error}
)} {/* Button mit Tooltip-Wrapper (disabled buttons zeigen keine nativen tooltips!) */}
) ``` #### 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 (`
`) - [ ] 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`