import { z } from "zod"; import type { FrontendActionManifest } from "./types.js"; type Definition = { manifest: FrontendActionManifest; inputSchema: z.ZodTypeAny; outputSchema: z.ZodTypeAny }; const zoomInput = z.object({ x: z.number().finite(), y: z.number().finite(), source_crs: z.enum(["EPSG:3857", "EPSG:4326"]).optional(), zoom: z.number().finite().optional(), duration_ms: z.number().finite().nonnegative().optional() }).strict(); const id = z.string().trim().min(1).max(256); const isoTime = z.string().datetime({ offset: true }); const timeRange = { start_time: isoTime.optional(), end_time: isoTime.optional(), }; const requiredTimeRange = { start_time: isoTime, end_time: isoTime, }; const locateInput = z.object({ ids: z.array(id).min(1).max(100), feature_type: z.enum(["junction", "pipe", "valve", "reservoir", "pump", "tank", "conduit", "orifice", "outfall"]), }).strict(); const historyInput = z.object({ feature_infos: z.array(z.tuple([id, id])).min(1).max(100), data_type: z.enum(["realtime", "scheme", "none"]), ...requiredTimeRange, }).strict().refine((value) => Date.parse(value.start_time) < Date.parse(value.end_time), "start_time must precede end_time"); const scadaInput = z.object({ device_id: id.optional(), device_ids: z.array(id).min(1).max(100).optional(), ...timeRange, }).strict().refine((value) => Boolean(value.device_id) !== Boolean(value.device_ids), "provide exactly one of device_id or device_ids") .refine((value) => !value.start_time || !value.end_time || Date.parse(value.start_time) <= Date.parse(value.end_time), "start_time must not be after end_time"); const definitions: Definition[] = [ { manifest: { id: "zoom_to_map", version: "frontend-action@1", risk: "view", timeoutMs: 10_000, supportedSurfaces: ["map_overlay"] }, inputSchema: zoomInput, outputSchema: z.object({}).passthrough() }, { manifest: { id: "locate_features", version: "frontend-action@1", risk: "view", timeoutMs: 15_000, supportedSurfaces: ["map_overlay"] }, inputSchema: locateInput, outputSchema: z.object({ locatedIds: z.array(id), missingIds: z.array(id), featureType: id, bounds: z.tuple([z.number(), z.number(), z.number(), z.number()]) }).strict() }, { manifest: { id: "view_history", version: "frontend-action@1", risk: "view", timeoutMs: 10_000, supportedSurfaces: ["side_panel", "canvas"] }, inputSchema: historyInput, outputSchema: z.object({ component: z.literal("HistoryPanel"), rendered: z.literal(true), itemCount: z.number().int().nonnegative() }).strict() }, { manifest: { id: "view_scada", version: "frontend-action@1", risk: "view", timeoutMs: 10_000, supportedSurfaces: ["side_panel", "canvas"] }, inputSchema: scadaInput, outputSchema: z.object({ component: z.literal("ScadaPanel"), rendered: z.literal(true), itemCount: z.number().int().nonnegative() }).strict() }, ]; export const frontendActionRegistry = definitions.map((item) => item.manifest); export const getFrontendActionDefinition = (name: string) => definitions.find((item) => item.manifest.id === name); export const frontendActionRegistryResponse = { schema_version: "frontend-action-registry@1" as const, actions: frontendActionRegistry };