PR 3 du chantier admin. Tableau de bord metriques : snapshot de compteurs
+ series temporelles journalieres.
Schema (migration admin_metrics) :
- AnalyticsEvent (type String libre, actorType/actorId sans FK, context Json,
index [type, created_at]) + WorkerHeartbeat (cable en PR 4).
- colonnes createdAt @default(now()) sur UserProfile / Recipe / Planning /
PlanningItem (lecture admin uniquement ; lignes existantes = timestamp de
la migration).
Instrumentation (lib/analytics.service.ts, fire-and-forget) :
- analytics.recordEvent(type, {actorId?, context?}) : retourne void, insert
detache, echec loggue+avale, jamais de latence sur la requete.
- points d'appel : user.signup, recipe.created, recipe.imported,
planning.item_added, tech_step.correction_submitted, shopping_list.viewed.
API : GET /admin/metrics?days= (7-365, defaut 30, requireAdmin) ->
admin-metrics.service.ts. bucketByDay pur (zero-remplissage, teste sans
base). MetricsView dans packages/shared.
Front : DashboardPage (tuiles KPI + un graphe recharts par serie + listes
recettes-par-source / evenements), logique pure dans dashboard.ts, i18n
admin.dashboard.*. AdminApiClient.getMetrics.
reset-db.ts truncate analytics_events + worker_heartbeats.
Tests : Mocha admin-metrics.test.ts (bucketByDay pur x2 verts ; snapshot,
series zero-remplies, event user.signup fire-and-forget) ; Cypress
dashboard.cy.ts (2 verts). specs/backend-architecture.md : section admin.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
30 lines
1.3 KiB
TypeScript
30 lines
1.3 KiB
TypeScript
import { z } from "zod";
|
|
|
|
// Shared between apps/api (server-side validation, source of truth) and
|
|
// apps/admin-web (client-side validation for instant feedback). Same
|
|
// rationale as schemas/auth.ts — one set of rules, French messages surfaced
|
|
// as-is in the admin login form.
|
|
|
|
/**
|
|
* Payload accepted by `POST /admin/auth/login`. Deliberately its own schema
|
|
* (not a re-export of `loginSchema`): the admin surface is a separate
|
|
* contract from the end-user one even though the shape currently matches.
|
|
*/
|
|
export const adminLoginSchema = z.object({
|
|
email: z.string().trim().toLowerCase().email("Email invalide"),
|
|
password: z.string().min(1, "Le mot de passe est requis"),
|
|
});
|
|
/** Inferred TS type for {@link adminLoginSchema}'s validated output. */
|
|
export type AdminLoginInput = z.infer<typeof adminLoginSchema>;
|
|
|
|
/**
|
|
* Query params for `GET /admin/metrics` — `?days=` bounds how far back the
|
|
* time-series go (and how many daily buckets they carry). Coerced from the
|
|
* query string; clamped to a sane window so a huge value can't make the
|
|
* dashboard scan the whole history.
|
|
*/
|
|
export const getMetricsSchema = z.object({
|
|
days: z.coerce.number().int().min(7).max(365).default(30),
|
|
});
|
|
/** Inferred TS type for {@link getMetricsSchema}'s validated output. */
|
|
export type GetMetricsInput = z.infer<typeof getMetricsSchema>;
|