Files
khadhroony-bot3/kb-app-demo-desktop/frontend/ts/demo_store_replay_candidates.ts

1010 lines
44 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_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<DemoStoreReplayTransactionRow> | null = null;
let programTable: Api<DemoStoreReplayProgramRow> | null = null;
let mintTable: Api<DemoStoreReplayEntityRow> | null = null;
let ownerTable: Api<DemoStoreReplayEntityRow> | null = null;
let accountTable: Api<DemoStoreReplayEntityRow> | 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<EntityKindCode, StoreBlockState>([
["mint", createStoreBlockState()],
["owner", createStoreBlockState()],
["account_key", createStoreBlockState()],
]);
type TransactionBaseRequest = Omit<DemoStoreReplayTransactionRequest, "cursor" | "blockIndex">;
type ProgramBaseRequest = Omit<DemoStoreReplayProgramRequest, "cursor" | "blockIndex">;
type EntityBaseRequest = Omit<DemoStoreReplayEntityRequest, "cursor" | "blockIndex">;
let transactionBaseRequest: TransactionBaseRequest | null = null;
let programBaseRequest: ProgramBaseRequest | null = null;
const entityBaseRequests = new Map<EntityKindCode, EntityBaseRequest>();
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 optionalInputValue(selector: string): string | null {
const value = element<HTMLInputElement | HTMLSelectElement>(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("&amp;")
.split("<").join("&lt;")
.split(">").join("&gt;")
.split('"').join("&quot;")
.split("'").join("&#039;");
}
function codeDisplay(value: unknown, type: string): string {
if (type !== "display") {
return String(value ?? "");
}
return `<code class="text-break">${escapeHtml(value)}</code>`;
}
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 `<span class="d-inline-flex align-items-center gap-1 text-nowrap"><code data-bs-toggle="tooltip" data-bs-placement="top" title="${escapedText}">${compactText}</code><button class="btn btn-sm btn-link text-secondary p-0 replay-cell-copy" type="button" data-bs-toggle="tooltip" data-bs-placement="top" title="Copier ${escapedLabel}" aria-label="Copier ${escapedLabel}" data-copy-value="${escapedText}" data-copy-label="${escapedLabel}"><i class="fa-regular fa-copy" aria-hidden="true"></i></button></span>`;
};
}
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 `<span class="badge ${className}">${escapeHtml(text)}</span>`;
}
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 '<span class="badge text-bg-secondary">absent</span>';
}
if (row.transactionFailed === true) {
return '<span class="badge text-bg-danger" title="La transaction Solana a échoué on-chain, mais son graphe core a été extrait.">échec on-chain</span>';
}
return '<span class="badge text-bg-success">présent</span>';
}
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<DemoStoreReplayTransactionRow> {
return new DataTable<DemoStoreReplayTransactionRow>("#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<DemoStoreReplayProgramRow> {
return new DataTable<DemoStoreReplayProgramRow>("#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<DemoStoreReplayEntityRow> {
return new DataTable<DemoStoreReplayEntityRow>(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<DemoStoreReplayEntityRow> | null {
if (kind === "mint") {
return mintTable;
}
if (kind === "owner") {
return ownerTable;
}
return accountTable;
}
function replaceRows<T>(table: Api<T>, rows: T[]): void {
table.clear();
table.rows.add(rows);
table.draw();
}
function resetTableState<T>(table: Api<T> | null): void {
if (table === null) {
return;
}
table.search("");
table.columns().search("");
table.rows().deselect();
table.draw();
}
function selectedOrFilteredRows<T>(table: Api<T>): 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<T>(table: Api<T>, 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<HTMLElement>("#replayCandidateStatusBadge");
badge.textContent = operationInProgress ? (busy ? message : "Chargement en cours") : "Prêt";
badge.className = operationInProgress ? "badge text-bg-warning" : "badge text-bg-success";
document.querySelectorAll<HTMLButtonElement>("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 dentité 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<HTMLInputElement>("#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<void> {
element<HTMLInputElement>("#transactionSignatureContainsInput").value = "";
element<HTMLInputElement>("#transactionMinSlotInput").value = "";
element<HTMLInputElement>("#transactionMaxSlotInput").value = "";
element<HTMLSelectElement>("#transactionRawStateSelect").value = "";
element<HTMLSelectElement>("#transactionLedgerStatusSelect").value = "";
element<HTMLInputElement>("#transactionProgramIdInput").value = "";
element<HTMLSelectElement>("#transactionProgramScopeSelect").value = "any";
element<HTMLSelectElement>("#transactionEntityKindSelect").value = "";
element<HTMLInputElement>("#transactionEntityValueInput").value = "";
element<HTMLInputElement>("#transactionNewestFirstInput").checked = true;
resetTableState(transactionTable);
await startTransactionLoad();
}
async function resetProgramFilters(): Promise<void> {
element<HTMLSelectElement>("#knownProgramSelect").value = "";
element<HTMLInputElement>("#programContainsInput").value = "";
resetTableState(programTable);
await startProgramLoad();
}
async function resetEntityFilters(config: EntityViewConfig): Promise<void> {
element<HTMLInputElement>(config.containsSelector).value = "";
resetTableState(entityTable(config.kind));
await startEntityLoad(config);
}
async function startTransactionLoad(): Promise<void> {
transactionBaseRequest = buildTransactionBaseRequest();
resetStoreBlockState(transactionBlockState);
await loadTransactionBlock(1, null);
}
async function loadTransactionBlock(blockIndex: number, cursor: string | null): Promise<void> {
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<DemoStoreReplayTransactionPagePayload>(
"load_demo_store_replay_transactions",
{ request },
);
replaceRows(transactionTable, payload.rows);
applyStoreBlockMetadata(transactionBlockState, payload.page, transactionBlockControls);
element<HTMLElement>("#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<HTMLElement>("#transactionResultSummary").textContent = `Erreur : ${message}`;
renderStoreBlockControls(transactionBlockState, transactionBlockControls);
frontendError(tracingTarget, `transaction candidate loading failed: ${message}`);
} finally {
setBusy(false, "Prêt");
}
}
async function startProgramLoad(): Promise<void> {
programBaseRequest = buildProgramBaseRequest();
resetStoreBlockState(programBlockState);
await loadProgramBlock(1, null);
}
async function loadProgramBlock(blockIndex: number, cursor: string | null): Promise<void> {
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<DemoStoreReplayProgramPagePayload>(
"load_demo_store_replay_programs",
{ request },
);
replaceRows(programTable, payload.rows);
applyStoreBlockMetadata(programBlockState, payload.page, programBlockControls);
element<HTMLElement>("#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<HTMLElement>("#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<void> {
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<void> {
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<DemoStoreReplayEntityPagePayload>(
"load_demo_store_replay_entities",
{ request },
);
replaceRows(table, payload.rows);
applyStoreBlockMetadata(entityBlockState(config.kind), payload.page, config.blockControls);
element<HTMLElement>(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<HTMLElement>(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<void> {
const targetBlock = transactionBlockState.currentBlock - 1;
const cursor = cursorForBlock(transactionBlockState, targetBlock);
if (targetBlock < 1 || cursor === undefined) {
return;
}
await loadTransactionBlock(targetBlock, cursor);
}
async function loadNextTransactionBlock(): Promise<void> {
if (transactionBlockState.nextCursor === null) {
return;
}
await loadTransactionBlock(transactionBlockState.currentBlock + 1, transactionBlockState.nextCursor);
}
async function loadPreviousProgramBlock(): Promise<void> {
const targetBlock = programBlockState.currentBlock - 1;
const cursor = cursorForBlock(programBlockState, targetBlock);
if (targetBlock < 1 || cursor === undefined) {
return;
}
await loadProgramBlock(targetBlock, cursor);
}
async function loadNextProgramBlock(): Promise<void> {
if (programBlockState.nextCursor === null) {
return;
}
await loadProgramBlock(programBlockState.currentBlock + 1, programBlockState.nextCursor);
}
async function loadPreviousEntityBlock(config: EntityViewConfig): Promise<void> {
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<void> {
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<void> {
if (lines.length === 0) {
throw new Error(`Aucune ${label} à copier.`);
}
await navigator.clipboard.writeText(lines.join("\n"));
element<HTMLElement>("#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<void> {
if (rowCount === 0) {
throw new Error("Aucune ligne à exporter.");
}
const path = await invoke<string>("export_demo_store_replay_csv", { fileName, content });
const badge = element<HTMLElement>("#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<void> {
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<void> {
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<void> {
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<HTMLButtonElement>("#transactionsTabButton")).show();
}
async function useSelectedProgram(): Promise<void> {
if (programTable === null) {
return;
}
const row = selectedOrSingleFilteredRow(programTable, "programme");
element<HTMLInputElement>("#transactionProgramIdInput").value = row.programId;
element<HTMLSelectElement>("#transactionProgramScopeSelect").value = "any";
showTransactionsTab();
await startTransactionLoad();
}
async function useSelectedEntity(config: EntityViewConfig): Promise<void> {
const table = entityTable(config.kind);
if (table === null) {
return;
}
const row = selectedOrSingleFilteredRow(table, config.label);
element<HTMLSelectElement>("#transactionEntityKindSelect").value = config.kind;
element<HTMLInputElement>("#transactionEntityValueInput").value = row.entityValue;
showTransactionsTab();
await startTransactionLoad();
}
async function openCoreExtraction(): Promise<void> {
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<void> {
const options = await invoke<DemoStoreReplayOptionsPayload>("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<HTMLElement>("#replayCandidateProfileBadge").textContent = `Profil ${options.activeProfileName}`;
element<HTMLElement>("#replayCandidateDsn").textContent = options.connectionDescriptor;
element<HTMLElement>("#replayCandidateBlockSize").textContent = `${options.blockSize} lignes`;
populateKnownPrograms(options.knownPrograms);
}
function populateKnownPrograms(programs: DemoStoreReplayKnownProgramOption[]): void {
const select = element<HTMLSelectElement>("#knownProgramSelect");
const datalist = element<HTMLDataListElement>("#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<HTMLElement>('[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<HTMLButtonElement>("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<HTMLElement>("#replayCandidateStatusBadge").textContent = `Erreur : ${message}`;
element<HTMLElement>("#replayCandidateStatusBadge").className = "badge text-bg-danger";
frontendError(tracingTarget, `${action} failed: ${message}`);
}
function installHandlers(): void {
element<HTMLButtonElement>("#loadTransactionsButton").addEventListener("click", () => void startTransactionLoad());
element<HTMLButtonElement>("#resetTransactionsButton").addEventListener("click", () => {
void resetTransactionFilters().catch(error => reportUiError("reset transaction filters", error));
});
element<HTMLButtonElement>(transactionBlockControls.firstSelector).addEventListener("click", () => void loadTransactionBlock(1, null));
element<HTMLButtonElement>(transactionBlockControls.previousSelector).addEventListener("click", () => void loadPreviousTransactionBlock());
element<HTMLButtonElement>(transactionBlockControls.nextSelector).addEventListener("click", () => void loadNextTransactionBlock());
element<HTMLButtonElement>("#loadProgramsButton").addEventListener("click", () => void startProgramLoad());
element<HTMLButtonElement>("#resetProgramsButton").addEventListener("click", () => {
void resetProgramFilters().catch(error => reportUiError("reset program filters", error));
});
element<HTMLButtonElement>(programBlockControls.firstSelector).addEventListener("click", () => void loadProgramBlock(1, null));
element<HTMLButtonElement>(programBlockControls.previousSelector).addEventListener("click", () => void loadPreviousProgramBlock());
element<HTMLButtonElement>(programBlockControls.nextSelector).addEventListener("click", () => void loadNextProgramBlock());
element<HTMLButtonElement>("#openCoreExtractionButton").addEventListener("click", () => void openCoreExtraction());
element<HTMLButtonElement>("#copySignaturesButton").addEventListener("click", () => {
void copyLines(transactionRowsForExport().map(row => row.signature), "signature").catch(error => reportUiError("copy signatures", error));
});
element<HTMLButtonElement>("#exportTransactionsCsvButton").addEventListener("click", () => {
void exportTransactionsCsv().catch(error => reportUiError("export transactions CSV", error));
});
element<HTMLButtonElement>("#copyProgramsButton").addEventListener("click", () => {
void copyLines(programRowsForExport().map(row => row.programId), "program ID").catch(error => reportUiError("copy programs", error));
});
element<HTMLButtonElement>("#exportProgramsCsvButton").addEventListener("click", () => {
void exportProgramsCsv().catch(error => reportUiError("export programs CSV", error));
});
element<HTMLButtonElement>("#useSelectedProgramButton").addEventListener("click", () => {
void useSelectedProgram().catch(error => reportUiError("use selected program", error));
});
for (const config of entityViewConfigs) {
element<HTMLButtonElement>(config.loadButtonSelector).addEventListener("click", () => void startEntityLoad(config));
element<HTMLButtonElement>(config.resetButtonSelector).addEventListener("click", () => {
void resetEntityFilters(config).catch(error => reportUiError(`reset ${config.pluralLabel} filters`, error));
});
element<HTMLButtonElement>(config.blockControls.firstSelector).addEventListener("click", () => void loadEntityBlock(config, 1, null));
element<HTMLButtonElement>(config.blockControls.previousSelector).addEventListener("click", () => void loadPreviousEntityBlock(config));
element<HTMLButtonElement>(config.blockControls.nextSelector).addEventListener("click", () => void loadNextEntityBlock(config));
element<HTMLButtonElement>(config.copyButtonSelector).addEventListener("click", () => {
void copyLines(entityRowsForExport(config.kind).map(row => row.entityValue), config.label).catch(error => reportUiError(`copy ${config.pluralLabel}`, error));
});
element<HTMLButtonElement>(config.exportButtonSelector).addEventListener("click", () => {
void exportEntityCsv(config).catch(error => reportUiError(`export ${config.pluralLabel} CSV`, error));
});
element<HTMLButtonElement>(config.useButtonSelector).addEventListener("click", () => {
void useSelectedEntity(config).catch(error => reportUiError(`use selected ${config.label}`, error));
});
element<HTMLInputElement>(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<HTMLInputElement | HTMLSelectElement>(selector).addEventListener("change", () => {
transactionBaseRequest = null;
markStoreBlockFiltersDirty(transactionBlockState, transactionBlockControls);
});
element<HTMLInputElement | HTMLSelectElement>(selector).addEventListener("input", () => {
transactionBaseRequest = null;
markStoreBlockFiltersDirty(transactionBlockState, transactionBlockControls);
});
}
element<HTMLInputElement>("#programContainsInput").addEventListener("input", () => {
programBaseRequest = null;
markStoreBlockFiltersDirty(programBlockState, programBlockControls);
});
element<HTMLSelectElement>("#knownProgramSelect").addEventListener("change", event => {
const target = event.currentTarget as HTMLSelectElement;
element<HTMLInputElement>("#programContainsInput").value = target.value;
programBaseRequest = null;
markStoreBlockFiltersDirty(programBlockState, programBlockControls);
});
document.querySelectorAll<HTMLButtonElement>('[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));
});