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>
51 lines
2 KiB
TypeScript
51 lines
2 KiB
TypeScript
import type { MetricsSnapshotView, MetricsTimeBucket } from "@batch-cooking/shared";
|
|
|
|
/**
|
|
* Pure helpers for `DashboardPage` — number/date formatting and the tile
|
|
* list, kept out of the `.tsx` (repo convention: no derivation logic in a
|
|
* component file) so they're trivially testable.
|
|
*/
|
|
|
|
/** French-grouped integer, e.g. `1234` → `"1 234"`. */
|
|
export function formatCount(value: number): string {
|
|
return value.toLocaleString("fr-FR");
|
|
}
|
|
|
|
/** `"2026-08-28"` → `"28/08"` for a compact chart axis tick. */
|
|
export function shortDay(isoDate: string): string {
|
|
const [, month, day] = isoDate.split("-");
|
|
return `${day}/${month}`;
|
|
}
|
|
|
|
/** Sum of a time series — the "total over the window" figure shown next to each chart. */
|
|
export function seriesTotal(buckets: MetricsTimeBucket[]): number {
|
|
return buckets.reduce((sum, bucket) => sum + bucket.count, 0);
|
|
}
|
|
|
|
/** One KPI tile: an i18n label key and the snapshot value it reads. */
|
|
export interface KpiTile {
|
|
labelKey: string;
|
|
value: number;
|
|
}
|
|
|
|
/**
|
|
* The dashboard's KPI tiles, in display order. `labelKey` resolves under
|
|
* `admin.dashboard.kpi.*`. Kept here (not inline in JSX) so the set is one
|
|
* list to reorder/extend.
|
|
*/
|
|
export function kpiTiles(snapshot: MetricsSnapshotView): KpiTile[] {
|
|
return [
|
|
{ labelKey: "users", value: snapshot.users },
|
|
{ labelKey: "households", value: snapshot.households },
|
|
{ labelKey: "activeHouseholds", value: snapshot.activeHouseholds },
|
|
{ labelKey: "recipes", value: snapshot.recipes },
|
|
{ labelKey: "recipesImported", value: snapshot.recipesImported },
|
|
{ labelKey: "plannings", value: snapshot.plannings },
|
|
{ labelKey: "planningItems", value: snapshot.planningItems },
|
|
{ labelKey: "favorites", value: snapshot.favorites },
|
|
{ labelKey: "corrections", value: snapshot.corrections },
|
|
{ labelKey: "correctionsUnconsumed", value: snapshot.correctionsUnconsumed },
|
|
{ labelKey: "trainingSuggestions", value: snapshot.trainingSuggestions },
|
|
{ labelKey: "admins", value: snapshot.admins },
|
|
];
|
|
}
|