From 56066fb14bbb6168d0484148a95f75137cfb29f0 Mon Sep 17 00:00:00 2001 From: Enes Selim Date: Wed, 22 Jul 2026 15:45:28 +0300 Subject: [PATCH 01/19] wip: initial ivr emulator loading --- .../providers/IvrEmulatorContextProvider.tsx | 46 ++++ .../ElectionEvent/EditElectionEventIvr.tsx | 15 ++ .../resources/ElectionEvent/IvrEmulator.tsx | 40 +++ .../admin-portal/src/services/IvrEmulator.ts | 53 ++++ .../services/generated/ivr_emulator_wasm.d.ts | 41 ++++ .../services/generated/ivr_emulator_wasm.js | 231 ++++++++++++++++++ packages/admin-portal/webpack.config.cjs | 13 +- 7 files changed, 436 insertions(+), 3 deletions(-) create mode 100644 packages/admin-portal/src/providers/IvrEmulatorContextProvider.tsx create mode 100644 packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx create mode 100644 packages/admin-portal/src/services/IvrEmulator.ts create mode 100644 packages/admin-portal/src/services/generated/ivr_emulator_wasm.d.ts create mode 100644 packages/admin-portal/src/services/generated/ivr_emulator_wasm.js diff --git a/packages/admin-portal/src/providers/IvrEmulatorContextProvider.tsx b/packages/admin-portal/src/providers/IvrEmulatorContextProvider.tsx new file mode 100644 index 00000000000..8c882821007 --- /dev/null +++ b/packages/admin-portal/src/providers/IvrEmulatorContextProvider.tsx @@ -0,0 +1,46 @@ +import React, {createContext, useContext, useEffect, useState} from "react" +import {IvrEmulatorApi, IvrEmulatorError, loadIvrEmulator} from "@/services/IvrEmulator" + +export enum IvrStatus { + UNAVAILABLE = "unavailable", + LOADING = "loading", + READY = "ready", + ERROR = "error", +} + +export interface IvrEmulatorContextType { + status: IvrStatus + api?: IvrEmulatorApi +} +export const IvrEmulatorContext = createContext(undefined) +export const IvrEmulatorContextProvider: React.FC<{children: React.ReactNode}> = ({children}) => { + const [status, setStatus] = useState(IvrStatus.LOADING) + const [api, setApi] = useState(undefined) + const url = "/wasm/ivr_emulator_wasm_bg.wasm" + + useEffect(() => { + loadIvrEmulator(url) + ?.then((resolvedApi) => { + setStatus(IvrStatus.READY) + setApi(resolvedApi) + }) + .catch((e) => { + if (e instanceof IvrEmulatorError && e.operation === "fetch") { + setStatus(IvrStatus.UNAVAILABLE) + } else { + setStatus(IvrStatus.ERROR) + } + }) + }, []) + const value = {status, api} + + return {children} +} + +export const useIvrEmulator = (): IvrEmulatorContextType => { + const emulator = useContext(IvrEmulatorContext) + if (!emulator) { + throw new Error("useIvrEmulator can't be used outside IvrEmulatorContext") + } + return emulator +} diff --git a/packages/admin-portal/src/resources/ElectionEvent/EditElectionEventIvr.tsx b/packages/admin-portal/src/resources/ElectionEvent/EditElectionEventIvr.tsx index e53c679f56b..692f47d33bc 100644 --- a/packages/admin-portal/src/resources/ElectionEvent/EditElectionEventIvr.tsx +++ b/packages/admin-portal/src/resources/ElectionEvent/EditElectionEventIvr.tsx @@ -9,7 +9,9 @@ import {IPermissions} from "@/types/keycloak" import {PhoneBlacklist} from "./PhoneBlacklist" import {IvrConfig} from "./IvrConfig" import {IvrPrompts} from "./IvrPrompts" +import {IvrEmulator} from "./IvrEmulator" import {Box} from "@mui/material" +import {IvrEmulatorContextProvider} from "@/providers/IvrEmulatorContextProvider" const ConfigTab: React.FC = () => ( Loading...}> @@ -29,6 +31,14 @@ const PromptsTab: React.FC = () => ( ) +const EmulatorTab: React.FC = () => ( + Loading...}> + + + + +) + export const EditElectionEventIvr: React.FC = () => { const {t} = useTranslation() const authContext = useContext(AuthContext) @@ -52,6 +62,11 @@ export const EditElectionEventIvr: React.FC = () => { }) } + tabs.push({ + label: "Emulator", + component: EmulatorTab, + }) + return ( diff --git a/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx b/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx new file mode 100644 index 00000000000..1e592a683df --- /dev/null +++ b/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2026 Sequent Tech Inc +// +// SPDX-License-Identifier: AGPL-3.0-only +import React, {useEffect} from "react" +import {useRecordContext} from "react-admin" +import {Box, Button} from "@mui/material" +import {useTranslation} from "react-i18next" +import {Sequent_Backend_Election_Event} from "@/gql/graphql" +import {ElectionHeaderStyles} from "@/components/styles/ElectionHeaderStyles" +import {useIvrEmulator} from "@/providers/IvrEmulatorContextProvider" + +export const IvrEmulator: React.FC = () => { + const {t} = useTranslation() + const record = useRecordContext() + const {status: emulatorStatus, api} = useIvrEmulator() + + useEffect(() => { + if (api) { + api.welcome() + } + }, [api]) + if (!record?.id) { + return null + } + + return ( + + + {t("electionEventScreen.ivr.config.infoMsg")} + + +
`{emulatorStatus}`
+ + + + + +
+ ) +} diff --git a/packages/admin-portal/src/services/IvrEmulator.ts b/packages/admin-portal/src/services/IvrEmulator.ts new file mode 100644 index 00000000000..092cac24b21 --- /dev/null +++ b/packages/admin-portal/src/services/IvrEmulator.ts @@ -0,0 +1,53 @@ +import loadIvrWasm, {welcome} from "./generated/ivr_emulator_wasm" + +const api = {welcome} +export type IvrEmulatorApi = typeof api +let initPromise: Promise | null = null + +export class IvrEmulatorError extends Error { + constructor( + readonly operation: "fetch" | "load" | "init", + readonly msg: string, + options?: ErrorOptions + ) { + super(`Ivr emulator "${operation}" failed: ${msg}`, options) + } +} + +const fetchWasm = async (url: string | URL): Promise => { + let response: Response + try { + response = await fetch(url) + } catch (cause) { + throw new IvrEmulatorError("fetch", `failed to fetch the wasm binary from "${url}"`, { + cause, + }) + } + + if (!response.ok) { + throw new IvrEmulatorError("fetch", `response for "${url}" was "${response.status}"`) + } + + return response +} + +export const loadIvrEmulator = (wasmUrl: string): Promise | null => { + if (!initPromise) { + initPromise ??= fetchWasm(wasmUrl) + .then(async (moduleResp) => { + try { + await loadIvrWasm({module_or_path: moduleResp}) + } catch (cause) { + throw new IvrEmulatorError("load", "failed to load the wasm", {cause}) + } + + return api + }) + .catch((e: any) => { + console.error("Failed to init the emulator", e) + throw e + }) + } + + return initPromise +} diff --git a/packages/admin-portal/src/services/generated/ivr_emulator_wasm.d.ts b/packages/admin-portal/src/services/generated/ivr_emulator_wasm.d.ts new file mode 100644 index 00000000000..ade4935e61e --- /dev/null +++ b/packages/admin-portal/src/services/generated/ivr_emulator_wasm.d.ts @@ -0,0 +1,41 @@ +/* tslint:disable */ +/* eslint-disable */ + +export function init(): void; + +export function welcome(): void; + +export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; + +export interface InitOutput { + readonly memory: WebAssembly.Memory; + readonly init: () => void; + readonly welcome: () => void; + readonly __wbindgen_free: (a: number, b: number, c: number) => void; + readonly __wbindgen_malloc: (a: number, b: number) => number; + readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; + readonly __wbindgen_externrefs: WebAssembly.Table; + readonly __wbindgen_start: () => void; +} + +export type SyncInitInput = BufferSource | WebAssembly.Module; + +/** + * Instantiates the given `module`, which can either be bytes or + * a precompiled `WebAssembly.Module`. + * + * @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated. + * + * @returns {InitOutput} + */ +export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput; + +/** + * If `module_or_path` is {RequestInfo} or {URL}, makes a request and + * for everything else, calls `WebAssembly.instantiate` directly. + * + * @param {{ module_or_path: InitInput | Promise }} module_or_path - Passing `InitInput` directly is deprecated. + * + * @returns {Promise} + */ +export default function __wbg_init (module_or_path: { module_or_path: InitInput | Promise } | InitInput | Promise): Promise; diff --git a/packages/admin-portal/src/services/generated/ivr_emulator_wasm.js b/packages/admin-portal/src/services/generated/ivr_emulator_wasm.js new file mode 100644 index 00000000000..59018decf8d --- /dev/null +++ b/packages/admin-portal/src/services/generated/ivr_emulator_wasm.js @@ -0,0 +1,231 @@ +/* @ts-self-types="./ivr_emulator_wasm.d.ts" */ + +export function init() { + wasm.init(); +} + +export function welcome() { + wasm.welcome(); +} + +function __wbg_get_imports() { + const import0 = { + __proto__: null, + __wbg_error_a6fa202b58aa1cd3: function(arg0, arg1) { + let deferred0_0; + let deferred0_1; + try { + deferred0_0 = arg0; + deferred0_1 = arg1; + console.error(getStringFromWasm0(arg0, arg1)); + } finally { + wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); + } + }, + __wbg_log_b7740bca91d51df8: function(arg0, arg1) { + console.log(getStringFromWasm0(arg0, arg1)); + }, + __wbg_new_227d7c05414eb861: function() { + const ret = new Error(); + return ret; + }, + __wbg_stack_3b0d974bbf31e44f: function(arg0, arg1) { + const ret = arg1.stack; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbindgen_init_externref_table: function() { + const table = wasm.__wbindgen_externrefs; + const offset = table.grow(4); + table.set(0, undefined); + table.set(offset + 0, undefined); + table.set(offset + 1, null); + table.set(offset + 2, true); + table.set(offset + 3, false); + }, + }; + return { + __proto__: null, + "./ivr_emulator_wasm_bg.js": import0, + }; +} + +let cachedDataViewMemory0 = null; +function getDataViewMemory0() { + if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { + cachedDataViewMemory0 = new DataView(wasm.memory.buffer); + } + return cachedDataViewMemory0; +} + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return decodeText(ptr, len); +} + +let cachedUint8ArrayMemory0 = null; +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function passStringToWasm0(arg, malloc, realloc) { + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8ArrayMemory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); + const ret = cachedTextEncoder.encodeInto(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); +cachedTextDecoder.decode(); +const MAX_SAFARI_DECODE_BYTES = 2146435072; +let numBytesDecoded = 0; +function decodeText(ptr, len) { + numBytesDecoded += len; + if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) { + cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + cachedTextDecoder.decode(); + numBytesDecoded = len; + } + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +const cachedTextEncoder = new TextEncoder(); + +if (!('encodeInto' in cachedTextEncoder)) { + cachedTextEncoder.encodeInto = function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; + }; +} + +let WASM_VECTOR_LEN = 0; + +let wasmModule, wasm; +function __wbg_finalize_init(instance, module) { + wasm = instance.exports; + wasmModule = module; + cachedDataViewMemory0 = null; + cachedUint8ArrayMemory0 = null; + wasm.__wbindgen_start(); + return wasm; +} + +async function __wbg_load(module, imports) { + if (typeof Response === 'function' && module instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + return await WebAssembly.instantiateStreaming(module, imports); + } catch (e) { + const validResponse = module.ok && expectedResponseType(module.type); + + if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + + } else { throw e; } + } + } + + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + } else { + return instance; + } + } + + function expectedResponseType(type) { + switch (type) { + case 'basic': case 'cors': case 'default': return true; + } + return false; + } +} + +function initSync(module) { + if (wasm !== undefined) return wasm; + + + if (module !== undefined) { + if (Object.getPrototypeOf(module) === Object.prototype) { + ({module} = module) + } else { + console.warn('using deprecated parameters for `initSync()`; pass a single object instead') + } + } + + const imports = __wbg_get_imports(); + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } + const instance = new WebAssembly.Instance(module, imports); + return __wbg_finalize_init(instance, module); +} + +async function __wbg_init(module_or_path) { + if (wasm !== undefined) return wasm; + + + if (module_or_path !== undefined) { + if (Object.getPrototypeOf(module_or_path) === Object.prototype) { + ({module_or_path} = module_or_path) + } else { + console.warn('using deprecated parameters for the initialization function; pass a single object instead') + } + } + + + const imports = __wbg_get_imports(); + + if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { + module_or_path = fetch(module_or_path); + } + + const { instance, module } = await __wbg_load(await module_or_path, imports); + + return __wbg_finalize_init(instance, module); +} + +export { initSync, __wbg_init as default }; diff --git a/packages/admin-portal/webpack.config.cjs b/packages/admin-portal/webpack.config.cjs index 9149c927776..812ef7918b2 100644 --- a/packages/admin-portal/webpack.config.cjs +++ b/packages/admin-portal/webpack.config.cjs @@ -123,9 +123,16 @@ module.exports = function (env, argv) { }), ], devServer: { - static: { - directory: path.resolve(__dirname, "dist"), - }, + static: [ + {directory: path.resolve(__dirname, "dist")}, + { + directory: path.resolve( + __dirname, + "../../beyond/packages/ivr-emulator-wasm/web/wasm" + ), + publicPath: "/wasm", + }, + ], compress: true, // Enable gzip compression port: 3002, // Run on port 3002 open: true, // Automatically open the browser From b37e8ba92242037d15c88a73122eb8dc71958ca2 Mon Sep 17 00:00:00 2001 From: Enes Selim Date: Fri, 24 Jul 2026 20:03:09 +0300 Subject: [PATCH 02/19] imp: add the initial interface for the emulator --- .../providers/IvrEmulatorContextProvider.tsx | 12 +- .../resources/ElectionEvent/IvrEmulator.tsx | 194 ++++++++++- .../admin-portal/src/services/IvrEmulator.ts | 5 +- .../services/generated/ivr_emulator_wasm.d.ts | 36 +- .../services/generated/ivr_emulator_wasm.js | 326 +++++++++++++++++- 5 files changed, 542 insertions(+), 31 deletions(-) diff --git a/packages/admin-portal/src/providers/IvrEmulatorContextProvider.tsx b/packages/admin-portal/src/providers/IvrEmulatorContextProvider.tsx index 8c882821007..7fbf293213e 100644 --- a/packages/admin-portal/src/providers/IvrEmulatorContextProvider.tsx +++ b/packages/admin-portal/src/providers/IvrEmulatorContextProvider.tsx @@ -1,7 +1,7 @@ import React, {createContext, useContext, useEffect, useState} from "react" import {IvrEmulatorApi, IvrEmulatorError, loadIvrEmulator} from "@/services/IvrEmulator" -export enum IvrStatus { +export enum IvrApiStatus { UNAVAILABLE = "unavailable", LOADING = "loading", READY = "ready", @@ -9,26 +9,26 @@ export enum IvrStatus { } export interface IvrEmulatorContextType { - status: IvrStatus + status: IvrApiStatus api?: IvrEmulatorApi } export const IvrEmulatorContext = createContext(undefined) export const IvrEmulatorContextProvider: React.FC<{children: React.ReactNode}> = ({children}) => { - const [status, setStatus] = useState(IvrStatus.LOADING) + const [status, setStatus] = useState(IvrApiStatus.LOADING) const [api, setApi] = useState(undefined) const url = "/wasm/ivr_emulator_wasm_bg.wasm" useEffect(() => { loadIvrEmulator(url) ?.then((resolvedApi) => { - setStatus(IvrStatus.READY) + setStatus(IvrApiStatus.READY) setApi(resolvedApi) }) .catch((e) => { if (e instanceof IvrEmulatorError && e.operation === "fetch") { - setStatus(IvrStatus.UNAVAILABLE) + setStatus(IvrApiStatus.UNAVAILABLE) } else { - setStatus(IvrStatus.ERROR) + setStatus(IvrApiStatus.ERROR) } }) }, []) diff --git a/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx b/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx index 1e592a683df..b7aa9c3c3a1 100644 --- a/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx +++ b/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx @@ -1,24 +1,194 @@ // SPDX-FileCopyrightText: 2026 Sequent Tech Inc // // SPDX-License-Identifier: AGPL-3.0-only -import React, {useEffect} from "react" +import React, {useMemo, useRef, useState} from "react" import {useRecordContext} from "react-admin" -import {Box, Button} from "@mui/material" +import {Alert, Box, Button, TextField} from "@mui/material" import {useTranslation} from "react-i18next" import {Sequent_Backend_Election_Event} from "@/gql/graphql" import {ElectionHeaderStyles} from "@/components/styles/ElectionHeaderStyles" -import {useIvrEmulator} from "@/providers/IvrEmulatorContextProvider" +import {IvrApiStatus, useIvrEmulator} from "@/providers/IvrEmulatorContextProvider" +import {v4} from "uuid" +import Paper from "@mui/material/Paper" +import {IvrEmulatorApi, Action, IvrEmulatorDriver, PromptInfo} from "@/services/IvrEmulator" +import DialpadIcon from "@mui/icons-material/Dialpad" +import TimerIcon from "@mui/icons-material/Timer" +import {theme} from "@sequentech/ui-essentials" + +type ExpectedInput = Extract +type Status = "Ready" | "Running" | "ExpectingInput" | "Disconnected" + +const newContactId = (): string => { + return v4() +} + +const EmulatorInterface: React.FC<{api: IvrEmulatorApi; onDisconnect?: () => void}> = ({ + api, + onDisconnect, +}) => { + const emulator = useRef(undefined) + const [prompts, setPrompts] = useState([]) + const [expectedInput, setExpectedInput] = useState() + const addPrompt = (prompt: PromptInfo) => { + setPrompts([...prompts, prompt]) + } + const [status, setStatus] = useState("Disconnected") + const [error, setError] = useState("") + const [input, setInput] = useState("") + const [executing, setExecuting] = useState(false) + + const startSession = () => { + if (emulator.current) { + console.warn("Attempted to start another emulator session while there's one running") + return + } + try { + emulator.current = new api.IvrEmulatorDriver("+123", newContactId()) + runEmulator(emulator.current) + setStatus("Ready") + } catch (e) { + console.error("Failed to create the emulator", e) + } + } + + const disposeEmulator = (localEmulator: IvrEmulatorDriver) => { + if (localEmulator === emulator.current) { + emulator.current = undefined + } + localEmulator.free() + } + + const canSendInput = useMemo(() => { + return status === "ExpectingInput" && Boolean(input.trim()) + }, [input, status]) + + const sendTimeout = async () => { + if (emulator.current) { + emulator.current.send_timeout() + runEmulator(emulator.current) + } else { + console.warn("Attempted to sendTimeout while emulator is undefined") + } + } + + const sendInput = async () => { + if (emulator.current) { + emulator.current.send_input(input) + runEmulator(emulator.current) + } else { + console.warn("Attempted to sendInput while emulator is undefined") + } + } + + const executeEmulatorLoop = async (emulator: IvrEmulatorDriver) => { + while (true) { + setStatus("Running") + const action = await emulator.execute(true) + switch (action.type) { + case "Prompt": + addPrompt(action.prompt) + break + case "Noop": + break + case "ExpectInput": + addPrompt(action.prompt) + setExpectedInput(action) + setStatus("ExpectingInput") + return + case "Disconnect": + addPrompt(action.prompt) + setStatus("Disconnected") + disposeEmulator(emulator) + onDisconnect?.() + return + } + } + } + + const runEmulator = (emulator: IvrEmulatorDriver) => { + if (executing) { + return + } + + setExecuting(true) + executeEmulatorLoop(emulator) + .catch((e) => { + console.error("Failed to execute the emulator", e) + setError(`${e}`) + }) + .finally(() => setExecuting(false)) + } + + return ( + + {error ? {error} : null} + {status === "Disconnected" ? Disconnected : null} + + + {prompts.map((prompt) => ( + + {prompt.prompt_text},{prompt.language},{prompt.voice_id} + + ))} + + + + setInput(e.target.value.replace(/[^0-9*#]/g, ""))} + slotProps={{ + htmlInput: {pattern: "[0-9*#]*", maxLength: expectedInput?.max_digits}, + }} + onKeyDown={sendInput} + disabled={!canSendInput} + autoFocus + sx={{fontFamily: "monospace"}} + placeholder={`Enter your input (max digis=${expectedInput?.max_digits}, valid inputs=${expectedInput?.valid_inputs}, timeout=${expectedInput?.timeout}s)`} + /> +
+ + +
+
+ + {status === "Disconnected" ? ( + + ) : null} +
+ ) +} export const IvrEmulator: React.FC = () => { const {t} = useTranslation() const record = useRecordContext() - const {status: emulatorStatus, api} = useIvrEmulator() + const {status: apiStatus, api} = useIvrEmulator() - useEffect(() => { - if (api) { - api.welcome() + const statusMsg = useMemo(() => { + switch (apiStatus) { + case IvrApiStatus.UNAVAILABLE: + return "The IVR emulator is not available in your environment" + case IvrApiStatus.LOADING: + return "Loading..." + case IvrApiStatus.ERROR: + return "Error loading the IVR emulator" + case IvrApiStatus.READY: + return "The emulator is ready" } - }, [api]) + }, [apiStatus]) + if (!record?.id) { return null } @@ -29,12 +199,8 @@ export const IvrEmulator: React.FC = () => { {t("electionEventScreen.ivr.config.infoMsg")} -
`{emulatorStatus}`
- - - - - +
`{statusMsg}`
+ {api ? : null}
) } diff --git a/packages/admin-portal/src/services/IvrEmulator.ts b/packages/admin-portal/src/services/IvrEmulator.ts index 092cac24b21..5f54e990a72 100644 --- a/packages/admin-portal/src/services/IvrEmulator.ts +++ b/packages/admin-portal/src/services/IvrEmulator.ts @@ -1,7 +1,8 @@ -import loadIvrWasm, {welcome} from "./generated/ivr_emulator_wasm" +import loadIvrWasm, {IvrEmulatorDriver} from "./generated/ivr_emulator_wasm" -const api = {welcome} +const api = {IvrEmulatorDriver} export type IvrEmulatorApi = typeof api +export type {IvrEmulatorDriver, Action, PromptInfo} from "./generated/ivr_emulator_wasm" let initPromise: Promise | null = null export class IvrEmulatorError extends Error { diff --git a/packages/admin-portal/src/services/generated/ivr_emulator_wasm.d.ts b/packages/admin-portal/src/services/generated/ivr_emulator_wasm.d.ts index ade4935e61e..d167e2bd418 100644 --- a/packages/admin-portal/src/services/generated/ivr_emulator_wasm.d.ts +++ b/packages/admin-portal/src/services/generated/ivr_emulator_wasm.d.ts @@ -1,20 +1,48 @@ /* tslint:disable */ /* eslint-disable */ +export interface PromptInfo { + prompt_text: string; + language: string; + voice_id: string; +} -export function init(): void; +export type Action = { type: "Prompt"; prompt: PromptInfo } | { type: "ExpectInput"; prompt: PromptInfo; valid_inputs: string; max_digits: number; timeout: number } | { type: "Disconnect"; prompt: PromptInfo } | { type: "Noop" }; + + +export class IvrEmulatorDriver { + free(): void; + [Symbol.dispose](): void; + attributes(): any; + execute(until_io: boolean): Promise; + constructor(caller_number: string, contact_id: string); + send_input(input: string): void; + send_timeout(): void; +} -export function welcome(): void; +export function init(): void; export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; export interface InitOutput { readonly memory: WebAssembly.Memory; + readonly __wbg_ivremulatordriver_free: (a: number, b: number) => void; + readonly ivremulatordriver_attributes: (a: number) => [number, number, number]; + readonly ivremulatordriver_execute: (a: number, b: number) => any; + readonly ivremulatordriver_new: (a: number, b: number, c: number, d: number) => [number, number, number]; + readonly ivremulatordriver_send_input: (a: number, b: number, c: number) => void; + readonly ivremulatordriver_send_timeout: (a: number) => void; readonly init: () => void; - readonly welcome: () => void; - readonly __wbindgen_free: (a: number, b: number, c: number) => void; + readonly ring_core_0_17_14__bn_mul_mont: (a: number, b: number, c: number, d: number, e: number, f: number) => void; + readonly wasm_bindgen__closure__destroy__h2ffbb988ee498a71: (a: number, b: number) => void; + readonly wasm_bindgen__convert__closures_____invoke__h9effed663e3278d7: (a: number, b: number, c: any) => [number, number]; + readonly wasm_bindgen__convert__closures_____invoke__h331bfc0ec3639989: (a: number, b: number, c: any, d: any) => void; readonly __wbindgen_malloc: (a: number, b: number) => number; readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; + readonly __wbindgen_exn_store: (a: number) => void; + readonly __externref_table_alloc: () => number; readonly __wbindgen_externrefs: WebAssembly.Table; + readonly __wbindgen_free: (a: number, b: number, c: number) => void; + readonly __externref_table_dealloc: (a: number) => void; readonly __wbindgen_start: () => void; } diff --git a/packages/admin-portal/src/services/generated/ivr_emulator_wasm.js b/packages/admin-portal/src/services/generated/ivr_emulator_wasm.js index 59018decf8d..e78ef08bc69 100644 --- a/packages/admin-portal/src/services/generated/ivr_emulator_wasm.js +++ b/packages/admin-portal/src/services/generated/ivr_emulator_wasm.js @@ -1,16 +1,114 @@ /* @ts-self-types="./ivr_emulator_wasm.d.ts" */ -export function init() { - wasm.init(); +export class IvrEmulatorDriver { + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + IvrEmulatorDriverFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_ivremulatordriver_free(ptr, 0); + } + /** + * @returns {any} + */ + attributes() { + const ret = wasm.ivremulatordriver_attributes(this.__wbg_ptr); + if (ret[2]) { + throw takeFromExternrefTable0(ret[1]); + } + return takeFromExternrefTable0(ret[0]); + } + /** + * @param {boolean} until_io + * @returns {Action} + */ + execute(until_io) { + const ret = wasm.ivremulatordriver_execute(this.__wbg_ptr, until_io); + return ret; + } + /** + * @param {string} caller_number + * @param {string} contact_id + */ + constructor(caller_number, contact_id) { + const ptr0 = passStringToWasm0(caller_number, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passStringToWasm0(contact_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + const ret = wasm.ivremulatordriver_new(ptr0, len0, ptr1, len1); + if (ret[2]) { + throw takeFromExternrefTable0(ret[1]); + } + this.__wbg_ptr = ret[0] >>> 0; + IvrEmulatorDriverFinalization.register(this, this.__wbg_ptr, this); + return this; + } + /** + * @param {string} input + */ + send_input(input) { + const ptr0 = passStringToWasm0(input, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + wasm.ivremulatordriver_send_input(this.__wbg_ptr, ptr0, len0); + } + send_timeout() { + wasm.ivremulatordriver_send_timeout(this.__wbg_ptr); + } } +if (Symbol.dispose) IvrEmulatorDriver.prototype[Symbol.dispose] = IvrEmulatorDriver.prototype.free; -export function welcome() { - wasm.welcome(); +export function init() { + wasm.init(); } function __wbg_get_imports() { const import0 = { __proto__: null, + __wbg_Error_83742b46f01ce22d: function(arg0, arg1) { + const ret = Error(getStringFromWasm0(arg0, arg1)); + return ret; + }, + __wbg_String_8564e559799eccda: function(arg0, arg1) { + const ret = String(arg1); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg___wbindgen_is_function_3c846841762788c1: function(arg0) { + const ret = typeof(arg0) === 'function'; + return ret; + }, + __wbg___wbindgen_is_object_781bc9f159099513: function(arg0) { + const val = arg0; + const ret = typeof(val) === 'object' && val !== null; + return ret; + }, + __wbg___wbindgen_is_string_7ef6b97b02428fae: function(arg0) { + const ret = typeof(arg0) === 'string'; + return ret; + }, + __wbg___wbindgen_is_undefined_52709e72fb9f179c: function(arg0) { + const ret = arg0 === undefined; + return ret; + }, + __wbg___wbindgen_throw_6ddd609b62940d55: function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }, + __wbg__wbg_cb_unref_6b5b6b8576d35cb1: function(arg0) { + arg0._wbg_cb_unref(); + }, + __wbg_call_2d781c1f4d5c0ef8: function() { return handleError(function (arg0, arg1, arg2) { + const ret = arg0.call(arg1, arg2); + return ret; + }, arguments); }, + __wbg_crypto_38df2bab126b63dc: function(arg0) { + const ret = arg0.crypto; + return ret; + }, __wbg_error_a6fa202b58aa1cd3: function(arg0, arg1) { let deferred0_0; let deferred0_1; @@ -22,13 +120,106 @@ function __wbg_get_imports() { wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); } }, - __wbg_log_b7740bca91d51df8: function(arg0, arg1) { + __wbg_getRandomValues_c44a50d8cfdaebeb: function() { return handleError(function (arg0, arg1) { + arg0.getRandomValues(arg1); + }, arguments); }, + __wbg_getTime_1dad7b5386ddd2d9: function(arg0) { + const ret = arg0.getTime(); + return ret; + }, + __wbg_getTimezoneOffset_639bcf2dde21158b: function(arg0) { + const ret = arg0.getTimezoneOffset(); + return ret; + }, + __wbg_length_ea16607d7b61445b: function(arg0) { + const ret = arg0.length; + return ret; + }, + __wbg_log_82574c09882d9d1e: function(arg0, arg1) { console.log(getStringFromWasm0(arg0, arg1)); }, + __wbg_msCrypto_bd5a034af96bcba6: function(arg0) { + const ret = arg0.msCrypto; + return ret; + }, + __wbg_new_0_1dcafdf5e786e876: function() { + const ret = new Date(); + return ret; + }, __wbg_new_227d7c05414eb861: function() { const ret = new Error(); return ret; }, + __wbg_new_49d5571bd3f0c4d4: function() { + const ret = new Map(); + return ret; + }, + __wbg_new_ab79df5bd7c26067: function() { + const ret = new Object(); + return ret; + }, + __wbg_new_fd94ca5c9639abd2: function(arg0) { + const ret = new Date(arg0); + return ret; + }, + __wbg_new_typed_aaaeaf29cf802876: function(arg0, arg1) { + try { + var state0 = {a: arg0, b: arg1}; + var cb0 = (arg0, arg1) => { + const a = state0.a; + state0.a = 0; + try { + return wasm_bindgen__convert__closures_____invoke__h331bfc0ec3639989(a, state0.b, arg0, arg1); + } finally { + state0.a = a; + } + }; + const ret = new Promise(cb0); + return ret; + } finally { + state0.a = state0.b = 0; + } + }, + __wbg_new_with_length_825018a1616e9e55: function(arg0) { + const ret = new Uint8Array(arg0 >>> 0); + return ret; + }, + __wbg_node_84ea875411254db1: function(arg0) { + const ret = arg0.node; + return ret; + }, + __wbg_process_44c7a14e11e9f69e: function(arg0) { + const ret = arg0.process; + return ret; + }, + __wbg_prototypesetcall_d62e5099504357e6: function(arg0, arg1, arg2) { + Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2); + }, + __wbg_queueMicrotask_0c399741342fb10f: function(arg0) { + const ret = arg0.queueMicrotask; + return ret; + }, + __wbg_queueMicrotask_a082d78ce798393e: function(arg0) { + queueMicrotask(arg0); + }, + __wbg_randomFillSync_6c25eac9869eb53c: function() { return handleError(function (arg0, arg1) { + arg0.randomFillSync(arg1); + }, arguments); }, + __wbg_require_b4edbdcf3e2a1ef0: function() { return handleError(function () { + const ret = module.require; + return ret; + }, arguments); }, + __wbg_resolve_ae8d83246e5bcc12: function(arg0) { + const ret = Promise.resolve(arg0); + return ret; + }, + __wbg_set_6be42768c690e380: function(arg0, arg1, arg2) { + arg0[arg1] = arg2; + }, + __wbg_set_bf7251625df30a02: function(arg0, arg1, arg2) { + const ret = arg0.set(arg1, arg2); + return ret; + }, __wbg_stack_3b0d974bbf31e44f: function(arg0, arg1) { const ret = arg1.stack; const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); @@ -36,6 +227,54 @@ function __wbg_get_imports() { getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); }, + __wbg_static_accessor_GLOBAL_8adb955bd33fac2f: function() { + const ret = typeof global === 'undefined' ? null : global; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, + __wbg_static_accessor_GLOBAL_THIS_ad356e0db91c7913: function() { + const ret = typeof globalThis === 'undefined' ? null : globalThis; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, + __wbg_static_accessor_SELF_f207c857566db248: function() { + const ret = typeof self === 'undefined' ? null : self; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, + __wbg_static_accessor_WINDOW_bb9f1ba69d61b386: function() { + const ret = typeof window === 'undefined' ? null : window; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, + __wbg_subarray_a068d24e39478a8a: function(arg0, arg1, arg2) { + const ret = arg0.subarray(arg1 >>> 0, arg2 >>> 0); + return ret; + }, + __wbg_then_098abe61755d12f6: function(arg0, arg1) { + const ret = arg0.then(arg1); + return ret; + }, + __wbg_versions_276b2795b1c6a219: function(arg0) { + const ret = arg0.versions; + return ret; + }, + __wbindgen_cast_0000000000000001: function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { dtor_idx: 72, function: Function { arguments: [Externref], shim_idx: 73, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen__closure__destroy__h2ffbb988ee498a71, wasm_bindgen__convert__closures_____invoke__h9effed663e3278d7); + return ret; + }, + __wbindgen_cast_0000000000000002: function(arg0) { + // Cast intrinsic for `F64 -> Externref`. + const ret = arg0; + return ret; + }, + __wbindgen_cast_0000000000000003: function(arg0, arg1) { + // Cast intrinsic for `Ref(Slice(U8)) -> NamedExternref("Uint8Array")`. + const ret = getArrayU8FromWasm0(arg0, arg1); + return ret; + }, + __wbindgen_cast_0000000000000004: function(arg0, arg1) { + // Cast intrinsic for `Ref(String) -> Externref`. + const ret = getStringFromWasm0(arg0, arg1); + return ret; + }, __wbindgen_init_externref_table: function() { const table = wasm.__wbindgen_externrefs; const offset = table.grow(4); @@ -52,6 +291,36 @@ function __wbg_get_imports() { }; } +function wasm_bindgen__convert__closures_____invoke__h9effed663e3278d7(arg0, arg1, arg2) { + const ret = wasm.wasm_bindgen__convert__closures_____invoke__h9effed663e3278d7(arg0, arg1, arg2); + if (ret[1]) { + throw takeFromExternrefTable0(ret[0]); + } +} + +function wasm_bindgen__convert__closures_____invoke__h331bfc0ec3639989(arg0, arg1, arg2, arg3) { + wasm.wasm_bindgen__convert__closures_____invoke__h331bfc0ec3639989(arg0, arg1, arg2, arg3); +} + +const IvrEmulatorDriverFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_ivremulatordriver_free(ptr >>> 0, 1)); + +function addToExternrefTable0(obj) { + const idx = wasm.__externref_table_alloc(); + wasm.__wbindgen_externrefs.set(idx, obj); + return idx; +} + +const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(state => state.dtor(state.a, state.b)); + +function getArrayU8FromWasm0(ptr, len) { + ptr = ptr >>> 0; + return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len); +} + let cachedDataViewMemory0 = null; function getDataViewMemory0() { if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { @@ -73,6 +342,47 @@ function getUint8ArrayMemory0() { return cachedUint8ArrayMemory0; } +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + const idx = addToExternrefTable0(e); + wasm.__wbindgen_exn_store(idx); + } +} + +function isLikeNone(x) { + return x === undefined || x === null; +} + +function makeMutClosure(arg0, arg1, dtor, f) { + const state = { a: arg0, b: arg1, cnt: 1, dtor }; + const real = (...args) => { + + // First up with a closure we increment the internal reference + // count. This ensures that the Rust closure environment won't + // be deallocated while we're invoking it. + state.cnt++; + const a = state.a; + state.a = 0; + try { + return f(a, state.b, ...args); + } finally { + state.a = a; + real._wbg_cb_unref(); + } + }; + real._wbg_cb_unref = () => { + if (--state.cnt === 0) { + state.dtor(state.a, state.b); + state.a = 0; + CLOSURE_DTORS.unregister(state); + } + }; + CLOSURE_DTORS.register(real, state, state); + return real; +} + function passStringToWasm0(arg, malloc, realloc) { if (realloc === undefined) { const buf = cachedTextEncoder.encode(arg); @@ -110,6 +420,12 @@ function passStringToWasm0(arg, malloc, realloc) { return ptr; } +function takeFromExternrefTable0(idx) { + const value = wasm.__wbindgen_externrefs.get(idx); + wasm.__externref_table_dealloc(idx); + return value; +} + let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); cachedTextDecoder.decode(); const MAX_SAFARI_DECODE_BYTES = 2146435072; From 4e67601c3ed9f3154ebf114af2c8286886daab67 Mon Sep 17 00:00:00 2001 From: Enes Selim Date: Mon, 27 Jul 2026 13:58:20 +0300 Subject: [PATCH 03/19] imp: enable config generation and emulator lifecycle management --- .../resources/ElectionEvent/IvrEmulator.tsx | 306 +++++++++-- .../admin-portal/src/services/IvrEmulator.ts | 7 +- .../services/generated/ivr_emulator_wasm.d.ts | 40 +- .../services/generated/ivr_emulator_wasm.js | 475 ++++++++++++++---- 4 files changed, 677 insertions(+), 151 deletions(-) diff --git a/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx b/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx index b7aa9c3c3a1..37e9b324920 100644 --- a/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx +++ b/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx @@ -1,33 +1,194 @@ // SPDX-FileCopyrightText: 2026 Sequent Tech Inc // // SPDX-License-Identifier: AGPL-3.0-only -import React, {useMemo, useRef, useState} from "react" -import {useRecordContext} from "react-admin" -import {Alert, Box, Button, TextField} from "@mui/material" +import React, {useEffect, useMemo, useRef, useState} from "react" +import {BooleanInput, useGetList, useRecordContext} from "react-admin" +import {FormProvider, useForm} from "react-hook-form" +import {Alert, Box, Button, Stack, TextField} from "@mui/material" import {useTranslation} from "react-i18next" -import {Sequent_Backend_Election_Event} from "@/gql/graphql" +import { + Sequent_Backend_Ballot_Style, + Sequent_Backend_Election, + Sequent_Backend_Election_Event, +} from "@/gql/graphql" import {ElectionHeaderStyles} from "@/components/styles/ElectionHeaderStyles" import {IvrApiStatus, useIvrEmulator} from "@/providers/IvrEmulatorContextProvider" import {v4} from "uuid" import Paper from "@mui/material/Paper" -import {IvrEmulatorApi, Action, IvrEmulatorDriver, PromptInfo} from "@/services/IvrEmulator" +import { + IvrEmulatorApi, + Action, + IvrEmulatorDriver, + PromptInfo, + EmulatorConfig, +} from "@/services/IvrEmulator" import DialpadIcon from "@mui/icons-material/Dialpad" import TimerIcon from "@mui/icons-material/Timer" import {theme} from "@sequentech/ui-essentials" +import SelectArea from "@/components/area/SelectArea" +import {useFormContext, useWatch} from "react-hook-form" +import {useAliasRenderer} from "@/hooks/useAliasRenderer" +import {FormStyles} from "@/components/styles/FormStyles" type ExpectedInput = Extract type Status = "Ready" | "Running" | "ExpectingInput" | "Disconnected" -const newContactId = (): string => { - return v4() +const CALLER_NUMBER = "+1234567890" + +const generateContactId = (): string => v4() + +type ConfigFormValues = { + areaId: string + electionIds: string[] + blacklistCaller: boolean } -const EmulatorInterface: React.FC<{api: IvrEmulatorApi; onDisconnect?: () => void}> = ({ - api, - onDisconnect, -}) => { - const emulator = useRef(undefined) +const ConfigFormBody: React.FC<{ + electionEvent: Sequent_Backend_Election_Event + onStartSession: (config: EmulatorConfig) => void +}> = ({electionEvent, onStartSession}) => { + const aliasRenderer = useAliasRenderer() + const {control, getValues} = useFormContext() + + const areaId = useWatch({control, name: "areaId"}) + const electionIds = useWatch({control, name: "electionIds"}) + + const {data: rawBallotStyles} = useGetList( + "sequent_backend_ballot_style", + { + pagination: {page: 1, perPage: 300}, + // Sort in ascending order so newer styles overwrite older ones + // when constructing the map. + sort: {field: "created_at", order: "ASC"}, + filter: { + "tenant_id": electionEvent.tenant_id, + "election_event_id": electionEvent.id, + "area_id": areaId, + "deleted_at@_is_null": null, + }, + }, + { + enabled: !!areaId, + } + ) + + const availableBallotStyles = useMemo>(() => { + return new Map( + rawBallotStyles?.flatMap((style) => { + if ( + style.election_id && + typeof style.election_id === "string" && + style.ballot_eml + ) { + return [[style.election_id, style.ballot_eml]] + } else { + return [] + } + }) ?? [] + ) + }, [rawBallotStyles]) + + const {data: rawElections} = useGetList( + "sequent_backend_election", + { + pagination: {page: 1, perPage: 300}, + sort: {field: "name", order: "DESC"}, + filter: { + "tenant_id": electionEvent.tenant_id, + "election_event_id": electionEvent.id, + "id@_in": Array.from(availableBallotStyles.keys()), + }, + }, + { + enabled: availableBallotStyles.size > 0, + } + ) + + const electionChoices = useMemo<{id: string; name: string}[]>( + () => + rawElections + ?.map((e) => ({ + id: e.id, + name: aliasRenderer(e), + })) + .sort((a, b) => a.name.localeCompare(b.name)) ?? [], + [rawElections] + ) + + const selectedBallotStyles = useMemo>(() => { + let filtered = availableBallotStyles + .entries() + .filter(([electionId, _style]) => electionIds.includes(electionId)) + return new Map(filtered) + }, [availableBallotStyles, electionIds]) + + const generateConfig = (data: ConfigFormValues): EmulatorConfig => { + return { + tenant_id: electionEvent.tenant_id, + election_event_id: electionEvent.id, + caller_number: CALLER_NUMBER, + contact_id: generateContactId(), + blacklisted_numbers: data.blacklistCaller ? [CALLER_NUMBER] : [], + open_elections: Array.from(selectedBallotStyles.keys()), + election_event: JSON.stringify(electionEvent), + ballot_styles: Array.from(selectedBallotStyles.values()), + } + } + + const onSubmit = () => { + let config = generateConfig(getValues()) + onStartSession(config) + } + + return ( + + + + + + Resolved {selectedBallotStyles.size} ballot styles for the selected area and + elections + + + + ) +} + +const ConfigForm: React.FC<{ + electionEvent: Sequent_Backend_Election_Event + onStartSession: (config: EmulatorConfig) => void +}> = ({electionEvent, onStartSession}) => { + const methods = useForm({ + defaultValues: {areaId: "", electionIds: [], blacklistCaller: false}, + }) + + return ( + +
+ + +
+ ) +} + +const EmulatorInterface: React.FC<{ + api: IvrEmulatorApi + config: EmulatorConfig + onStatusChange?: (status: Status) => void +}> = ({api, config, onStatusChange}) => { const [prompts, setPrompts] = useState([]) + const emulator = useRef(undefined) const [expectedInput, setExpectedInput] = useState() const addPrompt = (prompt: PromptInfo) => { setPrompts([...prompts, prompt]) @@ -37,52 +198,61 @@ const EmulatorInterface: React.FC<{api: IvrEmulatorApi; onDisconnect?: () => voi const [input, setInput] = useState("") const [executing, setExecuting] = useState(false) - const startSession = () => { + const canSendInput = useMemo( + () => status === "ExpectingInput" && Boolean(input.trim()), + [input, status] + ) + + const changeStatus = (value: Status): void => { + setStatus(value) + onStatusChange?.(value) + } + + const startSession = (config: EmulatorConfig): void => { if (emulator.current) { - console.warn("Attempted to start another emulator session while there's one running") + console.warn("Refusing to start another emulator session while there's one running") return } + try { - emulator.current = new api.IvrEmulatorDriver("+123", newContactId()) + console.log("Creating a new session with config", config) + emulator.current = new api.IvrEmulatorDriver(config) runEmulator(emulator.current) - setStatus("Ready") } catch (e) { console.error("Failed to create the emulator", e) } } - const disposeEmulator = (localEmulator: IvrEmulatorDriver) => { - if (localEmulator === emulator.current) { - emulator.current = undefined + const sendTimeout = async () => { + if (!emulator.current) { + console.warn("Attempted to sendTimeout without an active emulator") + return } - localEmulator.free() + emulator.current.send_timeout() + runEmulator(emulator.current) } - const canSendInput = useMemo(() => { - return status === "ExpectingInput" && Boolean(input.trim()) - }, [input, status]) - - const sendTimeout = async () => { - if (emulator.current) { - emulator.current.send_timeout() - runEmulator(emulator.current) - } else { - console.warn("Attempted to sendTimeout while emulator is undefined") + const sendInput = async () => { + if (!emulator.current) { + console.warn("Attempted to sendInput without an active emulator") + return } + emulator.current.send_input(input) + runEmulator(emulator.current) } - const sendInput = async () => { - if (emulator.current) { - emulator.current.send_input(input) - runEmulator(emulator.current) - } else { - console.warn("Attempted to sendInput while emulator is undefined") + const disposeEmulator = (disposed: IvrEmulatorDriver): void => { + console.info("Disposing the emulator") + if (emulator.current === disposed) { + emulator.current = undefined + console.info("Cleared current emulator") } + disposed.free() } const executeEmulatorLoop = async (emulator: IvrEmulatorDriver) => { while (true) { - setStatus("Running") + changeStatus("Running") const action = await emulator.execute(true) switch (action.type) { case "Prompt": @@ -93,13 +263,12 @@ const EmulatorInterface: React.FC<{api: IvrEmulatorApi; onDisconnect?: () => voi case "ExpectInput": addPrompt(action.prompt) setExpectedInput(action) - setStatus("ExpectingInput") + changeStatus("ExpectingInput") return case "Disconnect": addPrompt(action.prompt) - setStatus("Disconnected") + changeStatus("Disconnected") disposeEmulator(emulator) - onDisconnect?.() return } } @@ -119,6 +288,11 @@ const EmulatorInterface: React.FC<{api: IvrEmulatorApi; onDisconnect?: () => voi .finally(() => setExecuting(false)) } + useEffect(() => { + setStatus("Ready") + startSession(config) + }, []) + return ( {error ? {error} : null} @@ -143,7 +317,7 @@ const EmulatorInterface: React.FC<{api: IvrEmulatorApi; onDisconnect?: () => voi disabled={!canSendInput} autoFocus sx={{fontFamily: "monospace"}} - placeholder={`Enter your input (max digis=${expectedInput?.max_digits}, valid inputs=${expectedInput?.valid_inputs}, timeout=${expectedInput?.timeout}s)`} + placeholder={`Enter your input (max digis=${expectedInput?.max_digits ?? ""}, valid inputs=${expectedInput?.valid_inputs ?? ""}, timeout=${expectedInput?.timeout ?? 0}s)`} />
voi
- - {status === "Disconnected" ? ( - - ) : null} ) } @@ -175,17 +343,23 @@ export const IvrEmulator: React.FC = () => { const {t} = useTranslation() const record = useRecordContext() const {status: apiStatus, api} = useIvrEmulator() + const [emulatorStatus, setEmulatorStatus] = useState(undefined) + const [config, setConfig] = useState(undefined) - const statusMsg = useMemo(() => { + const statusAlert = useMemo(() => { switch (apiStatus) { case IvrApiStatus.UNAVAILABLE: - return "The IVR emulator is not available in your environment" + return ( + + The emulator system is not available in your environment + + ) case IvrApiStatus.LOADING: - return "Loading..." + return Loading the emulator system... case IvrApiStatus.ERROR: - return "Error loading the IVR emulator" + return Error loading the emulator case IvrApiStatus.READY: - return "The emulator is ready" + return The emulator system is loaded and ready } }, [apiStatus]) @@ -193,14 +367,36 @@ export const IvrEmulator: React.FC = () => { return null } + const onStartSession = (config: EmulatorConfig): void => { + setEmulatorStatus("Ready") + setConfig(config) + } + return ( - {t("electionEventScreen.ivr.config.infoMsg")} + Select an area and desired elections to experience the IVR session. -
`{statusMsg}`
- {api ? : null} +
+ {statusAlert} + {record && !emulatorStatus ? ( + + ) : null} + {api && config ? ( +
+ + + +
+ ) : null} +
) } diff --git a/packages/admin-portal/src/services/IvrEmulator.ts b/packages/admin-portal/src/services/IvrEmulator.ts index 5f54e990a72..120e254d56f 100644 --- a/packages/admin-portal/src/services/IvrEmulator.ts +++ b/packages/admin-portal/src/services/IvrEmulator.ts @@ -2,7 +2,12 @@ import loadIvrWasm, {IvrEmulatorDriver} from "./generated/ivr_emulator_wasm" const api = {IvrEmulatorDriver} export type IvrEmulatorApi = typeof api -export type {IvrEmulatorDriver, Action, PromptInfo} from "./generated/ivr_emulator_wasm" +export type { + IvrEmulatorDriver, + Action, + PromptInfo, + EmulatorConfig, +} from "./generated/ivr_emulator_wasm" let initPromise: Promise | null = null export class IvrEmulatorError extends Error { diff --git a/packages/admin-portal/src/services/generated/ivr_emulator_wasm.d.ts b/packages/admin-portal/src/services/generated/ivr_emulator_wasm.d.ts index d167e2bd418..58a8ba38c9d 100644 --- a/packages/admin-portal/src/services/generated/ivr_emulator_wasm.d.ts +++ b/packages/admin-portal/src/services/generated/ivr_emulator_wasm.d.ts @@ -1,5 +1,31 @@ /* tslint:disable */ /* eslint-disable */ +export interface EmulatorConfig { + /** + * E164 number (+132...) of the caller + */ + caller_number: string; + contact_id: string; + tenant_id: string; + election_event_id: string; + /** + * Jsonified election event. + */ + election_event: string; + /** + * Jsonified `ballot_eml`s + */ + ballot_styles: string[]; + /** + * `ElectionId`s + */ + open_elections: string[]; + /** + * E164 numbers (+132..) + */ + blacklisted_numbers: string[]; +} + export interface PromptInfo { prompt_text: string; language: string; @@ -14,7 +40,7 @@ export class IvrEmulatorDriver { [Symbol.dispose](): void; attributes(): any; execute(until_io: boolean): Promise; - constructor(caller_number: string, contact_id: string); + constructor(config: EmulatorConfig); send_input(input: string): void; send_timeout(): void; } @@ -25,22 +51,24 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl export interface InitOutput { readonly memory: WebAssembly.Memory; + readonly init: () => void; readonly __wbg_ivremulatordriver_free: (a: number, b: number) => void; readonly ivremulatordriver_attributes: (a: number) => [number, number, number]; readonly ivremulatordriver_execute: (a: number, b: number) => any; - readonly ivremulatordriver_new: (a: number, b: number, c: number, d: number) => [number, number, number]; + readonly ivremulatordriver_new: (a: any) => [number, number, number]; readonly ivremulatordriver_send_input: (a: number, b: number, c: number) => void; readonly ivremulatordriver_send_timeout: (a: number) => void; - readonly init: () => void; readonly ring_core_0_17_14__bn_mul_mont: (a: number, b: number, c: number, d: number, e: number, f: number) => void; - readonly wasm_bindgen__closure__destroy__h2ffbb988ee498a71: (a: number, b: number) => void; - readonly wasm_bindgen__convert__closures_____invoke__h9effed663e3278d7: (a: number, b: number, c: any) => [number, number]; - readonly wasm_bindgen__convert__closures_____invoke__h331bfc0ec3639989: (a: number, b: number, c: any, d: any) => void; + readonly wasm_bindgen__closure__destroy__hdb4b145fc5c0ff94: (a: number, b: number) => void; + readonly wasm_bindgen__convert__closures_____invoke__ha2bc130ecf8461d3: (a: number, b: number, c: any) => [number, number]; + readonly wasm_bindgen__convert__closures_____invoke__hfcaf3c30771b308a: (a: number, b: number, c: any, d: any) => void; + readonly wasm_bindgen__convert__closures_____invoke__h563360c8d8d07198: (a: number, b: number) => number; readonly __wbindgen_malloc: (a: number, b: number) => number; readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; readonly __wbindgen_exn_store: (a: number) => void; readonly __externref_table_alloc: () => number; readonly __wbindgen_externrefs: WebAssembly.Table; + readonly __externref_drop_slice: (a: number, b: number) => void; readonly __wbindgen_free: (a: number, b: number, c: number) => void; readonly __externref_table_dealloc: (a: number) => void; readonly __wbindgen_start: () => void; diff --git a/packages/admin-portal/src/services/generated/ivr_emulator_wasm.js b/packages/admin-portal/src/services/generated/ivr_emulator_wasm.js index e78ef08bc69..b5c6e7889be 100644 --- a/packages/admin-portal/src/services/generated/ivr_emulator_wasm.js +++ b/packages/admin-portal/src/services/generated/ivr_emulator_wasm.js @@ -1,5 +1,7 @@ /* @ts-self-types="./ivr_emulator_wasm.d.ts" */ +//#region exports + export class IvrEmulatorDriver { __destroy_into_raw() { const ptr = this.__wbg_ptr; @@ -15,6 +17,8 @@ export class IvrEmulatorDriver { * @returns {any} */ attributes() { + if (this.__wbg_ptr == 0) throw new Error('Attempt to use a moved value'); + _assertNum(this.__wbg_ptr); const ret = wasm.ivremulatordriver_attributes(this.__wbg_ptr); if (ret[2]) { throw takeFromExternrefTable0(ret[1]); @@ -26,19 +30,17 @@ export class IvrEmulatorDriver { * @returns {Action} */ execute(until_io) { + if (this.__wbg_ptr == 0) throw new Error('Attempt to use a moved value'); + _assertNum(this.__wbg_ptr); + _assertBoolean(until_io); const ret = wasm.ivremulatordriver_execute(this.__wbg_ptr, until_io); return ret; } /** - * @param {string} caller_number - * @param {string} contact_id + * @param {EmulatorConfig} config */ - constructor(caller_number, contact_id) { - const ptr0 = passStringToWasm0(caller_number, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - const ptr1 = passStringToWasm0(contact_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - const ret = wasm.ivremulatordriver_new(ptr0, len0, ptr1, len1); + constructor(config) { + const ret = wasm.ivremulatordriver_new(config); if (ret[2]) { throw takeFromExternrefTable0(ret[1]); } @@ -50,11 +52,15 @@ export class IvrEmulatorDriver { * @param {string} input */ send_input(input) { + if (this.__wbg_ptr == 0) throw new Error('Attempt to use a moved value'); + _assertNum(this.__wbg_ptr); const ptr0 = passStringToWasm0(input, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len0 = WASM_VECTOR_LEN; wasm.ivremulatordriver_send_input(this.__wbg_ptr, ptr0, len0); } send_timeout() { + if (this.__wbg_ptr == 0) throw new Error('Attempt to use a moved value'); + _assertNum(this.__wbg_ptr); wasm.ivremulatordriver_send_timeout(this.__wbg_ptr); } } @@ -64,52 +70,125 @@ export function init() { wasm.init(); } +//#endregion + +//#region wasm imports + function __wbg_get_imports() { const import0 = { __proto__: null, - __wbg_Error_83742b46f01ce22d: function(arg0, arg1) { + __wbg_Error_83742b46f01ce22d: function() { return logError(function (arg0, arg1) { const ret = Error(getStringFromWasm0(arg0, arg1)); return ret; - }, - __wbg_String_8564e559799eccda: function(arg0, arg1) { + }, arguments); }, + __wbg_String_8564e559799eccda: function() { return logError(function (arg0, arg1) { const ret = String(arg1); const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len1 = WASM_VECTOR_LEN; getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, arguments); }, + __wbg___wbindgen_boolean_get_c0f3f60bac5a78d1: function(arg0) { + const v = arg0; + const ret = typeof(v) === 'boolean' ? v : undefined; + if (!isLikeNone(ret)) { + _assertBoolean(ret); + } + return isLikeNone(ret) ? 0xFFFFFF : ret ? 1 : 0; + }, + __wbg___wbindgen_debug_string_5398f5bb970e0daa: function(arg0, arg1) { + const ret = debugString(arg1); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg___wbindgen_in_41dbb8413020e076: function(arg0, arg1) { + const ret = arg0 in arg1; + _assertBoolean(ret); + return ret; }, __wbg___wbindgen_is_function_3c846841762788c1: function(arg0) { const ret = typeof(arg0) === 'function'; + _assertBoolean(ret); return ret; }, __wbg___wbindgen_is_object_781bc9f159099513: function(arg0) { const val = arg0; const ret = typeof(val) === 'object' && val !== null; + _assertBoolean(ret); return ret; }, __wbg___wbindgen_is_string_7ef6b97b02428fae: function(arg0) { const ret = typeof(arg0) === 'string'; + _assertBoolean(ret); return ret; }, __wbg___wbindgen_is_undefined_52709e72fb9f179c: function(arg0) { const ret = arg0 === undefined; + _assertBoolean(ret); return ret; }, + __wbg___wbindgen_jsval_loose_eq_5bcc3bed3c69e72b: function(arg0, arg1) { + const ret = arg0 == arg1; + _assertBoolean(ret); + return ret; + }, + __wbg___wbindgen_number_get_34bb9d9dcfa21373: function(arg0, arg1) { + const obj = arg1; + const ret = typeof(obj) === 'number' ? obj : undefined; + if (!isLikeNone(ret)) { + _assertNum(ret); + } + getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true); + }, + __wbg___wbindgen_string_get_395e606bd0ee4427: function(arg0, arg1) { + const obj = arg1; + const ret = typeof(obj) === 'string' ? obj : undefined; + var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, __wbg___wbindgen_throw_6ddd609b62940d55: function(arg0, arg1) { throw new Error(getStringFromWasm0(arg0, arg1)); }, - __wbg__wbg_cb_unref_6b5b6b8576d35cb1: function(arg0) { + __wbg__wbg_cb_unref_6b5b6b8576d35cb1: function() { return logError(function (arg0) { arg0._wbg_cb_unref(); - }, + }, arguments); }, __wbg_call_2d781c1f4d5c0ef8: function() { return handleError(function (arg0, arg1, arg2) { const ret = arg0.call(arg1, arg2); return ret; }, arguments); }, - __wbg_crypto_38df2bab126b63dc: function(arg0) { + __wbg_call_e133b57c9155d22c: function() { return handleError(function (arg0, arg1) { + const ret = arg0.call(arg1); + return ret; + }, arguments); }, + __wbg_createTask_6eb3a8b6dd2f87c9: function() { return handleError(function (arg0, arg1) { + const ret = console.createTask(getStringFromWasm0(arg0, arg1)); + return ret; + }, arguments); }, + __wbg_crypto_38df2bab126b63dc: function() { return logError(function (arg0) { const ret = arg0.crypto; return ret; - }, - __wbg_error_a6fa202b58aa1cd3: function(arg0, arg1) { + }, arguments); }, + __wbg_debug_eaef3b49d572d680: function() { return logError(function (arg0, arg1) { + var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice(); + wasm.__wbindgen_free(arg0, arg1 * 4, 4); + console.debug(...v0); + }, arguments); }, + __wbg_done_08ce71ee07e3bd17: function() { return logError(function (arg0) { + const ret = arg0.done; + _assertBoolean(ret); + return ret; + }, arguments); }, + __wbg_error_71b0e71161a5f3a0: function() { return logError(function (arg0, arg1) { + var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice(); + wasm.__wbindgen_free(arg0, arg1 * 4, 4); + console.error(...v0); + }, arguments); }, + __wbg_error_a6fa202b58aa1cd3: function() { return logError(function (arg0, arg1) { let deferred0_0; let deferred0_1; try { @@ -119,57 +198,115 @@ function __wbg_get_imports() { } finally { wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); } - }, + }, arguments); }, __wbg_getRandomValues_c44a50d8cfdaebeb: function() { return handleError(function (arg0, arg1) { arg0.getRandomValues(arg1); }, arguments); }, - __wbg_getTime_1dad7b5386ddd2d9: function(arg0) { + __wbg_getTime_1dad7b5386ddd2d9: function() { return logError(function (arg0) { const ret = arg0.getTime(); return ret; - }, - __wbg_getTimezoneOffset_639bcf2dde21158b: function(arg0) { + }, arguments); }, + __wbg_getTimezoneOffset_639bcf2dde21158b: function() { return logError(function (arg0) { const ret = arg0.getTimezoneOffset(); return ret; - }, - __wbg_length_ea16607d7b61445b: function(arg0) { + }, arguments); }, + __wbg_get_326e41e095fb2575: function() { return handleError(function (arg0, arg1) { + const ret = Reflect.get(arg0, arg1); + return ret; + }, arguments); }, + __wbg_get_unchecked_329cfe50afab7352: function() { return logError(function (arg0, arg1) { + const ret = arg0[arg1 >>> 0]; + return ret; + }, arguments); }, + __wbg_get_with_ref_key_6412cf3094599694: function() { return logError(function (arg0, arg1) { + const ret = arg0[arg1]; + return ret; + }, arguments); }, + __wbg_instanceof_ArrayBuffer_101e2bf31071a9f6: function() { return logError(function (arg0) { + let result; + try { + result = arg0 instanceof ArrayBuffer; + } catch (_) { + result = false; + } + const ret = result; + _assertBoolean(ret); + return ret; + }, arguments); }, + __wbg_instanceof_Uint8Array_740438561a5b956d: function() { return logError(function (arg0) { + let result; + try { + result = arg0 instanceof Uint8Array; + } catch (_) { + result = false; + } + const ret = result; + _assertBoolean(ret); + return ret; + }, arguments); }, + __wbg_isArray_33b91feb269ff46e: function() { return logError(function (arg0) { + const ret = Array.isArray(arg0); + _assertBoolean(ret); + return ret; + }, arguments); }, + __wbg_iterator_d8f549ec8fb061b1: function() { return logError(function () { + const ret = Symbol.iterator; + return ret; + }, arguments); }, + __wbg_length_b3416cf66a5452c8: function() { return logError(function (arg0) { const ret = arg0.length; + _assertNum(ret); return ret; - }, - __wbg_log_82574c09882d9d1e: function(arg0, arg1) { + }, arguments); }, + __wbg_length_ea16607d7b61445b: function() { return logError(function (arg0) { + const ret = arg0.length; + _assertNum(ret); + return ret; + }, arguments); }, + __wbg_log_7a0760e115750083: function() { return logError(function (arg0, arg1) { + var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice(); + wasm.__wbindgen_free(arg0, arg1 * 4, 4); + console.log(...v0); + }, arguments); }, + __wbg_log_82574c09882d9d1e: function() { return logError(function (arg0, arg1) { console.log(getStringFromWasm0(arg0, arg1)); - }, - __wbg_msCrypto_bd5a034af96bcba6: function(arg0) { + }, arguments); }, + __wbg_msCrypto_bd5a034af96bcba6: function() { return logError(function (arg0) { const ret = arg0.msCrypto; return ret; - }, - __wbg_new_0_1dcafdf5e786e876: function() { + }, arguments); }, + __wbg_new_0_1dcafdf5e786e876: function() { return logError(function () { const ret = new Date(); return ret; - }, - __wbg_new_227d7c05414eb861: function() { + }, arguments); }, + __wbg_new_227d7c05414eb861: function() { return logError(function () { const ret = new Error(); return ret; - }, - __wbg_new_49d5571bd3f0c4d4: function() { + }, arguments); }, + __wbg_new_49d5571bd3f0c4d4: function() { return logError(function () { const ret = new Map(); return ret; - }, - __wbg_new_ab79df5bd7c26067: function() { + }, arguments); }, + __wbg_new_5f486cdf45a04d78: function() { return logError(function (arg0) { + const ret = new Uint8Array(arg0); + return ret; + }, arguments); }, + __wbg_new_ab79df5bd7c26067: function() { return logError(function () { const ret = new Object(); return ret; - }, - __wbg_new_fd94ca5c9639abd2: function(arg0) { + }, arguments); }, + __wbg_new_fd94ca5c9639abd2: function() { return logError(function (arg0) { const ret = new Date(arg0); return ret; - }, - __wbg_new_typed_aaaeaf29cf802876: function(arg0, arg1) { + }, arguments); }, + __wbg_new_typed_aaaeaf29cf802876: function() { return logError(function (arg0, arg1) { try { var state0 = {a: arg0, b: arg1}; var cb0 = (arg0, arg1) => { const a = state0.a; state0.a = 0; try { - return wasm_bindgen__convert__closures_____invoke__h331bfc0ec3639989(a, state0.b, arg0, arg1); + return wasm_bindgen__convert__closures_____invoke__hfcaf3c30771b308a(a, state0.b, arg0, arg1); } finally { state0.a = a; } @@ -179,29 +316,37 @@ function __wbg_get_imports() { } finally { state0.a = state0.b = 0; } - }, - __wbg_new_with_length_825018a1616e9e55: function(arg0) { + }, arguments); }, + __wbg_new_with_length_825018a1616e9e55: function() { return logError(function (arg0) { const ret = new Uint8Array(arg0 >>> 0); return ret; - }, - __wbg_node_84ea875411254db1: function(arg0) { + }, arguments); }, + __wbg_next_11b99ee6237339e3: function() { return handleError(function (arg0) { + const ret = arg0.next(); + return ret; + }, arguments); }, + __wbg_next_e01a967809d1aa68: function() { return logError(function (arg0) { + const ret = arg0.next; + return ret; + }, arguments); }, + __wbg_node_84ea875411254db1: function() { return logError(function (arg0) { const ret = arg0.node; return ret; - }, - __wbg_process_44c7a14e11e9f69e: function(arg0) { + }, arguments); }, + __wbg_process_44c7a14e11e9f69e: function() { return logError(function (arg0) { const ret = arg0.process; return ret; - }, - __wbg_prototypesetcall_d62e5099504357e6: function(arg0, arg1, arg2) { + }, arguments); }, + __wbg_prototypesetcall_d62e5099504357e6: function() { return logError(function (arg0, arg1, arg2) { Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2); - }, - __wbg_queueMicrotask_0c399741342fb10f: function(arg0) { + }, arguments); }, + __wbg_queueMicrotask_0c399741342fb10f: function() { return logError(function (arg0) { const ret = arg0.queueMicrotask; return ret; - }, - __wbg_queueMicrotask_a082d78ce798393e: function(arg0) { + }, arguments); }, + __wbg_queueMicrotask_a082d78ce798393e: function() { return logError(function (arg0) { queueMicrotask(arg0); - }, + }, arguments); }, __wbg_randomFillSync_6c25eac9869eb53c: function() { return handleError(function (arg0, arg1) { arg0.randomFillSync(arg1); }, arguments); }, @@ -209,72 +354,100 @@ function __wbg_get_imports() { const ret = module.require; return ret; }, arguments); }, - __wbg_resolve_ae8d83246e5bcc12: function(arg0) { + __wbg_resolve_ae8d83246e5bcc12: function() { return logError(function (arg0) { const ret = Promise.resolve(arg0); return ret; - }, - __wbg_set_6be42768c690e380: function(arg0, arg1, arg2) { + }, arguments); }, + __wbg_run_78b7b601add6ed6b: function() { return logError(function (arg0, arg1, arg2) { + try { + var state0 = {a: arg1, b: arg2}; + var cb0 = () => { + const a = state0.a; + state0.a = 0; + try { + return wasm_bindgen__convert__closures_____invoke__h563360c8d8d07198(a, state0.b, ); + } finally { + state0.a = a; + } + }; + const ret = arg0.run(cb0); + _assertBoolean(ret); + return ret; + } finally { + state0.a = state0.b = 0; + } + }, arguments); }, + __wbg_set_6be42768c690e380: function() { return logError(function (arg0, arg1, arg2) { arg0[arg1] = arg2; - }, - __wbg_set_bf7251625df30a02: function(arg0, arg1, arg2) { + }, arguments); }, + __wbg_set_bf7251625df30a02: function() { return logError(function (arg0, arg1, arg2) { const ret = arg0.set(arg1, arg2); return ret; - }, - __wbg_stack_3b0d974bbf31e44f: function(arg0, arg1) { + }, arguments); }, + __wbg_stack_3b0d974bbf31e44f: function() { return logError(function (arg0, arg1) { const ret = arg1.stack; const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len1 = WASM_VECTOR_LEN; getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); - }, - __wbg_static_accessor_GLOBAL_8adb955bd33fac2f: function() { + }, arguments); }, + __wbg_static_accessor_GLOBAL_8adb955bd33fac2f: function() { return logError(function () { const ret = typeof global === 'undefined' ? null : global; return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); - }, - __wbg_static_accessor_GLOBAL_THIS_ad356e0db91c7913: function() { + }, arguments); }, + __wbg_static_accessor_GLOBAL_THIS_ad356e0db91c7913: function() { return logError(function () { const ret = typeof globalThis === 'undefined' ? null : globalThis; return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); - }, - __wbg_static_accessor_SELF_f207c857566db248: function() { + }, arguments); }, + __wbg_static_accessor_SELF_f207c857566db248: function() { return logError(function () { const ret = typeof self === 'undefined' ? null : self; return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); - }, - __wbg_static_accessor_WINDOW_bb9f1ba69d61b386: function() { + }, arguments); }, + __wbg_static_accessor_WINDOW_bb9f1ba69d61b386: function() { return logError(function () { const ret = typeof window === 'undefined' ? null : window; return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); - }, - __wbg_subarray_a068d24e39478a8a: function(arg0, arg1, arg2) { + }, arguments); }, + __wbg_subarray_a068d24e39478a8a: function() { return logError(function (arg0, arg1, arg2) { const ret = arg0.subarray(arg1 >>> 0, arg2 >>> 0); return ret; - }, - __wbg_then_098abe61755d12f6: function(arg0, arg1) { + }, arguments); }, + __wbg_then_098abe61755d12f6: function() { return logError(function (arg0, arg1) { const ret = arg0.then(arg1); return ret; - }, - __wbg_versions_276b2795b1c6a219: function(arg0) { + }, arguments); }, + __wbg_value_21fc78aab0322612: function() { return logError(function (arg0) { + const ret = arg0.value; + return ret; + }, arguments); }, + __wbg_versions_276b2795b1c6a219: function() { return logError(function (arg0) { const ret = arg0.versions; return ret; - }, - __wbindgen_cast_0000000000000001: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { dtor_idx: 72, function: Function { arguments: [Externref], shim_idx: 73, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. - const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen__closure__destroy__h2ffbb988ee498a71, wasm_bindgen__convert__closures_____invoke__h9effed663e3278d7); + }, arguments); }, + __wbg_warn_3a37cdd7216f1479: function() { return logError(function (arg0, arg1) { + var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice(); + wasm.__wbindgen_free(arg0, arg1 * 4, 4); + console.warn(...v0); + }, arguments); }, + __wbindgen_cast_0000000000000001: function() { return logError(function (arg0, arg1) { + // Cast intrinsic for `Closure(Closure { dtor_idx: 357, function: Function { arguments: [Externref], shim_idx: 1097, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen__closure__destroy__hdb4b145fc5c0ff94, wasm_bindgen__convert__closures_____invoke__ha2bc130ecf8461d3); return ret; - }, - __wbindgen_cast_0000000000000002: function(arg0) { + }, arguments); }, + __wbindgen_cast_0000000000000002: function() { return logError(function (arg0) { // Cast intrinsic for `F64 -> Externref`. const ret = arg0; return ret; - }, - __wbindgen_cast_0000000000000003: function(arg0, arg1) { + }, arguments); }, + __wbindgen_cast_0000000000000003: function() { return logError(function (arg0, arg1) { // Cast intrinsic for `Ref(Slice(U8)) -> NamedExternref("Uint8Array")`. const ret = getArrayU8FromWasm0(arg0, arg1); return ret; - }, - __wbindgen_cast_0000000000000004: function(arg0, arg1) { + }, arguments); }, + __wbindgen_cast_0000000000000004: function() { return logError(function (arg0, arg1) { // Cast intrinsic for `Ref(String) -> Externref`. const ret = getStringFromWasm0(arg0, arg1); return ret; - }, + }, arguments); }, __wbindgen_init_externref_table: function() { const table = wasm.__wbindgen_externrefs; const offset = table.grow(4); @@ -291,31 +464,132 @@ function __wbg_get_imports() { }; } -function wasm_bindgen__convert__closures_____invoke__h9effed663e3278d7(arg0, arg1, arg2) { - const ret = wasm.wasm_bindgen__convert__closures_____invoke__h9effed663e3278d7(arg0, arg1, arg2); + +//#endregion +function wasm_bindgen__convert__closures_____invoke__h563360c8d8d07198(arg0, arg1) { + _assertNum(arg0); + _assertNum(arg1); + const ret = wasm.wasm_bindgen__convert__closures_____invoke__h563360c8d8d07198(arg0, arg1); + return ret !== 0; +} + +function wasm_bindgen__convert__closures_____invoke__ha2bc130ecf8461d3(arg0, arg1, arg2) { + _assertNum(arg0); + _assertNum(arg1); + const ret = wasm.wasm_bindgen__convert__closures_____invoke__ha2bc130ecf8461d3(arg0, arg1, arg2); if (ret[1]) { throw takeFromExternrefTable0(ret[0]); } } -function wasm_bindgen__convert__closures_____invoke__h331bfc0ec3639989(arg0, arg1, arg2, arg3) { - wasm.wasm_bindgen__convert__closures_____invoke__h331bfc0ec3639989(arg0, arg1, arg2, arg3); +function wasm_bindgen__convert__closures_____invoke__hfcaf3c30771b308a(arg0, arg1, arg2, arg3) { + _assertNum(arg0); + _assertNum(arg1); + wasm.wasm_bindgen__convert__closures_____invoke__hfcaf3c30771b308a(arg0, arg1, arg2, arg3); } const IvrEmulatorDriverFinalization = (typeof FinalizationRegistry === 'undefined') ? { register: () => {}, unregister: () => {} } : new FinalizationRegistry(ptr => wasm.__wbg_ivremulatordriver_free(ptr >>> 0, 1)); + +//#region intrinsics function addToExternrefTable0(obj) { const idx = wasm.__externref_table_alloc(); wasm.__wbindgen_externrefs.set(idx, obj); return idx; } +function _assertBoolean(n) { + if (typeof(n) !== 'boolean') { + throw new Error(`expected a boolean argument, found ${typeof(n)}`); + } +} + +function _assertNum(n) { + if (typeof(n) !== 'number') throw new Error(`expected a number argument, found ${typeof(n)}`); +} + const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined') ? { register: () => {}, unregister: () => {} } : new FinalizationRegistry(state => state.dtor(state.a, state.b)); +function debugString(val) { + // primitive types + const type = typeof val; + if (type == 'number' || type == 'boolean' || val == null) { + return `${val}`; + } + if (type == 'string') { + return `"${val}"`; + } + if (type == 'symbol') { + const description = val.description; + if (description == null) { + return 'Symbol'; + } else { + return `Symbol(${description})`; + } + } + if (type == 'function') { + const name = val.name; + if (typeof name == 'string' && name.length > 0) { + return `Function(${name})`; + } else { + return 'Function'; + } + } + // objects + if (Array.isArray(val)) { + const length = val.length; + let debug = '['; + if (length > 0) { + debug += debugString(val[0]); + } + for(let i = 1; i < length; i++) { + debug += ', ' + debugString(val[i]); + } + debug += ']'; + return debug; + } + // Test for built-in + const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); + let className; + if (builtInMatches && builtInMatches.length > 1) { + className = builtInMatches[1]; + } else { + // Failed to match the standard '[object ClassName]' + return toString.call(val); + } + if (className == 'Object') { + // we're a user defined class or Object + // JSON.stringify avoids problems with cycles, and is generally much + // easier than looping through ownProperties of `val`. + try { + return 'Object(' + JSON.stringify(val) + ')'; + } catch (_) { + return 'Object'; + } + } + // errors + if (val instanceof Error) { + return `${val.name}: ${val.message}\n${val.stack}`; + } + // TODO we could test for more things here, like `Set`s and `Map`s. + return className; +} + +function getArrayJsValueFromWasm0(ptr, len) { + ptr = ptr >>> 0; + const mem = getDataViewMemory0(); + const result = []; + for (let i = ptr; i < ptr + 4 * len; i += 4) { + result.push(wasm.__wbindgen_externrefs.get(mem.getUint32(i, true))); + } + wasm.__externref_drop_slice(ptr, len); + return result; +} + function getArrayU8FromWasm0(ptr, len) { ptr = ptr >>> 0; return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len); @@ -355,6 +629,22 @@ function isLikeNone(x) { return x === undefined || x === null; } +function logError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + let error = (function () { + try { + return e instanceof Error ? `${e.message}\n\nStack:\n${e.stack}` : e.toString(); + } catch(_) { + return ""; + } + }()); + console.error("wasm-bindgen: imported JS function that was not marked as `catch` threw an error:", error); + throw e; + } +} + function makeMutClosure(arg0, arg1, dtor, f) { const state = { a: arg0, b: arg1, cnt: 1, dtor }; const real = (...args) => { @@ -384,6 +674,7 @@ function makeMutClosure(arg0, arg1, dtor, f) { } function passStringToWasm0(arg, malloc, realloc) { + if (typeof(arg) !== 'string') throw new Error(`expected a string argument, found ${typeof(arg)}`); if (realloc === undefined) { const buf = cachedTextEncoder.encode(arg); const ptr = malloc(buf.length, 1) >>> 0; @@ -411,7 +702,7 @@ function passStringToWasm0(arg, malloc, realloc) { ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); const ret = cachedTextEncoder.encodeInto(arg, view); - + if (ret.read !== arg.length) throw new Error('failed to pass whole string'); offset += ret.written; ptr = realloc(ptr, len, offset, 1) >>> 0; } @@ -455,6 +746,10 @@ if (!('encodeInto' in cachedTextEncoder)) { let WASM_VECTOR_LEN = 0; + +//#endregion + +//#region wasm loading let wasmModule, wasm; function __wbg_finalize_init(instance, module) { wasm = instance.exports; @@ -545,3 +840,5 @@ async function __wbg_init(module_or_path) { } export { initSync, __wbg_init as default }; +//#endregion +export { wasm as __wasm } From 6eabada1bb894696c87f4fabaea8c6f1b8e37b2d Mon Sep 17 00:00:00 2001 From: Enes Selim Date: Mon, 27 Jul 2026 20:40:01 +0300 Subject: [PATCH 04/19] imp: allow setting ivr emulator tracing log levels --- .../admin-portal/src/services/IvrEmulator.ts | 8 +- .../services/generated/ivr_emulator_wasm.d.ts | 15 +- .../services/generated/ivr_emulator_wasm.js | 335 ++++++------------ 3 files changed, 132 insertions(+), 226 deletions(-) diff --git a/packages/admin-portal/src/services/IvrEmulator.ts b/packages/admin-portal/src/services/IvrEmulator.ts index 120e254d56f..1c2140275f5 100644 --- a/packages/admin-portal/src/services/IvrEmulator.ts +++ b/packages/admin-portal/src/services/IvrEmulator.ts @@ -1,4 +1,4 @@ -import loadIvrWasm, {IvrEmulatorDriver} from "./generated/ivr_emulator_wasm" +import loadIvrWasm, {WasmConfig, IvrEmulatorDriver} from "./generated/ivr_emulator_wasm" const api = {IvrEmulatorDriver} export type IvrEmulatorApi = typeof api @@ -42,7 +42,11 @@ export const loadIvrEmulator = (wasmUrl: string): Promise | null initPromise ??= fetchWasm(wasmUrl) .then(async (moduleResp) => { try { - await loadIvrWasm({module_or_path: moduleResp}) + let ivrWasm = await loadIvrWasm({module_or_path: moduleResp}) + let config: WasmConfig = { + logging: localStorage.getItem("sq.ivr-emulator.logging") ?? undefined, + } + ivrWasm.init(config) } catch (cause) { throw new IvrEmulatorError("load", "failed to load the wasm", {cause}) } diff --git a/packages/admin-portal/src/services/generated/ivr_emulator_wasm.d.ts b/packages/admin-portal/src/services/generated/ivr_emulator_wasm.d.ts index 58a8ba38c9d..2e7c09805c1 100644 --- a/packages/admin-portal/src/services/generated/ivr_emulator_wasm.d.ts +++ b/packages/admin-portal/src/services/generated/ivr_emulator_wasm.d.ts @@ -32,6 +32,10 @@ export interface PromptInfo { voice_id: string; } +export interface WasmConfig { + logging: string | undefined; +} + export type Action = { type: "Prompt"; prompt: PromptInfo } | { type: "ExpectInput"; prompt: PromptInfo; valid_inputs: string; max_digits: number; timeout: number } | { type: "Disconnect"; prompt: PromptInfo } | { type: "Noop" }; @@ -45,24 +49,23 @@ export class IvrEmulatorDriver { send_timeout(): void; } -export function init(): void; +export function init(wasm_config: WasmConfig): void; export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; export interface InitOutput { readonly memory: WebAssembly.Memory; - readonly init: () => void; readonly __wbg_ivremulatordriver_free: (a: number, b: number) => void; + readonly init: (a: any) => void; readonly ivremulatordriver_attributes: (a: number) => [number, number, number]; readonly ivremulatordriver_execute: (a: number, b: number) => any; readonly ivremulatordriver_new: (a: any) => [number, number, number]; readonly ivremulatordriver_send_input: (a: number, b: number, c: number) => void; readonly ivremulatordriver_send_timeout: (a: number) => void; readonly ring_core_0_17_14__bn_mul_mont: (a: number, b: number, c: number, d: number, e: number, f: number) => void; - readonly wasm_bindgen__closure__destroy__hdb4b145fc5c0ff94: (a: number, b: number) => void; - readonly wasm_bindgen__convert__closures_____invoke__ha2bc130ecf8461d3: (a: number, b: number, c: any) => [number, number]; - readonly wasm_bindgen__convert__closures_____invoke__hfcaf3c30771b308a: (a: number, b: number, c: any, d: any) => void; - readonly wasm_bindgen__convert__closures_____invoke__h563360c8d8d07198: (a: number, b: number) => number; + readonly wasm_bindgen__closure__destroy__h7baa5a84311a3b04: (a: number, b: number) => void; + readonly wasm_bindgen__convert__closures_____invoke__h5fed3dcc0d3dcd7b: (a: number, b: number, c: any) => [number, number]; + readonly wasm_bindgen__convert__closures_____invoke__h331bfc0ec3639989: (a: number, b: number, c: any, d: any) => void; readonly __wbindgen_malloc: (a: number, b: number) => number; readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; readonly __wbindgen_exn_store: (a: number) => void; diff --git a/packages/admin-portal/src/services/generated/ivr_emulator_wasm.js b/packages/admin-portal/src/services/generated/ivr_emulator_wasm.js index b5c6e7889be..f9772c093e2 100644 --- a/packages/admin-portal/src/services/generated/ivr_emulator_wasm.js +++ b/packages/admin-portal/src/services/generated/ivr_emulator_wasm.js @@ -1,7 +1,5 @@ /* @ts-self-types="./ivr_emulator_wasm.d.ts" */ -//#region exports - export class IvrEmulatorDriver { __destroy_into_raw() { const ptr = this.__wbg_ptr; @@ -17,8 +15,6 @@ export class IvrEmulatorDriver { * @returns {any} */ attributes() { - if (this.__wbg_ptr == 0) throw new Error('Attempt to use a moved value'); - _assertNum(this.__wbg_ptr); const ret = wasm.ivremulatordriver_attributes(this.__wbg_ptr); if (ret[2]) { throw takeFromExternrefTable0(ret[1]); @@ -30,9 +26,6 @@ export class IvrEmulatorDriver { * @returns {Action} */ execute(until_io) { - if (this.__wbg_ptr == 0) throw new Error('Attempt to use a moved value'); - _assertNum(this.__wbg_ptr); - _assertBoolean(until_io); const ret = wasm.ivremulatordriver_execute(this.__wbg_ptr, until_io); return ret; } @@ -52,48 +45,40 @@ export class IvrEmulatorDriver { * @param {string} input */ send_input(input) { - if (this.__wbg_ptr == 0) throw new Error('Attempt to use a moved value'); - _assertNum(this.__wbg_ptr); const ptr0 = passStringToWasm0(input, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len0 = WASM_VECTOR_LEN; wasm.ivremulatordriver_send_input(this.__wbg_ptr, ptr0, len0); } send_timeout() { - if (this.__wbg_ptr == 0) throw new Error('Attempt to use a moved value'); - _assertNum(this.__wbg_ptr); wasm.ivremulatordriver_send_timeout(this.__wbg_ptr); } } if (Symbol.dispose) IvrEmulatorDriver.prototype[Symbol.dispose] = IvrEmulatorDriver.prototype.free; -export function init() { - wasm.init(); +/** + * @param {WasmConfig} wasm_config + */ +export function init(wasm_config) { + wasm.init(wasm_config); } -//#endregion - -//#region wasm imports - function __wbg_get_imports() { const import0 = { __proto__: null, - __wbg_Error_83742b46f01ce22d: function() { return logError(function (arg0, arg1) { + __wbg_Error_83742b46f01ce22d: function(arg0, arg1) { const ret = Error(getStringFromWasm0(arg0, arg1)); return ret; - }, arguments); }, - __wbg_String_8564e559799eccda: function() { return logError(function (arg0, arg1) { + }, + __wbg_String_8564e559799eccda: function(arg0, arg1) { const ret = String(arg1); const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len1 = WASM_VECTOR_LEN; getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); - }, arguments); }, + }, __wbg___wbindgen_boolean_get_c0f3f60bac5a78d1: function(arg0) { const v = arg0; const ret = typeof(v) === 'boolean' ? v : undefined; - if (!isLikeNone(ret)) { - _assertBoolean(ret); - } return isLikeNone(ret) ? 0xFFFFFF : ret ? 1 : 0; }, __wbg___wbindgen_debug_string_5398f5bb970e0daa: function(arg0, arg1) { @@ -105,41 +90,32 @@ function __wbg_get_imports() { }, __wbg___wbindgen_in_41dbb8413020e076: function(arg0, arg1) { const ret = arg0 in arg1; - _assertBoolean(ret); return ret; }, __wbg___wbindgen_is_function_3c846841762788c1: function(arg0) { const ret = typeof(arg0) === 'function'; - _assertBoolean(ret); return ret; }, __wbg___wbindgen_is_object_781bc9f159099513: function(arg0) { const val = arg0; const ret = typeof(val) === 'object' && val !== null; - _assertBoolean(ret); return ret; }, __wbg___wbindgen_is_string_7ef6b97b02428fae: function(arg0) { const ret = typeof(arg0) === 'string'; - _assertBoolean(ret); return ret; }, __wbg___wbindgen_is_undefined_52709e72fb9f179c: function(arg0) { const ret = arg0 === undefined; - _assertBoolean(ret); return ret; }, __wbg___wbindgen_jsval_loose_eq_5bcc3bed3c69e72b: function(arg0, arg1) { const ret = arg0 == arg1; - _assertBoolean(ret); return ret; }, __wbg___wbindgen_number_get_34bb9d9dcfa21373: function(arg0, arg1) { const obj = arg1; const ret = typeof(obj) === 'number' ? obj : undefined; - if (!isLikeNone(ret)) { - _assertNum(ret); - } getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true); getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true); }, @@ -154,9 +130,9 @@ function __wbg_get_imports() { __wbg___wbindgen_throw_6ddd609b62940d55: function(arg0, arg1) { throw new Error(getStringFromWasm0(arg0, arg1)); }, - __wbg__wbg_cb_unref_6b5b6b8576d35cb1: function() { return logError(function (arg0) { + __wbg__wbg_cb_unref_6b5b6b8576d35cb1: function(arg0) { arg0._wbg_cb_unref(); - }, arguments); }, + }, __wbg_call_2d781c1f4d5c0ef8: function() { return handleError(function (arg0, arg1, arg2) { const ret = arg0.call(arg1, arg2); return ret; @@ -165,30 +141,25 @@ function __wbg_get_imports() { const ret = arg0.call(arg1); return ret; }, arguments); }, - __wbg_createTask_6eb3a8b6dd2f87c9: function() { return handleError(function (arg0, arg1) { - const ret = console.createTask(getStringFromWasm0(arg0, arg1)); - return ret; - }, arguments); }, - __wbg_crypto_38df2bab126b63dc: function() { return logError(function (arg0) { + __wbg_crypto_38df2bab126b63dc: function(arg0) { const ret = arg0.crypto; return ret; - }, arguments); }, - __wbg_debug_eaef3b49d572d680: function() { return logError(function (arg0, arg1) { + }, + __wbg_debug_eaef3b49d572d680: function(arg0, arg1) { var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice(); wasm.__wbindgen_free(arg0, arg1 * 4, 4); console.debug(...v0); - }, arguments); }, - __wbg_done_08ce71ee07e3bd17: function() { return logError(function (arg0) { + }, + __wbg_done_08ce71ee07e3bd17: function(arg0) { const ret = arg0.done; - _assertBoolean(ret); return ret; - }, arguments); }, - __wbg_error_71b0e71161a5f3a0: function() { return logError(function (arg0, arg1) { + }, + __wbg_error_71b0e71161a5f3a0: function(arg0, arg1) { var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice(); wasm.__wbindgen_free(arg0, arg1 * 4, 4); console.error(...v0); - }, arguments); }, - __wbg_error_a6fa202b58aa1cd3: function() { return logError(function (arg0, arg1) { + }, + __wbg_error_a6fa202b58aa1cd3: function(arg0, arg1) { let deferred0_0; let deferred0_1; try { @@ -198,31 +169,31 @@ function __wbg_get_imports() { } finally { wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); } - }, arguments); }, + }, __wbg_getRandomValues_c44a50d8cfdaebeb: function() { return handleError(function (arg0, arg1) { arg0.getRandomValues(arg1); }, arguments); }, - __wbg_getTime_1dad7b5386ddd2d9: function() { return logError(function (arg0) { + __wbg_getTime_1dad7b5386ddd2d9: function(arg0) { const ret = arg0.getTime(); return ret; - }, arguments); }, - __wbg_getTimezoneOffset_639bcf2dde21158b: function() { return logError(function (arg0) { + }, + __wbg_getTimezoneOffset_639bcf2dde21158b: function(arg0) { const ret = arg0.getTimezoneOffset(); return ret; - }, arguments); }, + }, __wbg_get_326e41e095fb2575: function() { return handleError(function (arg0, arg1) { const ret = Reflect.get(arg0, arg1); return ret; }, arguments); }, - __wbg_get_unchecked_329cfe50afab7352: function() { return logError(function (arg0, arg1) { + __wbg_get_unchecked_329cfe50afab7352: function(arg0, arg1) { const ret = arg0[arg1 >>> 0]; return ret; - }, arguments); }, - __wbg_get_with_ref_key_6412cf3094599694: function() { return logError(function (arg0, arg1) { + }, + __wbg_get_with_ref_key_6412cf3094599694: function(arg0, arg1) { const ret = arg0[arg1]; return ret; - }, arguments); }, - __wbg_instanceof_ArrayBuffer_101e2bf31071a9f6: function() { return logError(function (arg0) { + }, + __wbg_instanceof_ArrayBuffer_101e2bf31071a9f6: function(arg0) { let result; try { result = arg0 instanceof ArrayBuffer; @@ -230,10 +201,9 @@ function __wbg_get_imports() { result = false; } const ret = result; - _assertBoolean(ret); return ret; - }, arguments); }, - __wbg_instanceof_Uint8Array_740438561a5b956d: function() { return logError(function (arg0) { + }, + __wbg_instanceof_Uint8Array_740438561a5b956d: function(arg0) { let result; try { result = arg0 instanceof Uint8Array; @@ -241,72 +211,68 @@ function __wbg_get_imports() { result = false; } const ret = result; - _assertBoolean(ret); return ret; - }, arguments); }, - __wbg_isArray_33b91feb269ff46e: function() { return logError(function (arg0) { + }, + __wbg_isArray_33b91feb269ff46e: function(arg0) { const ret = Array.isArray(arg0); - _assertBoolean(ret); return ret; - }, arguments); }, - __wbg_iterator_d8f549ec8fb061b1: function() { return logError(function () { + }, + __wbg_iterator_d8f549ec8fb061b1: function() { const ret = Symbol.iterator; return ret; - }, arguments); }, - __wbg_length_b3416cf66a5452c8: function() { return logError(function (arg0) { + }, + __wbg_length_b3416cf66a5452c8: function(arg0) { const ret = arg0.length; - _assertNum(ret); return ret; - }, arguments); }, - __wbg_length_ea16607d7b61445b: function() { return logError(function (arg0) { + }, + __wbg_length_ea16607d7b61445b: function(arg0) { const ret = arg0.length; - _assertNum(ret); return ret; - }, arguments); }, - __wbg_log_7a0760e115750083: function() { return logError(function (arg0, arg1) { + }, + __wbg_log_7a0760e115750083: function(arg0, arg1) { var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice(); wasm.__wbindgen_free(arg0, arg1 * 4, 4); console.log(...v0); - }, arguments); }, - __wbg_log_82574c09882d9d1e: function() { return logError(function (arg0, arg1) { + }, + __wbg_log_82574c09882d9d1e: function(arg0, arg1) { console.log(getStringFromWasm0(arg0, arg1)); - }, arguments); }, - __wbg_msCrypto_bd5a034af96bcba6: function() { return logError(function (arg0) { + }, + __wbg_msCrypto_bd5a034af96bcba6: function(arg0) { const ret = arg0.msCrypto; return ret; - }, arguments); }, - __wbg_new_0_1dcafdf5e786e876: function() { return logError(function () { + }, + __wbg_new_0_1dcafdf5e786e876: function() { const ret = new Date(); return ret; - }, arguments); }, - __wbg_new_227d7c05414eb861: function() { return logError(function () { + }, + __wbg_new_227d7c05414eb861: function() { const ret = new Error(); return ret; - }, arguments); }, - __wbg_new_49d5571bd3f0c4d4: function() { return logError(function () { + }, + __wbg_new_49d5571bd3f0c4d4: function() { const ret = new Map(); return ret; - }, arguments); }, - __wbg_new_5f486cdf45a04d78: function() { return logError(function (arg0) { + }, + __wbg_new_5f486cdf45a04d78: function(arg0) { const ret = new Uint8Array(arg0); return ret; - }, arguments); }, - __wbg_new_ab79df5bd7c26067: function() { return logError(function () { + }, + __wbg_new_ab79df5bd7c26067: function() { const ret = new Object(); return ret; - }, arguments); }, - __wbg_new_fd94ca5c9639abd2: function() { return logError(function (arg0) { + }, + __wbg_new_fd94ca5c9639abd2: function(arg0) { const ret = new Date(arg0); return ret; - }, arguments); }, - __wbg_new_typed_aaaeaf29cf802876: function() { return logError(function (arg0, arg1) { + }, + __wbg_new_typed_aaaeaf29cf802876: function(arg0, arg1) { try { var state0 = {a: arg0, b: arg1}; var cb0 = (arg0, arg1) => { const a = state0.a; state0.a = 0; try { - return wasm_bindgen__convert__closures_____invoke__hfcaf3c30771b308a(a, state0.b, arg0, arg1); + return wasm_bindgen__convert__closures_____invoke__h331bfc0ec3639989(a, state0.b, arg0, arg1); } finally { state0.a = a; } @@ -316,37 +282,37 @@ function __wbg_get_imports() { } finally { state0.a = state0.b = 0; } - }, arguments); }, - __wbg_new_with_length_825018a1616e9e55: function() { return logError(function (arg0) { + }, + __wbg_new_with_length_825018a1616e9e55: function(arg0) { const ret = new Uint8Array(arg0 >>> 0); return ret; - }, arguments); }, + }, __wbg_next_11b99ee6237339e3: function() { return handleError(function (arg0) { const ret = arg0.next(); return ret; }, arguments); }, - __wbg_next_e01a967809d1aa68: function() { return logError(function (arg0) { + __wbg_next_e01a967809d1aa68: function(arg0) { const ret = arg0.next; return ret; - }, arguments); }, - __wbg_node_84ea875411254db1: function() { return logError(function (arg0) { + }, + __wbg_node_84ea875411254db1: function(arg0) { const ret = arg0.node; return ret; - }, arguments); }, - __wbg_process_44c7a14e11e9f69e: function() { return logError(function (arg0) { + }, + __wbg_process_44c7a14e11e9f69e: function(arg0) { const ret = arg0.process; return ret; - }, arguments); }, - __wbg_prototypesetcall_d62e5099504357e6: function() { return logError(function (arg0, arg1, arg2) { + }, + __wbg_prototypesetcall_d62e5099504357e6: function(arg0, arg1, arg2) { Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2); - }, arguments); }, - __wbg_queueMicrotask_0c399741342fb10f: function() { return logError(function (arg0) { + }, + __wbg_queueMicrotask_0c399741342fb10f: function(arg0) { const ret = arg0.queueMicrotask; return ret; - }, arguments); }, - __wbg_queueMicrotask_a082d78ce798393e: function() { return logError(function (arg0) { + }, + __wbg_queueMicrotask_a082d78ce798393e: function(arg0) { queueMicrotask(arg0); - }, arguments); }, + }, __wbg_randomFillSync_6c25eac9869eb53c: function() { return handleError(function (arg0, arg1) { arg0.randomFillSync(arg1); }, arguments); }, @@ -354,100 +320,81 @@ function __wbg_get_imports() { const ret = module.require; return ret; }, arguments); }, - __wbg_resolve_ae8d83246e5bcc12: function() { return logError(function (arg0) { + __wbg_resolve_ae8d83246e5bcc12: function(arg0) { const ret = Promise.resolve(arg0); return ret; - }, arguments); }, - __wbg_run_78b7b601add6ed6b: function() { return logError(function (arg0, arg1, arg2) { - try { - var state0 = {a: arg1, b: arg2}; - var cb0 = () => { - const a = state0.a; - state0.a = 0; - try { - return wasm_bindgen__convert__closures_____invoke__h563360c8d8d07198(a, state0.b, ); - } finally { - state0.a = a; - } - }; - const ret = arg0.run(cb0); - _assertBoolean(ret); - return ret; - } finally { - state0.a = state0.b = 0; - } - }, arguments); }, - __wbg_set_6be42768c690e380: function() { return logError(function (arg0, arg1, arg2) { + }, + __wbg_set_6be42768c690e380: function(arg0, arg1, arg2) { arg0[arg1] = arg2; - }, arguments); }, - __wbg_set_bf7251625df30a02: function() { return logError(function (arg0, arg1, arg2) { + }, + __wbg_set_bf7251625df30a02: function(arg0, arg1, arg2) { const ret = arg0.set(arg1, arg2); return ret; - }, arguments); }, - __wbg_stack_3b0d974bbf31e44f: function() { return logError(function (arg0, arg1) { + }, + __wbg_stack_3b0d974bbf31e44f: function(arg0, arg1) { const ret = arg1.stack; const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len1 = WASM_VECTOR_LEN; getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); - }, arguments); }, - __wbg_static_accessor_GLOBAL_8adb955bd33fac2f: function() { return logError(function () { + }, + __wbg_static_accessor_GLOBAL_8adb955bd33fac2f: function() { const ret = typeof global === 'undefined' ? null : global; return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); - }, arguments); }, - __wbg_static_accessor_GLOBAL_THIS_ad356e0db91c7913: function() { return logError(function () { + }, + __wbg_static_accessor_GLOBAL_THIS_ad356e0db91c7913: function() { const ret = typeof globalThis === 'undefined' ? null : globalThis; return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); - }, arguments); }, - __wbg_static_accessor_SELF_f207c857566db248: function() { return logError(function () { + }, + __wbg_static_accessor_SELF_f207c857566db248: function() { const ret = typeof self === 'undefined' ? null : self; return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); - }, arguments); }, - __wbg_static_accessor_WINDOW_bb9f1ba69d61b386: function() { return logError(function () { + }, + __wbg_static_accessor_WINDOW_bb9f1ba69d61b386: function() { const ret = typeof window === 'undefined' ? null : window; return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); - }, arguments); }, - __wbg_subarray_a068d24e39478a8a: function() { return logError(function (arg0, arg1, arg2) { + }, + __wbg_subarray_a068d24e39478a8a: function(arg0, arg1, arg2) { const ret = arg0.subarray(arg1 >>> 0, arg2 >>> 0); return ret; - }, arguments); }, - __wbg_then_098abe61755d12f6: function() { return logError(function (arg0, arg1) { + }, + __wbg_then_098abe61755d12f6: function(arg0, arg1) { const ret = arg0.then(arg1); return ret; - }, arguments); }, - __wbg_value_21fc78aab0322612: function() { return logError(function (arg0) { + }, + __wbg_value_21fc78aab0322612: function(arg0) { const ret = arg0.value; return ret; - }, arguments); }, - __wbg_versions_276b2795b1c6a219: function() { return logError(function (arg0) { + }, + __wbg_versions_276b2795b1c6a219: function(arg0) { const ret = arg0.versions; return ret; - }, arguments); }, - __wbg_warn_3a37cdd7216f1479: function() { return logError(function (arg0, arg1) { + }, + __wbg_warn_3a37cdd7216f1479: function(arg0, arg1) { var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice(); wasm.__wbindgen_free(arg0, arg1 * 4, 4); console.warn(...v0); - }, arguments); }, - __wbindgen_cast_0000000000000001: function() { return logError(function (arg0, arg1) { - // Cast intrinsic for `Closure(Closure { dtor_idx: 357, function: Function { arguments: [Externref], shim_idx: 1097, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. - const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen__closure__destroy__hdb4b145fc5c0ff94, wasm_bindgen__convert__closures_____invoke__ha2bc130ecf8461d3); + }, + __wbindgen_cast_0000000000000001: function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { dtor_idx: 245, function: Function { arguments: [Externref], shim_idx: 246, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen__closure__destroy__h7baa5a84311a3b04, wasm_bindgen__convert__closures_____invoke__h5fed3dcc0d3dcd7b); return ret; - }, arguments); }, - __wbindgen_cast_0000000000000002: function() { return logError(function (arg0) { + }, + __wbindgen_cast_0000000000000002: function(arg0) { // Cast intrinsic for `F64 -> Externref`. const ret = arg0; return ret; - }, arguments); }, - __wbindgen_cast_0000000000000003: function() { return logError(function (arg0, arg1) { + }, + __wbindgen_cast_0000000000000003: function(arg0, arg1) { // Cast intrinsic for `Ref(Slice(U8)) -> NamedExternref("Uint8Array")`. const ret = getArrayU8FromWasm0(arg0, arg1); return ret; - }, arguments); }, - __wbindgen_cast_0000000000000004: function() { return logError(function (arg0, arg1) { + }, + __wbindgen_cast_0000000000000004: function(arg0, arg1) { // Cast intrinsic for `Ref(String) -> Externref`. const ret = getStringFromWasm0(arg0, arg1); return ret; - }, arguments); }, + }, __wbindgen_init_externref_table: function() { const table = wasm.__wbindgen_externrefs; const offset = table.grow(4); @@ -464,52 +411,27 @@ function __wbg_get_imports() { }; } - -//#endregion -function wasm_bindgen__convert__closures_____invoke__h563360c8d8d07198(arg0, arg1) { - _assertNum(arg0); - _assertNum(arg1); - const ret = wasm.wasm_bindgen__convert__closures_____invoke__h563360c8d8d07198(arg0, arg1); - return ret !== 0; -} - -function wasm_bindgen__convert__closures_____invoke__ha2bc130ecf8461d3(arg0, arg1, arg2) { - _assertNum(arg0); - _assertNum(arg1); - const ret = wasm.wasm_bindgen__convert__closures_____invoke__ha2bc130ecf8461d3(arg0, arg1, arg2); +function wasm_bindgen__convert__closures_____invoke__h5fed3dcc0d3dcd7b(arg0, arg1, arg2) { + const ret = wasm.wasm_bindgen__convert__closures_____invoke__h5fed3dcc0d3dcd7b(arg0, arg1, arg2); if (ret[1]) { throw takeFromExternrefTable0(ret[0]); } } -function wasm_bindgen__convert__closures_____invoke__hfcaf3c30771b308a(arg0, arg1, arg2, arg3) { - _assertNum(arg0); - _assertNum(arg1); - wasm.wasm_bindgen__convert__closures_____invoke__hfcaf3c30771b308a(arg0, arg1, arg2, arg3); +function wasm_bindgen__convert__closures_____invoke__h331bfc0ec3639989(arg0, arg1, arg2, arg3) { + wasm.wasm_bindgen__convert__closures_____invoke__h331bfc0ec3639989(arg0, arg1, arg2, arg3); } const IvrEmulatorDriverFinalization = (typeof FinalizationRegistry === 'undefined') ? { register: () => {}, unregister: () => {} } : new FinalizationRegistry(ptr => wasm.__wbg_ivremulatordriver_free(ptr >>> 0, 1)); - -//#region intrinsics function addToExternrefTable0(obj) { const idx = wasm.__externref_table_alloc(); wasm.__wbindgen_externrefs.set(idx, obj); return idx; } -function _assertBoolean(n) { - if (typeof(n) !== 'boolean') { - throw new Error(`expected a boolean argument, found ${typeof(n)}`); - } -} - -function _assertNum(n) { - if (typeof(n) !== 'number') throw new Error(`expected a number argument, found ${typeof(n)}`); -} - const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined') ? { register: () => {}, unregister: () => {} } : new FinalizationRegistry(state => state.dtor(state.a, state.b)); @@ -629,22 +551,6 @@ function isLikeNone(x) { return x === undefined || x === null; } -function logError(f, args) { - try { - return f.apply(this, args); - } catch (e) { - let error = (function () { - try { - return e instanceof Error ? `${e.message}\n\nStack:\n${e.stack}` : e.toString(); - } catch(_) { - return ""; - } - }()); - console.error("wasm-bindgen: imported JS function that was not marked as `catch` threw an error:", error); - throw e; - } -} - function makeMutClosure(arg0, arg1, dtor, f) { const state = { a: arg0, b: arg1, cnt: 1, dtor }; const real = (...args) => { @@ -674,7 +580,6 @@ function makeMutClosure(arg0, arg1, dtor, f) { } function passStringToWasm0(arg, malloc, realloc) { - if (typeof(arg) !== 'string') throw new Error(`expected a string argument, found ${typeof(arg)}`); if (realloc === undefined) { const buf = cachedTextEncoder.encode(arg); const ptr = malloc(buf.length, 1) >>> 0; @@ -702,7 +607,7 @@ function passStringToWasm0(arg, malloc, realloc) { ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); const ret = cachedTextEncoder.encodeInto(arg, view); - if (ret.read !== arg.length) throw new Error('failed to pass whole string'); + offset += ret.written; ptr = realloc(ptr, len, offset, 1) >>> 0; } @@ -746,10 +651,6 @@ if (!('encodeInto' in cachedTextEncoder)) { let WASM_VECTOR_LEN = 0; - -//#endregion - -//#region wasm loading let wasmModule, wasm; function __wbg_finalize_init(instance, module) { wasm = instance.exports; @@ -840,5 +741,3 @@ async function __wbg_init(module_or_path) { } export { initSync, __wbg_init as default }; -//#endregion -export { wasm as __wasm } From 41d66370bbd00a38925542219b30293dab51fa31 Mon Sep 17 00:00:00 2001 From: Enes Selim Date: Mon, 27 Jul 2026 23:38:42 +0300 Subject: [PATCH 05/19] imp: apply filters locally Our local framework strips off advanced hasura filters. --- .../resources/ElectionEvent/IvrEmulator.tsx | 44 ++++++++----------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx b/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx index 37e9b324920..b9add83326c 100644 --- a/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx +++ b/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx @@ -61,10 +61,9 @@ const ConfigFormBody: React.FC<{ // when constructing the map. sort: {field: "created_at", order: "ASC"}, filter: { - "tenant_id": electionEvent.tenant_id, - "election_event_id": electionEvent.id, - "area_id": areaId, - "deleted_at@_is_null": null, + tenant_id: electionEvent.tenant_id, + election_event_id: electionEvent.id, + area_id: areaId, }, }, { @@ -78,7 +77,8 @@ const ConfigFormBody: React.FC<{ if ( style.election_id && typeof style.election_id === "string" && - style.ballot_eml + style.ballot_eml && + !style.deleted_at ) { return [[style.election_id, style.ballot_eml]] } else { @@ -88,39 +88,33 @@ const ConfigFormBody: React.FC<{ ) }, [rawBallotStyles]) - const {data: rawElections} = useGetList( - "sequent_backend_election", - { - pagination: {page: 1, perPage: 300}, - sort: {field: "name", order: "DESC"}, - filter: { - "tenant_id": electionEvent.tenant_id, - "election_event_id": electionEvent.id, - "id@_in": Array.from(availableBallotStyles.keys()), - }, + const {data: rawElections} = useGetList("sequent_backend_election", { + pagination: {page: 1, perPage: 300}, + sort: {field: "name", order: "DESC"}, + filter: { + tenant_id: electionEvent.tenant_id, + election_event_id: electionEvent.id, }, - { - enabled: availableBallotStyles.size > 0, - } - ) + }) const electionChoices = useMemo<{id: string; name: string}[]>( () => rawElections - ?.map((e) => ({ + ?.filter((e) => availableBallotStyles.has(e.id)) + .map((e) => ({ id: e.id, name: aliasRenderer(e), })) .sort((a, b) => a.name.localeCompare(b.name)) ?? [], - [rawElections] + [rawElections, availableBallotStyles] ) - const selectedBallotStyles = useMemo>(() => { + const resolvedBallotStyles = useMemo>(() => { let filtered = availableBallotStyles .entries() .filter(([electionId, _style]) => electionIds.includes(electionId)) return new Map(filtered) - }, [availableBallotStyles, electionIds]) + }, [electionIds, availableBallotStyles]) const generateConfig = (data: ConfigFormValues): EmulatorConfig => { return { @@ -129,9 +123,9 @@ const ConfigFormBody: React.FC<{ caller_number: CALLER_NUMBER, contact_id: generateContactId(), blacklisted_numbers: data.blacklistCaller ? [CALLER_NUMBER] : [], - open_elections: Array.from(selectedBallotStyles.keys()), + open_elections: Array.from(resolvedBallotStyles.keys()), election_event: JSON.stringify(electionEvent), - ballot_styles: Array.from(selectedBallotStyles.values()), + ballot_styles: Array.from(resolvedBallotStyles.values()), } } From 2b9daf688c1bc7dbf65932d69e8f0b23a8097694 Mon Sep 17 00:00:00 2001 From: Enes Selim Date: Mon, 27 Jul 2026 23:50:57 +0300 Subject: [PATCH 06/19] imp: pretty prompt lines --- .../resources/ElectionEvent/IvrEmulator.tsx | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx b/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx index b9add83326c..37e6f6d6d56 100644 --- a/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx +++ b/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx @@ -176,6 +176,33 @@ const ConfigForm: React.FC<{ ) } +const PromptLine: React.FC<{prompt: PromptInfo}> = ({prompt}) => { + const promptBody = useMemo(() => { + // Strip off the root ssml tag. + return prompt.prompt_text.replace(/^/, "").replace(/<\/speak>$/, "") + }, [prompt]) + const lang = useMemo(() => { + return prompt.language.slice(0, 2).toUpperCase() + }, [prompt]) + const langTitle = useMemo(() => { + return `${prompt.language}, ${prompt.voice_id}` + }, [prompt]) + + return ( + + + {lang} + + + {promptBody} + + + ) +} + const EmulatorInterface: React.FC<{ api: IvrEmulatorApi config: EmulatorConfig @@ -292,11 +319,9 @@ const EmulatorInterface: React.FC<{ {error ? {error} : null} {status === "Disconnected" ? Disconnected : null} - + {prompts.map((prompt) => ( - - {prompt.prompt_text},{prompt.language},{prompt.voice_id} - + ))} From 503f5d825fd154fa097af6bb25610b731a774a46 Mon Sep 17 00:00:00 2001 From: Enes Selim Date: Mon, 27 Jul 2026 23:52:12 +0300 Subject: [PATCH 07/19] imp: stable order and keys for prompt lines --- .../resources/ElectionEvent/IvrEmulator.tsx | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx b/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx index 37e6f6d6d56..1c7c23f77d3 100644 --- a/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx +++ b/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx @@ -176,7 +176,7 @@ const ConfigForm: React.FC<{ ) } -const PromptLine: React.FC<{prompt: PromptInfo}> = ({prompt}) => { +const PromptLine: React.FC<{key: number; prompt: PromptInfo}> = ({key, prompt}) => { const promptBody = useMemo(() => { // Strip off the root ssml tag. return prompt.prompt_text.replace(/^/, "").replace(/<\/speak>$/, "") @@ -189,7 +189,10 @@ const PromptLine: React.FC<{prompt: PromptInfo}> = ({prompt}) => { }, [prompt]) return ( - + void }> = ({api, config, onStatusChange}) => { - const [prompts, setPrompts] = useState([]) + const [prompts, setPrompts] = useState<[number, PromptInfo][]>([]) const emulator = useRef(undefined) const [expectedInput, setExpectedInput] = useState() - const addPrompt = (prompt: PromptInfo) => { - setPrompts([...prompts, prompt]) - } const [status, setStatus] = useState("Disconnected") const [error, setError] = useState("") const [input, setInput] = useState("") const [executing, setExecuting] = useState(false) + const nextLogId = useRef(0) + + const addPrompt = (prompt: PromptInfo) => { + let key = nextLogId.current++ + setPrompts((current) => [...current, [key, prompt]]) + } const canSendInput = useMemo( () => status === "ExpectingInput" && Boolean(input.trim()), @@ -320,8 +326,8 @@ const EmulatorInterface: React.FC<{ {status === "Disconnected" ? Disconnected : null} - {prompts.map((prompt) => ( - + {prompts.map(([key, prompt]) => ( + ))} From b7ecf1869a5cc83d007b4e52fda2f600cedeedca Mon Sep 17 00:00:00 2001 From: Enes Selim Date: Mon, 27 Jul 2026 23:52:35 +0300 Subject: [PATCH 08/19] imp: clear the input field after sending it --- .../admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx b/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx index 1c7c23f77d3..3c682df4a3a 100644 --- a/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx +++ b/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx @@ -266,6 +266,7 @@ const EmulatorInterface: React.FC<{ } emulator.current.send_input(input) runEmulator(emulator.current) + setInput("") } const disposeEmulator = (disposed: IvrEmulatorDriver): void => { From 0f6ec9621cd31d8f90a0e8b44cab4a2e4ca3b3fe Mon Sep 17 00:00:00 2001 From: Enes Selim Date: Mon, 27 Jul 2026 23:53:16 +0300 Subject: [PATCH 09/19] imp: translate and add hints --- .../resources/ElectionEvent/IvrEmulator.tsx | 157 +++++++++++++----- packages/admin-portal/src/translations/cat.ts | 30 ++++ packages/admin-portal/src/translations/en.ts | 27 +++ packages/admin-portal/src/translations/es.ts | 30 ++++ packages/admin-portal/src/translations/eu.ts | 29 ++++ packages/admin-portal/src/translations/fr.ts | 30 ++++ packages/admin-portal/src/translations/gl.ts | 30 ++++ packages/admin-portal/src/translations/nl.ts | 29 ++++ packages/admin-portal/src/translations/tl.ts | 29 ++++ 9 files changed, 347 insertions(+), 44 deletions(-) diff --git a/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx b/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx index 3c682df4a3a..49da045803c 100644 --- a/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx +++ b/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx @@ -4,7 +4,7 @@ import React, {useEffect, useMemo, useRef, useState} from "react" import {BooleanInput, useGetList, useRecordContext} from "react-admin" import {FormProvider, useForm} from "react-hook-form" -import {Alert, Box, Button, Stack, TextField} from "@mui/material" +import {Alert, AlertTitle, Box, Button, Stack, TextField} from "@mui/material" import {useTranslation} from "react-i18next" import { Sequent_Backend_Ballot_Style, @@ -47,6 +47,7 @@ const ConfigFormBody: React.FC<{ electionEvent: Sequent_Backend_Election_Event onStartSession: (config: EmulatorConfig) => void }> = ({electionEvent, onStartSession}) => { + const {t} = useTranslation() const aliasRenderer = useAliasRenderer() const {control, getValues} = useFormContext() @@ -135,25 +136,36 @@ const ConfigFormBody: React.FC<{ } return ( - - + + - - Resolved {selectedBallotStyles.size} ballot styles for the selected area and - elections + + {resolvedBallotStyles.size < 1 ? ( + + {t("electionEventScreen.ivr.emulator.noStylesFound")} + + ) : null} - ) @@ -211,6 +223,7 @@ const EmulatorInterface: React.FC<{ config: EmulatorConfig onStatusChange?: (status: Status) => void }> = ({api, config, onStatusChange}) => { + const {t} = useTranslation() const [prompts, setPrompts] = useState<[number, PromptInfo][]>([]) const emulator = useRef(undefined) const [expectedInput, setExpectedInput] = useState() @@ -324,7 +337,9 @@ const EmulatorInterface: React.FC<{ return ( {error ? {error} : null} - {status === "Disconnected" ? Disconnected : null} + {status === "Disconnected" ? ( + t("electionEventScreen.ivr.emulator.disconnected") + ) : null} {prompts.map(([key, prompt]) => ( @@ -333,18 +348,29 @@ const EmulatorInterface: React.FC<{ - setInput(e.target.value.replace(/[^0-9*#]/g, ""))} - slotProps={{ - htmlInput: {pattern: "[0-9*#]*", maxLength: expectedInput?.max_digits}, +
{ + e.preventDefault() + canSendInput && sendInput() }} - onKeyDown={sendInput} - disabled={!canSendInput} - autoFocus - sx={{fontFamily: "monospace"}} - placeholder={`Enter your input (max digis=${expectedInput?.max_digits ?? ""}, valid inputs=${expectedInput?.valid_inputs ?? ""}, timeout=${expectedInput?.timeout ?? 0}s)`} - /> + > + setInput(e.target.value.replace(/[^0-9*#]/g, ""))} + slotProps={{ + htmlInput: {pattern: "[0-9*#]*", maxLength: expectedInput?.max_digits}, + }} + disabled={!expectedInput} + autoFocus + sx={{fontFamily: "monospace"}} + placeholder={t("electionEventScreen.ivr.emulator.inputPlaceholder", { + maxDigits: expectedInput?.max_digits ?? "", + validInputs: expectedInput?.valid_inputs ?? "", + timeout: expectedInput?.timeout ?? 0, + })} + /> +
- -
@@ -377,15 +412,23 @@ export const IvrEmulator: React.FC = () => { case IvrApiStatus.UNAVAILABLE: return ( - The emulator system is not available in your environment + {t("electionEventScreen.ivr.emulator.apiStatus.unavailable")} ) case IvrApiStatus.LOADING: - return Loading the emulator system... + return ( + + {t("electionEventScreen.ivr.emulator.apiStatus.loading")} + + ) case IvrApiStatus.ERROR: - return Error loading the emulator + return ( + + {t("electionEventScreen.ivr.emulator.apiStatus.error")} + + ) case IvrApiStatus.READY: - return The emulator system is loaded and ready + return null } }, [apiStatus]) @@ -401,26 +444,52 @@ export const IvrEmulator: React.FC = () => { return ( - Select an area and desired elections to experience the IVR session. + {t("electionEventScreen.ivr.emulator.infoMsg")}
{statusAlert} - {record && !emulatorStatus ? ( - - ) : null} - {api && config ? ( -
- - - -
+ {api ? ( + <> + + + {t("electionEventScreen.ivr.emulator.hints.title")} + + +
  • + {t("electionEventScreen.ivr.emulator.hints.publishRequired")} +
  • +
  • + {t( + "electionEventScreen.ivr.emulator.hints.eventChangesImmediate" + )} +
  • +
  • {t("electionEventScreen.ivr.emulator.hints.credentials")}
  • +
    +
    + {record && !emulatorStatus ? ( + + ) : null} + {api && config && emulatorStatus && record ? ( +
    + + emulatorStatus && setEmulatorStatus(status) + } + /> + + +
    + ) : null} + ) : null}
    diff --git a/packages/admin-portal/src/translations/cat.ts b/packages/admin-portal/src/translations/cat.ts index 9b7d1defcd4..4a2f5aed6b5 100644 --- a/packages/admin-portal/src/translations/cat.ts +++ b/packages/admin-portal/src/translations/cat.ts @@ -569,6 +569,36 @@ const catalanTranslation: TranslationType = { noFilterMatch: "Cap entrada no coincideix amb els filtres indicats", phoneRequired: "El nĆŗmero de telĆØfon Ć©s obligatori", }, + emulator: { + infoMsg: + "Seleccioneu una Ć rea i les eleccions desitjades per provar la sessió IVR.", + apiStatus: { + unavailable: "El sistema de l'emulador no estĆ  disponible al vostre entorn", + loading: "S'estĆ  carregant el sistema de l'emulador", + error: "S'ha produĆÆt un error en carregar el sistema de l'emulador", + }, + hints: { + title: "Consells", + publishRequired: + "Qualsevol canvi fet a les eleccions, les conteses o els candidats s'ha de publicar primer perquĆØ estigui disponible. A l'emulador nomĆ©s s'utilitzaran els estils de papereta publicats mĆ©s recentment per a l'Ć rea corresponent.", + eventChangesImmediate: + "Els canvis fets a l'esdeveniment electoral, com ara la configuració IVR o les substitucions dels missatges, estan disponibles immediatament en reiniciar la sessió de l'emulador.", + credentials: + 'L\'identificador de votant i el PIN vĆ lids són "123" i "123".', + }, + sendDtmf: "Envia una entrada DTMF", + sendTimeout: "Envia el temps d'espera", + disconnected: "Desconnectat", + startSession: "Inicia una sessió nova", + endSession: "Finalitza la sessió", + noStylesFound: + "No s'ha trobat cap estil de papereta publicat que coincideixi amb les vostres seleccions", + inputPlaceholder: + "IntroduĆÆu la vostra entrada (mĆ xim de dĆ­gits={{maxDigits}}, entrades vĆ lides={{validInputs}}, temps d'espera={{timeout}} s)", + blacklistCaller: "Bloqueja la persona que truca", + elections: "Eleccions", + area: "ƀrea", + }, }, stats: { elegibleVoters: "Electors", diff --git a/packages/admin-portal/src/translations/en.ts b/packages/admin-portal/src/translations/en.ts index b34197c90f9..b4ea98b1237 100644 --- a/packages/admin-portal/src/translations/en.ts +++ b/packages/admin-portal/src/translations/en.ts @@ -567,6 +567,33 @@ const englishTranslation = { noFilterMatch: "No entries match the given filters", phoneRequired: "Phone number is required", }, + emulator: { + infoMsg: "Select an area and desired elections to experience the IVR session.", + apiStatus: { + unavailable: "The emulator system is not available in your environment", + loading: "Loading the emulator system", + error: "Error loading the emulator system", + }, + hints: { + title: "Hints", + publishRequired: + "Any changes made to elections, contests or candidates must be published first to be available. Only the latest published ballot styles for the matching area will be used in the emulator.", + eventChangesImmediate: + "Changes made to the election event, such as IVR configuration or prompt overrides, are available immediately upon emulator session restart.", + credentials: 'The valid voter id and pin are "123" and "123".', + }, + sendDtmf: "Send DTMF input", + sendTimeout: "Send timeout", + disconnected: "Disconnected", + startSession: "Start new session", + endSession: "End the session", + noStylesFound: "No published ballot styles found matching your selections", + inputPlaceholder: + "Enter your input (max digits={{maxDigits}}, valid inputs={{validInputs}}, timeout={{timeout}}s)", + blacklistCaller: "Blocklist the caller", + elections: "Elections", + area: "Area", + }, }, stats: { elegibleVoters: "Eligible Voters", diff --git a/packages/admin-portal/src/translations/es.ts b/packages/admin-portal/src/translations/es.ts index 6147f6d8c02..60dd824d44d 100644 --- a/packages/admin-portal/src/translations/es.ts +++ b/packages/admin-portal/src/translations/es.ts @@ -570,6 +570,36 @@ const spanishTranslation: TranslationType = { noFilterMatch: "Ninguna entrada coincide con los filtros indicados", phoneRequired: "El nĆŗmero de telĆ©fono es obligatorio", }, + emulator: { + infoMsg: + "Seleccione un Ć”rea y las elecciones deseadas para probar la sesión de IVR.", + apiStatus: { + unavailable: "El sistema del emulador no estĆ” disponible en su entorno", + loading: "Cargando el sistema del emulador", + error: "Error al cargar el sistema del emulador", + }, + hints: { + title: "Sugerencias", + publishRequired: + "Cualquier cambio realizado en las elecciones, contiendas o candidatos debe publicarse primero para que estĆ© disponible. En el emulador solo se utilizarĆ”n los estilos de boleta publicados mĆ”s recientemente para el Ć”rea correspondiente.", + eventChangesImmediate: + "Los cambios realizados en el evento electoral, como la configuración de IVR o las modificaciones de los mensajes, estĆ”n disponibles inmediatamente al reiniciar la sesión del emulador.", + credentials: + 'El identificador de votante y el PIN vĆ”lidos son "123" y "123".', + }, + sendDtmf: "Enviar entrada DTMF", + sendTimeout: "Enviar tiempo de espera", + disconnected: "Desconectado", + startSession: "Iniciar nueva sesión", + endSession: "Finalizar la sesión", + noStylesFound: + "No se encontraron estilos de boleta publicados que coincidan con sus selecciones", + inputPlaceholder: + "Introduzca su entrada (mĆ”ximo de dĆ­gitos={{maxDigits}}, entradas vĆ”lidas={{validInputs}}, tiempo de espera={{timeout}} s)", + blacklistCaller: "Bloquear a la persona que llama", + elections: "Elecciones", + area: "Ɓrea", + }, }, stats: { elegibleVoters: "Electores", diff --git a/packages/admin-portal/src/translations/eu.ts b/packages/admin-portal/src/translations/eu.ts index 9385cc5ac40..4469fbc9f8b 100644 --- a/packages/admin-portal/src/translations/eu.ts +++ b/packages/admin-portal/src/translations/eu.ts @@ -569,6 +569,35 @@ const basqueTranslation: TranslationType = { noFilterMatch: "Ez dago emandako iragazkiekin bat datorren sarrerarik", phoneRequired: "Telefono-zenbakia nahitaezkoa da", }, + emulator: { + infoMsg: + "Hautatu eremu bat eta nahi dituzun hauteskundeak IVR saioa probatzeko.", + apiStatus: { + unavailable: "Emuladore-sistema ez dago erabilgarri zure ingurunean", + loading: "Emuladore-sistema kargatzen", + error: "Errorea emuladore-sistema kargatzean", + }, + hints: { + title: "Aholkuak", + publishRequired: + "Hauteskundeetan, lehiaketetan edo hautagaietan egindako edozein aldaketa lehenik argitaratu behar da erabilgarri egon dadin. Dagokion eremurako azkenik argitaratutako boto-paper estiloak soilik erabiliko dira emuladorean.", + eventChangesImmediate: + "Hauteskunde-ekitaldian egindako aldaketak, hala nola IVR konfigurazioa edo mezuen gainidazketak, berehala egongo dira erabilgarri emuladorearen saioa berrabiaraztean.", + credentials: 'Baliozko hautesle-IDa eta PINa "123" eta "123" dira.', + }, + sendDtmf: "Bidali DTMF sarrera", + sendTimeout: "Bidali denbora-muga", + disconnected: "Deskonektatuta", + startSession: "Hasi saio berria", + endSession: "Amaitu saioa", + noStylesFound: + "Ez da aurkitu zure hautapenekin bat datorren argitaratutako boto-paper estilorik", + inputPlaceholder: + "Idatzi sarrera (gehienezko digitu kopurua={{maxDigits}}, baliozko sarrerak={{validInputs}}, denbora-muga={{timeout}} s)", + blacklistCaller: "Blokeatu deitzailea", + elections: "Hauteskundeak", + area: "Eremua", + }, }, stats: { elegibleVoters: "Bozkatzaile Eskudunak", diff --git a/packages/admin-portal/src/translations/fr.ts b/packages/admin-portal/src/translations/fr.ts index 3ee4f15d97f..24dda5ee797 100644 --- a/packages/admin-portal/src/translations/fr.ts +++ b/packages/admin-portal/src/translations/fr.ts @@ -569,6 +569,36 @@ const frenchTranslation: TranslationType = { noFilterMatch: "Aucune entrĆ©e ne correspond aux filtres indiquĆ©s", phoneRequired: "Le numĆ©ro de tĆ©lĆ©phone est obligatoire", }, + emulator: { + infoMsg: + "SĆ©lectionnez une zone et les Ć©lections souhaitĆ©es pour tester la session IVR.", + apiStatus: { + unavailable: + "Le systĆØme d'Ć©mulation n'est pas disponible dans votre environnement", + loading: "Chargement du systĆØme d'Ć©mulation", + error: "Erreur lors du chargement du systĆØme d'Ć©mulation", + }, + hints: { + title: "Conseils", + publishRequired: + "Toute modification apportĆ©e aux Ć©lections, aux scrutins ou aux candidats doit d'abord ĆŖtre publiĆ©e pour ĆŖtre disponible. Seuls les styles de bulletin publiĆ©s les plus rĆ©cents correspondant Ć  la zone seront utilisĆ©s dans l'Ć©mulateur.", + eventChangesImmediate: + "Les modifications apportĆ©es Ć  l'Ć©vĆ©nement Ć©lectoral, telles que la configuration IVR ou la personnalisation des messages, sont disponibles immĆ©diatement aprĆØs le redĆ©marrage de la session de l'Ć©mulateur.", + credentials: + 'L\'identifiant d\'Ć©lecteur et le code PIN valides sont "123" et "123".', + }, + sendDtmf: "Envoyer une entrĆ©e DTMF", + sendTimeout: "Envoyer l'expiration du dĆ©lai", + disconnected: "DĆ©connectĆ©", + startSession: "DĆ©marrer une nouvelle session", + endSession: "Terminer la session", + noStylesFound: "Aucun style de bulletin publiĆ© ne correspond Ć  vos sĆ©lections", + inputPlaceholder: + "Saisissez votre entrĆ©e (nombre maximal de chiffres={{maxDigits}}, entrĆ©es valides={{validInputs}}, dĆ©lai d'expiration={{timeout}} s)", + blacklistCaller: "Bloquer l'appelant", + elections: "Ɖlections", + area: "Zone", + }, }, stats: { elegibleVoters: "Ɖlecteurs", diff --git a/packages/admin-portal/src/translations/gl.ts b/packages/admin-portal/src/translations/gl.ts index d9080de27b4..cb0bd0c383f 100644 --- a/packages/admin-portal/src/translations/gl.ts +++ b/packages/admin-portal/src/translations/gl.ts @@ -569,6 +569,36 @@ const galegoTranslation: TranslationType = { noFilterMatch: "Ningunha entrada coincide cos filtros indicados", phoneRequired: "O nĆŗmero de telĆ©fono Ć© obrigatorio", }, + emulator: { + infoMsg: + "Seleccione unha Ć”rea e as eleccións desexadas para probar a sesión IVR.", + apiStatus: { + unavailable: "O sistema do emulador non estĆ” dispoƱible na sĆŗa contorna", + loading: "Cargando o sistema do emulador", + error: "Erro ao cargar o sistema do emulador", + }, + hints: { + title: "Suxestións", + publishRequired: + "Calquera cambio realizado nas eleccións, nas contendas ou nos candidatos debe publicarse primeiro para que estea dispoƱible. No emulador só se utilizarĆ”n os estilos de papeleta publicados mĆ”is recentemente para a Ć”rea correspondente.", + eventChangesImmediate: + "Os cambios realizados no evento electoral, como a configuración IVR ou as modificacións das mensaxes, estĆ”n dispoƱibles inmediatamente ao reiniciar a sesión do emulador.", + credentials: + 'O identificador de votante e o PIN vĆ”lidos son "123" e "123".', + }, + sendDtmf: "Enviar entrada DTMF", + sendTimeout: "Enviar tempo de espera", + disconnected: "Desconectado", + startSession: "Iniciar unha nova sesión", + endSession: "Finalizar a sesión", + noStylesFound: + "Non se atoparon estilos de papeleta publicados que coincidan coas sĆŗas seleccións", + inputPlaceholder: + "Introduza a sĆŗa entrada (mĆ”ximo de dĆ­xitos={{maxDigits}}, entradas vĆ”lidas={{validInputs}}, tempo de espera={{timeout}} s)", + blacklistCaller: "Bloquear a persoa que chama", + elections: "Eleccións", + area: "Ɓrea", + }, }, stats: { elegibleVoters: "Votantes Elixibles", diff --git a/packages/admin-portal/src/translations/nl.ts b/packages/admin-portal/src/translations/nl.ts index f41acf60974..f0cae0a2739 100644 --- a/packages/admin-portal/src/translations/nl.ts +++ b/packages/admin-portal/src/translations/nl.ts @@ -567,6 +567,35 @@ const dutchTranslation: TranslationType = { noFilterMatch: "Geen vermeldingen komen overeen met de opgegeven filters", phoneRequired: "Telefoonnummer is verplicht", }, + emulator: { + infoMsg: + "Selecteer een gebied en de gewenste verkiezingen om de IVR-sessie te doorlopen.", + apiStatus: { + unavailable: "Het emulatorsysteem is niet beschikbaar in uw omgeving", + loading: "Het emulatorsysteem wordt geladen", + error: "Fout bij het laden van het emulatorsysteem", + }, + hints: { + title: "Tips", + publishRequired: + "Wijzigingen aan verkiezingen, verkiezingsonderdelen of kandidaten moeten eerst worden gepubliceerd voordat ze beschikbaar zijn. Alleen de meest recent gepubliceerde stembiljetstijlen voor het overeenkomstige gebied worden in de emulator gebruikt.", + eventChangesImmediate: + "Wijzigingen aan het verkiezingsevenement, zoals de IVR-configuratie of aangepaste prompts, zijn direct beschikbaar nadat de emulatorsessie opnieuw is gestart.", + credentials: 'De geldige kiezer-ID en pincode zijn "123" en "123".', + }, + sendDtmf: "DTMF-invoer verzenden", + sendTimeout: "Time-out verzenden", + disconnected: "Verbinding verbroken", + startSession: "Nieuwe sessie starten", + endSession: "Sessie beĆ«indigen", + noStylesFound: + "Geen gepubliceerde stembiljetstijlen gevonden die overeenkomen met uw selecties", + inputPlaceholder: + "Voer uw invoer in (maximaal aantal cijfers={{maxDigits}}, geldige invoer={{validInputs}}, time-out={{timeout}} sec.)", + blacklistCaller: "Beller blokkeren", + elections: "Verkiezingen", + area: "Gebied", + }, }, stats: { elegibleVoters: "Stemgerechtigde Kiezers", diff --git a/packages/admin-portal/src/translations/tl.ts b/packages/admin-portal/src/translations/tl.ts index 30325937da9..9428f9d8209 100644 --- a/packages/admin-portal/src/translations/tl.ts +++ b/packages/admin-portal/src/translations/tl.ts @@ -568,6 +568,35 @@ const tagalogTranslation: TranslationType = { noFilterMatch: "Walang entry na tumutugma sa ibinigay na mga filter", phoneRequired: "Kinakailangan ang numero ng telepono", }, + emulator: { + infoMsg: + "Pumili ng lugar at ng mga nais na halalan upang subukan ang IVR session.", + apiStatus: { + unavailable: "Hindi available ang emulator system sa iyong environment", + loading: "Nilo-load ang emulator system", + error: "Nagkaroon ng error sa pag-load ng emulator system", + }, + hints: { + title: "Mga pahiwatig", + publishRequired: + "Ang anumang pagbabagong ginawa sa mga halalan, mga contest, o mga kandidato ay dapat munang i-publish para maging available. Tanging ang pinakabagong na-publish na mga ballot style para sa tumutugmang lugar ang gagamitin sa emulator.", + eventChangesImmediate: + "Ang mga pagbabagong ginawa sa election event, gaya ng IVR configuration o mga pagbabago sa prompt, ay available kaagad kapag ni-restart ang emulator session.", + credentials: 'Ang valid na voter ID at PIN ay "123" at "123".', + }, + sendDtmf: "Magpadala ng DTMF input", + sendTimeout: "Magpadala ng timeout", + disconnected: "Nadiskonekta", + startSession: "Magsimula ng bagong session", + endSession: "Tapusin ang session", + noStylesFound: + "Walang nakitang na-publish na mga ballot style na tumutugma sa iyong mga pinili", + inputPlaceholder: + "Ilagay ang iyong input (max na digit={{maxDigits}}, mga valid na input={{validInputs}}, timeout={{timeout}} s)", + blacklistCaller: "I-block ang tumatawag", + elections: "Mga halalan", + area: "Lugar", + }, }, stats: { elegibleVoters: "Mga Kwalipikadong Botante", From 8f679d314e09bc7a24b3e738e610ef9496d586d8 Mon Sep 17 00:00:00 2001 From: Enes Selim Date: Tue, 28 Jul 2026 00:08:18 +0300 Subject: [PATCH 10/19] fix: actually translate the disconnect msg --- .../admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx b/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx index 49da045803c..d36e5e5b1a4 100644 --- a/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx +++ b/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx @@ -338,7 +338,7 @@ const EmulatorInterface: React.FC<{ {error ? {error} : null} {status === "Disconnected" ? ( - t("electionEventScreen.ivr.emulator.disconnected") + {t("electionEventScreen.ivr.emulator.disconnected")} ) : null} From 957092d7ea3567c0bd19c923bf0b0479e416e290 Mon Sep 17 00:00:00 2001 From: Enes Selim Date: Tue, 28 Jul 2026 00:17:10 +0300 Subject: [PATCH 11/19] imp: better "disconnected" notification --- .../resources/ElectionEvent/IvrEmulator.tsx | 108 ++++++++++-------- 1 file changed, 58 insertions(+), 50 deletions(-) diff --git a/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx b/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx index d36e5e5b1a4..2acfcfc76e7 100644 --- a/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx +++ b/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx @@ -335,11 +335,8 @@ const EmulatorInterface: React.FC<{ }, []) return ( - + {error ? {error} : null} - {status === "Disconnected" ? ( - {t("electionEventScreen.ivr.emulator.disconnected")} - ) : null} {prompts.map(([key, prompt]) => ( @@ -347,55 +344,66 @@ const EmulatorInterface: React.FC<{ ))} - -
    { - e.preventDefault() - canSendInput && sendInput() - }} - > - setInput(e.target.value.replace(/[^0-9*#]/g, ""))} - slotProps={{ - htmlInput: {pattern: "[0-9*#]*", maxLength: expectedInput?.max_digits}, + {status !== "Disconnected" ? ( + + { + e.preventDefault() + canSendInput && sendInput() }} - disabled={!expectedInput} - autoFocus - sx={{fontFamily: "monospace"}} - placeholder={t("electionEventScreen.ivr.emulator.inputPlaceholder", { - maxDigits: expectedInput?.max_digits ?? "", - validInputs: expectedInput?.valid_inputs ?? "", - timeout: expectedInput?.timeout ?? 0, - })} - /> - -
    - - -
    -
    + + + +
    + ) : null} + + {status === "Disconnected" ? ( + + {t("electionEventScreen.ivr.emulator.disconnected")} + + ) : null}
    ) } From 6d5d125a770f4b354f429e186ead6496dc867c08 Mon Sep 17 00:00:00 2001 From: Enes Selim Date: Tue, 28 Jul 2026 00:53:21 +0300 Subject: [PATCH 12/19] imp: use id instead of reserved "key" as prop name --- .../src/resources/ElectionEvent/IvrEmulator.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx b/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx index 2acfcfc76e7..b7bca0862d3 100644 --- a/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx +++ b/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx @@ -188,7 +188,7 @@ const ConfigForm: React.FC<{ ) } -const PromptLine: React.FC<{key: number; prompt: PromptInfo}> = ({key, prompt}) => { +const PromptLine: React.FC<{id: number; prompt: PromptInfo}> = ({id, prompt}) => { const promptBody = useMemo(() => { // Strip off the root ssml tag. return prompt.prompt_text.replace(/^/, "").replace(/<\/speak>$/, "") @@ -202,7 +202,7 @@ const PromptLine: React.FC<{key: number; prompt: PromptInfo}> = ({key, prompt}) return ( { - let key = nextLogId.current++ - setPrompts((current) => [...current, [key, prompt]]) + let id = nextLogId.current++ + setPrompts((current) => [...current, [id, prompt]]) } const canSendInput = useMemo( @@ -339,8 +339,8 @@ const EmulatorInterface: React.FC<{ {error ? {error} : null} - {prompts.map(([key, prompt]) => ( - + {prompts.map(([id, prompt]) => ( + ))} From 8a2a2a8b5a5a7b40302495a957aaa25b522958a3 Mon Sep 17 00:00:00 2001 From: Enes Selim Date: Tue, 28 Jul 2026 00:53:55 +0300 Subject: [PATCH 13/19] imp: better manage the lifecycle of wasm ivr emulators --- .../resources/ElectionEvent/IvrEmulator.tsx | 72 ++++++++++++++----- 1 file changed, 56 insertions(+), 16 deletions(-) diff --git a/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx b/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx index b7bca0862d3..cba37bde990 100644 --- a/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx +++ b/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx @@ -226,11 +226,12 @@ const EmulatorInterface: React.FC<{ const {t} = useTranslation() const [prompts, setPrompts] = useState<[number, PromptInfo][]>([]) const emulator = useRef(undefined) + const toDispose = useRef(new WeakSet()) + const inFlight = useRef(new WeakSet()) const [expectedInput, setExpectedInput] = useState() const [status, setStatus] = useState("Disconnected") const [error, setError] = useState("") const [input, setInput] = useState("") - const [executing, setExecuting] = useState(false) const nextLogId = useRef(0) const addPrompt = (prompt: PromptInfo) => { @@ -263,38 +264,65 @@ const EmulatorInterface: React.FC<{ } } - const sendTimeout = async () => { - if (!emulator.current) { + const sendTimeout = () => { + const current = emulator.current + if (!current) { console.warn("Attempted to sendTimeout without an active emulator") return } - emulator.current.send_timeout() - runEmulator(emulator.current) + if (inFlight.current.has(current)) { + return + } + + current.send_timeout() + runEmulator(current) } - const sendInput = async () => { - if (!emulator.current) { + const sendInput = () => { + const current = emulator.current + if (!current) { console.warn("Attempted to sendInput without an active emulator") return } - emulator.current.send_input(input) - runEmulator(emulator.current) + if (inFlight.current.has(current)) { + return + } + + current.send_input(input) + runEmulator(current) setInput("") } + const releaseDisposed = (disposed: IvrEmulatorDriver): void => { + if (!toDispose.current.has(disposed) || inFlight.current.has(disposed)) { + return + } + console.log("Releasing disposed emulator") + toDispose.current.delete(disposed) + disposed.free() + } + + // disposeEmulator must not be called again after the emulator has been released. const disposeEmulator = (disposed: IvrEmulatorDriver): void => { - console.info("Disposing the emulator") + console.debug("Disposing emulator") if (emulator.current === disposed) { emulator.current = undefined - console.info("Cleared current emulator") + console.debug("Cleared current emulator") } - disposed.free() + toDispose.current.add(disposed) + releaseDisposed(disposed) } const executeEmulatorLoop = async (emulator: IvrEmulatorDriver) => { while (true) { changeStatus("Running") const action = await emulator.execute(true) + + // If disposal was requested while we were executing the wasm, + // like the component being unmounted, free the resources and stop working. + if (toDispose.current.has(emulator)) { + return + } switch (action.type) { case "Prompt": addPrompt(action.prompt) @@ -316,22 +344,34 @@ const EmulatorInterface: React.FC<{ } const runEmulator = (emulator: IvrEmulatorDriver) => { - if (executing) { + if (inFlight.current.has(emulator)) { return } - setExecuting(true) + inFlight.current.add(emulator) executeEmulatorLoop(emulator) .catch((e) => { console.error("Failed to execute the emulator", e) - setError(`${e}`) + if (!toDispose.current.has(emulator)) { + setError(`${e}`) + disposeEmulator(emulator) + } + }) + .finally(() => { + inFlight.current.delete(emulator) + releaseDisposed(emulator) }) - .finally(() => setExecuting(false)) } useEffect(() => { setStatus("Ready") startSession(config) + + return () => { + if (emulator.current) { + disposeEmulator(emulator.current) + } + } }, []) return ( From bd057454d6d1d15655115ae06ec22d6212bf3473 Mon Sep 17 00:00:00 2001 From: Enes Selim Date: Tue, 28 Jul 2026 06:31:58 +0300 Subject: [PATCH 14/19] imp: load shim dynamically too Because offsets and names change on each compilation, we need to bundle it together with the wasm binary itself. --- .../providers/IvrEmulatorContextProvider.tsx | 2 +- .../admin-portal/src/services/IvrEmulator.ts | 81 +- .../services/generated/ivr_emulator_wasm.js | 743 ------------------ 3 files changed, 60 insertions(+), 766 deletions(-) delete mode 100644 packages/admin-portal/src/services/generated/ivr_emulator_wasm.js diff --git a/packages/admin-portal/src/providers/IvrEmulatorContextProvider.tsx b/packages/admin-portal/src/providers/IvrEmulatorContextProvider.tsx index 7fbf293213e..c75ba1ee2d3 100644 --- a/packages/admin-portal/src/providers/IvrEmulatorContextProvider.tsx +++ b/packages/admin-portal/src/providers/IvrEmulatorContextProvider.tsx @@ -16,7 +16,7 @@ export const IvrEmulatorContext = createContext = ({children}) => { const [status, setStatus] = useState(IvrApiStatus.LOADING) const [api, setApi] = useState(undefined) - const url = "/wasm/ivr_emulator_wasm_bg.wasm" + const url = "/wasm/ivr_emulator_wasm" useEffect(() => { loadIvrEmulator(url) diff --git a/packages/admin-portal/src/services/IvrEmulator.ts b/packages/admin-portal/src/services/IvrEmulator.ts index 1c2140275f5..40a42f8a0ce 100644 --- a/packages/admin-portal/src/services/IvrEmulator.ts +++ b/packages/admin-portal/src/services/IvrEmulator.ts @@ -1,7 +1,8 @@ -import loadIvrWasm, {WasmConfig, IvrEmulatorDriver} from "./generated/ivr_emulator_wasm" +import type {WasmConfig} from "./generated/ivr_emulator_wasm" +type IvrWasmModule = typeof import("./generated/ivr_emulator_wasm") + +export type IvrEmulatorApi = Pick -const api = {IvrEmulatorDriver} -export type IvrEmulatorApi = typeof api export type { IvrEmulatorDriver, Action, @@ -20,6 +21,20 @@ export class IvrEmulatorError extends Error { } } +const resolveUrls = (baseUrl: string) => { + const base = new URL(baseUrl, document.baseURI) + base.search = "" + base.hash = "" + + const jsUrl = new URL(base) + jsUrl.pathname += ".js" + + const wasmUrl = new URL(base) + wasmUrl.pathname += "_bg.wasm" + + return {jsUrl, wasmUrl} +} + const fetchWasm = async (url: string | URL): Promise => { let response: Response try { @@ -37,26 +52,48 @@ const fetchWasm = async (url: string | URL): Promise => { return response } -export const loadIvrEmulator = (wasmUrl: string): Promise | null => { +const fetchShim = async (url: URL): Promise => { + try { + return (await import( + /* @vite-ignore */ + /* webpackIgnore: true */ + url.href + )) as IvrWasmModule + } catch (cause) { + throw new IvrEmulatorError("load", `import of js shim failed from ${url}`, {cause}) + } +} + +const fetchAndLoad = async (baseUrl: string): Promise => { + const {jsUrl, wasmUrl} = resolveUrls(baseUrl) + const [ivrModule, wasmResponse] = await Promise.all([fetchShim(jsUrl), fetchWasm(wasmUrl)]) + + try { + // Let wasm bindgen init itself + await ivrModule.default({module_or_path: wasmResponse}) + } catch (cause) { + throw new IvrEmulatorError("load", `failed to load the wasm from "${wasmUrl}"`, {cause}) + } + try { + let config: WasmConfig = { + logging: localStorage.getItem("sq.ivr-emulator.logging") ?? undefined, + } + ivrModule.init(config) + } catch (cause) { + throw new IvrEmulatorError("init", `init call failed`, {cause}) + } + return ivrModule +} + +export const loadIvrEmulator = (baseUrl: string): Promise | null => { if (!initPromise) { - initPromise ??= fetchWasm(wasmUrl) - .then(async (moduleResp) => { - try { - let ivrWasm = await loadIvrWasm({module_or_path: moduleResp}) - let config: WasmConfig = { - logging: localStorage.getItem("sq.ivr-emulator.logging") ?? undefined, - } - ivrWasm.init(config) - } catch (cause) { - throw new IvrEmulatorError("load", "failed to load the wasm", {cause}) - } - - return api - }) - .catch((e: any) => { - console.error("Failed to init the emulator", e) - throw e - }) + initPromise ??= fetchAndLoad(baseUrl).catch((e: any) => { + // Allow retry + initPromise = null + + console.error("Failed to init the emulator", e) + throw e + }) } return initPromise diff --git a/packages/admin-portal/src/services/generated/ivr_emulator_wasm.js b/packages/admin-portal/src/services/generated/ivr_emulator_wasm.js deleted file mode 100644 index f9772c093e2..00000000000 --- a/packages/admin-portal/src/services/generated/ivr_emulator_wasm.js +++ /dev/null @@ -1,743 +0,0 @@ -/* @ts-self-types="./ivr_emulator_wasm.d.ts" */ - -export class IvrEmulatorDriver { - __destroy_into_raw() { - const ptr = this.__wbg_ptr; - this.__wbg_ptr = 0; - IvrEmulatorDriverFinalization.unregister(this); - return ptr; - } - free() { - const ptr = this.__destroy_into_raw(); - wasm.__wbg_ivremulatordriver_free(ptr, 0); - } - /** - * @returns {any} - */ - attributes() { - const ret = wasm.ivremulatordriver_attributes(this.__wbg_ptr); - if (ret[2]) { - throw takeFromExternrefTable0(ret[1]); - } - return takeFromExternrefTable0(ret[0]); - } - /** - * @param {boolean} until_io - * @returns {Action} - */ - execute(until_io) { - const ret = wasm.ivremulatordriver_execute(this.__wbg_ptr, until_io); - return ret; - } - /** - * @param {EmulatorConfig} config - */ - constructor(config) { - const ret = wasm.ivremulatordriver_new(config); - if (ret[2]) { - throw takeFromExternrefTable0(ret[1]); - } - this.__wbg_ptr = ret[0] >>> 0; - IvrEmulatorDriverFinalization.register(this, this.__wbg_ptr, this); - return this; - } - /** - * @param {string} input - */ - send_input(input) { - const ptr0 = passStringToWasm0(input, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - wasm.ivremulatordriver_send_input(this.__wbg_ptr, ptr0, len0); - } - send_timeout() { - wasm.ivremulatordriver_send_timeout(this.__wbg_ptr); - } -} -if (Symbol.dispose) IvrEmulatorDriver.prototype[Symbol.dispose] = IvrEmulatorDriver.prototype.free; - -/** - * @param {WasmConfig} wasm_config - */ -export function init(wasm_config) { - wasm.init(wasm_config); -} - -function __wbg_get_imports() { - const import0 = { - __proto__: null, - __wbg_Error_83742b46f01ce22d: function(arg0, arg1) { - const ret = Error(getStringFromWasm0(arg0, arg1)); - return ret; - }, - __wbg_String_8564e559799eccda: function(arg0, arg1) { - const ret = String(arg1); - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); - }, - __wbg___wbindgen_boolean_get_c0f3f60bac5a78d1: function(arg0) { - const v = arg0; - const ret = typeof(v) === 'boolean' ? v : undefined; - return isLikeNone(ret) ? 0xFFFFFF : ret ? 1 : 0; - }, - __wbg___wbindgen_debug_string_5398f5bb970e0daa: function(arg0, arg1) { - const ret = debugString(arg1); - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); - }, - __wbg___wbindgen_in_41dbb8413020e076: function(arg0, arg1) { - const ret = arg0 in arg1; - return ret; - }, - __wbg___wbindgen_is_function_3c846841762788c1: function(arg0) { - const ret = typeof(arg0) === 'function'; - return ret; - }, - __wbg___wbindgen_is_object_781bc9f159099513: function(arg0) { - const val = arg0; - const ret = typeof(val) === 'object' && val !== null; - return ret; - }, - __wbg___wbindgen_is_string_7ef6b97b02428fae: function(arg0) { - const ret = typeof(arg0) === 'string'; - return ret; - }, - __wbg___wbindgen_is_undefined_52709e72fb9f179c: function(arg0) { - const ret = arg0 === undefined; - return ret; - }, - __wbg___wbindgen_jsval_loose_eq_5bcc3bed3c69e72b: function(arg0, arg1) { - const ret = arg0 == arg1; - return ret; - }, - __wbg___wbindgen_number_get_34bb9d9dcfa21373: function(arg0, arg1) { - const obj = arg1; - const ret = typeof(obj) === 'number' ? obj : undefined; - getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true); - }, - __wbg___wbindgen_string_get_395e606bd0ee4427: function(arg0, arg1) { - const obj = arg1; - const ret = typeof(obj) === 'string' ? obj : undefined; - var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - var len1 = WASM_VECTOR_LEN; - getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); - }, - __wbg___wbindgen_throw_6ddd609b62940d55: function(arg0, arg1) { - throw new Error(getStringFromWasm0(arg0, arg1)); - }, - __wbg__wbg_cb_unref_6b5b6b8576d35cb1: function(arg0) { - arg0._wbg_cb_unref(); - }, - __wbg_call_2d781c1f4d5c0ef8: function() { return handleError(function (arg0, arg1, arg2) { - const ret = arg0.call(arg1, arg2); - return ret; - }, arguments); }, - __wbg_call_e133b57c9155d22c: function() { return handleError(function (arg0, arg1) { - const ret = arg0.call(arg1); - return ret; - }, arguments); }, - __wbg_crypto_38df2bab126b63dc: function(arg0) { - const ret = arg0.crypto; - return ret; - }, - __wbg_debug_eaef3b49d572d680: function(arg0, arg1) { - var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice(); - wasm.__wbindgen_free(arg0, arg1 * 4, 4); - console.debug(...v0); - }, - __wbg_done_08ce71ee07e3bd17: function(arg0) { - const ret = arg0.done; - return ret; - }, - __wbg_error_71b0e71161a5f3a0: function(arg0, arg1) { - var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice(); - wasm.__wbindgen_free(arg0, arg1 * 4, 4); - console.error(...v0); - }, - __wbg_error_a6fa202b58aa1cd3: function(arg0, arg1) { - let deferred0_0; - let deferred0_1; - try { - deferred0_0 = arg0; - deferred0_1 = arg1; - console.error(getStringFromWasm0(arg0, arg1)); - } finally { - wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); - } - }, - __wbg_getRandomValues_c44a50d8cfdaebeb: function() { return handleError(function (arg0, arg1) { - arg0.getRandomValues(arg1); - }, arguments); }, - __wbg_getTime_1dad7b5386ddd2d9: function(arg0) { - const ret = arg0.getTime(); - return ret; - }, - __wbg_getTimezoneOffset_639bcf2dde21158b: function(arg0) { - const ret = arg0.getTimezoneOffset(); - return ret; - }, - __wbg_get_326e41e095fb2575: function() { return handleError(function (arg0, arg1) { - const ret = Reflect.get(arg0, arg1); - return ret; - }, arguments); }, - __wbg_get_unchecked_329cfe50afab7352: function(arg0, arg1) { - const ret = arg0[arg1 >>> 0]; - return ret; - }, - __wbg_get_with_ref_key_6412cf3094599694: function(arg0, arg1) { - const ret = arg0[arg1]; - return ret; - }, - __wbg_instanceof_ArrayBuffer_101e2bf31071a9f6: function(arg0) { - let result; - try { - result = arg0 instanceof ArrayBuffer; - } catch (_) { - result = false; - } - const ret = result; - return ret; - }, - __wbg_instanceof_Uint8Array_740438561a5b956d: function(arg0) { - let result; - try { - result = arg0 instanceof Uint8Array; - } catch (_) { - result = false; - } - const ret = result; - return ret; - }, - __wbg_isArray_33b91feb269ff46e: function(arg0) { - const ret = Array.isArray(arg0); - return ret; - }, - __wbg_iterator_d8f549ec8fb061b1: function() { - const ret = Symbol.iterator; - return ret; - }, - __wbg_length_b3416cf66a5452c8: function(arg0) { - const ret = arg0.length; - return ret; - }, - __wbg_length_ea16607d7b61445b: function(arg0) { - const ret = arg0.length; - return ret; - }, - __wbg_log_7a0760e115750083: function(arg0, arg1) { - var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice(); - wasm.__wbindgen_free(arg0, arg1 * 4, 4); - console.log(...v0); - }, - __wbg_log_82574c09882d9d1e: function(arg0, arg1) { - console.log(getStringFromWasm0(arg0, arg1)); - }, - __wbg_msCrypto_bd5a034af96bcba6: function(arg0) { - const ret = arg0.msCrypto; - return ret; - }, - __wbg_new_0_1dcafdf5e786e876: function() { - const ret = new Date(); - return ret; - }, - __wbg_new_227d7c05414eb861: function() { - const ret = new Error(); - return ret; - }, - __wbg_new_49d5571bd3f0c4d4: function() { - const ret = new Map(); - return ret; - }, - __wbg_new_5f486cdf45a04d78: function(arg0) { - const ret = new Uint8Array(arg0); - return ret; - }, - __wbg_new_ab79df5bd7c26067: function() { - const ret = new Object(); - return ret; - }, - __wbg_new_fd94ca5c9639abd2: function(arg0) { - const ret = new Date(arg0); - return ret; - }, - __wbg_new_typed_aaaeaf29cf802876: function(arg0, arg1) { - try { - var state0 = {a: arg0, b: arg1}; - var cb0 = (arg0, arg1) => { - const a = state0.a; - state0.a = 0; - try { - return wasm_bindgen__convert__closures_____invoke__h331bfc0ec3639989(a, state0.b, arg0, arg1); - } finally { - state0.a = a; - } - }; - const ret = new Promise(cb0); - return ret; - } finally { - state0.a = state0.b = 0; - } - }, - __wbg_new_with_length_825018a1616e9e55: function(arg0) { - const ret = new Uint8Array(arg0 >>> 0); - return ret; - }, - __wbg_next_11b99ee6237339e3: function() { return handleError(function (arg0) { - const ret = arg0.next(); - return ret; - }, arguments); }, - __wbg_next_e01a967809d1aa68: function(arg0) { - const ret = arg0.next; - return ret; - }, - __wbg_node_84ea875411254db1: function(arg0) { - const ret = arg0.node; - return ret; - }, - __wbg_process_44c7a14e11e9f69e: function(arg0) { - const ret = arg0.process; - return ret; - }, - __wbg_prototypesetcall_d62e5099504357e6: function(arg0, arg1, arg2) { - Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2); - }, - __wbg_queueMicrotask_0c399741342fb10f: function(arg0) { - const ret = arg0.queueMicrotask; - return ret; - }, - __wbg_queueMicrotask_a082d78ce798393e: function(arg0) { - queueMicrotask(arg0); - }, - __wbg_randomFillSync_6c25eac9869eb53c: function() { return handleError(function (arg0, arg1) { - arg0.randomFillSync(arg1); - }, arguments); }, - __wbg_require_b4edbdcf3e2a1ef0: function() { return handleError(function () { - const ret = module.require; - return ret; - }, arguments); }, - __wbg_resolve_ae8d83246e5bcc12: function(arg0) { - const ret = Promise.resolve(arg0); - return ret; - }, - __wbg_set_6be42768c690e380: function(arg0, arg1, arg2) { - arg0[arg1] = arg2; - }, - __wbg_set_bf7251625df30a02: function(arg0, arg1, arg2) { - const ret = arg0.set(arg1, arg2); - return ret; - }, - __wbg_stack_3b0d974bbf31e44f: function(arg0, arg1) { - const ret = arg1.stack; - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); - }, - __wbg_static_accessor_GLOBAL_8adb955bd33fac2f: function() { - const ret = typeof global === 'undefined' ? null : global; - return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); - }, - __wbg_static_accessor_GLOBAL_THIS_ad356e0db91c7913: function() { - const ret = typeof globalThis === 'undefined' ? null : globalThis; - return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); - }, - __wbg_static_accessor_SELF_f207c857566db248: function() { - const ret = typeof self === 'undefined' ? null : self; - return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); - }, - __wbg_static_accessor_WINDOW_bb9f1ba69d61b386: function() { - const ret = typeof window === 'undefined' ? null : window; - return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); - }, - __wbg_subarray_a068d24e39478a8a: function(arg0, arg1, arg2) { - const ret = arg0.subarray(arg1 >>> 0, arg2 >>> 0); - return ret; - }, - __wbg_then_098abe61755d12f6: function(arg0, arg1) { - const ret = arg0.then(arg1); - return ret; - }, - __wbg_value_21fc78aab0322612: function(arg0) { - const ret = arg0.value; - return ret; - }, - __wbg_versions_276b2795b1c6a219: function(arg0) { - const ret = arg0.versions; - return ret; - }, - __wbg_warn_3a37cdd7216f1479: function(arg0, arg1) { - var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice(); - wasm.__wbindgen_free(arg0, arg1 * 4, 4); - console.warn(...v0); - }, - __wbindgen_cast_0000000000000001: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { dtor_idx: 245, function: Function { arguments: [Externref], shim_idx: 246, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. - const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen__closure__destroy__h7baa5a84311a3b04, wasm_bindgen__convert__closures_____invoke__h5fed3dcc0d3dcd7b); - return ret; - }, - __wbindgen_cast_0000000000000002: function(arg0) { - // Cast intrinsic for `F64 -> Externref`. - const ret = arg0; - return ret; - }, - __wbindgen_cast_0000000000000003: function(arg0, arg1) { - // Cast intrinsic for `Ref(Slice(U8)) -> NamedExternref("Uint8Array")`. - const ret = getArrayU8FromWasm0(arg0, arg1); - return ret; - }, - __wbindgen_cast_0000000000000004: function(arg0, arg1) { - // Cast intrinsic for `Ref(String) -> Externref`. - const ret = getStringFromWasm0(arg0, arg1); - return ret; - }, - __wbindgen_init_externref_table: function() { - const table = wasm.__wbindgen_externrefs; - const offset = table.grow(4); - table.set(0, undefined); - table.set(offset + 0, undefined); - table.set(offset + 1, null); - table.set(offset + 2, true); - table.set(offset + 3, false); - }, - }; - return { - __proto__: null, - "./ivr_emulator_wasm_bg.js": import0, - }; -} - -function wasm_bindgen__convert__closures_____invoke__h5fed3dcc0d3dcd7b(arg0, arg1, arg2) { - const ret = wasm.wasm_bindgen__convert__closures_____invoke__h5fed3dcc0d3dcd7b(arg0, arg1, arg2); - if (ret[1]) { - throw takeFromExternrefTable0(ret[0]); - } -} - -function wasm_bindgen__convert__closures_____invoke__h331bfc0ec3639989(arg0, arg1, arg2, arg3) { - wasm.wasm_bindgen__convert__closures_____invoke__h331bfc0ec3639989(arg0, arg1, arg2, arg3); -} - -const IvrEmulatorDriverFinalization = (typeof FinalizationRegistry === 'undefined') - ? { register: () => {}, unregister: () => {} } - : new FinalizationRegistry(ptr => wasm.__wbg_ivremulatordriver_free(ptr >>> 0, 1)); - -function addToExternrefTable0(obj) { - const idx = wasm.__externref_table_alloc(); - wasm.__wbindgen_externrefs.set(idx, obj); - return idx; -} - -const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined') - ? { register: () => {}, unregister: () => {} } - : new FinalizationRegistry(state => state.dtor(state.a, state.b)); - -function debugString(val) { - // primitive types - const type = typeof val; - if (type == 'number' || type == 'boolean' || val == null) { - return `${val}`; - } - if (type == 'string') { - return `"${val}"`; - } - if (type == 'symbol') { - const description = val.description; - if (description == null) { - return 'Symbol'; - } else { - return `Symbol(${description})`; - } - } - if (type == 'function') { - const name = val.name; - if (typeof name == 'string' && name.length > 0) { - return `Function(${name})`; - } else { - return 'Function'; - } - } - // objects - if (Array.isArray(val)) { - const length = val.length; - let debug = '['; - if (length > 0) { - debug += debugString(val[0]); - } - for(let i = 1; i < length; i++) { - debug += ', ' + debugString(val[i]); - } - debug += ']'; - return debug; - } - // Test for built-in - const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); - let className; - if (builtInMatches && builtInMatches.length > 1) { - className = builtInMatches[1]; - } else { - // Failed to match the standard '[object ClassName]' - return toString.call(val); - } - if (className == 'Object') { - // we're a user defined class or Object - // JSON.stringify avoids problems with cycles, and is generally much - // easier than looping through ownProperties of `val`. - try { - return 'Object(' + JSON.stringify(val) + ')'; - } catch (_) { - return 'Object'; - } - } - // errors - if (val instanceof Error) { - return `${val.name}: ${val.message}\n${val.stack}`; - } - // TODO we could test for more things here, like `Set`s and `Map`s. - return className; -} - -function getArrayJsValueFromWasm0(ptr, len) { - ptr = ptr >>> 0; - const mem = getDataViewMemory0(); - const result = []; - for (let i = ptr; i < ptr + 4 * len; i += 4) { - result.push(wasm.__wbindgen_externrefs.get(mem.getUint32(i, true))); - } - wasm.__externref_drop_slice(ptr, len); - return result; -} - -function getArrayU8FromWasm0(ptr, len) { - ptr = ptr >>> 0; - return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len); -} - -let cachedDataViewMemory0 = null; -function getDataViewMemory0() { - if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { - cachedDataViewMemory0 = new DataView(wasm.memory.buffer); - } - return cachedDataViewMemory0; -} - -function getStringFromWasm0(ptr, len) { - ptr = ptr >>> 0; - return decodeText(ptr, len); -} - -let cachedUint8ArrayMemory0 = null; -function getUint8ArrayMemory0() { - if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { - cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); - } - return cachedUint8ArrayMemory0; -} - -function handleError(f, args) { - try { - return f.apply(this, args); - } catch (e) { - const idx = addToExternrefTable0(e); - wasm.__wbindgen_exn_store(idx); - } -} - -function isLikeNone(x) { - return x === undefined || x === null; -} - -function makeMutClosure(arg0, arg1, dtor, f) { - const state = { a: arg0, b: arg1, cnt: 1, dtor }; - const real = (...args) => { - - // First up with a closure we increment the internal reference - // count. This ensures that the Rust closure environment won't - // be deallocated while we're invoking it. - state.cnt++; - const a = state.a; - state.a = 0; - try { - return f(a, state.b, ...args); - } finally { - state.a = a; - real._wbg_cb_unref(); - } - }; - real._wbg_cb_unref = () => { - if (--state.cnt === 0) { - state.dtor(state.a, state.b); - state.a = 0; - CLOSURE_DTORS.unregister(state); - } - }; - CLOSURE_DTORS.register(real, state, state); - return real; -} - -function passStringToWasm0(arg, malloc, realloc) { - if (realloc === undefined) { - const buf = cachedTextEncoder.encode(arg); - const ptr = malloc(buf.length, 1) >>> 0; - getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); - WASM_VECTOR_LEN = buf.length; - return ptr; - } - - let len = arg.length; - let ptr = malloc(len, 1) >>> 0; - - const mem = getUint8ArrayMemory0(); - - let offset = 0; - - for (; offset < len; offset++) { - const code = arg.charCodeAt(offset); - if (code > 0x7F) break; - mem[ptr + offset] = code; - } - if (offset !== len) { - if (offset !== 0) { - arg = arg.slice(offset); - } - ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; - const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); - const ret = cachedTextEncoder.encodeInto(arg, view); - - offset += ret.written; - ptr = realloc(ptr, len, offset, 1) >>> 0; - } - - WASM_VECTOR_LEN = offset; - return ptr; -} - -function takeFromExternrefTable0(idx) { - const value = wasm.__wbindgen_externrefs.get(idx); - wasm.__externref_table_dealloc(idx); - return value; -} - -let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); -cachedTextDecoder.decode(); -const MAX_SAFARI_DECODE_BYTES = 2146435072; -let numBytesDecoded = 0; -function decodeText(ptr, len) { - numBytesDecoded += len; - if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) { - cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); - cachedTextDecoder.decode(); - numBytesDecoded = len; - } - return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); -} - -const cachedTextEncoder = new TextEncoder(); - -if (!('encodeInto' in cachedTextEncoder)) { - cachedTextEncoder.encodeInto = function (arg, view) { - const buf = cachedTextEncoder.encode(arg); - view.set(buf); - return { - read: arg.length, - written: buf.length - }; - }; -} - -let WASM_VECTOR_LEN = 0; - -let wasmModule, wasm; -function __wbg_finalize_init(instance, module) { - wasm = instance.exports; - wasmModule = module; - cachedDataViewMemory0 = null; - cachedUint8ArrayMemory0 = null; - wasm.__wbindgen_start(); - return wasm; -} - -async function __wbg_load(module, imports) { - if (typeof Response === 'function' && module instanceof Response) { - if (typeof WebAssembly.instantiateStreaming === 'function') { - try { - return await WebAssembly.instantiateStreaming(module, imports); - } catch (e) { - const validResponse = module.ok && expectedResponseType(module.type); - - if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') { - console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); - - } else { throw e; } - } - } - - const bytes = await module.arrayBuffer(); - return await WebAssembly.instantiate(bytes, imports); - } else { - const instance = await WebAssembly.instantiate(module, imports); - - if (instance instanceof WebAssembly.Instance) { - return { instance, module }; - } else { - return instance; - } - } - - function expectedResponseType(type) { - switch (type) { - case 'basic': case 'cors': case 'default': return true; - } - return false; - } -} - -function initSync(module) { - if (wasm !== undefined) return wasm; - - - if (module !== undefined) { - if (Object.getPrototypeOf(module) === Object.prototype) { - ({module} = module) - } else { - console.warn('using deprecated parameters for `initSync()`; pass a single object instead') - } - } - - const imports = __wbg_get_imports(); - if (!(module instanceof WebAssembly.Module)) { - module = new WebAssembly.Module(module); - } - const instance = new WebAssembly.Instance(module, imports); - return __wbg_finalize_init(instance, module); -} - -async function __wbg_init(module_or_path) { - if (wasm !== undefined) return wasm; - - - if (module_or_path !== undefined) { - if (Object.getPrototypeOf(module_or_path) === Object.prototype) { - ({module_or_path} = module_or_path) - } else { - console.warn('using deprecated parameters for the initialization function; pass a single object instead') - } - } - - - const imports = __wbg_get_imports(); - - if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { - module_or_path = fetch(module_or_path); - } - - const { instance, module } = await __wbg_load(await module_or_path, imports); - - return __wbg_finalize_init(instance, module); -} - -export { initSync, __wbg_init as default }; From c6f5c161f97df27eae6ea6aa00a94a0d9f3c30c0 Mon Sep 17 00:00:00 2001 From: Enes Selim Date: Tue, 28 Jul 2026 08:35:16 +0300 Subject: [PATCH 15/19] imp: allow setting the emulator wasm url from deployment --- packages/admin-portal/public/global-settings.json | 1 + .../src/providers/IvrEmulatorContextProvider.tsx | 7 ++++++- .../admin-portal/src/providers/SettingsContextProvider.tsx | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/admin-portal/public/global-settings.json b/packages/admin-portal/public/global-settings.json index e4f33917b39..7961d0b587e 100644 --- a/packages/admin-portal/public/global-settings.json +++ b/packages/admin-portal/public/global-settings.json @@ -4,6 +4,7 @@ "ONLINE_VOTING_CLIENT_ID": "admin-portal", "KEYCLOAK_URL": "http://localhost:8090/", "HASURA_URL": "http://localhost:8080/v1/graphql", + "IVR_EMULATOR_BASE_URL": "/wasm/ivr_emulator_wasm", "APP_VERSION": "dev", "APP_HASH": "dev", "DEFAULT_EMAIL_SUBJECT": {"en": "Participate in {{election_event.name}}"}, diff --git a/packages/admin-portal/src/providers/IvrEmulatorContextProvider.tsx b/packages/admin-portal/src/providers/IvrEmulatorContextProvider.tsx index c75ba1ee2d3..ddfb9e82531 100644 --- a/packages/admin-portal/src/providers/IvrEmulatorContextProvider.tsx +++ b/packages/admin-portal/src/providers/IvrEmulatorContextProvider.tsx @@ -1,5 +1,6 @@ import React, {createContext, useContext, useEffect, useState} from "react" import {IvrEmulatorApi, IvrEmulatorError, loadIvrEmulator} from "@/services/IvrEmulator" +import {SettingsContext} from "@/providers/SettingsContextProvider" export enum IvrApiStatus { UNAVAILABLE = "unavailable", @@ -12,11 +13,15 @@ export interface IvrEmulatorContextType { status: IvrApiStatus api?: IvrEmulatorApi } + export const IvrEmulatorContext = createContext(undefined) export const IvrEmulatorContextProvider: React.FC<{children: React.ReactNode}> = ({children}) => { const [status, setStatus] = useState(IvrApiStatus.LOADING) const [api, setApi] = useState(undefined) - const url = "/wasm/ivr_emulator_wasm" + const {globalSettings} = useContext(SettingsContext) + const url = globalSettings.IVR_EMULATOR_BASE_URL + ? globalSettings.IVR_EMULATOR_BASE_URL + : "/wasm/ivr_emulator_wasm" useEffect(() => { loadIvrEmulator(url) diff --git a/packages/admin-portal/src/providers/SettingsContextProvider.tsx b/packages/admin-portal/src/providers/SettingsContextProvider.tsx index cc671d77a14..81a1207821a 100644 --- a/packages/admin-portal/src/providers/SettingsContextProvider.tsx +++ b/packages/admin-portal/src/providers/SettingsContextProvider.tsx @@ -22,6 +22,7 @@ export interface GlobalSettings { PUBLIC_BUCKET_URL: string VOTING_PORTAL_URL: string RESULTS_PORTAL_URL: string + IVR_EMULATOR_BASE_URL: string ACTIVATE_MIRU_EXPORT: boolean CUSTOM_URLS_DOMAIN_NAME: string } @@ -87,6 +88,7 @@ const defaultSettingsValues: SettingsContextValues = { PUBLIC_BUCKET_URL: "http://127.0.0.1:9002/public/", VOTING_PORTAL_URL: "http://localhost:3000", RESULTS_PORTAL_URL: "http://localhost:3004", + IVR_EMULATOR_BASE_URL: "/wasm/ivr_emulator_wasm", ACTIVATE_MIRU_EXPORT: false, CUSTOM_URLS_DOMAIN_NAME: "google.com", }, From 9f0588b16fa46e9aa5071f30f6d3826450c5fe32 Mon Sep 17 00:00:00 2001 From: Enes Selim Date: Tue, 28 Jul 2026 08:35:29 +0300 Subject: [PATCH 16/19] imp: improve fetch error handling --- packages/admin-portal/src/services/IvrEmulator.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/admin-portal/src/services/IvrEmulator.ts b/packages/admin-portal/src/services/IvrEmulator.ts index 40a42f8a0ce..f1c891f1b46 100644 --- a/packages/admin-portal/src/services/IvrEmulator.ts +++ b/packages/admin-portal/src/services/IvrEmulator.ts @@ -18,6 +18,9 @@ export class IvrEmulatorError extends Error { options?: ErrorOptions ) { super(`Ivr emulator "${operation}" failed: ${msg}`, options) + + this.name = new.target.name + Object.setPrototypeOf(this, new.target.prototype) } } @@ -60,7 +63,7 @@ const fetchShim = async (url: URL): Promise => { url.href )) as IvrWasmModule } catch (cause) { - throw new IvrEmulatorError("load", `import of js shim failed from ${url}`, {cause}) + throw new IvrEmulatorError("fetch", `import of js shim failed from ${url}`, {cause}) } } From aa457ff65b414fd3a0a11cb71ba5346401ded532 Mon Sep 17 00:00:00 2001 From: Enes Selim Date: Tue, 28 Jul 2026 08:52:32 +0300 Subject: [PATCH 17/19] chore: slightly more understandable comment about emulator cleanup logic --- .../admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx b/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx index cba37bde990..f244af32cf1 100644 --- a/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx +++ b/packages/admin-portal/src/resources/ElectionEvent/IvrEmulator.tsx @@ -319,7 +319,8 @@ const EmulatorInterface: React.FC<{ const action = await emulator.execute(true) // If disposal was requested while we were executing the wasm, - // like the component being unmounted, free the resources and stop working. + // such as the component being unmounted, stop working + // and allow releasing the emulator with its normal flow (the finally block). if (toDispose.current.has(emulator)) { return } From 1d8332bfd1c458a0da7c4bfc5299173de2751ad0 Mon Sep 17 00:00:00 2001 From: Enes Selim Date: Tue, 28 Jul 2026 09:01:03 +0300 Subject: [PATCH 18/19] chore: fix licenses for the new files --- REUSE.toml | 104 +++++++++--------- .../providers/IvrEmulatorContextProvider.tsx | 3 + .../admin-portal/src/services/IvrEmulator.ts | 3 + 3 files changed, 61 insertions(+), 49 deletions(-) diff --git a/REUSE.toml b/REUSE.toml index d3b93aac1de..f42d898658d 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -9,53 +9,53 @@ SPDX-PackageDownloadLocation = "https://github.com/sequentech" [[annotations]] path = [ - "hasura/**", - ".ackrc", - "packages/.ackrc", - "packages/.gitignore", - "packages/yarn.lock", - "packages/devenv.lock", - "packages/Cargo.lock", - "packages/Cargo.lock.copy", - ".envrc", - ".release-it.json", - ".release-commit", - "devenv.lock", - "devenv.nix", - "graphql.schema.json", - "yarn.lock", - "packages/.ignore", - "packages/admin-portal/.ignore", - "packages/step-cli/.ignore", - "packages/ballot-verifier/.ignore", - "packages/braid/.ignore", - "packages/sequent-core/.ignore", - "packages/step-cli/src/graphql/**", - "packages/strand/.ignore", - "packages/ui-essentials/.ignore", - "packages/voting-portal/.ignore", - "packages/windmill/.ignore", - "packages/package.json", - "packages/admin-portal/graphql.schema.json", - "packages/admin-portal/logs/**", - "packages/admin-portal/tests_output/**", - "packages/voting-portal/graphql.schema.json", - "packages/voting-portal/logs/**", - "packages/voting-portal/src/gql/**", - "packages/voting-portal/tests_output/**", - "packages/voting-portal/reports/**", - "packages/windmill/src/graphql/**", - "packages/immudb-rs/proto/immudb/**", - "packages/keycloak-extensions/message-otp-authenticator/src/main/resources/theme-resources/templates/**", - "packages/keycloak-extensions/inetum-authenticator/src/main/resources/theme-resources/resources/inetum-sdk-4.0.3/**", - "packages/keycloak-extensions/conditional-authenticators/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper", - "docs/assets/**", - "docs/docusaurus/**", - ".devcontainer/minio/public-assets/**", - ".devcontainer/minio/certs.json", - "packages/orare/doc_renderer/assets/**", - "packages/windmill/external-bin/janitor/**.xlsx", - "packages/step-cli/rust-local-target/release/step-cli" + "hasura/**", + ".ackrc", + "packages/.ackrc", + "packages/.gitignore", + "packages/yarn.lock", + "packages/devenv.lock", + "packages/Cargo.lock", + "packages/Cargo.lock.copy", + ".envrc", + ".release-it.json", + ".release-commit", + "devenv.lock", + "devenv.nix", + "graphql.schema.json", + "yarn.lock", + "packages/.ignore", + "packages/admin-portal/.ignore", + "packages/step-cli/.ignore", + "packages/ballot-verifier/.ignore", + "packages/braid/.ignore", + "packages/sequent-core/.ignore", + "packages/step-cli/src/graphql/**", + "packages/strand/.ignore", + "packages/ui-essentials/.ignore", + "packages/voting-portal/.ignore", + "packages/windmill/.ignore", + "packages/package.json", + "packages/admin-portal/graphql.schema.json", + "packages/admin-portal/logs/**", + "packages/admin-portal/tests_output/**", + "packages/voting-portal/graphql.schema.json", + "packages/voting-portal/logs/**", + "packages/voting-portal/src/gql/**", + "packages/voting-portal/tests_output/**", + "packages/voting-portal/reports/**", + "packages/windmill/src/graphql/**", + "packages/immudb-rs/proto/immudb/**", + "packages/keycloak-extensions/message-otp-authenticator/src/main/resources/theme-resources/templates/**", + "packages/keycloak-extensions/inetum-authenticator/src/main/resources/theme-resources/resources/inetum-sdk-4.0.3/**", + "packages/keycloak-extensions/conditional-authenticators/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper", + "docs/assets/**", + "docs/docusaurus/**", + ".devcontainer/minio/public-assets/**", + ".devcontainer/minio/certs.json", + "packages/orare/doc_renderer/assets/**", + "packages/windmill/external-bin/janitor/**.xlsx", + "packages/step-cli/rust-local-target/release/step-cli" ] precedence = "aggregate" SPDX-FileCopyrightText = "2025 Sequent Tech " @@ -75,8 +75,8 @@ SPDX-License-Identifier = "Apache-2.0" [[annotations]] path = [ - "packages/admin-portal/public/intl-tel-input/**", - "packages/keycloak-extensions/sequent-theme/src/main/resources/theme/sequent.admin-portal/login/resources/intl-tel-input-23.3.2/**" + "packages/admin-portal/public/intl-tel-input/**", + "packages/keycloak-extensions/sequent-theme/src/main/resources/theme/sequent.admin-portal/login/resources/intl-tel-input-23.3.2/**" ] precedence = "aggregate" SPDX-FileCopyrightText = ["", "2014-2016 Jack O'Connor"] @@ -105,3 +105,9 @@ path = "packages/admin-portal/public/sql-wasm.wasm" precedence = "aggregate" SPDX-FileCopyrightText = ["", "2025 Sequent Tech "] SPDX-License-Identifier = "MIT" + +[[annotations]] +path = "packages/admin-portal/src/services/generated/ivr_emulator_wasm.d.ts" +precedence = "aggregate" +SPDX-FileCopyrightText = ["", "2026 Sequent Tech Inc "] +SPDX-License-Identifier = "AGPL-3.0-only" diff --git a/packages/admin-portal/src/providers/IvrEmulatorContextProvider.tsx b/packages/admin-portal/src/providers/IvrEmulatorContextProvider.tsx index ddfb9e82531..817a102268d 100644 --- a/packages/admin-portal/src/providers/IvrEmulatorContextProvider.tsx +++ b/packages/admin-portal/src/providers/IvrEmulatorContextProvider.tsx @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 Sequent Tech Inc +// +// SPDX-License-Identifier: AGPL-3.0-only import React, {createContext, useContext, useEffect, useState} from "react" import {IvrEmulatorApi, IvrEmulatorError, loadIvrEmulator} from "@/services/IvrEmulator" import {SettingsContext} from "@/providers/SettingsContextProvider" diff --git a/packages/admin-portal/src/services/IvrEmulator.ts b/packages/admin-portal/src/services/IvrEmulator.ts index f1c891f1b46..8903d544dfe 100644 --- a/packages/admin-portal/src/services/IvrEmulator.ts +++ b/packages/admin-portal/src/services/IvrEmulator.ts @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 Sequent Tech Inc +// +// SPDX-License-Identifier: AGPL-3.0-only import type {WasmConfig} from "./generated/ivr_emulator_wasm" type IvrWasmModule = typeof import("./generated/ivr_emulator_wasm") From 16752dc076eec3905525f7a4f3fbd86f7d7c2236 Mon Sep 17 00:00:00 2001 From: Enes Selim Date: Tue, 28 Jul 2026 09:03:01 +0300 Subject: [PATCH 19/19] chore: ignore format of the generated type file --- packages/admin-portal/.prettierignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/admin-portal/.prettierignore b/packages/admin-portal/.prettierignore index 8fa0b5d75c0..ef65cdec781 100644 --- a/packages/admin-portal/.prettierignore +++ b/packages/admin-portal/.prettierignore @@ -47,4 +47,5 @@ src/gql/ .github public/tinymce/ public/intl-tel-input/ -public/sql-wasm.js \ No newline at end of file +public/sql-wasm.js +src/services/generated/ivr_emulator_wasm.d.ts