v0.5.3-pre.005-fix010

This commit is contained in:
2026-08-14 14:40:36 +02:00
parent b6583bd9fb
commit 734a27f2ca
59 changed files with 4337 additions and 747 deletions

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/demo_decode_replay.ts
// version: 8
// version: 9
import * as bootstrap from "bootstrap";
import "simplebar";
@@ -9,11 +9,21 @@ 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";
@@ -23,6 +33,15 @@ import type { DemoTransactionAnnotationRow } from "./bindings/kb_app_demo_deskto
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);
@@ -250,7 +269,7 @@ function renderAnnotations(rows: DemoTransactionAnnotationRow[]): void {
annotationJournalTable = new DataTable<DemoTransactionAnnotationRow>("#annotationJournalTable", {
autoWidth: false,
pageLength: 25,
lengthMenu: [10, 25, 50, 100],
lengthMenu: [10, 25, 50, 100, 250, 500],
order: [[0, "desc"]],
scrollX: true,
language: {
@@ -265,23 +284,53 @@ function renderAnnotations(rows: DemoTransactionAnnotationRow[]): void {
element<HTMLElement>("#annotationJournalStatus").textContent = `${rows.length} annotation(s) committed chargée(s).`;
}
async function loadAnnotations(): Promise<void> {
const request: DemoTransactionAnnotationRequest = {
async function startAnnotationLoad(): Promise<void> {
annotationBaseRequest = {
signatureContains: inputValue("#annotationSignatureInput") || null,
limit: integerValue("#annotationLimitInput"),
};
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 limit=${request.limit}`);
const rows = await invoke<DemoTransactionAnnotationRow[]>("demo_decode_replay_annotations", { request });
renderAnnotations(rows);
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_annotations completed rows=${rows.length}`);
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}`);
@@ -357,7 +406,14 @@ document.addEventListener("DOMContentLoaded", () => {
element<HTMLButtonElement>("#startDecodeReplayButton").addEventListener("click", () => void executeDecodeReplay());
element<HTMLButtonElement>("#cancelDecodeReplayButton").addEventListener("click", () => void cancelDecodeReplay());
element<HTMLButtonElement>("#loadDecodeDiagnosticsButton").addEventListener("click", () => void loadDiagnostics());
element<HTMLButtonElement>("#loadAnnotationsButton").addEventListener("click", () => void loadAnnotations());
element<HTMLButtonElement>("#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));

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/demo_execution_metadata.ts
// version: 14
// version: 15
import * as bootstrap from "bootstrap";
import "simplebar";
@@ -479,7 +479,9 @@ async function prepareProfile(): Promise<void> {
setExecutionRunning(executionRunning);
updateProfileStatus("Préparation", "badge text-bg-warning");
try {
frontendDebug("kb-app-demo-desktop.frontend.execution_metadata", `Invoke demo_execution_devnet_prepare_profile profile=${profileName}`);
const readiness = await invoke<DemoExecutionDevnetStoreReadinessPayload>("demo_execution_devnet_prepare_profile", { profileName });
frontendDebug("kb-app-demo-desktop.frontend.execution_metadata", `Invoke demo_execution_devnet_prepare_profile completed profile=${profileName} resources=${readiness.availableResourcesAfter}/${readiness.expectedResources}`);
synchronizeProfileSelections(profileName);
window.localStorage.setItem(profileStorageKey, profileName);
for (const selector of profileOutputSelectors) {
@@ -580,9 +582,18 @@ document.addEventListener("DOMContentLoaded", () => {
void prepareProfile();
});
}
element<HTMLButtonElement>("#prepareMetadataProfileButton").addEventListener("click", () => void prepareProfile());
element<HTMLButtonElement>("#prepareToken2022MetadataProfileButton").addEventListener("click", () => void prepareProfile());
element<HTMLButtonElement>("#prepareSolanaProgramMetadataProfileButton").addEventListener("click", () => void prepareProfile());
element<HTMLButtonElement>("#prepareMetadataProfileButton").addEventListener("click", () => {
frontendDebug("kb-app-demo-desktop.frontend.execution_metadata", "Metaplex profile load button clicked");
void prepareProfile();
});
element<HTMLButtonElement>("#prepareToken2022MetadataProfileButton").addEventListener("click", () => {
frontendDebug("kb-app-demo-desktop.frontend.execution_metadata", "Token-2022 Metadata profile load button clicked");
void prepareProfile();
});
element<HTMLButtonElement>("#prepareSolanaProgramMetadataProfileButton").addEventListener("click", () => {
frontendDebug("kb-app-demo-desktop.frontend.execution_metadata", "Solana Program Metadata profile load button clicked");
void prepareProfile();
});
element<HTMLButtonElement>("#refreshMetadataScenariosButton").addEventListener("click", () => void loadScenarios());
element<HTMLTextAreaElement>("#metadataExecutionLogOutput").addEventListener("app-log-clear", () => { metadataLogLines.length = 0; });
void listen<DemoExecutionMetadataProgressPayload>("demo-execution-metadata-progress", event => appendMetadataLog(event.payload));

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/demo_execution_spl.ts
// version: 11
// version: 13
import * as bootstrap from "bootstrap";
import "simplebar";
@@ -19,13 +19,23 @@ import type { DemoExecutionMemoSummaryPayload } from "./bindings/kb_app_demo_des
import type { DemoExecutionSplTokenRequest } from "./bindings/kb_app_demo_desktop/demo_spl_token/DemoExecutionSplTokenRequest.ts";
import type { DemoExecutionSplTokenSummaryPayload } from "./bindings/kb_app_demo_desktop/demo_spl_token/DemoExecutionSplTokenSummaryPayload.ts";
import type { DemoSplTokenJournalRequest } from "./bindings/kb_app_demo_desktop/demo_spl_token/DemoSplTokenJournalRequest.ts";
import type { DemoSplTokenJournalPagePayload } from "./bindings/kb_app_demo_desktop/demo_spl_token/DemoSplTokenJournalPagePayload.ts";
import type { DemoSplTokenJournalRow } from "./bindings/kb_app_demo_desktop/demo_spl_token/DemoSplTokenJournalRow.ts";
import type { DemoExecutionSplAtaRequest } from "./bindings/kb_app_demo_desktop/demo_spl_ata/DemoExecutionSplAtaRequest.ts";
import type { DemoExecutionSplAtaSummaryPayload } from "./bindings/kb_app_demo_desktop/demo_spl_ata/DemoExecutionSplAtaSummaryPayload.ts";
import type { DemoSplAtaDerivationPayload } from "./bindings/kb_app_demo_desktop/demo_spl_ata/DemoSplAtaDerivationPayload.ts";
import type { DemoSplAtaDerivationRequest } from "./bindings/kb_app_demo_desktop/demo_spl_ata/DemoSplAtaDerivationRequest.ts";
import type { DemoSplAtaJournalRequest } from "./bindings/kb_app_demo_desktop/demo_spl_ata/DemoSplAtaJournalRequest.ts";
import type { DemoSplAtaJournalRow } from "./bindings/kb_app_demo_desktop/demo_spl_ata/DemoSplAtaJournalRow.ts";
import type { DemoSplAtaJournalPagePayload } from "./bindings/kb_app_demo_desktop/demo_spl_ata/DemoSplAtaJournalPagePayload.ts";
import {
applyStoreBlockMetadata,
createStoreBlockState,
cursorForBlock,
markStoreBlockFiltersDirty,
renderStoreBlockControls,
resetStoreBlockState,
type StoreBlockControls,
} from "./demo_store_block_pagination";
import type { DemoExecutionSplToken2022Request } from "./bindings/kb_app_demo_desktop/demo_spl_token_2022/DemoExecutionSplToken2022Request.ts";
import type { DemoExecutionSplToken2022SummaryPayload } from "./bindings/kb_app_demo_desktop/demo_spl_token_2022/DemoExecutionSplToken2022SummaryPayload.ts";
import type { DemoSplToken2022FixturePayload } from "./bindings/kb_app_demo_desktop/demo_spl_token_2022/DemoSplToken2022FixturePayload.ts";
@@ -40,6 +50,24 @@ let running = false;
let profileStoreReady = false;
let profileOptions: DemoExecutionSolanaCoreProfileOption[] = [];
let token_2022Fixture: DemoSplToken2022FixturePayload | null = null;
const tokenJournalBlockState = createStoreBlockState();
const ataJournalBlockState = createStoreBlockState();
const tokenJournalBlockControls: StoreBlockControls = {
firstSelector: "#tokenJournalFirstBlockButton",
previousSelector: "#tokenJournalPreviousBlockButton",
nextSelector: "#tokenJournalNextBlockButton",
summarySelector: "#tokenJournalBlockSummary",
};
const ataJournalBlockControls: StoreBlockControls = {
firstSelector: "#ataJournalFirstBlockButton",
previousSelector: "#ataJournalPreviousBlockButton",
nextSelector: "#ataJournalNextBlockButton",
summarySelector: "#ataJournalBlockSummary",
};
type TokenJournalBaseRequest = Omit<DemoSplTokenJournalRequest, "cursor" | "blockIndex">;
type AtaJournalBaseRequest = Omit<DemoSplAtaJournalRequest, "cursor" | "blockIndex">;
let tokenJournalBaseRequest: TokenJournalBaseRequest | null = null;
let ataJournalBaseRequest: AtaJournalBaseRequest | null = null;
function element<T extends HTMLElement>(selector: string): T {
const value = document.querySelector<T>(selector);
@@ -400,7 +428,7 @@ async function executeAta(submit: boolean): Promise<void> {
element<HTMLInputElement>("#ataJournalSignatureInput").value = summary.transactionSignature;
element<HTMLInputElement>("#ataJournalMintInput").value = request.mint;
element<HTMLInputElement>("#ataJournalAddressInput").value = summary.associatedTokenAccount;
await loadAtaJournal();
await startAtaJournalLoad();
}
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
@@ -412,27 +440,52 @@ async function executeAta(submit: boolean): Promise<void> {
}
}
function ataJournalRequest(): DemoSplAtaJournalRequest {
function ataJournalBaseFilters(): AtaJournalBaseRequest {
return {
profileName: element<HTMLSelectElement>("#executionProfileSelect").value,
signatureContains: optionalText("#ataJournalSignatureInput"),
mint: optionalText("#ataJournalMintInput"),
associatedTokenAccount: optionalText("#ataJournalAddressInput"),
operation: optionalText("#ataJournalOperationInput"),
limit: integerValue("#ataJournalLimitInput"),
};
}
async function loadAtaJournal(): Promise<void> {
async function startAtaJournalLoad(): Promise<void> {
ataJournalBaseRequest = ataJournalBaseFilters();
resetStoreBlockState(ataJournalBlockState);
await loadAtaJournalBlock(1);
}
async function loadAtaJournalBlock(blockIndex: number): Promise<void> {
if (ataJournalBaseRequest === null) {
markStoreBlockFiltersDirty(ataJournalBlockState, ataJournalBlockControls);
return;
}
const cursor = cursorForBlock(ataJournalBlockState, blockIndex);
if (cursor === undefined) {
return;
}
const button = element<HTMLButtonElement>("#refreshAtaJournalButton");
button.disabled = true;
element<HTMLButtonElement>(ataJournalBlockControls.firstSelector).disabled = true;
element<HTMLButtonElement>(ataJournalBlockControls.previousSelector).disabled = true;
element<HTMLButtonElement>(ataJournalBlockControls.nextSelector).disabled = true;
const request: DemoSplAtaJournalRequest = {
...ataJournalBaseRequest,
cursor,
blockIndex,
};
try {
const rows = await invoke<DemoSplAtaJournalRow[]>("demo_spl_ata_journal", { request: ataJournalRequest() });
frontendDebug("kb-app-demo-desktop.frontend.demo_execution_spl", `Invoke demo_spl_ata_journal block=${blockIndex}`);
const payload = await invoke<DemoSplAtaJournalPagePayload>("demo_spl_ata_journal", { request });
applyStoreBlockMetadata(ataJournalBlockState, payload.page, ataJournalBlockControls);
frontendDebug("kb-app-demo-desktop.frontend.demo_execution_spl", `Invoke demo_spl_ata_journal completed block=${payload.page.blockIndex}/${payload.page.totalBlocks} rows=${payload.rows.length}`);
element<HTMLTextAreaElement>("#ataJournalOutput").value = JSON.stringify({
title: "Journal lifecycle ATA",
status: "success",
count: rows.length,
rows: rows.map(row => ({
block: payload.page,
count: payload.rows.length,
rows: payload.rows.map(row => ({
...row,
payloadJson: parsedJson(row.payloadJson),
})),
@@ -445,11 +498,17 @@ async function loadAtaJournal(): Promise<void> {
message,
error: message,
}, null, 2);
renderStoreBlockControls(ataJournalBlockState, ataJournalBlockControls);
} finally {
button.disabled = false;
}
}
function markAtaJournalFiltersDirty(): void {
ataJournalBaseRequest = null;
markStoreBlockFiltersDirty(ataJournalBlockState, ataJournalBlockControls);
}
async function execute(submit: boolean): Promise<void> {
if (running) {
return;
@@ -567,7 +626,7 @@ async function executeToken(submit: boolean): Promise<void> {
});
if (summary.transactionSignature) {
element<HTMLInputElement>("#tokenJournalSignatureInput").value = summary.transactionSignature;
await loadTokenJournal();
await startTokenJournalLoad();
}
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
@@ -579,7 +638,7 @@ async function executeToken(submit: boolean): Promise<void> {
}
}
function tokenJournalRequest(): DemoSplTokenJournalRequest {
function tokenJournalBaseFilters(): TokenJournalBaseRequest {
return {
profileName: element<HTMLSelectElement>("#executionProfileSelect").value,
signatureContains: optionalText("#tokenJournalSignatureInput"),
@@ -587,7 +646,6 @@ function tokenJournalRequest(): DemoSplTokenJournalRequest {
account: optionalText("#tokenJournalAccountInput"),
family: element<HTMLSelectElement>("#tokenJournalFamilySelect").value || null,
operation: optionalText("#tokenJournalOperationInput"),
limit: integerValue("#tokenJournalLimitInput"),
};
}
@@ -831,17 +889,42 @@ async function executeToken2022(submit: boolean): Promise<void> {
}
}
async function loadTokenJournal(): Promise<void> {
async function startTokenJournalLoad(): Promise<void> {
tokenJournalBaseRequest = tokenJournalBaseFilters();
resetStoreBlockState(tokenJournalBlockState);
await loadTokenJournalBlock(1);
}
async function loadTokenJournalBlock(blockIndex: number): Promise<void> {
if (tokenJournalBaseRequest === null) {
markStoreBlockFiltersDirty(tokenJournalBlockState, tokenJournalBlockControls);
return;
}
const cursor = cursorForBlock(tokenJournalBlockState, blockIndex);
if (cursor === undefined) {
return;
}
const button = element<HTMLButtonElement>("#refreshTokenJournalButton");
button.disabled = true;
element<HTMLButtonElement>(tokenJournalBlockControls.firstSelector).disabled = true;
element<HTMLButtonElement>(tokenJournalBlockControls.previousSelector).disabled = true;
element<HTMLButtonElement>(tokenJournalBlockControls.nextSelector).disabled = true;
const request: DemoSplTokenJournalRequest = {
...tokenJournalBaseRequest,
cursor,
blockIndex,
};
try {
const rows = await invoke<DemoSplTokenJournalRow[]>("demo_spl_token_journal", { request: tokenJournalRequest() });
renderTokenJournal(rows);
frontendDebug("kb-app-demo-desktop.frontend.demo_execution_spl", `Invoke demo_spl_token_journal block=${blockIndex}`);
const payload = await invoke<DemoSplTokenJournalPagePayload>("demo_spl_token_journal", { request });
applyStoreBlockMetadata(tokenJournalBlockState, payload.page, tokenJournalBlockControls);
frontendDebug("kb-app-demo-desktop.frontend.demo_execution_spl", `Invoke demo_spl_token_journal completed block=${payload.page.blockIndex}/${payload.page.totalBlocks} rows=${payload.rows.length}`);
renderTokenJournal(payload.rows);
appendLog({
timestamp: new Date().toISOString(),
level: "info",
stage: "spl_token_journal",
message: `${rows.length} événement(s) matérialisé(s) chargé(s).`,
message: `${payload.rows.length} événement(s) matérialisé(s) chargé(s) dans le bloc ${payload.page.blockIndex}/${payload.page.totalBlocks}.`,
});
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
@@ -851,12 +934,18 @@ async function loadTokenJournal(): Promise<void> {
message,
error: message,
}, null, 2);
renderStoreBlockControls(tokenJournalBlockState, tokenJournalBlockControls);
frontendError("kb-app-demo-desktop.frontend.demo_execution_spl", `Token journal loading failed: ${message}`);
} finally {
button.disabled = false;
}
}
function markTokenJournalFiltersDirty(): void {
tokenJournalBaseRequest = null;
markStoreBlockFiltersDirty(tokenJournalBlockState, tokenJournalBlockControls);
}
async function generateRecipient(): Promise<void> {
try {
const payload = await invoke<DemoExecutionSolanaCoreGeneratedRecipientPayload>("demo_execution_solana_core_generate_recipient");
@@ -898,7 +987,9 @@ async function prepareSelectedProfile(): Promise<void> {
}
appendLog({ timestamp: new Date().toISOString(), level: "info", stage: "store_prepare", message: `Vérification du store du profil ${profile.name}.` });
try {
frontendDebug("kb-app-demo-desktop.frontend.demo_execution_spl", `Invoke demo_execution_devnet_prepare_profile profile=${profile.name}`);
const readiness = await invoke<DemoExecutionDevnetStoreReadinessPayload>("demo_execution_devnet_prepare_profile", { profileName: profile.name });
frontendDebug("kb-app-demo-desktop.frontend.demo_execution_spl", `Invoke demo_execution_devnet_prepare_profile completed profile=${profile.name} resources=${readiness.availableResourcesAfter}/${readiness.expectedResources}`);
profileStoreReady = true;
appendLog({ timestamp: new Date().toISOString(), level: "info", stage: "store_ready", message: `Store ${readiness.backend} prêt: ${readiness.availableResourcesAfter}/${readiness.expectedResources} ressources, créées=${readiness.createdResources}, autoInit=${readiness.autoInitializeSchema}.` });
} catch (caughtError) {
@@ -948,6 +1039,8 @@ document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(item => new bootstrap.Tooltip(item));
element<HTMLSelectElement>("#executionProfileSelect").addEventListener("change", () => {
updateProfileHelp();
markTokenJournalFiltersDirty();
markAtaJournalFiltersDirty();
void prepareSelectedProfile();
});
element<HTMLButtonElement>("#generateRecipientButton").addEventListener("click", () => {
@@ -982,8 +1075,27 @@ document.addEventListener("DOMContentLoaded", () => {
void executeToken2022(true);
});
element<HTMLButtonElement>("#refreshTokenJournalButton").addEventListener("click", () => {
void loadTokenJournal();
frontendDebug("kb-app-demo-desktop.frontend.demo_execution_spl", "Token journal load button clicked");
void startTokenJournalLoad();
});
element<HTMLButtonElement>("#tokenJournalFirstBlockButton").addEventListener("click", () => {
void loadTokenJournalBlock(1);
});
element<HTMLButtonElement>("#tokenJournalPreviousBlockButton").addEventListener("click", () => {
void loadTokenJournalBlock(tokenJournalBlockState.currentBlock - 1);
});
element<HTMLButtonElement>("#tokenJournalNextBlockButton").addEventListener("click", () => {
void loadTokenJournalBlock(tokenJournalBlockState.currentBlock + 1);
});
for (const selector of [
"#tokenJournalSignatureInput",
"#tokenJournalMintInput",
"#tokenJournalAccountInput",
"#tokenJournalOperationInput",
]) {
element<HTMLInputElement>(selector).addEventListener("input", markTokenJournalFiltersDirty);
}
element<HTMLSelectElement>("#tokenJournalFamilySelect").addEventListener("change", markTokenJournalFiltersDirty);
element<HTMLButtonElement>("#deriveAtaButton").addEventListener("click", () => {
void deriveAta();
});
@@ -994,8 +1106,26 @@ document.addEventListener("DOMContentLoaded", () => {
void executeAta(true);
});
element<HTMLButtonElement>("#refreshAtaJournalButton").addEventListener("click", () => {
void loadAtaJournal();
frontendDebug("kb-app-demo-desktop.frontend.demo_execution_spl", "ATA journal load button clicked");
void startAtaJournalLoad();
});
element<HTMLButtonElement>("#ataJournalFirstBlockButton").addEventListener("click", () => {
void loadAtaJournalBlock(1);
});
element<HTMLButtonElement>("#ataJournalPreviousBlockButton").addEventListener("click", () => {
void loadAtaJournalBlock(ataJournalBlockState.currentBlock - 1);
});
element<HTMLButtonElement>("#ataJournalNextBlockButton").addEventListener("click", () => {
void loadAtaJournalBlock(ataJournalBlockState.currentBlock + 1);
});
for (const selector of [
"#ataJournalSignatureInput",
"#ataJournalMintInput",
"#ataJournalAddressInput",
"#ataJournalOperationInput",
]) {
element<HTMLInputElement>(selector).addEventListener("input", markAtaJournalFiltersDirty);
}
element<HTMLButtonElement>("#cancelExecutionButton").addEventListener("click", () => {
void cancelExecution();
});

View File

@@ -0,0 +1,114 @@
// file: kb-app-demo-desktop/frontend/ts/demo_store_block_pagination.ts
// version: 1
export type StoreBlockMetadata = {
blockSize: number;
blockIndex: number;
totalRows: string;
totalBlocks: number;
nextCursor: string | null;
};
export type StoreBlockState = {
blockSize: number;
currentBlock: number;
totalBlocks: number;
totalRows: string;
nextCursor: string | null;
cursors: Map<number, string | null>;
};
export type StoreBlockControls = {
firstSelector: string;
previousSelector: string;
nextSelector: string;
summarySelector: string;
};
export function createStoreBlockState(): StoreBlockState {
return {
blockSize: 500,
currentBlock: 0,
totalBlocks: 0,
totalRows: "0",
nextCursor: null,
cursors: new Map<number, string | null>([[1, null]]),
};
}
export function resetStoreBlockState(state: StoreBlockState): void {
state.blockSize = 500;
state.currentBlock = 0;
state.totalBlocks = 0;
state.totalRows = "0";
state.nextCursor = null;
state.cursors = new Map<number, string | null>([[1, null]]);
}
export function applyStoreBlockMetadata(
state: StoreBlockState,
metadata: StoreBlockMetadata,
controls: StoreBlockControls,
): void {
state.blockSize = metadata.blockSize;
state.currentBlock = metadata.blockIndex;
state.totalBlocks = metadata.totalBlocks;
state.totalRows = metadata.totalRows;
state.nextCursor = metadata.nextCursor;
if (metadata.blockIndex > 0 && metadata.nextCursor !== null) {
state.cursors.set(metadata.blockIndex + 1, metadata.nextCursor);
}
renderStoreBlockControls(state, controls);
}
export function renderStoreBlockControls(
state: StoreBlockState,
controls: StoreBlockControls,
): void {
const firstButton = document.querySelector<HTMLButtonElement>(controls.firstSelector);
const previousButton = document.querySelector<HTMLButtonElement>(controls.previousSelector);
const nextButton = document.querySelector<HTMLButtonElement>(controls.nextSelector);
const summary = document.querySelector<HTMLElement>(controls.summarySelector);
const hasRows = state.totalBlocks > 0;
if (firstButton !== null) {
firstButton.disabled = !hasRows || state.currentBlock <= 1;
}
if (previousButton !== null) {
previousButton.disabled = !hasRows || state.currentBlock <= 1;
}
if (nextButton !== null) {
nextButton.disabled = state.nextCursor === null;
}
if (summary !== null) {
summary.textContent = hasRows
? `Bloc ${state.currentBlock} / ${state.totalBlocks} · ${state.totalRows} résultat(s) · ${state.blockSize}/bloc`
: `Bloc 0 / 0 · 0 résultat · ${state.blockSize}/bloc`;
}
}
export function markStoreBlockFiltersDirty(
state: StoreBlockState,
controls: StoreBlockControls,
): void {
resetStoreBlockState(state);
const firstButton = document.querySelector<HTMLButtonElement>(controls.firstSelector);
const previousButton = document.querySelector<HTMLButtonElement>(controls.previousSelector);
const nextButton = document.querySelector<HTMLButtonElement>(controls.nextSelector);
const summary = document.querySelector<HTMLElement>(controls.summarySelector);
if (firstButton !== null) {
firstButton.disabled = true;
}
if (previousButton !== null) {
previousButton.disabled = true;
}
if (nextButton !== null) {
nextButton.disabled = true;
}
if (summary !== null) {
summary.textContent = "Filtres modifiés — rechargez le premier bloc.";
}
}
export function cursorForBlock(state: StoreBlockState, blockIndex: number): string | null | undefined {
return state.cursors.get(blockIndex);
}

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/demo_store_core.ts
// version: 6
// version: 7
import * as bootstrap from "bootstrap";
import "simplebar";
@@ -35,7 +35,7 @@ document.addEventListener("DOMContentLoaded", () => {
frontendDebug("kb_app_demo_desktop.frontend.demo_store_core", "Core store demo window loaded");
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
document.querySelector<HTMLButtonElement>("#refreshSqlCoreButton")?.addEventListener("click", () => {
document.querySelector<HTMLButtonElement>("#refreshStoreCoreButton")?.addEventListener("click", () => {
void refreshCore();
});
void refreshCore();

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/demo_store_diag.ts
// version: 5
// version: 6
import * as bootstrap from "bootstrap";
import "simplebar";
@@ -50,10 +50,10 @@ document.addEventListener("DOMContentLoaded", () => {
frontendDebug("kb_app_demo_desktop.frontend.demo_store_diag", "Store diagnostics demo window loaded");
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
document.querySelector<HTMLButtonElement>("#refreshSqlDiagButton")?.addEventListener("click", () => {
document.querySelector<HTMLButtonElement>("#refreshStoreDiagButton")?.addEventListener("click", () => {
void refreshDiagnostics();
});
document.querySelector<HTMLButtonElement>("#copySqlDiagButton")?.addEventListener("click", () => {
document.querySelector<HTMLButtonElement>("#copyStoreDiagButton")?.addEventListener("click", () => {
void copyDiagnostics();
});
void refreshDiagnostics();

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/demo_store_raw.ts
// version: 6
// version: 7
import * as bootstrap from "bootstrap";
import "simplebar";
@@ -35,7 +35,7 @@ document.addEventListener("DOMContentLoaded", () => {
frontendDebug("kb_app_demo_desktop.frontend.demo_store_raw", "Raw store demo window loaded");
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
document.querySelector<HTMLButtonElement>("#refreshSqlRawButton")?.addEventListener("click", () => {
document.querySelector<HTMLButtonElement>("#refreshStoreRawButton")?.addEventListener("click", () => {
void refreshRaw();
});
void refreshRaw();

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/demo_store_replay_candidates.ts
// version: 12
// version: 14
import * as bootstrap from "bootstrap";
import "simplebar";
@@ -10,12 +10,25 @@ 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";
@@ -30,7 +43,7 @@ type EntityViewConfig = {
pluralLabel: string;
tableSelector: string;
containsSelector: string;
limitSelector: string;
blockControls: StoreBlockControls;
summarySelector: string;
loadButtonSelector: string;
resetButtonSelector: string;
@@ -48,7 +61,12 @@ const entityViewConfigs: EntityViewConfig[] = [
pluralLabel: "mints",
tableSelector: "#mintCandidateTable",
containsSelector: "#mintContainsInput",
limitSelector: "#mintLimitInput",
blockControls: {
firstSelector: "#mintFirstBlockButton",
previousSelector: "#mintPreviousBlockButton",
nextSelector: "#mintNextBlockButton",
summarySelector: "#mintBlockSummary",
},
summarySelector: "#mintResultSummary",
loadButtonSelector: "#loadMintsButton",
resetButtonSelector: "#resetMintsButton",
@@ -63,7 +81,12 @@ const entityViewConfigs: EntityViewConfig[] = [
pluralLabel: "owners",
tableSelector: "#ownerCandidateTable",
containsSelector: "#ownerContainsInput",
limitSelector: "#ownerLimitInput",
blockControls: {
firstSelector: "#ownerFirstBlockButton",
previousSelector: "#ownerPreviousBlockButton",
nextSelector: "#ownerNextBlockButton",
summarySelector: "#ownerBlockSummary",
},
summarySelector: "#ownerResultSummary",
loadButtonSelector: "#loadOwnersButton",
resetButtonSelector: "#resetOwnersButton",
@@ -78,7 +101,12 @@ const entityViewConfigs: EntityViewConfig[] = [
pluralLabel: "comptes",
tableSelector: "#accountCandidateTable",
containsSelector: "#accountContainsInput",
limitSelector: "#accountLimitInput",
blockControls: {
firstSelector: "#accountFirstBlockButton",
previousSelector: "#accountPreviousBlockButton",
nextSelector: "#accountNextBlockButton",
summarySelector: "#accountBlockSummary",
},
summarySelector: "#accountResultSummary",
loadButtonSelector: "#loadAccountsButton",
resetButtonSelector: "#resetAccountsButton",
@@ -94,7 +122,31 @@ let mintTable: Api<DemoStoreReplayEntityRow> | null = null;
let ownerTable: Api<DemoStoreReplayEntityRow> | null = null;
let accountTable: Api<DemoStoreReplayEntityRow> | null = null;
let busyOperationCount = 0;
let maximumReplayCandidateLimit = 500;
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);
@@ -117,24 +169,6 @@ function requiredInputValue(selector: string): string {
return value;
}
function positiveIntegerValue(selector: string): number {
const value = Number.parseInt(requiredInputValue(selector), 10);
if (!Number.isSafeInteger(value) || value <= 0) {
throw new Error(`Valeur entière positive invalide pour ${selector}`);
}
return value;
}
function boundedPositiveIntegerValue(selector: string): number {
const value = positiveIntegerValue(selector);
if (value > maximumReplayCandidateLimit) {
throw new Error(
`La limite doit être comprise entre 1 et ${maximumReplayCandidateLimit} pour ${selector}`,
);
}
return value;
}
function optionalNonNegativeIntegerValue(selector: string): number | null {
const raw = optionalInputValue(selector);
if (raw === null) {
@@ -285,7 +319,7 @@ function createTransactionTable(): Api<DemoStoreReplayTransactionRow> {
],
order: [[2, "desc"]],
pageLength: 10,
lengthMenu: [10, 25, 50, 100, 250],
lengthMenu: [10, 25, 50, 100, 250, 500],
select: { style: "multi", selector: "td:first-child", headerCheckbox: "select-all" },
language: commonLanguage(
"Aucune transaction chargée",
@@ -319,7 +353,7 @@ function createProgramTable(): Api<DemoStoreReplayProgramRow> {
],
order: [[3, "desc"]],
pageLength: 10,
lengthMenu: [10, 25, 50, 100, 250],
lengthMenu: [10, 25, 50, 100, 250, 500],
select: { style: "multi", selector: "td:first-child", headerCheckbox: "select-all" },
language: commonLanguage(
"Aucun programme chargé",
@@ -352,7 +386,7 @@ function createEntityTable(config: EntityViewConfig): Api<DemoStoreReplayEntityR
],
order: [[2, "desc"]],
pageLength: 10,
lengthMenu: [10, 25, 50, 100, 250],
lengthMenu: [10, 25, 50, 100, 250, 500],
select: { style: "multi", selector: "td:first-child", headerCheckbox: "select-all" },
language: commonLanguage(
`Aucun ${config.label} chargé`,
@@ -412,6 +446,21 @@ function selectedOrSingleFilteredRow<T>(table: Api<T>, label: string): T {
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;
@@ -419,13 +468,20 @@ function setBusy(busy: boolean, message: string): void {
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") {
button.disabled = operationInProgress;
if (button.id === "openCoreExtractionButton") {
return;
}
if (!operationInProgress && button.dataset.storeBlockNavigation === "true") {
return;
}
button.disabled = operationInProgress;
});
if (!operationInProgress) {
restoreLoadedStoreBlockControls();
}
}
function buildTransactionRequest(): DemoStoreReplayTransactionRequest {
function buildTransactionBaseRequest(): TransactionBaseRequest {
const entityKind = optionalInputValue("#transactionEntityKindSelect");
const entityValue = optionalInputValue("#transactionEntityValueInput");
if ((entityKind === null) !== (entityValue === null)) {
@@ -441,11 +497,31 @@ function buildTransactionRequest(): DemoStoreReplayTransactionRequest {
programScope: requiredInputValue("#transactionProgramScopeSelect"),
entityKind,
entityValue,
limit: boundedPositiveIntegerValue("#transactionLimitInput"),
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 = "";
@@ -456,95 +532,190 @@ async function resetTransactionFilters(): Promise<void> {
element<HTMLSelectElement>("#transactionProgramScopeSelect").value = "any";
element<HTMLSelectElement>("#transactionEntityKindSelect").value = "";
element<HTMLInputElement>("#transactionEntityValueInput").value = "";
element<HTMLInputElement>("#transactionLimitInput").value = "500";
element<HTMLInputElement>("#transactionNewestFirstInput").checked = true;
resetTableState(transactionTable);
await loadTransactions();
await startTransactionLoad();
}
async function resetProgramFilters(): Promise<void> {
element<HTMLSelectElement>("#knownProgramSelect").value = "";
element<HTMLInputElement>("#programContainsInput").value = "";
element<HTMLInputElement>("#programLimitInput").value = "500";
resetTableState(programTable);
await loadPrograms();
await startProgramLoad();
}
async function resetEntityFilters(config: EntityViewConfig): Promise<void> {
element<HTMLInputElement>(config.containsSelector).value = "";
element<HTMLInputElement>(config.limitSelector).value = "500";
resetTableState(entityTable(config.kind));
await loadEntity(config);
await startEntityLoad(config);
}
async function loadTransactions(): Promise<void> {
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 = buildTransactionRequest();
const rows = await invoke<DemoStoreReplayTransactionRow[]>("load_demo_store_replay_transactions", { request });
replaceRows(transactionTable, rows);
element<HTMLElement>("#transactionResultSummary").textContent = `${rows.length} transaction(s) chargée(s) depuis le store actif.`;
frontendDebug(tracingTarget, `loaded ${rows.length} replay transaction candidates`);
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 loadPrograms(): Promise<void> {
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 = {
programIdContains: optionalInputValue("#programContainsInput"),
limit: boundedPositiveIntegerValue("#programLimitInput"),
...programBaseRequest,
cursor,
blockIndex,
};
const rows = await invoke<DemoStoreReplayProgramRow[]>("load_demo_store_replay_programs", { request });
replaceRows(programTable, rows);
element<HTMLElement>("#programResultSummary").textContent = `${rows.length} programme(s) chargé(s), avec occurrences top-level, inner et logs liés.`;
frontendDebug(tracingTarget, `loaded ${rows.length} replay program summaries`);
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 loadEntity(config: EntityViewConfig): Promise<void> {
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 = {
entityKind: config.kind,
entityValueContains: optionalInputValue(config.containsSelector),
limit: boundedPositiveIntegerValue(config.limitSelector),
...baseRequest,
cursor,
blockIndex,
};
const rows = await invoke<DemoStoreReplayEntityRow[]>("load_demo_store_replay_entities", { request });
replaceRows(table, rows);
element<HTMLElement>(config.summarySelector).textContent = `${rows.length} ${config.pluralLabel} chargé(s) depuis le store actif.`;
frontendDebug(tracingTarget, `loaded ${rows.length} replay ${config.kind} summaries`);
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.`);
@@ -635,7 +806,7 @@ async function useSelectedProgram(): Promise<void> {
element<HTMLInputElement>("#transactionProgramIdInput").value = row.programId;
element<HTMLSelectElement>("#transactionProgramScopeSelect").value = "any";
showTransactionsTab();
await loadTransactions();
await startTransactionLoad();
}
async function useSelectedEntity(config: EntityViewConfig): Promise<void> {
@@ -647,7 +818,7 @@ async function useSelectedEntity(config: EntityViewConfig): Promise<void> {
element<HTMLSelectElement>("#transactionEntityKindSelect").value = config.kind;
element<HTMLInputElement>("#transactionEntityValueInput").value = row.entityValue;
showTransactionsTab();
await loadTransactions();
await startTransactionLoad();
}
async function openCoreExtraction(): Promise<void> {
@@ -661,16 +832,12 @@ async function openCoreExtraction(): Promise<void> {
async function loadOptions(): Promise<void> {
const options = await invoke<DemoStoreReplayOptionsPayload>("demo_store_replay_options");
if (!Number.isSafeInteger(options.maximumLimit) || options.maximumLimit <= 0) {
throw new Error("La limite maximale des candidats replay retournée par le backend est invalide.");
if (!Number.isSafeInteger(options.blockSize) || options.blockSize !== 500) {
throw new Error("La taille de bloc Store retournée par le backend est invalide.");
}
maximumReplayCandidateLimit = options.maximumLimit;
element<HTMLElement>("#replayCandidateProfileBadge").textContent = `Profil ${options.activeProfileName}`;
element<HTMLElement>("#replayCandidateDsn").textContent = options.connectionDescriptor;
element<HTMLElement>("#replayCandidateMaximumLimit").textContent = String(options.maximumLimit);
document.querySelectorAll<HTMLInputElement>("input[type=number][max]").forEach(input => {
input.max = String(options.maximumLimit);
});
element<HTMLElement>("#replayCandidateBlockSize").textContent = `${options.blockSize} lignes`;
populateKnownPrograms(options.knownPrograms);
}
@@ -729,14 +896,20 @@ function reportUiError(action: string, caughtError: unknown): void {
}
function installHandlers(): void {
element<HTMLButtonElement>("#loadTransactionsButton").addEventListener("click", () => void loadTransactions());
element<HTMLButtonElement>("#loadTransactionsButton").addEventListener("click", () => void startTransactionLoad());
element<HTMLButtonElement>("#resetTransactionsButton").addEventListener("click", () => {
void resetTransactionFilters().catch(error => reportUiError("reset transaction filters", error));
});
element<HTMLButtonElement>("#loadProgramsButton").addEventListener("click", () => void loadPrograms());
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));
@@ -754,10 +927,13 @@ function installHandlers(): void {
void useSelectedProgram().catch(error => reportUiError("use selected program", error));
});
for (const config of entityViewConfigs) {
element<HTMLButtonElement>(config.loadButtonSelector).addEventListener("click", () => void loadEntity(config));
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));
});
@@ -767,16 +943,49 @@ function installHandlers(): void {
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");
@@ -790,11 +999,11 @@ document.addEventListener("DOMContentLoaded", () => {
installInlineCopyHandler();
void loadOptions()
.then(() => Promise.all([
loadTransactions(),
loadPrograms(),
loadEntity(entityViewConfigs[0]),
loadEntity(entityViewConfigs[1]),
loadEntity(entityViewConfigs[2]),
startTransactionLoad(),
startProgramLoad(),
startEntityLoad(entityViewConfigs[0]),
startEntityLoad(entityViewConfigs[1]),
startEntityLoad(entityViewConfigs[2]),
]))
.catch(caughtError => reportUiError("initial loading", caughtError));
});