mitai-jinkendo/.claude/docs/technical/FEATURE_ENFORCEMENT_MAPPING.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

338 lines
9.5 KiB
Markdown

# Feature Enforcement Mapping
**Version:** v9c Phase 2
**Status:** Planning
**Datum:** 20. März 2026
---
## Übersicht
Dieses Dokument definiert, welche API-Endpoints welche Features prüfen müssen.
---
## Feature-Katalog (nach Cleanup)
### Data Features (count, never)
1. `weight_entries` - Gewichtseinträge
2. `circumference_entries` - Umfangsmessungen
3. `caliper_entries` - Hautfaltenmessungen
4. `nutrition_entries` - Ernährungseinträge
5. `activity_entries` - Trainingseinträge
6. `photos` - Progress-Fotos
### AI Features
7. `ai_calls` - KI-Einzelanalysen (count, monthly)
8. `ai_pipeline` - KI-Pipeline-Analyse (boolean, never)
### Export/Import Features
9. `data_export` - Daten exportieren (count, monthly)
10. `data_import` - Daten importieren (count, monthly)
---
## Endpoint → Feature Mapping
### Weight Router (`/api/weight`)
| Endpoint | Method | Feature | Action |
|----------|--------|---------|--------|
| `/api/weight` | POST | `weight_entries` | Check before create, increment after |
| `/api/weight` | GET | - | No check (reading is always allowed) |
| `/api/weight/{id}` | PUT | - | No check (editing existing is allowed) |
| `/api/weight/{id}` | DELETE | - | No check (deleting is allowed) |
**Rationale:** Limit bezieht sich auf Gesamtanzahl Einträge (COUNT), nicht auf API-Calls.
---
### Circumference Router (`/api/circumference`)
| Endpoint | Method | Feature | Action |
|----------|--------|---------|--------|
| `/api/circumference` | POST | `circumference_entries` | Check before, increment after |
| `/api/circumference` | GET | - | No check |
| `/api/circumference/{id}` | PUT | - | No check |
| `/api/circumference/{id}` | DELETE | - | No check |
---
### Caliper Router (`/api/caliper`)
| Endpoint | Method | Feature | Action |
|----------|--------|---------|--------|
| `/api/caliper` | POST | `caliper_entries` | Check before, increment after |
| `/api/caliper` | GET | - | No check |
| `/api/caliper/{id}` | PUT | - | No check |
| `/api/caliper/{id}` | DELETE | - | No check |
---
### Nutrition Router (`/api/nutrition`)
| Endpoint | Method | Feature | Action |
|----------|--------|---------|--------|
| `/api/nutrition` | POST | `nutrition_entries` | Check before, increment after |
| `/api/nutrition` | GET | - | No check |
| `/api/nutrition/{id}` | PUT | - | No check |
| `/api/nutrition/{id}` | DELETE | - | No check |
---
### Activity Router (`/api/activity`)
| Endpoint | Method | Feature | Action |
|----------|--------|---------|--------|
| `/api/activity` | POST | `activity_entries` | Check before, increment after |
| `/api/activity` | GET | - | No check |
| `/api/activity/{id}` | PUT | - | No check |
| `/api/activity/{id}` | DELETE | - | No check |
---
### Photos Router (`/api/photos`)
| Endpoint | Method | Feature | Action |
|----------|--------|---------|--------|
| `/api/photos/upload` | POST | `photos` | Check before, increment after |
| `/api/photos` | GET | - | No check |
| `/api/photos/{id}` | DELETE | - | No check (deleting is allowed) |
---
### Insights Router (`/api/insights`)
| Endpoint | Method | Feature | Action |
|----------|--------|---------|--------|
| `/api/insights/run/{slug}` | POST | `ai_calls` | Check before, increment after |
| `/api/insights/pipeline` | POST | `ai_pipeline` (boolean) | Check before (no increment for boolean) |
| `/api/insights` | GET | - | No check |
| `/api/insights/{id}` | GET | - | No check |
**Rationale:**
- `ai_calls` = count-based, monthly reset
- `ai_pipeline` = boolean (enabled/disabled), no usage tracking
---
### Export Router (`/api/export`)
| Endpoint | Method | Feature | Action |
|----------|--------|---------|--------|
| `/api/export/csv` | GET | `data_export` | Check before, increment after |
| `/api/export/json` | GET | `data_export` | Check before, increment after |
| `/api/export/zip` | GET | `data_export` | Check before, increment after |
**Rationale:** Ein Feature für alle 3 Export-Typen (konsolidiert).
---
### Import Router (`/api/import`)
| Endpoint | Method | Feature | Action |
|----------|--------|---------|--------|
| `/api/nutrition/import/fddb` | POST | `data_import` | Check before, increment after |
| `/api/activity/import/csv` | POST | `data_import` | Check before, increment after |
| `/api/import/zip` | POST | `data_import` | Check before, increment after |
**Rationale:** Ein Feature für alle Import-Typen.
---
## Implementation Pattern (Phase 2: Non-Blocking Logging)
### Pattern für count-based Features
```python
from auth import require_auth, check_feature_access, increment_feature_usage
import logging
logger = logging.getLogger(__name__)
@router.post("/api/weight")
def create_weight(data: dict, session: dict = Depends(require_auth)):
profile_id = session['profile_id']
# Phase 2: Check access (log only, don't block)
access = check_feature_access(profile_id, 'weight_entries')
if not access['allowed']:
logger.warning(
f"[FEATURE-LIMIT] User {profile_id} would be blocked: "
f"weight_entries limit_exceeded ({access['used']}/{access['limit']})"
)
# NOTE: Phase 2 does NOT raise HTTPException - just logs!
# Actual logic
# ... create weight entry ...
# Phase 2: Increment usage (even if limit would be exceeded)
increment_feature_usage(profile_id, 'weight_entries')
return {"ok": True, "id": entry_id}
```
### Pattern für boolean Features
```python
@router.post("/api/insights/pipeline")
def run_pipeline(session: dict = Depends(require_auth)):
profile_id = session['profile_id']
# Phase 2: Check access (log only)
access = check_feature_access(profile_id, 'ai_pipeline')
if not access['allowed']:
logger.warning(
f"[FEATURE-LIMIT] User {profile_id} would be blocked: "
f"ai_pipeline disabled"
)
# NOTE: Phase 2 does NOT raise HTTPException!
# Actual logic
# ... run pipeline ...
# No increment for boolean features
return {"ok": True}
```
---
## Phase 3: Frontend Display (ohne Gates)
### Usage-Counter anzeigen
```jsx
// Example: WeightPage.jsx
import { useEffect, useState } from 'react'
import api from '../utils/api'
function WeightPage() {
const [usage, setUsage] = useState(null)
useEffect(() => {
// Fetch usage info
api.get('/api/features/weight_entries/check-access')
.then(res => setUsage(res))
}, [])
return (
<div>
<h1>Gewicht</h1>
{/* Phase 3: Display usage (non-blocking) */}
{usage && usage.limit !== null && (
<div className="usage-badge">
{usage.used} / {usage.limit} Einträge
{usage.remaining !== null && usage.remaining < 5 && (
<span className="warning">
Nur noch {usage.remaining} Einträge verfügbar
</span>
)}
</div>
)}
{/* Button is NOT disabled in Phase 3 */}
<button onClick={createEntry}>
Gewicht hinzufügen
</button>
</div>
)
}
```
---
## Phase 4: Enforcement aktivieren (opt-in)
### Feature-Flag System
```python
# In app_settings table
INSERT INTO app_settings (key, value, description)
VALUES ('feature_enforcement_enabled', 'false', 'Enable/disable feature limit enforcement');
```
### Modified Pattern (mit Enforcement)
```python
def create_weight(data: dict, session: dict = Depends(require_auth)):
profile_id = session['profile_id']
# Check if enforcement is enabled
enforcement_enabled = get_app_setting('feature_enforcement_enabled', False)
# Check access
access = check_feature_access(profile_id, 'weight_entries')
if not access['allowed']:
if enforcement_enabled:
# Phase 4: BLOCK
raise HTTPException(
status_code=429,
detail=f"Limit erreicht: {access['used']}/{access['limit']} Gewichtseinträge. Upgrade für mehr."
)
else:
# Phase 2/3: LOG ONLY
logger.warning(
f"[FEATURE-LIMIT] User {profile_id} would be blocked: "
f"weight_entries limit_exceeded ({access['used']}/{access['limit']})"
)
# Actual logic
# ...
```
---
## Rollout-Strategie
### Phase 2: Log-Only (1-2 Wochen)
- Alle Checks implementiert
- Nur Logging, keine Blocks
- **Monitoring**: Wie oft würde blockiert?
- **Analyse**: Gibt es falsche Limits?
### Phase 3: Display-Only (1 Woche)
- Frontend zeigt Usage an
- Buttons NICHT disabled
- **User-Feedback**: Ist Usage-Anzeige klar?
- **Testing**: Funktioniert Counter korrekt?
### Phase 4: Enforcement (schrittweise)
1. Admin-Account testen (enforcement=true nur für Admin)
2. Test-User (1-2 Accounts)
3. Rollout an alle (feature_enforcement_enabled=true)
### Rollback-Plan
- `UPDATE app_settings SET value='false' WHERE key='feature_enforcement_enabled'`
- Sofortiger Rollback ohne Code-Deploy
---
## Testing-Checklist
### Unit-Tests (Backend)
- [ ] `check_feature_access()` mit allen Hierarchien
- [ ] `increment_feature_usage()` mit Reset-Logik
- [ ] Count-based Features (limit erreicht)
- [ ] Boolean Features (enabled/disabled)
- [ ] Monthly reset funktioniert
### Integration-Tests
- [ ] POST weight-entry bis Limit erreicht
- [ ] Limit wird korrekt in Response angezeigt
- [ ] Reset nach Monatswechsel
- [ ] User-Override überschreibt Tier-Limit
- [ ] Access-Grant überschreibt Base-Tier
### Frontend-Tests
- [ ] Usage-Counter aktualisiert nach Create
- [ ] Warning bei < 5 remaining
- [ ] Unlimited zeigt "∞"
- [ ] Disabled-Features zeigen Upgrade-Hinweis
---
**Letzte Aktualisierung:** 20. März 2026
**Autor:** Lars Stommer + Claude Opus 4.6