# Spec: Protection Coverage & Streak (DiGA-Kernmetrik)

Ersetzt die aktuell **kaputte** Streak-Anzeige (`streaks.current_days` steht fest
auf 0, Profile-Page liest `me.streak`=0 + `created_at` als Datum statt des echten
Streak-Starts). Neues, optimistisches Modell auf Basis des tatsächlichen
**Schutz-Zustands** (VPN-Filter ODER MDM aktiv) statt eines bei jedem Slip
auf 0 fallenden Streaks.

## Leitprinzip
Optimistisch + motivierend. Kein Wert fällt je auf 0. Ein Cooldown/Disable wird
nur zur kleinen „ungeschützt"-Scheibe, frisst nicht den Fortschritt.

---

## 1. Datenmodell — `protection_state_log` (NEU, append-only)

Transitions-Log des Schutz-Zustands pro User.

| Feld | Typ | Bedeutung |
|---|---|---|
| `id` | uuid pk | |
| `userId` | uuid | |
| `active` | boolean | true = Schutz AN, false = AUS — der Zustand **ab** `occurredAt` |
| `source` | enum: `vpn` \| `mdm` \| `cooldown_disable` \| `client` \| `system` | woher die Transition kam |
| `occurredAt` | timestamptz | Zeitpunkt des Übergangs |
| `createdAt` | timestamptz default now | |

Index: `(userId, occurredAt)`. **Dedup:** kein neues Event schreiben, wenn `active`
== letzter bekannter Zustand des Users.

> Alte `streaks` / `streak_events` / `profiles.streak` **NICHT entfernen** (andere
> Consumer: coach, scores). Diese Coverage-Schicht ist additiv; nur die Profile-UI
> wird umgestellt.

---

## 2. Ingestion (wann wird geloggt)

- **Client meldet Übergänge:** `POST /api/protection/event` body `{ active: boolean, source: 'vpn'|'mdm' }`.
  Aufgerufen aus `useProtectionState`/`lib/protection`, wenn der kombinierte Schutz-Zustand an↔aus kippt. Client-seitig dedupen (nur bei echtem Wechsel). Server dedupt zusätzlich gegen letzten DB-Zustand.
- **Server-seitig:** Beim Cooldown-Resolve, wo der Schutz abgeschaltet wird (`api/cooldown/status.get.ts`, dort wird `protectionDisabledAt` gesetzt) → zusätzlich `{ active:false, source:'cooldown_disable' }` ins Log appenden.
- **`firstProtectionAt`** (= Tag X) = `occurredAt` des allerersten `active:true`-Events des Users.

---

## 3. Compute — `GET /api/protection/coverage`

Read-time, **kein Cron**.

```json
{
  "firstProtectionAt": "2026-05-01T10:00:00Z",   // Tag X, oder null
  "protectedDays":   80,
  "unprotectedDays": 20,
  "currentStreakDays": 3,
  "longestStreakDays": 14
}
```

**Berechnungsregeln:**
- Fenster: `firstProtectionAt` (auf UTC-Tagesgrenze normalisiert) → **heute** (UTC).
- Aus den geordneten Transitions + `now` als Ende des aktuellen Intervalls die protected/unprotected-Intervalle rekonstruieren.
- **Tages-Auflösung, großzügig:** Ein Kalendertag (UTC) zählt als **UNGESCHÜTZT** nur, wenn der Schutz an dem Tag **insgesamt > 6h aus** war. Sonst **GESCHÜTZT**. (Kurze Unterbrechungen killen den Tag nicht.)
- `protectedDays` = Anzahl geschützter Tage im Fenster.
- `unprotectedDays` = (Tage seit X) − `protectedDays`.
- `currentStreakDays` = Anzahl **zusammenhängender** geschützter Tage, die bis heute (bzw. zum letzten Tag) durchlaufen.
- `longestStreakDays` = längster je erreichter zusammenhängender geschützter Tage-Run (Rekord).
- `firstProtectionAt == null` (nie Schutz aktiviert) → alle Werte 0 / null.

---

## 4. Frontend — Profile Streak-Section (ersetzt alte Logik)

Daten via `GET /api/protection/coverage` (React Query). **Alte Logik entfernen**
(`profile/index.tsx` Z.181-186: `currentStreak=me.streak`, `streakStartDate=created_at`,
`longestDays=currentStreak`).

**Layout:**
1. **Half-Donut** (Stil/Komponente wie Mail-Page `MailDistributionChart` mit `hero`): zeigt **NUR** die Verteilung `protectedDays` vs `unprotectedDays` seit Tag X (z.B. 80% / 20%). Center-Label: geschützte Tage (z.B. „127 Tage geschützt") oder Prozent — UI-Entscheidung.
2. **Progress-Bar darunter** = aktuelle Schutzphase → Rekord:
   - `current < record` → Bar = `currentStreakDays / longestStreakDays`, Text „Noch {record−current} Tage bis zu deinem Rekord".
   - `current ≥ record` → Bar voll, Text „Neuer Rekord! {current} Tage 🎉" (Rekord zieht live mit).
   - `record == 0` (noch kein Rekord) → sinnvoller Erst-Zustand (z.B. „Deine erste Schutzphase: {current} Tage").

**i18n:** `%{var}`-Platzhalter (lib/i18n.ts), DE + EN.

---

## 5. Scope / Guards
- Backend: Prisma-Schema + **Migration** erstellen, lokal `pnpm build`-verifizieren, **NICHT pushen/deployen ohne User-GO**.
- Reporting + Rendering sind Frontend-only (kein Push nötig, landen im nächsten App-Build).
- Contract (Feldnamen/Endpunkte oben) ist verbindlich — Backend & UI müssen exakt dagegen bauen.
