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; /** * 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; /** * 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; /** Statuses a `TechStepTrainingSuggestion` can be filtered by / set to. */ export const TRAINING_SUGGESTION_STATUSES = ["pending", "applied", "rejected"] as const; /** One of {@link TRAINING_SUGGESTION_STATUSES}. */ export type TrainingSuggestionStatus = (typeof TRAINING_SUGGESTION_STATUSES)[number]; /** Query params for `GET /admin/tech-steps/suggestions` — every filter optional. */ export const listSuggestionsQuerySchema = z.object({ status: z.enum(TRAINING_SUGGESTION_STATUSES).optional(), sourceType: z.enum(["correction", "llm_audit"]).optional(), techStepKey: z.string().min(1).optional(), locale: z.string().min(1).optional(), }); /** Inferred TS type for {@link listSuggestionsQuerySchema}. */ export type ListSuggestionsQuery = z.infer; /** Query params for `GET /admin/tech-steps/corrections`. `consumed`/`hasCorrectedTechStep` are tri-state (omitted = no filter). */ export const listCorrectionsQuerySchema = z.object({ consumed: z.enum(["true", "false"]).optional(), hasCorrectedTechStep: z.enum(["true", "false"]).optional(), }); /** Inferred TS type for {@link listCorrectionsQuerySchema}. */ export type ListCorrectionsQuery = z.infer; /** * Body of `PATCH /admin/tech-steps/suggestions/:id` — curate a suggestion * before it feeds a `training_data.py` edit. Every field optional; at least * one must be present (enforced service-side). */ export const updateTrainingSuggestionSchema = z.object({ status: z.enum(TRAINING_SUGGESTION_STATUSES).optional(), suggestedSynonyms: z.array(z.string().min(1)).max(200).optional(), suggestedUtterances: z.array(z.string().min(1)).max(200).optional(), }); /** Inferred TS type for {@link updateTrainingSuggestionSchema}. */ export type UpdateTrainingSuggestionInput = z.infer; /** * Body of `POST /admin/tech-steps/retrain` — runs the F1 regression gate * then, if it passes, backfills every step and marks the given suggestion * ids. Both id lists optional (an empty run just re-gates + backfills). */ export const retrainRequestSchema = z.object({ appliedIds: z.array(z.number().int().positive()).max(500).optional(), rejectedIds: z.array(z.number().int().positive()).max(500).optional(), }); /** Inferred TS type for {@link retrainRequestSchema}. */ export type RetrainRequestInput = z.infer; /** Query params for `GET /admin/tech-steps/training-data-snippet`. */ export const trainingDataSnippetQuerySchema = z.object({ techStepKey: z.string().min(1), locale: z.string().min(1).default("fr"), status: z.enum(TRAINING_SUGGESTION_STATUSES).default("applied"), }); /** Inferred TS type for {@link trainingDataSnippetQuerySchema}. */ export type TrainingDataSnippetQuery = z.infer;