batchCooking/packages/shared/src/schemas/admin.ts
Nicolas 29432e5bf2 feat(admin): monitoring des microservices + heartbeat du worker LLM
PR 4 du chantier admin. Board de sante temps reel des dependances.

- POST /internal/tech-steps/heartbeat (requireInternalWorker) ->
  recordWorkerHeartbeat : upsert WorkerHeartbeat (cle fixe
  "tech-step-llm-worker"), lastRunAt/lastResult pour un ping "job".
  Schema workerHeartbeatSchema dans packages/shared.
- services/tech-step-llm-worker : api-client.postHeartbeat (best-effort,
  ne throw jamais) appele au boot (index.ts), a chaque tick et apres
  chaque job (scheduler.ts, avec job/ok/counts).
- admin-monitoring.service.ts + GET /admin/monitoring (requireAdmin) :
  sonde active bornee (~2 s) de Postgres (SELECT 1), l'API (uptime/RSS),
  tech-step-intent-service (/health), et le worker via son heartbeat.
  Statut up/degraded/down/unknown ; une sonde down ne casse ni les autres
  ni l'endpoint. Seuils worker : > 8 j degraded, > 21 j down.
- MonitoringView / ServiceHealthView dans packages/shared.
- Front : MonitoringPage (grille de cartes coloree par statut, re-poll
  15 s), logique pure monitoring.ts, i18n admin.monitoring.*,
  AdminApiClient.getMonitoring.
- Tests : Mocha admin-monitoring.test.ts (heartbeat 401/400/upsert
  job+boot ; GET /admin/monitoring 401, board 4 cibles, worker unknown
  sans heartbeat puis up apres) ; Cypress monitoring.cy.ts (2 verts).
  Worker mocha : 6/6 toujours verts.
- specs/backend-architecture.md : section monitoring. .gitignore :
  apps/admin-web/cypress/{screenshots,videos,downloads}.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 23:36:31 +02:00

49 lines
2.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>;
/**
* Payload of `POST /internal/tech-steps/heartbeat` — `services/tech-step-llm-worker`
* (which has no inbound HTTP of its own) reporting that it's alive. Sent on
* `boot`, on every scheduler `tick`, and after each `job` (with that job's
* name, outcome and counts). The worker key is fixed server-side (only one
* worker exists), so it isn't in the payload.
*/
export const workerHeartbeatSchema = z.object({
event: z.enum(["boot", "tick", "job"]),
/** The job that just ran — present only when `event === "job"`. */
job: z.string().max(100).optional(),
/** Whether that job succeeded — present only when `event === "job"`. */
ok: z.boolean().optional(),
/** Small `{ label: number }` summary of that job (e.g. `{ suggestions: 3 }`). */
counts: z.record(z.string(), z.number()).optional(),
});
/** Inferred TS type for {@link workerHeartbeatSchema}'s validated output. */
export type WorkerHeartbeatInput = z.infer<typeof workerHeartbeatSchema>;