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)
weight_entries - Gewichtseinträge
circumference_entries - Umfangsmessungen
caliper_entries - Hautfaltenmessungen
nutrition_entries - Ernährungseinträge
activity_entries - Trainingseinträge
photos - Progress-Fotos
AI Features
ai_calls - KI-Einzelanalysen (count, monthly)
ai_pipeline - KI-Pipeline-Analyse (boolean, never)
Export/Import Features
data_export - Daten exportieren (count, monthly)
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
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
@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
// 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
# 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)
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)
- Admin-Account testen (enforcement=true nur für Admin)
- Test-User (1-2 Accounts)
- 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)
Integration-Tests
Frontend-Tests
Letzte Aktualisierung: 20. März 2026
Autor: Lars Stommer + Claude Opus 4.6