- .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
338 lines
9.5 KiB
Markdown
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
|