batchCooking/apps/admin-web/src/pages/dashboard/dashboard.ts
Nicolas 3a416ea955 feat(admin): metriques d'utilisation (derive DB + AnalyticsEvent)
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>
2026-08-28 23:36:31 +02:00

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 },
];
}