// file: kb-app-demo-desktop/frontend/ts/demo_decode_replay.ts // version: 8 import * as bootstrap from "bootstrap"; import "simplebar"; import ResizeObserver from "resize-observer-polyfill"; import { invoke } from "@tauri-apps/api/core"; import DataTable, { type Api } from "datatables.net-bs5"; import "datatables.net-bs5/css/dataTables.bootstrap5.css"; import { listen } from "@tauri-apps/api/event"; import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log.ts"; import type { DemoDecodeDiagnosticsPayload } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoDecodeDiagnosticsPayload.ts"; import type { DemoDecodeReplayOptionsPayload } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoDecodeReplayOptionsPayload.ts"; import type { DemoDecodeReplayProgressPayload } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoDecodeReplayProgressPayload.ts"; import type { DemoDecodeReplayRequest } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoDecodeReplayRequest.ts"; import type { DemoDecodeReplaySummaryPayload } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoDecodeReplaySummaryPayload.ts"; import type { DemoTransactionAnnotationRequest } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoTransactionAnnotationRequest.ts"; import type { DemoTransactionAnnotationRow } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoTransactionAnnotationRow.ts"; (window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap; (window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver; const logLines: string[] = []; let running = false; let materializationAvailable = false; function element(selector: string): T { const value = document.querySelector(selector); if (!value) { throw new Error(`Missing UI element: ${selector}`); } return value; } function inputValue(selector: string): string { return element(selector).value.trim(); } function integerValue(selector: string): number { const value = Number.parseInt(inputValue(selector), 10); if (!Number.isFinite(value) || value < 1) { throw new Error(`Valeur numérique invalide pour ${selector}`); } return value; } function appendLog(payload: DemoDecodeReplayProgressPayload | { timestamp: string; level: string; message: string }): void { const progress = "completed" in payload ? ` [${payload.completed}/${payload.total}]` : ""; const campaign = "campaignId" in payload ? ` [${payload.campaignId}]` : ""; logLines.push(`${payload.timestamp} ${payload.level.toUpperCase()}${campaign}${progress} ${payload.message}`); while (logLines.length > 1000) { logLines.shift(); } const output = element("#decodeReplayLogOutput"); output.value = logLines.join("\n"); output.scrollTop = output.scrollHeight; } function explicitSignatureCount(): number { return element("#decodeReplaySignaturesTextarea") .value .split(/\r?\n/) .filter(value => value.trim().length > 0) .length; } function updateReplayScopeControls(): void { const signaturesPresent = explicitSignatureCount() > 0; const incompleteSignatures = inputValue("#decodeReplayStateSelect") === "incomplete_signatures"; const allMatchingInput = element("#decodeReplayAllSignaturesInput"); const forceInput = element("#decodeReplayForceInput"); const materializeInput = element("#decodeReplayMaterializeInput"); if (!running && (signaturesPresent || incompleteSignatures) && allMatchingInput.checked) { allMatchingInput.checked = false; } allMatchingInput.disabled = running || signaturesPresent || incompleteSignatures; const forceScopeMissing = !signaturesPresent && !allMatchingInput.checked; forceInput.disabled = running || forceScopeMissing || incompleteSignatures; if (!running && (forceScopeMissing || incompleteSignatures)) { forceInput.checked = false; } materializeInput.disabled = running || !materializationAvailable; if (!running && !materializationAvailable) { materializeInput.checked = false; } } function setRunning(value: boolean): void { running = value; element("#startDecodeReplayButton").disabled = value; element("#cancelDecodeReplayButton").disabled = !value; const badge = element("#decodeReplayStatusBadge"); badge.textContent = value ? "Replay en cours" : "Prêt"; badge.className = value ? "badge text-bg-warning" : "badge text-bg-success"; updateReplayScopeControls(); } function selectedDecoderNames(): string[] { return Array.from(document.querySelectorAll(".decode-replay-decoder-input:checked")) .map(input => input.value); } function buildRequest(): DemoDecodeReplayRequest { return { signaturesText: element("#decodeReplaySignaturesTextarea").value || null, programId: inputValue("#decodeReplayProgramIdInput") || null, instructionState: inputValue("#decodeReplayStateSelect"), instructionPathsText: element("#decodeReplayPathsTextarea").value || null, decoderNames: selectedDecoderNames(), limit: integerValue("#decodeReplayLimitInput"), maxConcurrentInputs: integerValue("#decodeReplayConcurrencyInput"), allCompatible: element("#decodeReplayAllCompatibleInput").checked, forceReplay: element("#decodeReplayForceInput").checked, forceReplayAllMatching: element("#decodeReplayAllSignaturesInput").checked, materializeAfterDecode: element("#decodeReplayMaterializeInput").checked, }; } function validationMessage(request: DemoDecodeReplayRequest): string | null { const signatureCount = request.signaturesText ? request.signaturesText.split(/\r?\n/).filter(value => value.trim().length > 0).length : 0; if (request.decoderNames.length === 0) { return "Sélectionner au moins un décodeur."; } if (request.instructionState === "incomplete_signatures" && request.forceReplay) { return "Le mode signatures incomplètes rejoue son périmètre sans Force replay global."; } if (request.forceReplay && signatureCount === 0 && !request.forceReplayAllMatching) { return "Le force replay nécessite des signatures explicites ou l’autorisation Toutes les signatures."; } if (request.forceReplayAllMatching && signatureCount > 0) { return "Le mode « Toutes les signatures » ne peut pas être combiné avec une liste de signatures explicites."; } if (request.forceReplayAllMatching && !request.forceReplay) { return "Le mode « Toutes les signatures » est uniquement disponible avec Force replay."; } if (request.materializeAfterDecode && !materializationAvailable) { return "Aucun matérialiseur n’est actuellement enregistré dans cette application."; } return null; } function reportValidationWarning(message: string): void { element("#decodeReplaySummaryOutput").value = JSON.stringify({ validation: message }, null, 2); appendLog({ timestamp: new Date().toISOString(), level: "warn", message }); frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Decode replay request rejected locally: ${message}`); } async function executeDecodeReplay(): Promise { if (running) { return; } try { const request = buildRequest(); const invalid = validationMessage(request); if (invalid) { reportValidationWarning(invalid); return; } setRunning(true); element("#decodeReplaySummaryOutput").value = "Exécution en cours..."; const signatureCount = request.signaturesText ? request.signaturesText.split(/\r?\n/).filter(value => value.trim().length > 0).length : 0; const requestSummary = `signatures=${signatureCount}, programId=${request.programId ?? "auto"}, state=${request.instructionState}, decoders=${request.decoderNames.join(",")}, limit=${request.limit}, concurrency=${request.maxConcurrentInputs}, allCompatible=${request.allCompatible}, forceReplay=${request.forceReplay}, allSignatures=${request.forceReplayAllMatching}, materialize=${request.materializeAfterDecode}`; appendLog({ timestamp: new Date().toISOString(), level: "info", message: `Démarrage du replay de décodage contextualisé : ${requestSummary}` }); frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_execute ${requestSummary}`); const summary = await invoke("demo_decode_replay_execute", { request }); element("#decodeReplaySummaryOutput").value = JSON.stringify(summary, null, 2); frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_execute completed summary=${JSON.stringify(summary)}`); appendLog({ timestamp: new Date().toISOString(), level: summary.cancelled || summary.failedInputs > 0 ? "warn" : "info", message: `Campagne ${summary.campaignId} terminée : completed=${summary.completed}, unmatched=${summary.unmatched}, failed=${summary.failedInputs}, notStarted=${summary.notStarted}`, }); } catch (caughtError) { const message = caughtError instanceof Error ? caughtError.message : String(caughtError); element("#decodeReplaySummaryOutput").value = JSON.stringify({ error: message }, null, 2); appendLog({ timestamp: new Date().toISOString(), level: "error", message }); frontendError("kb_app_demo_desktop.frontend.demo_decode_replay", `Decode replay failed: ${message}`); } finally { setRunning(false); } } async function cancelDecodeReplay(): Promise { try { frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", "Invoke demo_decode_replay_cancel"); const accepted = await invoke("demo_decode_replay_cancel"); frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_cancel completed accepted=${accepted}`); appendLog({ timestamp: new Date().toISOString(), level: accepted ? "warn" : "info", message: accepted ? "Demande d'arrêt envoyée." : "Aucune campagne active.", }); } catch (caughtError) { const message = caughtError instanceof Error ? caughtError.message : String(caughtError); frontendError("kb_app_demo_desktop.frontend.demo_decode_replay", `Cancellation failed: ${message}`); } } async function loadDiagnostics(): Promise { try { frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", "Invoke demo_decode_replay_diagnostics"); const diagnostics = await invoke("demo_decode_replay_diagnostics"); frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_diagnostics completed resources=${diagnostics.resources.length} coverage=${diagnostics.coverage.length}`); element("#decodeReplayDiagnosticsOutput").value = JSON.stringify(diagnostics, null, 2); } catch (caughtError) { const message = caughtError instanceof Error ? caughtError.message : String(caughtError); element("#decodeReplayDiagnosticsOutput").value = JSON.stringify({ error: message }, null, 2); frontendError("kb_app_demo_desktop.frontend.demo_decode_replay", `Diagnostics loading failed: ${message}`); } } function tableCell(value: string, title?: string): HTMLTableCellElement { const cell = document.createElement("td"); cell.textContent = value; if (title) { cell.title = title; } return cell; } function shortened(value: string, visible: number): string { return value.length > visible ? `${value.slice(0, visible)}…` : value; } let annotationJournalTable: Api | null = null; function renderAnnotations(rows: DemoTransactionAnnotationRow[]): void { annotationJournalTable?.destroy(); annotationJournalTable = null; const body = element("#annotationJournalBody"); body.replaceChildren(); for (const row of rows) { const tableRow = document.createElement("tr"); tableRow.append( tableCell(row.slot), tableCell(shortened(row.signature, 18), row.signature), tableCell(row.instructionPath), tableCell(row.generation, row.programId), tableCell(row.text), tableCell(String(row.payloadLengthBytes)), tableCell(row.verifiedSigners.join(", ") || "—"), tableCell(`${shortened(row.payloadSha256, 12)} · ${row.decoder}`, `${row.payloadSha256}\n${row.idempotenceKey}\ntransaction_annotations@${row.processorVersion}`), ); body.append(tableRow); } annotationJournalTable = new DataTable("#annotationJournalTable", { autoWidth: false, pageLength: 25, lengthMenu: [10, 25, 50, 100], order: [[0, "desc"]], scrollX: true, language: { emptyTable: "Aucune annotation.", info: "_START_ à _END_ sur _TOTAL_ annotations", infoEmpty: "0 annotation", lengthMenu: "Afficher _MENU_ annotations", search: "Filtrer :", zeroRecords: "Aucune annotation correspondante.", }, }); element("#annotationJournalStatus").textContent = `${rows.length} annotation(s) committed chargée(s).`; } async function loadAnnotations(): Promise { const request: DemoTransactionAnnotationRequest = { signatureContains: inputValue("#annotationSignatureInput") || null, limit: integerValue("#annotationLimitInput"), }; try { frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_annotations limit=${request.limit}`); const rows = await invoke("demo_decode_replay_annotations", { request }); renderAnnotations(rows); frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_annotations completed rows=${rows.length}`); } catch (caughtError) { const message = caughtError instanceof Error ? caughtError.message : String(caughtError); element("#annotationJournalStatus").textContent = `Erreur : ${message}`; frontendError("kb_app_demo_desktop.frontend.demo_decode_replay", `Annotation journal loading failed: ${message}`); } } async function openWindow(command: "open_demo_store_core_window" | "open_demo_store_replay_candidates_window"): Promise { try { frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke ${command}`); await invoke(command); frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke ${command} completed`); } catch (caughtError) { const message = caughtError instanceof Error ? caughtError.message : String(caughtError); frontendError("kb_app_demo_desktop.frontend.demo_decode_replay", `Window opening failed: ${message}`); } } async function loadOptions(): Promise { frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", "Invoke demo_decode_replay_options"); const options = await invoke("demo_decode_replay_options"); frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_options completed options=${JSON.stringify(options)}`); element("#decodeReplayVersionBadge").textContent = `Pipeline ${options.pipelineVersion}`; element("#decodeReplayLimitInput").value = String(options.defaultLimit); element("#decodeReplayConcurrencyInput").value = String(options.defaultMaxConcurrentInputs); const programOptions = element("#decodeReplayProgramIdOptions"); programOptions.replaceChildren(); for (const program of options.programs) { const option = document.createElement("option"); option.value = program.programId; option.label = program.code; programOptions.append(option); } const container = element("#decodeReplayDecoderList"); container.innerHTML = ""; for (const decoder of options.decoders) { const wrapper = document.createElement("div"); wrapper.className = "form-check border rounded p-3 ps-5 mb-2"; const input = document.createElement("input"); input.className = "form-check-input decode-replay-decoder-input"; input.type = "checkbox"; input.value = decoder.name; input.id = `decode-${decoder.name}`; input.checked = true; const label = document.createElement("label"); label.className = "form-check-label"; label.htmlFor = input.id; label.textContent = `${decoder.name} v${decoder.version} — ${decoder.programIds.length} programmes`; wrapper.append(input, label); container.append(wrapper); } materializationAvailable = options.materializerNames.length > 0; const materializeHelp = element("#decodeReplayMaterializeHelp"); materializeHelp.textContent = materializationAvailable ? `Matérialiseurs disponibles : ${options.materializerNames.join(", ")}` : "Aucun matérialiseur enregistré : option désactivée."; setRunning(options.running); } document.addEventListener("DOMContentLoaded", () => { installFrontendConsoleBridge("kb_app_demo_desktop.frontend.demo_decode_replay"); frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", "decode replay demo window loaded"); document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(item => new bootstrap.Tooltip(item)); element("#decodeReplaySignaturesTextarea").addEventListener("input", () => updateReplayScopeControls()); element("#decodeReplayStateSelect").addEventListener("change", () => updateReplayScopeControls()); element("#decodeReplayAllSignaturesInput").addEventListener("change", event => { const input = event.currentTarget as HTMLInputElement; if (input.checked) { element("#decodeReplayForceInput").checked = true; } updateReplayScopeControls(); }); element("#decodeReplayForceInput").addEventListener("change", event => { const input = event.currentTarget as HTMLInputElement; if (!input.checked) { element("#decodeReplayAllSignaturesInput").checked = false; } updateReplayScopeControls(); }); element("#startDecodeReplayButton").addEventListener("click", () => void executeDecodeReplay()); element("#cancelDecodeReplayButton").addEventListener("click", () => void cancelDecodeReplay()); element("#loadDecodeDiagnosticsButton").addEventListener("click", () => void loadDiagnostics()); element("#loadAnnotationsButton").addEventListener("click", () => void loadAnnotations()); element("#openReplayCandidatesFromDecodeButton").addEventListener("click", () => void openWindow("open_demo_store_replay_candidates_window")); element("#openCoreDiagnosticsFromDecodeButton").addEventListener("click", () => void openWindow("open_demo_store_core_window")); void listen("demo-decode-replay-progress", event => appendLog(event.payload)); void loadOptions().catch(caughtError => { const message = caughtError instanceof Error ? caughtError.message : String(caughtError); appendLog({ timestamp: new Date().toISOString(), level: "error", message }); frontendError("kb_app_demo_desktop.frontend.demo_decode_replay", `Options loading failed: ${message}`); }); });