PR 5 (derniere) du chantier admin. Remplace le duo CLI list-pending-training-suggestions.ts / retrain-tech-steps.ts par une UI. API (admin-tech-steps.service.ts, routes /admin/tech-steps/*, requireAdmin) : - GET /suggestions : TechStepTrainingSuggestion filtrees, groupees par technique, enrichies du contexte de la correction source. - GET /corrections : corrections brutes filtrables, incluant les suppressions correctedTechStepId:null invisibles ailleurs. - PATCH /suggestions/:id : edite synonymes/phrases et/ou status. - GET /training-data-snippet : bloc training_data.py a coller (lecture seule). - POST /retrain : runTechStepEvalSuite() (gate F1 vs MIN_OVERALL_F1) puis si passe backfillTechSteps() + marquage applied/rejected. Verrou memoire -> 409 RETRAIN_ALREADY_RUNNING. Gate echoue -> 200 gatePassed:false. N'edite pas le .py ni ne redemarre l'intent-service (manuel). Shared : nouveau ErrorCode RETRAIN_ALREADY_RUNNING (4023, + cle i18n apps/web), schemas (list*/update*/retrain*/snippet), types (TrainingSuggestion*/Correction*/RetrainResultView...). Front : CorrectionsPage (onglets Suggestions / Corrections brutes, bandeau caveat permanent, cartes editables + Appliquer/Rejeter, panneau snippet, panneau gate F1). Logique pure corrections.ts. i18n admin.corrections.*. AdminApiClient : 5 methodes. Tests : Mocha admin-tech-steps.test.ts (401 partout, groupement+filtre, PATCH 400/404/ok, corrections incluant removals, snippet, retrain shape + 409 concurrent) ; Cypress corrections.cy.ts (4 verts). Admin-web Cypress 13/13. specs/backend-architecture.md : section tri + retrain. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
80 lines
2.3 KiB
TypeScript
80 lines
2.3 KiB
TypeScript
import { HttpError } from "@batch-cooking/error-tools";
|
|
import { wrapAsyncHandler } from "@batch-cooking/express-tools";
|
|
import {
|
|
ErrorCode,
|
|
listCorrectionsQuerySchema,
|
|
listSuggestionsQuerySchema,
|
|
retrainRequestSchema,
|
|
trainingDataSnippetQuerySchema,
|
|
updateTrainingSuggestionSchema,
|
|
} from "@batch-cooking/shared";
|
|
import { Router } from "express";
|
|
import { requireAdmin } from "../../middlewares/require-admin.js";
|
|
import {
|
|
getTrainingDataSnippet,
|
|
listCorrections,
|
|
listSuggestions,
|
|
runRetrain,
|
|
updateSuggestion,
|
|
} from "./admin-tech-steps.service.js";
|
|
|
|
/** Router mounted at `/admin/tech-steps` (via `admin.routes.ts`) — every route behind {@link requireAdmin}. Correction/suggestion triage + the retrain trigger. */
|
|
export const adminTechStepsRouter = Router();
|
|
|
|
adminTechStepsRouter.get(
|
|
"/suggestions",
|
|
requireAdmin,
|
|
wrapAsyncHandler(async (req, res) => {
|
|
res.status(200).json(await listSuggestions(listSuggestionsQuerySchema.parse(req.query)));
|
|
}),
|
|
);
|
|
|
|
adminTechStepsRouter.get(
|
|
"/corrections",
|
|
requireAdmin,
|
|
wrapAsyncHandler(async (req, res) => {
|
|
res.status(200).json(await listCorrections(listCorrectionsQuerySchema.parse(req.query)));
|
|
}),
|
|
);
|
|
|
|
adminTechStepsRouter.get(
|
|
"/training-data-snippet",
|
|
requireAdmin,
|
|
wrapAsyncHandler(async (req, res) => {
|
|
res
|
|
.status(200)
|
|
.json(await getTrainingDataSnippet(trainingDataSnippetQuerySchema.parse(req.query)));
|
|
}),
|
|
);
|
|
|
|
adminTechStepsRouter.patch(
|
|
"/suggestions/:id",
|
|
requireAdmin,
|
|
wrapAsyncHandler(async (req, res) => {
|
|
const id = Number(req.params.id);
|
|
if (!Number.isInteger(id) || id <= 0) {
|
|
throw new HttpError(
|
|
400,
|
|
ErrorCode.VALIDATION_ERROR,
|
|
`Not a valid suggestion id: ${req.params.id}`,
|
|
);
|
|
}
|
|
const input = updateTrainingSuggestionSchema.parse(req.body);
|
|
res.status(200).json(await updateSuggestion(id, input));
|
|
}),
|
|
);
|
|
|
|
/**
|
|
* Runs the F1 regression gate then (if it passes) the full step backfill,
|
|
* and marks the given suggestion ids — see {@link runRetrain}. Long-running
|
|
* and process-locked: `409 RETRAIN_ALREADY_RUNNING` if one is already
|
|
* underway.
|
|
*/
|
|
adminTechStepsRouter.post(
|
|
"/retrain",
|
|
requireAdmin,
|
|
wrapAsyncHandler(async (req, res) => {
|
|
const input = retrainRequestSchema.parse(req.body);
|
|
res.status(200).json(await runRetrain(input));
|
|
}),
|
|
);
|