# 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`
