| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import { Router, type IRouter, type RequestHandler } from "express"; |
| import { and, asc, desc, eq, gte, inArray } from "drizzle-orm"; |
| import { |
| db, |
| networkEvolutionEvents, |
| networkPromotions, |
| networkVersions, |
| toolNetworks, |
| } from "@workspace/db"; |
| import { requireAuth, requireAdmin } from "../middlewares/auth"; |
| import { logger } from "../lib/logger"; |
| import { rollingFitness } from "../lib/evolution/fitness"; |
| import { evaluateTriggers } from "../lib/evolution/triggers"; |
| import { |
| computeShadowBudget, |
| getMostRecentShadowVariantId, |
| summariseShadow, |
| type ShadowSummary, |
| } from "../lib/evolution/shadow"; |
| import type { ShadowActivePairing } from "../lib/evolution/shadow-core"; |
| import { listEvents, recordEvent } from "../lib/evolution/events"; |
| import { |
| DEFAULT_EVOLUTION_STRATEGY, |
| getEvolutionStrategy, |
| listEvolutionStrategyNames, |
| } from "../lib/evolution/strategies"; |
| import { attemptAutoPromote } from "../lib/evolution/promote"; |
| import { tickRollbackWatch } from "../lib/evolution/rollback"; |
| import { runEvolutionTick } from "../lib/evolution/scheduler"; |
| import { |
| archiveSample, |
| listSamples, |
| runSuiteAgainstVariant, |
| } from "../lib/evolution/regression-suite"; |
|
|
| const router: IRouter = Router(); |
| const adminGuard = [requireAuth, requireAdmin] as const; |
|
|
| const clampInt = (raw: unknown, def: number, min: number, max: number): number => { |
| const n = Number(raw); |
| if (!Number.isFinite(n)) return def; |
| const i = Math.floor(n); |
| if (i < min) return min; |
| if (i > max) return max; |
| return i; |
| }; |
|
|
| class HttpError extends Error { |
| status: number; |
| code: string; |
| constructor(status: number, code: string, msg?: string) { |
| super(msg ?? code); |
| this.status = status; |
| this.code = code; |
| } |
| } |
|
|
| const sendError = ( |
| res: Parameters<RequestHandler>[1], |
| err: unknown, |
| fallback: string, |
| ): void => { |
| if (err instanceof HttpError) { |
| res.status(err.status).json({ error: err.code }); |
| return; |
| } |
| |
| |
| const msg = err instanceof Error ? err.message : ""; |
| if (/not[_\s-]?found/i.test(msg)) { |
| res.status(404).json({ error: msg.replace(/\s+/g, "_") }); |
| return; |
| } |
| if (/invalid|missing|required|already|conflict/i.test(msg)) { |
| res.status(409).json({ error: msg.replace(/\s+/g, "_") }); |
| return; |
| } |
| res.status(500).json({ error: fallback }); |
| }; |
|
|
| const listNetworks: RequestHandler = async (_req, res) => { |
| try { |
| const nets = await db |
| .select() |
| .from(toolNetworks) |
| .orderBy(desc(toolNetworks.updatedAt)); |
| const out = await Promise.all( |
| nets.map(async (n) => { |
| let fitness = null; |
| if (n.activeVariantId) { |
| fitness = await rollingFitness( |
| n.id, |
| n.activeVariantId, |
| n.problemClassPath, |
| ); |
| } |
| const shadowBudget = await computeShadowBudget(n.id); |
| |
| |
| |
| |
| const cfg = (n.config ?? {}) as Record<string, unknown>; |
| const evolutionStrategy = |
| typeof cfg["evolutionStrategy"] === "string" |
| ? (cfg["evolutionStrategy"] as string) |
| : DEFAULT_EVOLUTION_STRATEGY; |
| return { |
| id: n.id, |
| name: n.name, |
| problemClassPath: n.problemClassPath, |
| status: n.status, |
| activeVariantId: n.activeVariantId, |
| releaseTierFloor: n.releaseTierFloor, |
| builderModelTier: n.builderModelTier, |
| updatedAt: n.updatedAt, |
| fitness, |
| shadowBudget, |
| evolutionStrategy, |
| }; |
| }), |
| ); |
| res.json({ networks: out }); |
| } catch (err) { |
| logger.error({ err }, "evolution: listNetworks failed"); |
| sendError(res, err, "failed_to_list_networks"); |
| } |
| }; |
|
|
| const requireNetwork = async (networkId: string) => { |
| const row = ( |
| await db |
| .select() |
| .from(toolNetworks) |
| .where(eq(toolNetworks.id, networkId)) |
| .limit(1) |
| )[0]; |
| if (!row) throw new HttpError(404, "network_not_found"); |
| return row; |
| }; |
|
|
| const getFitness: RequestHandler = async (req, res) => { |
| const { networkId } = req.params as { networkId: string }; |
| const versionIdParam = (req.query.versionId as string | undefined) ?? null; |
| const days = clampInt(req.query.days, 7, 1, 90); |
| try { |
| const network = await requireNetwork(networkId); |
| const versions = await db |
| .select() |
| .from(networkVersions) |
| .where(eq(networkVersions.networkId, networkId)) |
| .orderBy(desc(networkVersions.createdAt)); |
| const targetVersionIds = versionIdParam |
| ? [versionIdParam] |
| : versions.map((v) => v.id).slice(0, 5); |
| const out = await Promise.all( |
| targetVersionIds.map(async (vid) => ({ |
| versionId: vid, |
| fitness: await rollingFitness(networkId, vid, network.problemClassPath, { |
| windowDays: days, |
| }), |
| })), |
| ); |
| res.json({ networkId, versions: out }); |
| } catch (err) { |
| logger.error({ err, networkId }, "evolution: fitness failed"); |
| sendError(res, err, "failed_to_compute_fitness"); |
| } |
| }; |
|
|
| const getEvents: RequestHandler = async (req, res) => { |
| const { networkId } = req.params as { networkId: string }; |
| const limit = clampInt(req.query.limit, 200, 1, 500); |
| try { |
| await requireNetwork(networkId); |
| const events = await listEvents({ networkId, limit }); |
| res.json({ events }); |
| } catch (err) { |
| logger.error({ err, networkId }, "evolution: events failed"); |
| sendError(res, err, "failed_to_list_events"); |
| } |
| }; |
|
|
| |
| |
| |
| |
| export interface BudgetTrendPoint { |
| eventId: string; |
| at: Date; |
| thresholdMs: number; |
| previousThresholdMs: number | null; |
| mode: string; |
| p75Ms: number | null; |
| safetyFactor: number | null; |
| sampleCount: number | null; |
| recentSkipRatio: number; |
| recentSampleCount: number; |
| } |
|
|
| export function eventsToBudgetTrendPoints( |
| events: Array<{ |
| id: string; |
| createdAt: Date; |
| payload: unknown; |
| }>, |
| ): BudgetTrendPoint[] { |
| const num = (v: unknown): number | null => |
| typeof v === "number" && Number.isFinite(v) ? v : null; |
| return events |
| .map((e) => { |
| const p = (e.payload ?? {}) as Record<string, unknown>; |
| const thresholdMs = num(p.thresholdMs); |
| if (thresholdMs === null) return null; |
| const mode = typeof p.mode === "string" ? p.mode : "unknown"; |
| return { |
| eventId: e.id, |
| at: e.createdAt, |
| thresholdMs, |
| previousThresholdMs: num(p.previousThresholdMs), |
| mode, |
| p75Ms: num(p.p75Ms), |
| safetyFactor: num(p.safetyFactor), |
| sampleCount: num(p.sampleCount), |
| recentSkipRatio: num(p.recentSkipRatio) ?? 0, |
| recentSampleCount: num(p.recentSampleCount) ?? 0, |
| }; |
| }) |
| .filter((x): x is BudgetTrendPoint => x !== null) |
| .reverse(); |
| } |
|
|
| |
| |
| |
| |
| |
| const getBudgetTrend: RequestHandler = async (req, res) => { |
| const { networkId } = req.params as { networkId: string }; |
| const limit = clampInt(req.query.limit, 50, 1, 200); |
| try { |
| await requireNetwork(networkId); |
| const events = await listEvents({ |
| networkId, |
| kind: "shadow_budget_threshold", |
| limit, |
| }); |
| const points = eventsToBudgetTrendPoints(events); |
| res.json({ networkId, points }); |
| } catch (err) { |
| logger.error({ err, networkId }, "evolution: budget trend failed"); |
| sendError(res, err, "failed_to_load_budget_trend"); |
| } |
| }; |
|
|
| const getShadow: RequestHandler = async (req, res) => { |
| const { networkId } = req.params as { networkId: string }; |
| const candidateId = req.query.candidateId as string | undefined; |
| if (!candidateId) { |
| return res.status(400).json({ error: "candidateId_required" }); |
| } |
| try { |
| const summary = await summariseShadow(networkId, candidateId); |
| res.json({ summary }); |
| } catch (err) { |
| logger.error({ err, networkId, candidateId }, "evolution: shadow failed"); |
| sendError(res, err, "failed_to_summarise_shadow"); |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| const SHADOW_OVERVIEW_WINDOW_DAYS = 7; |
|
|
| |
| |
| |
| |
| |
| |
| |
| export interface PairingPromotionLink { |
| promotionId: string; |
| |
| reason: string; |
| |
| toVariantId: string; |
| createdAt: Date; |
| |
| |
| |
| |
| |
| eventId: string | null; |
| } |
|
|
| export type EnrichedShadowActivePairing = ShadowActivePairing & { |
| endedByPromotion: PairingPromotionLink | null; |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function enrichPairingsWithPromotions( |
| pairings: ReadonlyArray<ShadowActivePairing>, |
| promotions: ReadonlyArray<{ |
| id: string; |
| fromVariantId: string | null; |
| toVariantId: string; |
| reason: string; |
| createdAt: Date; |
| }>, |
| eventByPromotionId: ReadonlyMap<string, string>, |
| ): EnrichedShadowActivePairing[] { |
| |
| |
| const byTimeAsc = promotions |
| .slice() |
| .sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()); |
| return pairings.map((p) => { |
| const lastTs = p.lastSampleAt ? p.lastSampleAt.getTime() : null; |
| const pr = byTimeAsc.find( |
| (x) => |
| x.fromVariantId === p.activeVariantId && |
| (lastTs === null || x.createdAt.getTime() >= lastTs), |
| ); |
| if (!pr) return { ...p, endedByPromotion: null }; |
| return { |
| ...p, |
| endedByPromotion: { |
| promotionId: pr.id, |
| reason: pr.reason, |
| toVariantId: pr.toVariantId, |
| createdAt: pr.createdAt, |
| eventId: eventByPromotionId.get(pr.id) ?? null, |
| }, |
| }; |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const getShadowOverview: RequestHandler = async (_req, res) => { |
| try { |
| const nets = await db |
| .select() |
| .from(toolNetworks) |
| .orderBy(desc(toolNetworks.updatedAt)); |
| const since = new Date( |
| Date.now() - SHADOW_OVERVIEW_WINDOW_DAYS * 24 * 60 * 60 * 1000, |
| ); |
| const items = await Promise.all( |
| nets.map(async (n) => { |
| const shadowVariantId = await getMostRecentShadowVariantId(n.id); |
| let summary: (Omit<ShadowSummary, "activePairings"> & { |
| activePairings: EnrichedShadowActivePairing[]; |
| }) | null = null; |
| let budgetSkippedPct: number | null = null; |
| if (shadowVariantId) { |
| const raw = await summariseShadow(n.id, shadowVariantId); |
| |
| |
| |
| const [promos, promoEvents] = await Promise.all([ |
| db |
| .select({ |
| id: networkPromotions.id, |
| fromVariantId: networkPromotions.fromVariantId, |
| toVariantId: networkPromotions.toVariantId, |
| reason: networkPromotions.reason, |
| createdAt: networkPromotions.createdAt, |
| }) |
| .from(networkPromotions) |
| .where( |
| and( |
| eq(networkPromotions.networkId, n.id), |
| gte(networkPromotions.createdAt, since), |
| ), |
| ) |
| .orderBy(asc(networkPromotions.createdAt)), |
| db |
| .select({ |
| id: networkEvolutionEvents.id, |
| promotionId: networkEvolutionEvents.promotionId, |
| }) |
| .from(networkEvolutionEvents) |
| .where( |
| and( |
| eq(networkEvolutionEvents.networkId, n.id), |
| inArray(networkEvolutionEvents.kind, ["promote", "rollback"]), |
| gte(networkEvolutionEvents.createdAt, since), |
| ), |
| ), |
| ]); |
| const eventByPromotionId = new Map<string, string>(); |
| for (const e of promoEvents) { |
| if (e.promotionId) eventByPromotionId.set(e.promotionId, e.id); |
| } |
| const enrichedPairings = enrichPairingsWithPromotions( |
| raw.activePairings, |
| promos, |
| eventByPromotionId, |
| ); |
| summary = { ...raw, activePairings: enrichedPairings }; |
| const total = summary.sampleCount + summary.budgetSkippedCount; |
| budgetSkippedPct = total > 0 ? summary.budgetSkippedCount / total : 0; |
| } |
| |
| |
| |
| const [shadowStarted, runnerError, budgetSkipped] = await Promise.all([ |
| listEvents({ networkId: n.id, kind: "shadow_started", limit: 5 }), |
| listEvents({ |
| networkId: n.id, |
| kind: "shadow_runner_error", |
| limit: 5, |
| }), |
| listEvents({ |
| networkId: n.id, |
| kind: "shadow_budget_skipped", |
| limit: 5, |
| }), |
| ]); |
| return { |
| networkId: n.id, |
| networkName: n.name, |
| problemClassPath: n.problemClassPath, |
| activeVariantId: n.activeVariantId, |
| shadowVariantId, |
| summary, |
| winDelta: summary ? summary.deltaMean : null, |
| budgetSkippedPct, |
| events: { |
| shadow_started: shadowStarted, |
| shadow_runner_error: runnerError, |
| shadow_budget_skipped: budgetSkipped, |
| }, |
| }; |
| }), |
| ); |
| res.json({ items }); |
| } catch (err) { |
| logger.error({ err }, "evolution: shadow overview failed"); |
| sendError(res, err, "failed_to_load_shadow_overview"); |
| } |
| }; |
|
|
| const getPromotions: RequestHandler = async (req, res) => { |
| const { networkId } = req.params as { networkId: string }; |
| try { |
| await requireNetwork(networkId); |
| const rows = await db |
| .select() |
| .from(networkPromotions) |
| .where(eq(networkPromotions.networkId, networkId)) |
| .orderBy(desc(networkPromotions.createdAt)) |
| .limit(50); |
| res.json({ promotions: rows }); |
| } catch (err) { |
| logger.error({ err, networkId }, "evolution: promotions failed"); |
| sendError(res, err, "failed_to_list_promotions"); |
| } |
| }; |
|
|
| const getRegression: RequestHandler = async (req, res) => { |
| const { networkId } = req.params as { networkId: string }; |
| try { |
| await requireNetwork(networkId); |
| const samples = await listSamples(networkId, "active"); |
| res.json({ samples }); |
| } catch (err) { |
| logger.error({ err, networkId }, "evolution: regression list failed"); |
| sendError(res, err, "failed_to_list_regression"); |
| } |
| }; |
|
|
| const postRegression: RequestHandler = async (req, res) => { |
| const { networkId } = req.params as { networkId: string }; |
| const body = (req.body ?? {}) as { |
| problemClassPath?: unknown; |
| label?: unknown; |
| inputPayload?: unknown; |
| expectedFloor?: unknown; |
| expectedShape?: unknown; |
| }; |
| if (typeof body.problemClassPath !== "string" || !body.problemClassPath) { |
| return res.status(400).json({ error: "problemClassPath_required" }); |
| } |
| if (!body.inputPayload || typeof body.inputPayload !== "object") { |
| return res.status(400).json({ error: "inputPayload_required" }); |
| } |
| try { |
| const row = await archiveSample({ |
| networkId, |
| problemClassPath: body.problemClassPath, |
| label: typeof body.label === "string" ? body.label : "", |
| inputPayload: body.inputPayload as Record<string, unknown>, |
| expectedFloor: |
| typeof body.expectedFloor === "number" ? body.expectedFloor : undefined, |
| expectedShape: |
| body.expectedShape && typeof body.expectedShape === "object" |
| ? (body.expectedShape as Record<string, unknown>) |
| : undefined, |
| createdBy: req.user?.id ?? "admin", |
| }); |
| res.status(201).json({ sample: row }); |
| } catch (err) { |
| logger.error({ err, networkId }, "evolution: regression archive failed"); |
| res.status(500).json({ error: "failed_to_archive_sample" }); |
| } |
| }; |
|
|
| const postEvaluate: RequestHandler = async (req, res) => { |
| const { networkId } = req.params as { networkId: string }; |
| try { |
| const evalResult = await evaluateTriggers(networkId); |
| res.json({ evaluation: evalResult }); |
| } catch (err) { |
| logger.error({ err, networkId }, "evolution: evaluate failed"); |
| res.status(500).json({ error: "failed_to_evaluate" }); |
| } |
| }; |
|
|
| const postPromote: RequestHandler = async (req, res) => { |
| const { networkId } = req.params as { networkId: string }; |
| const body = (req.body ?? {}) as { |
| candidateVariantId?: unknown; |
| minSamplesOverride?: unknown; |
| }; |
| if (typeof body.candidateVariantId !== "string" || !body.candidateVariantId) { |
| return res.status(400).json({ error: "candidateVariantId_required" }); |
| } |
| try { |
| const decision = await attemptAutoPromote({ |
| networkId, |
| candidateVariantId: body.candidateVariantId, |
| actor: req.user?.id ? `admin:${req.user.id}` : "admin_manual", |
| minSamplesOverride: |
| typeof body.minSamplesOverride === "number" |
| ? body.minSamplesOverride |
| : undefined, |
| }); |
| res.json({ decision }); |
| } catch (err) { |
| logger.error({ err, networkId }, "evolution: promote failed"); |
| res.status(500).json({ error: "failed_to_promote" }); |
| } |
| }; |
|
|
| const postRollbackTick: RequestHandler = async (_req, res) => { |
| try { |
| const results = await tickRollbackWatch(); |
| res.json({ results }); |
| } catch (err) { |
| logger.error({ err }, "evolution: rollback tick failed"); |
| res.status(500).json({ error: "failed_to_tick_rollback" }); |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export interface PatchStrategyValidation { |
| ok: boolean; |
| status: number; |
| body: { error?: string; available?: string[]; strategy?: string }; |
| } |
|
|
| export function validatePatchStrategyBody( |
| raw: unknown, |
| ): PatchStrategyValidation { |
| const body = (raw ?? {}) as { strategy?: unknown }; |
| if (typeof body.strategy !== "string" || body.strategy.length === 0) { |
| return { ok: false, status: 400, body: { error: "strategy_required" } }; |
| } |
| const target = body.strategy; |
| if (!getEvolutionStrategy(target)) { |
| return { |
| ok: false, |
| status: 400, |
| body: { |
| error: "unknown_strategy", |
| available: listEvolutionStrategyNames(), |
| }, |
| }; |
| } |
| return { ok: true, status: 200, body: { strategy: target } }; |
| } |
|
|
| const patchStrategy: RequestHandler = async (req, res) => { |
| const { networkId } = req.params as { networkId: string }; |
| const validation = validatePatchStrategyBody(req.body); |
| if (!validation.ok) { |
| return res.status(validation.status).json(validation.body); |
| } |
| const target = validation.body.strategy as string; |
| try { |
| const network = await requireNetwork(networkId); |
| const priorCfg = (network.config ?? {}) as Record<string, unknown>; |
| const from = |
| typeof priorCfg["evolutionStrategy"] === "string" |
| ? (priorCfg["evolutionStrategy"] as string) |
| : DEFAULT_EVOLUTION_STRATEGY; |
| if (from === target) { |
| return res.json({ |
| ok: true, |
| strategy: target, |
| unchanged: true, |
| available: listEvolutionStrategyNames(), |
| }); |
| } |
| const merged = { ...priorCfg, evolutionStrategy: target }; |
| await db |
| .update(toolNetworks) |
| .set({ config: merged, updatedAt: new Date() }) |
| .where(eq(toolNetworks.id, networkId)); |
| try { |
| await recordEvent({ |
| networkId, |
| kind: "strategy_changed", |
| payload: { |
| from, |
| to: target, |
| actorId: req.user?.id ?? "admin_manual", |
| }, |
| }); |
| } catch (eventErr) { |
| |
| logger.warn( |
| { err: eventErr, networkId, from, to: target }, |
| "evolution: strategy_changed event write failed (suppressed)", |
| ); |
| } |
| res.json({ |
| ok: true, |
| strategy: target, |
| from, |
| available: listEvolutionStrategyNames(), |
| }); |
| } catch (err) { |
| logger.error({ err, networkId, target }, "evolution: patch strategy failed"); |
| sendError(res, err, "failed_to_patch_strategy"); |
| } |
| }; |
|
|
| const postSchedulerTick: RequestHandler = async (_req, res) => { |
| try { |
| const summary = await runEvolutionTick(); |
| res.json({ summary }); |
| } catch (err) { |
| logger.error({ err }, "evolution: scheduler tick failed"); |
| res.status(500).json({ error: "failed_to_run_scheduler_tick" }); |
| } |
| }; |
|
|
| const postRunSuite: RequestHandler = async (req, res) => { |
| const { networkId } = req.params as { networkId: string }; |
| const body = (req.body ?? {}) as { variantId?: unknown }; |
| if (typeof body.variantId !== "string" || !body.variantId) { |
| return res.status(400).json({ error: "variantId_required" }); |
| } |
| try { |
| const result = await runSuiteAgainstVariant(networkId, body.variantId); |
| res.json({ result }); |
| } catch (err) { |
| logger.error({ err, networkId }, "evolution: regression run failed"); |
| res.status(500).json({ error: "failed_to_run_suite" }); |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const getBuilderEvents: RequestHandler = async (req, res) => { |
| const networkId = |
| typeof req.query.networkId === "string" ? req.query.networkId : undefined; |
| const limit = clampInt(req.query.limit, 50, 1, 200); |
| |
| |
| |
| |
| const hours = clampInt(req.query.hours, 24, 1, 168); |
| let cutoffMs = Date.now() - hours * 60 * 60 * 1000; |
| if (typeof req.query.since === "string" && req.query.since.length > 0) { |
| const parsed = Date.parse(req.query.since); |
| if (Number.isFinite(parsed)) { |
| cutoffMs = parsed; |
| } |
| } |
| try { |
| if (networkId) { |
| await requireNetwork(networkId); |
| } |
| const [proposed, none, errored] = await Promise.all([ |
| listEvents({ networkId, kind: "builder_proposed", limit }), |
| listEvents({ networkId, kind: "builder_no_candidate", limit }), |
| listEvents({ networkId, kind: "builder_error", limit }), |
| ]); |
| const events = [...proposed, ...none, ...errored] |
| .filter((e) => e.createdAt.getTime() >= cutoffMs) |
| .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()) |
| .slice(0, limit); |
| res.json({ events, windowHours: hours, since: new Date(cutoffMs).toISOString() }); |
| } catch (err) { |
| logger.error({ err, networkId }, "evolution: builder events failed"); |
| sendError(res, err, "failed_to_list_builder_events"); |
| } |
| }; |
|
|
| router.get("/admin/evolution/networks", ...adminGuard, listNetworks); |
| router.get( |
| "/admin/evolution/networks/:networkId/fitness", |
| ...adminGuard, |
| getFitness, |
| ); |
| router.get( |
| "/admin/evolution/networks/:networkId/events", |
| ...adminGuard, |
| getEvents, |
| ); |
| router.get( |
| "/admin/evolution/networks/:networkId/budget-trend", |
| ...adminGuard, |
| getBudgetTrend, |
| ); |
| router.get( |
| "/admin/evolution/networks/:networkId/shadow", |
| ...adminGuard, |
| getShadow, |
| ); |
| router.get( |
| "/admin/evolution/shadow-overview", |
| ...adminGuard, |
| getShadowOverview, |
| ); |
| router.get( |
| "/admin/evolution/networks/:networkId/promotions", |
| ...adminGuard, |
| getPromotions, |
| ); |
| router.get( |
| "/admin/evolution/networks/:networkId/regression", |
| ...adminGuard, |
| getRegression, |
| ); |
| router.post( |
| "/admin/evolution/networks/:networkId/regression", |
| ...adminGuard, |
| postRegression, |
| ); |
| router.post( |
| "/admin/evolution/networks/:networkId/regression/run", |
| ...adminGuard, |
| postRunSuite, |
| ); |
| router.post( |
| "/admin/evolution/networks/:networkId/triggers/evaluate", |
| ...adminGuard, |
| postEvaluate, |
| ); |
| router.post( |
| "/admin/evolution/networks/:networkId/promote", |
| ...adminGuard, |
| postPromote, |
| ); |
| router.patch( |
| "/admin/evolution/networks/:networkId/strategy", |
| ...adminGuard, |
| patchStrategy, |
| ); |
| router.post( |
| "/admin/evolution/rollback-tick", |
| ...adminGuard, |
| postRollbackTick, |
| ); |
| router.post( |
| "/admin/evolution/scheduler-tick", |
| ...adminGuard, |
| postSchedulerTick, |
| ); |
| router.get( |
| "/admin/evolution/builder-events", |
| ...adminGuard, |
| getBuilderEvents, |
| ); |
|
|
| export default router; |
|
|
| |
| |
| export const _handlers = { |
| listNetworks, |
| getFitness, |
| getEvents, |
| getBudgetTrend, |
| getShadow, |
| getShadowOverview, |
| getPromotions, |
| getRegression, |
| postRegression, |
| postEvaluate, |
| postPromote, |
| postRollbackTick, |
| postSchedulerTick, |
| postRunSuite, |
| getBuilderEvents, |
| patchStrategy, |
| }; |
|
|