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>
103 lines
2.3 KiB
SCSS
103 lines
2.3 KiB
SCSS
// =============================================================================
|
|
// DashboardPage — KPI tile grid + a grid of small trend charts + a couple of
|
|
// breakdown lists. Colocated with DashboardPage.tsx.
|
|
// =============================================================================
|
|
|
|
.kpi-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
|
|
gap: var(--space-sm);
|
|
margin-bottom: var(--space-xl);
|
|
}
|
|
|
|
.kpi-tile {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.15rem;
|
|
padding: var(--space-md);
|
|
background: var(--color-surface);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-md);
|
|
|
|
&__value {
|
|
font-family: var(--font-display);
|
|
font-size: var(--font-size-xl);
|
|
font-weight: 700;
|
|
color: var(--color-text);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
&__label {
|
|
font-size: var(--font-size-xs);
|
|
color: var(--color-text-muted);
|
|
}
|
|
}
|
|
|
|
.chart-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
|
gap: var(--space-md);
|
|
margin-bottom: var(--space-xl);
|
|
}
|
|
|
|
.chart-card {
|
|
padding: var(--space-md);
|
|
background: var(--color-surface);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-md);
|
|
|
|
&__head {
|
|
display: flex;
|
|
align-items: baseline;
|
|
justify-content: space-between;
|
|
gap: var(--space-sm);
|
|
margin-bottom: var(--space-sm);
|
|
|
|
h2 {
|
|
font-size: var(--font-size-md);
|
|
}
|
|
}
|
|
|
|
&__total {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--color-text-muted);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
}
|
|
|
|
.breakdown {
|
|
margin-bottom: var(--space-lg);
|
|
|
|
h2 {
|
|
font-size: var(--font-size-md);
|
|
margin-bottom: var(--space-sm);
|
|
}
|
|
|
|
&__list {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
background: var(--color-surface);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-md);
|
|
max-width: 30rem;
|
|
|
|
li {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: var(--space-md);
|
|
padding: var(--space-sm) var(--space-md);
|
|
border-bottom: 1px solid var(--color-border);
|
|
font-size: var(--font-size-sm);
|
|
|
|
&:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
span:last-child {
|
|
font-variant-numeric: tabular-nums;
|
|
color: var(--color-text-muted);
|
|
}
|
|
}
|
|
}
|
|
}
|