426 lines
23 KiB
TypeScript
426 lines
23 KiB
TypeScript
// file: kb-app-demo-desktop/frontend/ts/demo_decode_replay.ts
|
||
// version: 9
|
||
|
||
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 {
|
||
applyStoreBlockMetadata,
|
||
createStoreBlockState,
|
||
cursorForBlock,
|
||
markStoreBlockFiltersDirty,
|
||
resetStoreBlockState,
|
||
renderStoreBlockControls,
|
||
type StoreBlockControls,
|
||
} from "./demo_store_block_pagination.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 { DemoTransactionAnnotationPagePayload } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoTransactionAnnotationPagePayload.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;
|
||
const annotationBlockControls: StoreBlockControls = {
|
||
firstSelector: "#annotationFirstBlockButton",
|
||
previousSelector: "#annotationPreviousBlockButton",
|
||
nextSelector: "#annotationNextBlockButton",
|
||
summarySelector: "#annotationBlockSummary",
|
||
};
|
||
const annotationBlockState = createStoreBlockState();
|
||
type AnnotationBaseRequest = Omit<DemoTransactionAnnotationRequest, "cursor" | "blockIndex">;
|
||
let annotationBaseRequest: AnnotationBaseRequest | null = null;
|
||
|
||
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 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<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, 250, 500],
|
||
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 startAnnotationLoad(): Promise<void> {
|
||
annotationBaseRequest = {
|
||
signatureContains: inputValue("#annotationSignatureInput") || null,
|
||
};
|
||
resetStoreBlockState(annotationBlockState);
|
||
await loadAnnotationBlock(1, null);
|
||
}
|
||
|
||
async function loadAnnotationBlock(blockIndex: number, cursor: string | null): Promise<void> {
|
||
if (annotationBaseRequest === null) {
|
||
throw new Error("Rechargez les annotations pour initialiser les filtres Store.");
|
||
}
|
||
const request: DemoTransactionAnnotationRequest = {
|
||
...annotationBaseRequest,
|
||
cursor,
|
||
blockIndex,
|
||
};
|
||
try {
|
||
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_annotations block=${blockIndex}`);
|
||
const payload = await invoke<DemoTransactionAnnotationPagePayload>("demo_decode_replay_annotations", { request });
|
||
renderAnnotations(payload.rows);
|
||
applyStoreBlockMetadata(annotationBlockState, payload.page, annotationBlockControls);
|
||
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_annotations completed block=${payload.page.blockIndex}/${payload.page.totalBlocks} rows=${payload.rows.length}`);
|
||
} catch (caughtError) {
|
||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||
element<HTMLElement>("#annotationJournalStatus").textContent = `Erreur : ${message}`;
|
||
renderStoreBlockControls(annotationBlockState, annotationBlockControls);
|
||
frontendError("kb_app_demo_desktop.frontend.demo_decode_replay", `Annotation journal loading failed: ${message}`);
|
||
}
|
||
}
|
||
|
||
async function loadPreviousAnnotationBlock(): Promise<void> {
|
||
const targetBlock = annotationBlockState.currentBlock - 1;
|
||
const cursor = cursorForBlock(annotationBlockState, targetBlock);
|
||
if (targetBlock < 1 || cursor === undefined) {
|
||
return;
|
||
}
|
||
await loadAnnotationBlock(targetBlock, cursor);
|
||
}
|
||
|
||
async function loadNextAnnotationBlock(): Promise<void> {
|
||
if (annotationBlockState.nextCursor === null) {
|
||
return;
|
||
}
|
||
await loadAnnotationBlock(annotationBlockState.currentBlock + 1, annotationBlockState.nextCursor);
|
||
}
|
||
|
||
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 startAnnotationLoad());
|
||
element<HTMLButtonElement>(annotationBlockControls.firstSelector).addEventListener("click", () => void loadAnnotationBlock(1, null));
|
||
element<HTMLButtonElement>(annotationBlockControls.previousSelector).addEventListener("click", () => void loadPreviousAnnotationBlock());
|
||
element<HTMLButtonElement>(annotationBlockControls.nextSelector).addEventListener("click", () => void loadNextAnnotationBlock());
|
||
element<HTMLInputElement>("#annotationSignatureInput").addEventListener("input", () => {
|
||
annotationBaseRequest = null;
|
||
markStoreBlockFiltersDirty(annotationBlockState, annotationBlockControls);
|
||
});
|
||
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}`);
|
||
});
|
||
});
|