The old streak was non-functional: streaks.current_days was always 0 (never computed/incremented), and the profile page read me.streak (0) + account created_at as the "since" date — showing "0 days protected since <signup>" for everyone. This is the DiGA key metric, so it had to be rebuilt. New model: optimistic protection-coverage based on actual VPN/MDM protection state, never resets to 0. - backend: append-only protection_state_log + migration; POST /api/protection/event (ingestion, deduped) + GET /api/protection/coverage (read-time compute, no cron); server-side cooldown_disable event on cooldown resolve. Generous >6h-off/day rule. - frontend: report protection on/off transitions (initial + flips, deduped) from useProtectionState; rewrote profile StreakSection → half-donut (protected vs unprotected) + progress bar (current streak → personal record) + empty state. - coverage starts fresh from deploy (no historical backfill — clean data for DiGA). - spec: docs/specs/protection-coverage-streak.md (shared contract). - old streaks/streak_events/profiles.streak left intact (coach/scores consumers). Also adds go-to-market one-pagers under docs/marketing/. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
26 lines
833 B
TypeScript
26 lines
833 B
TypeScript
import { requireUser } from "../../utils/auth";
|
|
import { computeProtectionCoverage } from "../../db/protectionStateLog";
|
|
|
|
/**
|
|
* GET /api/protection/coverage
|
|
*
|
|
* Returns protection coverage and streak metrics computed read-time from
|
|
* the protection_state_log table (no cron, no materialized state).
|
|
*
|
|
* Response shape (spec §3):
|
|
* {
|
|
* firstProtectionAt: string | null, // ISO-8601, null = never activated
|
|
* protectedDays: number,
|
|
* unprotectedDays: number,
|
|
* currentStreakDays: number,
|
|
* longestStreakDays: number,
|
|
* }
|
|
*
|
|
* All values are 0 / null when the user has never activated protection.
|
|
*/
|
|
export default defineEventHandler(async (event) => {
|
|
const user = await requireUser(event);
|
|
const coverage = await computeProtectionCoverage(user.id);
|
|
return { success: true, data: coverage };
|
|
});
|