Files
khadhroony-bot3/kb-app-demo-desktop/frontend/ts/demo_decode_replay.ts
2026-08-11 22:22:40 +02:00

370 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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<T extends HTMLElement>(selector: string): T {
const value = document.querySelector<T>(selector);
if (!value) {
throw new Error(`Missing UI element: ${selector}`);
}
return value;
}
function inputValue(selector: string): string {
return element<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>(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<HTMLTextAreaElement>("#decodeReplayLogOutput");
output.value = logLines.join("\n");
output.scrollTop = output.scrollHeight;
}
function explicitSignatureCount(): number {
return element<HTMLTextAreaElement>("#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<HTMLInputElement>("#decodeReplayAllSignaturesInput");
const forceInput = element<HTMLInputElement>("#decodeReplayForceInput");
const materializeInput = element<HTMLInputElement>("#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<HTMLButtonElement>("#startDecodeReplayButton").disabled = value;
element<HTMLButtonElement>("#cancelDecodeReplayButton").disabled = !value;
const badge = element<HTMLElement>("#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<HTMLInputElement>(".decode-replay-decoder-input:checked"))
.map(input => input.value);
}
function buildRequest(): DemoDecodeReplayRequest {
return {
signaturesText: element<HTMLTextAreaElement>("#decodeReplaySignaturesTextarea").value || null,
programId: inputValue("#decodeReplayProgramIdInput") || null,
instructionState: inputValue("#decodeReplayStateSelect"),
instructionPathsText: element<HTMLTextAreaElement>("#decodeReplayPathsTextarea").value || null,
decoderNames: selectedDecoderNames(),
limit: integerValue("#decodeReplayLimitInput"),
maxConcurrentInputs: integerValue("#decodeReplayConcurrencyInput"),
allCompatible: element<HTMLInputElement>("#decodeReplayAllCompatibleInput").checked,
forceReplay: element<HTMLInputElement>("#decodeReplayForceInput").checked,
forceReplayAllMatching: element<HTMLInputElement>("#decodeReplayAllSignaturesInput").checked,
materializeAfterDecode: element<HTMLInputElement>("#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 lautorisation 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 nest actuellement enregistré dans cette application.";
}
return null;
}
function reportValidationWarning(message: string): void {
element<HTMLTextAreaElement>("#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<void> {
if (running) {
return;
}
try {
const request = buildRequest();
const invalid = validationMessage(request);
if (invalid) {
reportValidationWarning(invalid);
return;
}
setRunning(true);
element<HTMLTextAreaElement>("#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<DemoDecodeReplaySummaryPayload>("demo_decode_replay_execute", { request });
element<HTMLTextAreaElement>("#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<HTMLTextAreaElement>("#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<void> {
try {
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", "Invoke demo_decode_replay_cancel");
const accepted = await invoke<boolean>("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<void> {
try {
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", "Invoke demo_decode_replay_diagnostics");
const diagnostics = await invoke<DemoDecodeDiagnosticsPayload>("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<HTMLTextAreaElement>("#decodeReplayDiagnosticsOutput").value = JSON.stringify(diagnostics, null, 2);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLTextAreaElement>("#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<DemoTransactionAnnotationRow> | null = null;
function renderAnnotations(rows: DemoTransactionAnnotationRow[]): void {
annotationJournalTable?.destroy();
annotationJournalTable = null;
const body = element<HTMLTableSectionElement>("#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<DemoTransactionAnnotationRow>("#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<HTMLElement>("#annotationJournalStatus").textContent = `${rows.length} annotation(s) committed chargée(s).`;
}
async function loadAnnotations(): Promise<void> {
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<DemoTransactionAnnotationRow[]>("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<HTMLElement>("#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<void> {
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<void> {
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", "Invoke demo_decode_replay_options");
const options = await invoke<DemoDecodeReplayOptionsPayload>("demo_decode_replay_options");
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_options completed options=${JSON.stringify(options)}`);
element<HTMLElement>("#decodeReplayVersionBadge").textContent = `Pipeline ${options.pipelineVersion}`;
element<HTMLInputElement>("#decodeReplayLimitInput").value = String(options.defaultLimit);
element<HTMLInputElement>("#decodeReplayConcurrencyInput").value = String(options.defaultMaxConcurrentInputs);
const programOptions = element<HTMLDataListElement>("#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<HTMLElement>("#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<HTMLElement>("#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<HTMLTextAreaElement>("#decodeReplaySignaturesTextarea").addEventListener("input", () => updateReplayScopeControls());
element<HTMLSelectElement>("#decodeReplayStateSelect").addEventListener("change", () => updateReplayScopeControls());
element<HTMLInputElement>("#decodeReplayAllSignaturesInput").addEventListener("change", event => {
const input = event.currentTarget as HTMLInputElement;
if (input.checked) {
element<HTMLInputElement>("#decodeReplayForceInput").checked = true;
}
updateReplayScopeControls();
});
element<HTMLInputElement>("#decodeReplayForceInput").addEventListener("change", event => {
const input = event.currentTarget as HTMLInputElement;
if (!input.checked) {
element<HTMLInputElement>("#decodeReplayAllSignaturesInput").checked = false;
}
updateReplayScopeControls();
});
element<HTMLButtonElement>("#startDecodeReplayButton").addEventListener("click", () => void executeDecodeReplay());
element<HTMLButtonElement>("#cancelDecodeReplayButton").addEventListener("click", () => void cancelDecodeReplay());
element<HTMLButtonElement>("#loadDecodeDiagnosticsButton").addEventListener("click", () => void loadDiagnostics());
element<HTMLButtonElement>("#loadAnnotationsButton").addEventListener("click", () => void loadAnnotations());
element<HTMLButtonElement>("#openReplayCandidatesFromDecodeButton").addEventListener("click", () => void openWindow("open_demo_store_replay_candidates_window"));
element<HTMLButtonElement>("#openCoreDiagnosticsFromDecodeButton").addEventListener("click", () => void openWindow("open_demo_store_core_window"));
void listen<DemoDecodeReplayProgressPayload>("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}`);
});
});