// file: kb-app-demo-desktop/frontend/ts/demo_store_replay_candidates.ts // version: 14 import * as bootstrap from "bootstrap"; import "simplebar"; import ResizeObserver from "resize-observer-polyfill"; import DataTable, { type Api } from "datatables.net-bs5"; import "datatables.net-bs5/css/dataTables.bootstrap5.css"; import "datatables.net-select-bs5"; import "datatables.net-select-bs5/css/select.bootstrap5.css"; import { invoke } from "@tauri-apps/api/core"; import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log"; import { applyStoreBlockMetadata, createStoreBlockState, cursorForBlock, markStoreBlockFiltersDirty, resetStoreBlockState, renderStoreBlockControls, type StoreBlockControls, type StoreBlockState, } from "./demo_store_block_pagination"; import type { DemoStoreReplayEntityPagePayload } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayEntityPagePayload"; import type { DemoStoreReplayEntityRequest } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayEntityRequest"; import type { DemoStoreReplayEntityRow } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayEntityRow"; import type { DemoStoreReplayKnownProgramOption } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayKnownProgramOption"; import type { DemoStoreReplayOptionsPayload } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayOptionsPayload"; import type { DemoStoreReplayProgramPagePayload } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayProgramPagePayload"; import type { DemoStoreReplayProgramRequest } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayProgramRequest"; import type { DemoStoreReplayProgramRow } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayProgramRow"; import type { DemoStoreReplayTransactionPagePayload } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayTransactionPagePayload"; import type { DemoStoreReplayTransactionRequest } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayTransactionRequest"; import type { DemoStoreReplayTransactionRow } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayTransactionRow"; (window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap; (window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver; type EntityKindCode = "mint" | "owner" | "account_key"; type EntityViewConfig = { kind: EntityKindCode; label: string; pluralLabel: string; tableSelector: string; containsSelector: string; blockControls: StoreBlockControls; summarySelector: string; loadButtonSelector: string; resetButtonSelector: string; useButtonSelector: string; copyButtonSelector: string; exportButtonSelector: string; fileName: string; }; const tracingTarget = "kb_app_demo_desktop.frontend.demo_store_replay_candidates"; const entityViewConfigs: EntityViewConfig[] = [ { kind: "mint", label: "mint", pluralLabel: "mints", tableSelector: "#mintCandidateTable", containsSelector: "#mintContainsInput", blockControls: { firstSelector: "#mintFirstBlockButton", previousSelector: "#mintPreviousBlockButton", nextSelector: "#mintNextBlockButton", summarySelector: "#mintBlockSummary", }, summarySelector: "#mintResultSummary", loadButtonSelector: "#loadMintsButton", resetButtonSelector: "#resetMintsButton", useButtonSelector: "#useSelectedMintsButton", copyButtonSelector: "#copyMintsButton", exportButtonSelector: "#exportMintsCsvButton", fileName: "replay_mints.csv", }, { kind: "owner", label: "owner", pluralLabel: "owners", tableSelector: "#ownerCandidateTable", containsSelector: "#ownerContainsInput", blockControls: { firstSelector: "#ownerFirstBlockButton", previousSelector: "#ownerPreviousBlockButton", nextSelector: "#ownerNextBlockButton", summarySelector: "#ownerBlockSummary", }, summarySelector: "#ownerResultSummary", loadButtonSelector: "#loadOwnersButton", resetButtonSelector: "#resetOwnersButton", useButtonSelector: "#useSelectedOwnersButton", copyButtonSelector: "#copyOwnersButton", exportButtonSelector: "#exportOwnersCsvButton", fileName: "replay_owners.csv", }, { kind: "account_key", label: "compte", pluralLabel: "comptes", tableSelector: "#accountCandidateTable", containsSelector: "#accountContainsInput", blockControls: { firstSelector: "#accountFirstBlockButton", previousSelector: "#accountPreviousBlockButton", nextSelector: "#accountNextBlockButton", summarySelector: "#accountBlockSummary", }, summarySelector: "#accountResultSummary", loadButtonSelector: "#loadAccountsButton", resetButtonSelector: "#resetAccountsButton", useButtonSelector: "#useSelectedAccountsButton", copyButtonSelector: "#copyAccountsButton", exportButtonSelector: "#exportAccountsCsvButton", fileName: "replay_account_keys.csv", }, ]; let transactionTable: Api | null = null; let programTable: Api | null = null; let mintTable: Api | null = null; let ownerTable: Api | null = null; let accountTable: Api | null = null; let busyOperationCount = 0; const transactionBlockControls: StoreBlockControls = { firstSelector: "#transactionFirstBlockButton", previousSelector: "#transactionPreviousBlockButton", nextSelector: "#transactionNextBlockButton", summarySelector: "#transactionBlockSummary", }; const programBlockControls: StoreBlockControls = { firstSelector: "#programFirstBlockButton", previousSelector: "#programPreviousBlockButton", nextSelector: "#programNextBlockButton", summarySelector: "#programBlockSummary", }; const transactionBlockState = createStoreBlockState(); const programBlockState = createStoreBlockState(); const entityBlockStates = new Map([ ["mint", createStoreBlockState()], ["owner", createStoreBlockState()], ["account_key", createStoreBlockState()], ]); type TransactionBaseRequest = Omit; type ProgramBaseRequest = Omit; type EntityBaseRequest = Omit; let transactionBaseRequest: TransactionBaseRequest | null = null; let programBaseRequest: ProgramBaseRequest | null = null; const entityBaseRequests = new Map(); function element(selector: string): T { const value = document.querySelector(selector); if (!value) { throw new Error(`Missing UI element: ${selector}`); } return value; } function optionalInputValue(selector: string): string | null { const value = element(selector).value.trim(); return value.length === 0 ? null : value; } function requiredInputValue(selector: string): string { const value = optionalInputValue(selector); if (value === null) { throw new Error(`Valeur obligatoire manquante pour ${selector}`); } return value; } function optionalNonNegativeIntegerValue(selector: string): number | null { const raw = optionalInputValue(selector); if (raw === null) { return null; } const value = Number.parseInt(raw, 10); if (!Number.isSafeInteger(value) || value < 0) { throw new Error(`Valeur entière positive ou nulle invalide pour ${selector}`); } return value; } function escapeHtml(value: unknown): string { const text = String(value ?? ""); return text .split("&").join("&") .split("<").join("<") .split(">").join(">") .split('"').join(""") .split("'").join("'"); } function codeDisplay(value: unknown, type: string): string { if (type !== "display") { return String(value ?? ""); } return `${escapeHtml(value)}`; } function compactIdentifier(value: string, leadingLength: number = 12, trailingLength: number = 10): string { if (value.length <= leadingLength + trailingLength + 1) { return value; } return `${value.slice(0, leadingLength)}…${value.slice(-trailingLength)}`; } function copyableCodeDisplay(label: string): (value: unknown, type: string) => string { return (value: unknown, type: string): string => { const text = String(value ?? ""); if (type !== "display") { return text; } const escapedText = escapeHtml(text); const compactText = escapeHtml(compactIdentifier(text)); const escapedLabel = escapeHtml(label); return `${compactText}`; }; } function badgeDisplay(value: unknown, type: string): string { if (type !== "display") { return String(value ?? ""); } const text = String(value ?? "—"); const className = text === "succeeded" || text === "core_extracted" ? "text-bg-success" : text === "failed" ? "text-bg-danger" : text === "running" || text === "received" ? "text-bg-warning" : "text-bg-secondary"; return `${escapeHtml(text)}`; } function coreStatusText(row: DemoStoreReplayTransactionRow): string { if (!row.hasCoreTransaction) { return "absent"; } if (row.transactionFailed === true) { return "échec on-chain"; } return "présent"; } function coreDisplay(_value: unknown, type: string, row: DemoStoreReplayTransactionRow): string | number { if (type === "display") { if (!row.hasCoreTransaction) { return 'absent'; } if (row.transactionFailed === true) { return 'échec on-chain'; } return 'présent'; } if (type === "filter") { return coreStatusText(row); } if (row.transactionFailed === true) { return 0; } return row.hasCoreTransaction ? 1 : 2; } function occurrenceRatioDisplay(_value: unknown, type: string, row: DemoStoreReplayEntityRow): string | number { const ratio = row.transactionCount > 0 ? row.occurrenceCount / row.transactionCount : 0; if (type === "display" || type === "filter") { return ratio.toFixed(2); } return ratio; } function slotSpanDisplay(_value: unknown, type: string, row: DemoStoreReplayEntityRow): string | number { const span = Math.max(0, row.maxSlot - row.minSlot); if (type === "display" || type === "filter") { return String(span); } return span; } function commonLanguage(emptyTable: string, infoLabel: string, zeroRecords: string): object { return { emptyTable, info: `_START_ à _END_ sur _TOTAL_ ${infoLabel}`, infoEmpty: `0 ${infoLabel}`, infoFiltered: "(filtré depuis _MAX_)", lengthMenu: "Afficher _MENU_", search: "Filtre local :", zeroRecords, paginate: { first: "Premier", last: "Dernier", next: "Suivant", previous: "Précédent" }, select: { rows: { _: "%d lignes sélectionnées", 0: "", 1: "1 ligne sélectionnée" } }, }; } function createTransactionTable(): Api { return new DataTable("#transactionCandidateTable", { data: [], columns: [ { data: null, className: "replay-select-column text-center", width: "1%", orderable: false, searchable: false, render: DataTable.render.select(), }, { data: "signature", render: copyableCodeDisplay("la signature") }, { data: "slot" }, { data: "rawProcessingState", render: badgeDisplay }, { data: "hasCoreTransaction", render: coreDisplay }, { data: "ledgerStatus", render: badgeDisplay }, { data: "processorVersion", defaultContent: "—", render: codeDisplay }, { data: "attemptCount" }, { data: "topLevelInstructionCount" }, { data: "innerInstructionCount" }, { data: "topLevelProgramCount" }, { data: "innerProgramCount" }, { data: "updatedAt" }, ], order: [[2, "desc"]], pageLength: 10, lengthMenu: [10, 25, 50, 100, 250, 500], select: { style: "multi", selector: "td:first-child", headerCheckbox: "select-all" }, language: commonLanguage( "Aucune transaction chargée", "transactions", "Aucune transaction ne correspond au filtre local", ), drawCallback: () => initializeTooltips(), }); } function createProgramTable(): Api { return new DataTable("#programCandidateTable", { data: [], columns: [ { data: null, className: "replay-select-column text-center", width: "1%", orderable: false, searchable: false, render: DataTable.render.select(), }, { data: "programCode", defaultContent: "—", render: codeDisplay }, { data: "programId", render: copyableCodeDisplay("le program ID") }, { data: "transactionCount" }, { data: "topLevelInstructionCount" }, { data: "innerInstructionCount" }, { data: "logCount" }, { data: "minSlot" }, { data: "maxSlot" }, ], order: [[3, "desc"]], pageLength: 10, lengthMenu: [10, 25, 50, 100, 250, 500], select: { style: "multi", selector: "td:first-child", headerCheckbox: "select-all" }, language: commonLanguage( "Aucun programme chargé", "programmes", "Aucun programme ne correspond au filtre local", ), drawCallback: () => initializeTooltips(), }); } function createEntityTable(config: EntityViewConfig): Api { return new DataTable(config.tableSelector, { data: [], columns: [ { data: null, className: "replay-select-column text-center", width: "1%", orderable: false, searchable: false, render: DataTable.render.select(), }, { data: "entityValue", render: copyableCodeDisplay(config.label) }, { data: "transactionCount" }, { data: "occurrenceCount" }, { data: null, render: occurrenceRatioDisplay }, { data: "minSlot" }, { data: "maxSlot" }, { data: null, render: slotSpanDisplay }, ], order: [[2, "desc"]], pageLength: 10, lengthMenu: [10, 25, 50, 100, 250, 500], select: { style: "multi", selector: "td:first-child", headerCheckbox: "select-all" }, language: commonLanguage( `Aucun ${config.label} chargé`, config.pluralLabel, `Aucun ${config.label} ne correspond au filtre local`, ), drawCallback: () => initializeTooltips(), }); } function entityTable(kind: EntityKindCode): Api | null { if (kind === "mint") { return mintTable; } if (kind === "owner") { return ownerTable; } return accountTable; } function replaceRows(table: Api, rows: T[]): void { table.clear(); table.rows.add(rows); table.draw(); } function resetTableState(table: Api | null): void { if (table === null) { return; } table.search(""); table.columns().search(""); table.rows().deselect(); table.draw(); } function selectedOrFilteredRows(table: Api): T[] { const selected = table.rows({ selected: true, search: "applied" }).data().toArray(); if (selected.length > 0) { return selected; } return table.rows({ search: "applied" }).data().toArray(); } function selectedOrSingleFilteredRow(table: Api, label: string): T { const selected = table.rows({ selected: true }).data().toArray(); if (selected.length === 1) { return selected[0]; } if (selected.length > 1) { throw new Error(`Plusieurs ${label} sont cochés. Conservez une seule sélection.`); } const filtered = table.rows({ search: "applied" }).data().toArray(); if (filtered.length === 1) { return filtered[0]; } throw new Error(`Cochez exactement un ${label}, ou réduisez le filtre local à une seule ligne.`); } function restoreLoadedStoreBlockControls(): void { if (transactionBlockState.currentBlock > 0) { renderStoreBlockControls(transactionBlockState, transactionBlockControls); } if (programBlockState.currentBlock > 0) { renderStoreBlockControls(programBlockState, programBlockControls); } for (const config of entityViewConfigs) { const state = entityBlockState(config.kind); if (state.currentBlock > 0) { renderStoreBlockControls(state, config.blockControls); } } } function setBusy(busy: boolean, message: string): void { busyOperationCount = busy ? busyOperationCount + 1 : Math.max(0, busyOperationCount - 1); const operationInProgress = busyOperationCount > 0; const badge = element("#replayCandidateStatusBadge"); badge.textContent = operationInProgress ? (busy ? message : "Chargement en cours") : "Prêt"; badge.className = operationInProgress ? "badge text-bg-warning" : "badge text-bg-success"; document.querySelectorAll("button").forEach(button => { if (button.id === "openCoreExtractionButton") { return; } if (!operationInProgress && button.dataset.storeBlockNavigation === "true") { return; } button.disabled = operationInProgress; }); if (!operationInProgress) { restoreLoadedStoreBlockControls(); } } function buildTransactionBaseRequest(): TransactionBaseRequest { const entityKind = optionalInputValue("#transactionEntityKindSelect"); const entityValue = optionalInputValue("#transactionEntityValueInput"); if ((entityKind === null) !== (entityValue === null)) { throw new Error("Le type d’entité et sa valeur doivent être renseignés ensemble."); } return { signatureContains: optionalInputValue("#transactionSignatureContainsInput"), minSlot: optionalNonNegativeIntegerValue("#transactionMinSlotInput"), maxSlot: optionalNonNegativeIntegerValue("#transactionMaxSlotInput"), rawProcessingState: optionalInputValue("#transactionRawStateSelect"), ledgerStatus: optionalInputValue("#transactionLedgerStatusSelect"), programId: optionalInputValue("#transactionProgramIdInput"), programScope: requiredInputValue("#transactionProgramScopeSelect"), entityKind, entityValue, newestFirst: element("#transactionNewestFirstInput").checked, }; } function buildProgramBaseRequest(): ProgramBaseRequest { return { programIdContains: optionalInputValue("#programContainsInput"), }; } function buildEntityBaseRequest(config: EntityViewConfig): EntityBaseRequest { return { entityKind: config.kind, entityValueContains: optionalInputValue(config.containsSelector), }; } function entityBlockState(kind: EntityKindCode): StoreBlockState { const state = entityBlockStates.get(kind); if (state === undefined) { throw new Error(`État de pagination Store manquant pour ${kind}.`); } return state; } async function resetTransactionFilters(): Promise { element("#transactionSignatureContainsInput").value = ""; element("#transactionMinSlotInput").value = ""; element("#transactionMaxSlotInput").value = ""; element("#transactionRawStateSelect").value = ""; element("#transactionLedgerStatusSelect").value = ""; element("#transactionProgramIdInput").value = ""; element("#transactionProgramScopeSelect").value = "any"; element("#transactionEntityKindSelect").value = ""; element("#transactionEntityValueInput").value = ""; element("#transactionNewestFirstInput").checked = true; resetTableState(transactionTable); await startTransactionLoad(); } async function resetProgramFilters(): Promise { element("#knownProgramSelect").value = ""; element("#programContainsInput").value = ""; resetTableState(programTable); await startProgramLoad(); } async function resetEntityFilters(config: EntityViewConfig): Promise { element(config.containsSelector).value = ""; resetTableState(entityTable(config.kind)); await startEntityLoad(config); } async function startTransactionLoad(): Promise { transactionBaseRequest = buildTransactionBaseRequest(); resetStoreBlockState(transactionBlockState); await loadTransactionBlock(1, null); } async function loadTransactionBlock(blockIndex: number, cursor: string | null): Promise { if (transactionTable === null) { return; } if (transactionBaseRequest === null) { throw new Error("Rechargez les transactions pour initialiser les filtres Store."); } try { setBusy(true, "Chargement transactions"); const request: DemoStoreReplayTransactionRequest = { ...transactionBaseRequest, cursor, blockIndex, }; const payload = await invoke( "load_demo_store_replay_transactions", { request }, ); replaceRows(transactionTable, payload.rows); applyStoreBlockMetadata(transactionBlockState, payload.page, transactionBlockControls); element("#transactionResultSummary").textContent = `${payload.rows.length} transaction(s) dans le bloc Store courant.`; frontendDebug(tracingTarget, `loaded replay transaction block ${payload.page.blockIndex}/${payload.page.totalBlocks} with ${payload.rows.length} rows`); } catch (caughtError) { const message = caughtError instanceof Error ? caughtError.message : String(caughtError); element("#transactionResultSummary").textContent = `Erreur : ${message}`; renderStoreBlockControls(transactionBlockState, transactionBlockControls); frontendError(tracingTarget, `transaction candidate loading failed: ${message}`); } finally { setBusy(false, "Prêt"); } } async function startProgramLoad(): Promise { programBaseRequest = buildProgramBaseRequest(); resetStoreBlockState(programBlockState); await loadProgramBlock(1, null); } async function loadProgramBlock(blockIndex: number, cursor: string | null): Promise { if (programTable === null) { return; } if (programBaseRequest === null) { throw new Error("Rechargez les programmes pour initialiser les filtres Store."); } try { setBusy(true, "Chargement programmes"); const request: DemoStoreReplayProgramRequest = { ...programBaseRequest, cursor, blockIndex, }; const payload = await invoke( "load_demo_store_replay_programs", { request }, ); replaceRows(programTable, payload.rows); applyStoreBlockMetadata(programBlockState, payload.page, programBlockControls); element("#programResultSummary").textContent = `${payload.rows.length} programme(s) dans le bloc Store courant, avec occurrences top-level, inner et logs liés.`; frontendDebug(tracingTarget, `loaded replay program block ${payload.page.blockIndex}/${payload.page.totalBlocks} with ${payload.rows.length} rows`); } catch (caughtError) { const message = caughtError instanceof Error ? caughtError.message : String(caughtError); element("#programResultSummary").textContent = `Erreur : ${message}`; renderStoreBlockControls(programBlockState, programBlockControls); frontendError(tracingTarget, `program summary loading failed: ${message}`); } finally { setBusy(false, "Prêt"); } } async function startEntityLoad(config: EntityViewConfig): Promise { entityBaseRequests.set(config.kind, buildEntityBaseRequest(config)); resetStoreBlockState(entityBlockState(config.kind)); await loadEntityBlock(config, 1, null); } async function loadEntityBlock(config: EntityViewConfig, blockIndex: number, cursor: string | null): Promise { const table = entityTable(config.kind); if (table === null) { return; } const baseRequest = entityBaseRequests.get(config.kind); if (baseRequest === undefined) { throw new Error(`Rechargez les ${config.pluralLabel} pour initialiser les filtres Store.`); } try { setBusy(true, `Chargement ${config.pluralLabel}`); const request: DemoStoreReplayEntityRequest = { ...baseRequest, cursor, blockIndex, }; const payload = await invoke( "load_demo_store_replay_entities", { request }, ); replaceRows(table, payload.rows); applyStoreBlockMetadata(entityBlockState(config.kind), payload.page, config.blockControls); element(config.summarySelector).textContent = `${payload.rows.length} ${config.pluralLabel} dans le bloc Store courant.`; frontendDebug(tracingTarget, `loaded replay ${config.kind} block ${payload.page.blockIndex}/${payload.page.totalBlocks} with ${payload.rows.length} rows`); } catch (caughtError) { const message = caughtError instanceof Error ? caughtError.message : String(caughtError); element(config.summarySelector).textContent = `Erreur : ${message}`; renderStoreBlockControls(entityBlockState(config.kind), config.blockControls); frontendError(tracingTarget, `${config.kind} summary loading failed: ${message}`); } finally { setBusy(false, "Prêt"); } } async function loadPreviousTransactionBlock(): Promise { const targetBlock = transactionBlockState.currentBlock - 1; const cursor = cursorForBlock(transactionBlockState, targetBlock); if (targetBlock < 1 || cursor === undefined) { return; } await loadTransactionBlock(targetBlock, cursor); } async function loadNextTransactionBlock(): Promise { if (transactionBlockState.nextCursor === null) { return; } await loadTransactionBlock(transactionBlockState.currentBlock + 1, transactionBlockState.nextCursor); } async function loadPreviousProgramBlock(): Promise { const targetBlock = programBlockState.currentBlock - 1; const cursor = cursorForBlock(programBlockState, targetBlock); if (targetBlock < 1 || cursor === undefined) { return; } await loadProgramBlock(targetBlock, cursor); } async function loadNextProgramBlock(): Promise { if (programBlockState.nextCursor === null) { return; } await loadProgramBlock(programBlockState.currentBlock + 1, programBlockState.nextCursor); } async function loadPreviousEntityBlock(config: EntityViewConfig): Promise { const state = entityBlockState(config.kind); const targetBlock = state.currentBlock - 1; const cursor = cursorForBlock(state, targetBlock); if (targetBlock < 1 || cursor === undefined) { return; } await loadEntityBlock(config, targetBlock, cursor); } async function loadNextEntityBlock(config: EntityViewConfig): Promise { const state = entityBlockState(config.kind); if (state.nextCursor === null) { return; } await loadEntityBlock(config, state.currentBlock + 1, state.nextCursor); } async function copyLines(lines: string[], label: string): Promise { if (lines.length === 0) { throw new Error(`Aucune ${label} à copier.`); } await navigator.clipboard.writeText(lines.join("\n")); element("#replayCandidateStatusBadge").textContent = `${lines.length} ${label} copiée(s)`; } function csvCell(value: unknown): string { const text = value === null || value === undefined ? "" : String(value); return `"${text.split('"').join('""')}"`; } function csvContent(headers: string[], rows: unknown[][]): string { const lines = [headers, ...rows].map(row => row.map(csvCell).join(";")); return `${lines.join("\r\n")}\r\n`; } async function saveCsv(fileName: string, content: string, rowCount: number): Promise { if (rowCount === 0) { throw new Error("Aucune ligne à exporter."); } const path = await invoke("export_demo_store_replay_csv", { fileName, content }); const badge = element("#replayCandidateStatusBadge"); badge.textContent = `${rowCount} ligne(s) exportée(s) : ${path}`; badge.className = "badge text-bg-success"; frontendDebug(tracingTarget, `CSV export written: ${path}`); } async function exportTransactionsCsv(): Promise { const rows = transactionRowsForExport(); const content = csvContent( ["signature", "slot", "raw_processing_state", "retention_state", "core_status", "ledger_status", "processor_version", "attempt_count", "top_level_instruction_count", "inner_instruction_count", "top_level_program_count", "inner_program_count", "updated_at"], rows.map(row => [row.signature, row.slot, row.rawProcessingState, row.retentionState, coreStatusText(row), row.ledgerStatus, row.processorVersion, row.attemptCount, row.topLevelInstructionCount, row.innerInstructionCount, row.topLevelProgramCount, row.innerProgramCount, row.updatedAt]), ); await saveCsv("replay_transactions.csv", content, rows.length); } async function exportProgramsCsv(): Promise { const rows = programRowsForExport(); const content = csvContent( ["program_code", "program_id", "transaction_count", "top_level_instruction_count", "inner_instruction_count", "log_count", "min_slot", "max_slot"], rows.map(row => [row.programCode, row.programId, row.transactionCount, row.topLevelInstructionCount, row.innerInstructionCount, row.logCount, row.minSlot, row.maxSlot]), ); await saveCsv("replay_programs.csv", content, rows.length); } async function exportEntityCsv(config: EntityViewConfig): Promise { const rows = entityRowsForExport(config.kind); const content = csvContent( ["entity_kind", "entity_value", "transaction_count", "occurrence_count", "occurrences_per_transaction", "min_slot", "max_slot", "slot_span"], rows.map(row => [row.entityKind, row.entityValue, row.transactionCount, row.occurrenceCount, row.transactionCount > 0 ? row.occurrenceCount / row.transactionCount : 0, row.minSlot, row.maxSlot, Math.max(0, row.maxSlot - row.minSlot)]), ); await saveCsv(config.fileName, content, rows.length); } function transactionRowsForExport(): DemoStoreReplayTransactionRow[] { if (transactionTable === null) { return []; } return selectedOrFilteredRows(transactionTable); } function programRowsForExport(): DemoStoreReplayProgramRow[] { if (programTable === null) { return []; } return selectedOrFilteredRows(programTable); } function entityRowsForExport(kind: EntityKindCode): DemoStoreReplayEntityRow[] { const table = entityTable(kind); if (table === null) { return []; } return selectedOrFilteredRows(table); } function showTransactionsTab(): void { bootstrap.Tab.getOrCreateInstance(element("#transactionsTabButton")).show(); } async function useSelectedProgram(): Promise { if (programTable === null) { return; } const row = selectedOrSingleFilteredRow(programTable, "programme"); element("#transactionProgramIdInput").value = row.programId; element("#transactionProgramScopeSelect").value = "any"; showTransactionsTab(); await startTransactionLoad(); } async function useSelectedEntity(config: EntityViewConfig): Promise { const table = entityTable(config.kind); if (table === null) { return; } const row = selectedOrSingleFilteredRow(table, config.label); element("#transactionEntityKindSelect").value = config.kind; element("#transactionEntityValueInput").value = row.entityValue; showTransactionsTab(); await startTransactionLoad(); } async function openCoreExtraction(): Promise { try { await invoke("open_demo_core_extraction_window"); } catch (caughtError) { const message = caughtError instanceof Error ? caughtError.message : String(caughtError); frontendError(tracingTarget, `core extraction window opening failed: ${message}`); } } async function loadOptions(): Promise { const options = await invoke("demo_store_replay_options"); if (!Number.isSafeInteger(options.blockSize) || options.blockSize !== 500) { throw new Error("La taille de bloc Store retournée par le backend est invalide."); } element("#replayCandidateProfileBadge").textContent = `Profil ${options.activeProfileName}`; element("#replayCandidateDsn").textContent = options.connectionDescriptor; element("#replayCandidateBlockSize").textContent = `${options.blockSize} lignes`; populateKnownPrograms(options.knownPrograms); } function populateKnownPrograms(programs: DemoStoreReplayKnownProgramOption[]): void { const select = element("#knownProgramSelect"); const datalist = element("#knownProgramDatalist"); select.replaceChildren(new Option("Tous les programmes connus", "")); datalist.replaceChildren(); for (const program of programs) { const label = `${program.code} — ${program.programId}`; select.add(new Option(label, program.programId)); const option = document.createElement("option"); option.value = program.programId; option.label = program.code; datalist.append(option); } } function initializeTooltips(): void { document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(target => { bootstrap.Tooltip.getOrCreateInstance(target); }); } function adjustVisibleTables(): void { transactionTable?.columns.adjust(); programTable?.columns.adjust(); mintTable?.columns.adjust(); ownerTable?.columns.adjust(); accountTable?.columns.adjust(); } function installInlineCopyHandler(): void { document.addEventListener("click", event => { const source = event.target; if (!(source instanceof Element)) { return; } const button = source.closest("button[data-copy-value]"); if (button === null) { return; } event.preventDefault(); event.stopPropagation(); const value = button.dataset.copyValue ?? ""; const label = button.dataset.copyLabel ?? "valeur"; void copyLines([value], label).catch(error => reportUiError(`copy ${label}`, error)); }); } function reportUiError(action: string, caughtError: unknown): void { const message = caughtError instanceof Error ? caughtError.message : String(caughtError); element("#replayCandidateStatusBadge").textContent = `Erreur : ${message}`; element("#replayCandidateStatusBadge").className = "badge text-bg-danger"; frontendError(tracingTarget, `${action} failed: ${message}`); } function installHandlers(): void { element("#loadTransactionsButton").addEventListener("click", () => void startTransactionLoad()); element("#resetTransactionsButton").addEventListener("click", () => { void resetTransactionFilters().catch(error => reportUiError("reset transaction filters", error)); }); element(transactionBlockControls.firstSelector).addEventListener("click", () => void loadTransactionBlock(1, null)); element(transactionBlockControls.previousSelector).addEventListener("click", () => void loadPreviousTransactionBlock()); element(transactionBlockControls.nextSelector).addEventListener("click", () => void loadNextTransactionBlock()); element("#loadProgramsButton").addEventListener("click", () => void startProgramLoad()); element("#resetProgramsButton").addEventListener("click", () => { void resetProgramFilters().catch(error => reportUiError("reset program filters", error)); }); element(programBlockControls.firstSelector).addEventListener("click", () => void loadProgramBlock(1, null)); element(programBlockControls.previousSelector).addEventListener("click", () => void loadPreviousProgramBlock()); element(programBlockControls.nextSelector).addEventListener("click", () => void loadNextProgramBlock()); element("#openCoreExtractionButton").addEventListener("click", () => void openCoreExtraction()); element("#copySignaturesButton").addEventListener("click", () => { void copyLines(transactionRowsForExport().map(row => row.signature), "signature").catch(error => reportUiError("copy signatures", error)); }); element("#exportTransactionsCsvButton").addEventListener("click", () => { void exportTransactionsCsv().catch(error => reportUiError("export transactions CSV", error)); }); element("#copyProgramsButton").addEventListener("click", () => { void copyLines(programRowsForExport().map(row => row.programId), "program ID").catch(error => reportUiError("copy programs", error)); }); element("#exportProgramsCsvButton").addEventListener("click", () => { void exportProgramsCsv().catch(error => reportUiError("export programs CSV", error)); }); element("#useSelectedProgramButton").addEventListener("click", () => { void useSelectedProgram().catch(error => reportUiError("use selected program", error)); }); for (const config of entityViewConfigs) { element(config.loadButtonSelector).addEventListener("click", () => void startEntityLoad(config)); element(config.resetButtonSelector).addEventListener("click", () => { void resetEntityFilters(config).catch(error => reportUiError(`reset ${config.pluralLabel} filters`, error)); }); element(config.blockControls.firstSelector).addEventListener("click", () => void loadEntityBlock(config, 1, null)); element(config.blockControls.previousSelector).addEventListener("click", () => void loadPreviousEntityBlock(config)); element(config.blockControls.nextSelector).addEventListener("click", () => void loadNextEntityBlock(config)); element(config.copyButtonSelector).addEventListener("click", () => { void copyLines(entityRowsForExport(config.kind).map(row => row.entityValue), config.label).catch(error => reportUiError(`copy ${config.pluralLabel}`, error)); }); element(config.exportButtonSelector).addEventListener("click", () => { void exportEntityCsv(config).catch(error => reportUiError(`export ${config.pluralLabel} CSV`, error)); }); element(config.useButtonSelector).addEventListener("click", () => { void useSelectedEntity(config).catch(error => reportUiError(`use selected ${config.label}`, error)); }); element(config.containsSelector).addEventListener("input", () => { entityBaseRequests.delete(config.kind); markStoreBlockFiltersDirty(entityBlockState(config.kind), config.blockControls); }); } const transactionFilterSelectors = [ "#transactionSignatureContainsInput", "#transactionMinSlotInput", "#transactionMaxSlotInput", "#transactionRawStateSelect", "#transactionLedgerStatusSelect", "#transactionProgramIdInput", "#transactionProgramScopeSelect", "#transactionEntityKindSelect", "#transactionEntityValueInput", "#transactionNewestFirstInput", ]; for (const selector of transactionFilterSelectors) { element(selector).addEventListener("change", () => { transactionBaseRequest = null; markStoreBlockFiltersDirty(transactionBlockState, transactionBlockControls); }); element(selector).addEventListener("input", () => { transactionBaseRequest = null; markStoreBlockFiltersDirty(transactionBlockState, transactionBlockControls); }); } element("#programContainsInput").addEventListener("input", () => { programBaseRequest = null; markStoreBlockFiltersDirty(programBlockState, programBlockControls); }); element("#knownProgramSelect").addEventListener("change", event => { const target = event.currentTarget as HTMLSelectElement; element("#programContainsInput").value = target.value; programBaseRequest = null; markStoreBlockFiltersDirty(programBlockState, programBlockControls); }); document.querySelectorAll('[data-bs-toggle="tab"]').forEach(button => { button.addEventListener("shown.bs.tab", adjustVisibleTables); }); } document.addEventListener("DOMContentLoaded", () => { installFrontendConsoleBridge(tracingTarget); frontendDebug(tracingTarget, "Store replay candidate browser loaded"); transactionTable = createTransactionTable(); programTable = createProgramTable(); mintTable = createEntityTable(entityViewConfigs[0]); ownerTable = createEntityTable(entityViewConfigs[1]); accountTable = createEntityTable(entityViewConfigs[2]); initializeTooltips(); installHandlers(); installInlineCopyHandler(); void loadOptions() .then(() => Promise.all([ startTransactionLoad(), startProgramLoad(), startEntityLoad(entityViewConfigs[0]), startEntityLoad(entityViewConfigs[1]), startEntityLoad(entityViewConfigs[2]), ])) .catch(caughtError => reportUiError("initial loading", caughtError)); });