This commit is contained in:
2026-07-23 16:37:12 +02:00
parent 99c345f2f2
commit 0da75c1311
2159 changed files with 230833 additions and 0 deletions

View File

@@ -0,0 +1,267 @@
// file: kb_app_demo/frontend/ts/demo_backfill.ts
// version: 5
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log";
import type { DemoBackfillOptionsPayload } from "./bindings/kb_app_demo/demo_backfill/DemoBackfillOptionsPayload";
import type { DemoBackfillProgressPayload } from "./bindings/kb_app_demo/demo_backfill/DemoBackfillProgressPayload";
import type { DemoBackfillRequest } from "./bindings/kb_app_demo/demo_backfill/DemoBackfillRequest";
import type { DemoBackfillSummaryPayload } from "./bindings/kb_app_demo/demo_backfill/DemoBackfillSummaryPayload";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
const logLines: string[] = [];
const maximumLogLines = 1000;
let running = false;
function element<T extends HTMLElement>(selector: string): T {
const value = document.querySelector<T>(selector);
if (!value) {
throw new Error(`Missing UI element: ${selector}`);
}
return value;
}
function inputValue(selector: string): string {
return element<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>(selector).value.trim();
}
function integerValue(selector: string): number {
const parsed = Number.parseInt(inputValue(selector), 10);
if (!Number.isFinite(parsed)) {
throw new Error(`Valeur numérique invalide pour ${selector}`);
}
return parsed;
}
const base58Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
function decodesToSolanaAddress(value: string): boolean {
if (value.length === 0) {
return false;
}
let numericValue = 0n;
for (const character of value) {
const digit = base58Alphabet.indexOf(character);
if (digit < 0) {
return false;
}
numericValue = numericValue * 58n + BigInt(digit);
}
let decodedNonZeroBytes = 0;
let remaining = numericValue;
while (remaining > 0n) {
decodedNonZeroBytes += 1;
remaining >>= 8n;
}
let leadingZeroBytes = 0;
for (const character of value) {
if (character !== "1") {
break;
}
leadingZeroBytes += 1;
}
return leadingZeroBytes + decodedNonZeroBytes === 32;
}
function appendLog(payload: DemoBackfillProgressPayload | { timestamp: string; level: string; message: string }): void {
const progress = "completed" in payload && payload.completed !== null && payload.total !== null
? ` [${payload.completed}/${payload.total}]`
: "";
logLines.push(`${payload.timestamp} ${payload.level.toUpperCase()}${progress} ${payload.message}`);
while (logLines.length > maximumLogLines) {
logLines.shift();
}
element<HTMLTextAreaElement>("#backfillLogOutput").value = logLines.join("\n");
element<HTMLTextAreaElement>("#backfillLogOutput").scrollTop = element<HTMLTextAreaElement>("#backfillLogOutput").scrollHeight;
}
function setRunning(value: boolean): void {
running = value;
document.querySelectorAll<HTMLButtonElement>(".backfill-start-button").forEach(button => {
button.disabled = value;
});
element<HTMLButtonElement>("#cancelBackfillButton").disabled = !value;
const badge = element<HTMLElement>("#backfillStatusBadge");
badge.textContent = value ? "Backfill en cours" : "Prêt";
badge.className = value ? "badge text-bg-warning" : "badge text-bg-success";
}
function commonRequest(mode: string): DemoBackfillRequest {
return {
role: inputValue("#backfillRoleSelect"),
commitment: inputValue("#backfillCommitmentSelect"),
mode,
signaturesText: null,
address: null,
anchorSignature: null,
direction: null,
limit: 1,
pageSize: integerValue("#backfillPageSizeInput"),
maxPages: integerValue("#backfillMaxPagesInput"),
maxConcurrentRequests: integerValue("#backfillConcurrencyInput"),
maxRetries: integerValue("#backfillRetriesInput"),
};
}
function buildRequest(mode: string): DemoBackfillRequest {
const request = commonRequest(mode);
if (mode === "explicit_signatures") {
request.signaturesText = element<HTMLTextAreaElement>("#explicitSignaturesTextarea").value;
return request;
}
request.address = inputValue(`#${mode}AddressInput`);
const anchorSignature = inputValue(`#${mode}AnchorInput`);
request.anchorSignature = anchorSignature.length > 0 ? anchorSignature : null;
request.direction = inputValue(`#${mode}DirectionSelect`);
request.limit = integerValue(`#${mode}LimitInput`);
return request;
}
function validationMessage(request: DemoBackfillRequest): string | null {
if (request.mode === "explicit_signatures") {
const signatureCount = request.signaturesText
? request.signaturesText.split(/\r?\n/).filter(value => value.trim().length > 0).length
: 0;
if (signatureCount === 0) {
return "Ajouter au moins une signature explicite avant de lancer le backfill.";
}
return null;
}
if (!request.address || request.address.trim().length === 0) {
return "Ladresse est obligatoire pour ce mode de backfill.";
}
if (!decodesToSolanaAddress(request.address.trim())) {
return "Ladresse Solana doit être une valeur Base58 décodant exactement sur 32 octets.";
}
if (request.direction === "after" && (!request.anchorSignature || request.anchorSignature.trim().length === 0)) {
return "La signature dancrage est obligatoire pour rechercher des transactions plus récentes.";
}
return null;
}
function reportValidationWarning(message: string): void {
element<HTMLTextAreaElement>("#backfillSummaryOutput").value = JSON.stringify({ validation: message }, null, 2);
appendLog({ timestamp: new Date().toISOString(), level: "warn", message });
frontendDebug("kb_app_demo.frontend.demo_backfill", `Backfill request rejected locally: ${message}`);
}
function synchronizeAnchorRequirement(mode: "program" | "token" | "pool"): void {
const direction = element<HTMLSelectElement>(`#${mode}DirectionSelect`).value;
const anchor = element<HTMLInputElement>(`#${mode}AnchorInput`);
const required = direction === "after";
anchor.required = required;
anchor.placeholder = required
? "Signature obligatoire pour rechercher après cette transaction"
: "Vide : commencer depuis les transactions les plus récentes";
}
async function executeBackfill(mode: string): Promise<void> {
if (running) {
return;
}
try {
const request = buildRequest(mode);
const invalid = validationMessage(request);
if (invalid) {
reportValidationWarning(invalid);
return;
}
setRunning(true);
element<HTMLTextAreaElement>("#backfillSummaryOutput").value = "Exécution en cours...";
appendLog({ timestamp: new Date().toISOString(), level: "info", message: `Démarrage du mode ${mode}` });
const summary = await invoke<DemoBackfillSummaryPayload>("demo_backfill_execute", { request });
element<HTMLTextAreaElement>("#backfillSummaryOutput").value = JSON.stringify(summary, null, 2);
appendLog({
timestamp: new Date().toISOString(),
level: summary.cancelled ? "warn" : "info",
message: `Campagne terminée : completed=${summary.candidatesCompleted}, cancelled=${summary.candidatesCancelled}, notStarted=${summary.candidatesNotStarted}, inserted=${summary.canonicalInserted}, existing=${summary.existingSkipped}, missing=${summary.missing}, failed=${summary.failed}`,
});
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLTextAreaElement>("#backfillSummaryOutput").value = JSON.stringify({ error: message }, null, 2);
appendLog({ timestamp: new Date().toISOString(), level: "error", message });
frontendError("kb_app_demo.frontend.demo_backfill", `Backfill failed: ${message}`);
} finally {
setRunning(false);
}
}
async function cancelBackfill(): Promise<void> {
try {
const accepted = await invoke<boolean>("demo_backfill_cancel");
appendLog({
timestamp: new Date().toISOString(),
level: accepted ? "warn" : "info",
message: accepted ? "Demande d'arrêt envoyée." : "Aucune campagne active.",
});
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
frontendError("kb_app_demo.frontend.demo_backfill", `Cancellation failed: ${message}`);
}
}
async function loadOptions(): Promise<void> {
const options = await invoke<DemoBackfillOptionsPayload>("demo_backfill_options");
const roleSelect = element<HTMLSelectElement>("#backfillRoleSelect");
roleSelect.replaceChildren();
for (const role of options.roles) {
const option = document.createElement("option");
option.value = role.role;
option.textContent = `${role.role}${role.providers.join(", ")}`;
option.selected = role.role === options.defaultRole;
roleSelect.append(option);
}
if (options.roles.length === 0) {
const option = document.createElement("option");
option.value = "";
option.textContent = "Aucun rôle compatible";
roleSelect.append(option);
}
element<HTMLSelectElement>("#backfillCommitmentSelect").value = options.defaultCommitment;
element<HTMLInputElement>("#backfillPageSizeInput").value = String(options.defaultPageSize);
element<HTMLInputElement>("#backfillMaxPagesInput").value = String(options.defaultMaxPages);
element<HTMLInputElement>("#backfillConcurrencyInput").value = String(options.defaultMaxConcurrentRequests);
element<HTMLInputElement>("#backfillRetriesInput").value = String(options.defaultMaxRetries);
setRunning(options.running);
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge("kb_app_demo.frontend.demo_backfill");
frontendDebug("kb_app_demo.frontend.demo_backfill", "backfill demo window loaded");
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(item => new bootstrap.Tooltip(item));
for (const mode of ["program", "token", "pool"] as const) {
const direction = element<HTMLSelectElement>(`#${mode}DirectionSelect`);
direction.addEventListener("change", () => synchronizeAnchorRequirement(mode));
synchronizeAnchorRequirement(mode);
}
document.querySelectorAll<HTMLButtonElement>(".backfill-start-button").forEach(button => {
button.addEventListener("click", () => {
const mode = button.dataset.backfillMode;
if (mode) {
void executeBackfill(mode);
}
});
});
element<HTMLButtonElement>("#cancelBackfillButton").addEventListener("click", () => {
void cancelBackfill();
});
element<HTMLButtonElement>("#clearBackfillLogButton").addEventListener("click", () => {
logLines.length = 0;
element<HTMLTextAreaElement>("#backfillLogOutput").value = "";
});
void listen<DemoBackfillProgressPayload>("demo-backfill-progress", event => {
appendLog(event.payload);
});
void loadOptions().catch(caughtError => {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
appendLog({ timestamp: new Date().toISOString(), level: "error", message });
frontendError("kb_app_demo.frontend.demo_backfill", `Options loading failed: ${message}`);
});
});

View File

@@ -0,0 +1,84 @@
// file: kb_app_demo/frontend/ts/demo_config.ts
// version: 4
import * as bootstrap from "bootstrap";
import "simplebar";
import "@andypf/json-viewer";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log";
import type { DemoConfigPayload } from "./bindings/kb_app_demo/demo_config/DemoConfigPayload";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
function setText(selector: string, value: string): void {
const element = document.querySelector<HTMLElement>(selector);
if (element) {
element.textContent = value;
}
}
function renderJsonViewer(selector: string, value: unknown): void {
const container = document.querySelector<HTMLElement>(selector);
if (!container) {
return;
}
const serialized = JSON.stringify(value, null, 2) ?? "null";
const viewer = document.createElement("andypf-json-viewer");
viewer.setAttribute("indent", "2");
viewer.setAttribute("expanded", "1");
viewer.setAttribute("theme", "default-light");
viewer.setAttribute("show-data-types", "true");
viewer.setAttribute("show-toolbar", "false");
viewer.setAttribute("expand-icon-type", "arrow");
viewer.setAttribute("show-copy", "true");
viewer.setAttribute("show-size", "true");
viewer.setAttribute("expand-empty", "false");
viewer.setAttribute("data", serialized);
container.textContent = "";
container.appendChild(viewer);
}
function parseSchemaJson(schemaJson: string): unknown {
try {
return JSON.parse(schemaJson) as unknown;
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
return {
error: `Schema JSON parsing failed: ${message}`,
raw: schemaJson,
};
}
}
async function loadDemoConfig(): Promise<void> {
try {
const payload = await invoke<DemoConfigPayload>("load_demo_config");
setText("#configPath", payload.config_path);
setText("#activeProfileName", payload.active_profile_name);
setText("#activeEnvironment", payload.environment);
renderJsonViewer("#activeProfileJson", payload.active_profile);
renderJsonViewer("#fullConfigJson", payload.app_config);
renderJsonViewer("#schemaJson", parseSchemaJson(payload.schema_json));
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#activeProfileJson", `Erreur pendant le chargement de la configuration : ${message}`);
frontendError("kb_app_demo.frontend.demo_config", `Configuration loading failed: ${message}`);
}
}
document.addEventListener("DOMContentLoaded", async () => {
installFrontendConsoleBridge("kb_app_demo.frontend.demo_config");
frontendDebug("kb_app_demo.frontend.demo_config", "configuration demo window loaded");
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
const toastElList = document.querySelectorAll('.toast');
Array.from(toastElList).map(toastEl => new bootstrap.Toast(toastEl));
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]');
Array.from(popoverTriggerList).map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl));
await loadDemoConfig();
});

View File

@@ -0,0 +1,185 @@
// file: kb_app_demo/frontend/ts/demo_core_extraction.ts
// version: 4
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log";
import type { DemoCoreExtractionOptionsPayload } from "./bindings/kb_app_demo/demo_core_extraction/DemoCoreExtractionOptionsPayload";
import type { DemoCoreExtractionProgressPayload } from "./bindings/kb_app_demo/demo_core_extraction/DemoCoreExtractionProgressPayload";
import type { DemoCoreExtractionRequest } from "./bindings/kb_app_demo/demo_core_extraction/DemoCoreExtractionRequest";
import type { DemoCoreExtractionSummaryPayload } from "./bindings/kb_app_demo/demo_core_extraction/DemoCoreExtractionSummaryPayload";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
const logLines: string[] = [];
const maximumLogLines = 1000;
let running = false;
function element<T extends HTMLElement>(selector: string): T {
const value = document.querySelector<T>(selector);
if (!value) {
throw new Error(`Missing UI element: ${selector}`);
}
return value;
}
function inputValue(selector: string): string {
return element<HTMLInputElement | HTMLTextAreaElement>(selector).value.trim();
}
function integerValue(selector: string): number {
const parsed = Number.parseInt(inputValue(selector), 10);
if (!Number.isFinite(parsed)) {
throw new Error(`Valeur numérique invalide pour ${selector}`);
}
return parsed;
}
function nullableIntegerValue(selector: string): number | null {
const value = inputValue(selector);
if (value.length === 0) {
return null;
}
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed)) {
throw new Error(`Valeur numérique invalide pour ${selector}`);
}
return parsed;
}
function appendLog(payload: DemoCoreExtractionProgressPayload | { timestamp: string; level: string; message: string }): void {
const progress = "completed" in payload ? ` [${payload.completed}/${payload.total}]` : "";
logLines.push(`${payload.timestamp} ${payload.level.toUpperCase()}${progress} ${payload.message}`);
while (logLines.length > maximumLogLines) {
logLines.shift();
}
const output = element<HTMLTextAreaElement>("#coreExtractionLogOutput");
output.value = logLines.join("\n");
output.scrollTop = output.scrollHeight;
}
function setRunning(value: boolean): void {
running = value;
document.querySelectorAll<HTMLButtonElement>(".core-extraction-start-button").forEach(button => {
button.disabled = value;
});
element<HTMLButtonElement>("#cancelCoreExtractionButton").disabled = !value;
const badge = element<HTMLElement>("#coreExtractionStatusBadge");
badge.textContent = value ? "Extraction en cours" : "Prêt";
badge.className = value ? "badge text-bg-warning" : "badge text-bg-success";
}
function buildRequest(mode: string): DemoCoreExtractionRequest {
return {
mode,
signaturesText: mode === "signatures" ? element<HTMLTextAreaElement>("#coreExtractionSignaturesTextarea").value : null,
programId: mode === "program_id" ? inputValue("#coreExtractionProgramIdInput") : null,
minSlot: mode === "slot_range" ? nullableIntegerValue("#coreExtractionMinSlotInput") : null,
maxSlot: mode === "slot_range" ? nullableIntegerValue("#coreExtractionMaxSlotInput") : null,
limit: integerValue("#coreExtractionLimitInput"),
maxConcurrentExtractions: integerValue("#coreExtractionConcurrencyInput"),
forceReplay: element<HTMLInputElement>("#coreExtractionForceReplayInput").checked,
};
}
async function executeCoreExtraction(mode: string): Promise<void> {
if (running) {
return;
}
try {
const request = buildRequest(mode);
setRunning(true);
element<HTMLTextAreaElement>("#coreExtractionSummaryOutput").value = "Exécution en cours...";
appendLog({ timestamp: new Date().toISOString(), level: "info", message: `Démarrage du mode ${mode}` });
const summary = await invoke<DemoCoreExtractionSummaryPayload>("demo_core_extraction_execute", { request });
element<HTMLTextAreaElement>("#coreExtractionSummaryOutput").value = JSON.stringify(summary, null, 2);
appendLog({
timestamp: new Date().toISOString(),
level: summary.cancelled || Number(summary.failed) > 0 ? "warn" : "info",
message: `Campagne terminée : extracted=${summary.extracted}, skipped=${summary.skipped}, failed=${summary.failed}, cancelled=${summary.cancelledCandidates}, notStarted=${summary.notStarted}`,
});
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLTextAreaElement>("#coreExtractionSummaryOutput").value = JSON.stringify({ error: message }, null, 2);
appendLog({ timestamp: new Date().toISOString(), level: "error", message });
frontendError("kb_app_demo.frontend.demo_core_extraction", `Core extraction failed: ${message}`);
} finally {
setRunning(false);
}
}
async function cancelCoreExtraction(): Promise<void> {
try {
const accepted = await invoke<boolean>("demo_core_extraction_cancel");
appendLog({
timestamp: new Date().toISOString(),
level: accepted ? "warn" : "info",
message: accepted ? "Demande d'arrêt envoyée." : "Aucune campagne active.",
});
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
frontendError("kb_app_demo.frontend.demo_core_extraction", `Cancellation failed: ${message}`);
}
}
async function openDiagnostics(command: "open_demo_sql_pg_raw_window" | "open_demo_sql_pg_core_window" | "open_demo_sql_replay_candidates_window", label: string): Promise<void> {
try {
await invoke(command);
appendLog({ timestamp: new Date().toISOString(), level: "info", message: `${label} ouvert.` });
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
appendLog({ timestamp: new Date().toISOString(), level: "error", message });
frontendError("kb_app_demo.frontend.demo_core_extraction", `${label} opening failed: ${message}`);
}
}
async function loadOptions(): Promise<void> {
const options = await invoke<DemoCoreExtractionOptionsPayload>("demo_core_extraction_options");
element<HTMLElement>("#coreExtractionVersionBadge").textContent = `Version ${options.processorVersion}`;
element<HTMLInputElement>("#coreExtractionLimitInput").value = String(options.defaultLimit);
element<HTMLInputElement>("#coreExtractionConcurrencyInput").value = String(options.defaultMaxConcurrentExtractions);
setRunning(options.running);
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge("kb_app_demo.frontend.demo_core_extraction");
frontendDebug("kb_app_demo.frontend.demo_core_extraction", "core extraction demo window loaded");
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(item => new bootstrap.Tooltip(item));
document.querySelectorAll<HTMLButtonElement>(".core-extraction-start-button").forEach(button => {
button.addEventListener("click", () => {
const mode = button.dataset.coreExtractionMode;
if (mode) {
void executeCoreExtraction(mode);
}
});
});
element<HTMLButtonElement>("#openRawDiagnosticsButton").addEventListener("click", () => {
void openDiagnostics("open_demo_sql_pg_raw_window", "Diagnostic raw");
});
element<HTMLButtonElement>("#openCoreDiagnosticsButton").addEventListener("click", () => {
void openDiagnostics("open_demo_sql_pg_core_window", "Diagnostic core");
});
element<HTMLButtonElement>("#openReplayCandidatesButton").addEventListener("click", () => {
void openDiagnostics("open_demo_sql_replay_candidates_window", "Sélecteur de candidats replay");
});
element<HTMLButtonElement>("#cancelCoreExtractionButton").addEventListener("click", () => {
void cancelCoreExtraction();
});
element<HTMLButtonElement>("#clearCoreExtractionLogButton").addEventListener("click", () => {
logLines.length = 0;
element<HTMLTextAreaElement>("#coreExtractionLogOutput").value = "";
});
void listen<DemoCoreExtractionProgressPayload>("demo-core-extraction-progress", event => {
appendLog(event.payload);
});
void loadOptions().catch(caughtError => {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
appendLog({ timestamp: new Date().toISOString(), level: "error", message });
frontendError("kb_app_demo.frontend.demo_core_extraction", `Options loading failed: ${message}`);
});
});

View File

@@ -0,0 +1,340 @@
// file: kb_app_demo/frontend/ts/demo_decode_replay.ts
// version: 5
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log";
import type { DemoDecodeDiagnosticsPayload } from "./bindings/kb_app_demo/demo_decode_replay/DemoDecodeDiagnosticsPayload";
import type { DemoDecodeReplayOptionsPayload } from "./bindings/kb_app_demo/demo_decode_replay/DemoDecodeReplayOptionsPayload";
import type { DemoDecodeReplayProgressPayload } from "./bindings/kb_app_demo/demo_decode_replay/DemoDecodeReplayProgressPayload";
import type { DemoDecodeReplayRequest } from "./bindings/kb_app_demo/demo_decode_replay/DemoDecodeReplayRequest";
import type { DemoDecodeReplaySummaryPayload } from "./bindings/kb_app_demo/demo_decode_replay/DemoDecodeReplaySummaryPayload";
import type { DemoTransactionAnnotationRequest } from "./bindings/kb_app_demo/demo_decode_replay/DemoTransactionAnnotationRequest";
import type { DemoTransactionAnnotationRow } from "./bindings/kb_app_demo/demo_decode_replay/DemoTransactionAnnotationRow";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
const logLines: string[] = [];
let running = false;
let materializationAvailable = false;
function element<T extends HTMLElement>(selector: string): T {
const value = document.querySelector<T>(selector);
if (!value) {
throw new Error(`Missing UI element: ${selector}`);
}
return value;
}
function inputValue(selector: string): string {
return element<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>(selector).value.trim();
}
function integerValue(selector: string): number {
const value = Number.parseInt(inputValue(selector), 10);
if (!Number.isFinite(value) || value < 1) {
throw new Error(`Valeur numérique invalide pour ${selector}`);
}
return value;
}
function appendLog(payload: DemoDecodeReplayProgressPayload | { timestamp: string; level: string; message: string }): void {
const progress = "completed" in payload ? ` [${payload.completed}/${payload.total}]` : "";
const campaign = "campaignId" in payload ? ` [${payload.campaignId}]` : "";
logLines.push(`${payload.timestamp} ${payload.level.toUpperCase()}${campaign}${progress} ${payload.message}`);
while (logLines.length > 1000) {
logLines.shift();
}
const output = element<HTMLTextAreaElement>("#decodeReplayLogOutput");
output.value = logLines.join("\n");
output.scrollTop = output.scrollHeight;
}
function explicitSignatureCount(): number {
return element<HTMLTextAreaElement>("#decodeReplaySignaturesTextarea")
.value
.split(/\r?\n/)
.filter(value => value.trim().length > 0)
.length;
}
function updateReplayScopeControls(): void {
const signaturesPresent = explicitSignatureCount() > 0;
const incompleteSignatures = inputValue("#decodeReplayStateSelect") === "incomplete_signatures";
const allMatchingInput = element<HTMLInputElement>("#decodeReplayAllSignaturesInput");
const forceInput = element<HTMLInputElement>("#decodeReplayForceInput");
const materializeInput = element<HTMLInputElement>("#decodeReplayMaterializeInput");
if (!running && (signaturesPresent || incompleteSignatures) && allMatchingInput.checked) {
allMatchingInput.checked = false;
}
allMatchingInput.disabled = running || signaturesPresent || incompleteSignatures;
const forceScopeMissing = !signaturesPresent && !allMatchingInput.checked;
forceInput.disabled = running || forceScopeMissing || incompleteSignatures;
if (!running && (forceScopeMissing || incompleteSignatures)) {
forceInput.checked = false;
}
materializeInput.disabled = running || !materializationAvailable;
if (!running && !materializationAvailable) {
materializeInput.checked = false;
}
}
function setRunning(value: boolean): void {
running = value;
element<HTMLButtonElement>("#startDecodeReplayButton").disabled = value;
element<HTMLButtonElement>("#cancelDecodeReplayButton").disabled = !value;
const badge = element<HTMLElement>("#decodeReplayStatusBadge");
badge.textContent = value ? "Replay en cours" : "Prêt";
badge.className = value ? "badge text-bg-warning" : "badge text-bg-success";
updateReplayScopeControls();
}
function selectedDecoderNames(): string[] {
return Array.from(document.querySelectorAll<HTMLInputElement>(".decode-replay-decoder-input:checked"))
.map(input => input.value);
}
function buildRequest(): DemoDecodeReplayRequest {
return {
signaturesText: element<HTMLTextAreaElement>("#decodeReplaySignaturesTextarea").value || null,
programId: inputValue("#decodeReplayProgramIdInput") || null,
instructionState: inputValue("#decodeReplayStateSelect"),
instructionPathsText: element<HTMLTextAreaElement>("#decodeReplayPathsTextarea").value || null,
decoderNames: selectedDecoderNames(),
limit: integerValue("#decodeReplayLimitInput"),
maxConcurrentInputs: integerValue("#decodeReplayConcurrencyInput"),
allCompatible: element<HTMLInputElement>("#decodeReplayAllCompatibleInput").checked,
forceReplay: element<HTMLInputElement>("#decodeReplayForceInput").checked,
forceReplayAllMatching: element<HTMLInputElement>("#decodeReplayAllSignaturesInput").checked,
materializeAfterDecode: element<HTMLInputElement>("#decodeReplayMaterializeInput").checked,
};
}
function validationMessage(request: DemoDecodeReplayRequest): string | null {
const signatureCount = request.signaturesText
? request.signaturesText.split(/\r?\n/).filter(value => value.trim().length > 0).length
: 0;
if (request.decoderNames.length === 0) {
return "Sélectionner au moins un décodeur.";
}
if (request.instructionState === "incomplete_signatures" && request.forceReplay) {
return "Le mode signatures incomplètes rejoue son périmètre sans Force replay global.";
}
if (request.forceReplay && signatureCount === 0 && !request.forceReplayAllMatching) {
return "Le force replay nécessite des signatures explicites ou lautorisation Toutes les signatures.";
}
if (request.forceReplayAllMatching && signatureCount > 0) {
return "Le mode « Toutes les signatures » ne peut pas être combiné avec une liste de signatures explicites.";
}
if (request.forceReplayAllMatching && !request.forceReplay) {
return "Le mode « Toutes les signatures » est uniquement disponible avec Force replay.";
}
if (request.materializeAfterDecode && !materializationAvailable) {
return "Aucun matérialiseur nest actuellement enregistré dans cette application.";
}
return null;
}
function reportValidationWarning(message: string): void {
element<HTMLTextAreaElement>("#decodeReplaySummaryOutput").value = JSON.stringify({ validation: message }, null, 2);
appendLog({ timestamp: new Date().toISOString(), level: "warn", message });
frontendDebug("kb_app_demo.frontend.demo_decode_replay", `Decode replay request rejected locally: ${message}`);
}
async function executeDecodeReplay(): Promise<void> {
if (running) {
return;
}
try {
const request = buildRequest();
const invalid = validationMessage(request);
if (invalid) {
reportValidationWarning(invalid);
return;
}
setRunning(true);
element<HTMLTextAreaElement>("#decodeReplaySummaryOutput").value = "Exécution en cours...";
const signatureCount = request.signaturesText
? request.signaturesText.split(/\r?\n/).filter(value => value.trim().length > 0).length
: 0;
const requestSummary = `signatures=${signatureCount}, programId=${request.programId ?? "auto"}, state=${request.instructionState}, decoders=${request.decoderNames.join(",")}, limit=${request.limit}, concurrency=${request.maxConcurrentInputs}, allCompatible=${request.allCompatible}, forceReplay=${request.forceReplay}, allSignatures=${request.forceReplayAllMatching}, materialize=${request.materializeAfterDecode}`;
appendLog({ timestamp: new Date().toISOString(), level: "info", message: `Démarrage du replay de décodage contextualisé : ${requestSummary}` });
frontendDebug("kb_app_demo.frontend.demo_decode_replay", `Invoke demo_decode_replay_execute ${requestSummary}`);
const summary = await invoke<DemoDecodeReplaySummaryPayload>("demo_decode_replay_execute", { request });
element<HTMLTextAreaElement>("#decodeReplaySummaryOutput").value = JSON.stringify(summary, null, 2);
frontendDebug("kb_app_demo.frontend.demo_decode_replay", `Invoke demo_decode_replay_execute completed summary=${JSON.stringify(summary)}`);
appendLog({
timestamp: new Date().toISOString(),
level: summary.cancelled || summary.failedInputs > 0 ? "warn" : "info",
message: `Campagne ${summary.campaignId} terminée : completed=${summary.completed}, unmatched=${summary.unmatched}, failed=${summary.failedInputs}, notStarted=${summary.notStarted}`,
});
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLTextAreaElement>("#decodeReplaySummaryOutput").value = JSON.stringify({ error: message }, null, 2);
appendLog({ timestamp: new Date().toISOString(), level: "error", message });
frontendError("kb_app_demo.frontend.demo_decode_replay", `Decode replay failed: ${message}`);
} finally {
setRunning(false);
}
}
async function cancelDecodeReplay(): Promise<void> {
try {
frontendDebug("kb_app_demo.frontend.demo_decode_replay", "Invoke demo_decode_replay_cancel");
const accepted = await invoke<boolean>("demo_decode_replay_cancel");
frontendDebug("kb_app_demo.frontend.demo_decode_replay", `Invoke demo_decode_replay_cancel completed accepted=${accepted}`);
appendLog({
timestamp: new Date().toISOString(),
level: accepted ? "warn" : "info",
message: accepted ? "Demande d'arrêt envoyée." : "Aucune campagne active.",
});
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
frontendError("kb_app_demo.frontend.demo_decode_replay", `Cancellation failed: ${message}`);
}
}
async function loadDiagnostics(): Promise<void> {
try {
frontendDebug("kb_app_demo.frontend.demo_decode_replay", "Invoke demo_decode_replay_diagnostics");
const diagnostics = await invoke<DemoDecodeDiagnosticsPayload>("demo_decode_replay_diagnostics");
frontendDebug("kb_app_demo.frontend.demo_decode_replay", `Invoke demo_decode_replay_diagnostics completed tables=${diagnostics.tables.length} coverage=${diagnostics.coverage.length}`);
element<HTMLTextAreaElement>("#decodeReplayDiagnosticsOutput").value = JSON.stringify(diagnostics, null, 2);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLTextAreaElement>("#decodeReplayDiagnosticsOutput").value = JSON.stringify({ error: message }, null, 2);
frontendError("kb_app_demo.frontend.demo_decode_replay", `Diagnostics loading failed: ${message}`);
}
}
function tableCell(value: string, title?: string): HTMLTableCellElement {
const cell = document.createElement("td");
cell.textContent = value;
if (title) {
cell.title = title;
}
return cell;
}
function shortened(value: string, visible: number): string {
return value.length > visible ? `${value.slice(0, visible)}` : value;
}
function renderAnnotations(rows: DemoTransactionAnnotationRow[]): void {
const body = element<HTMLTableSectionElement>("#annotationJournalBody");
body.replaceChildren();
for (const row of rows) {
const tableRow = document.createElement("tr");
tableRow.append(
tableCell(row.slot),
tableCell(shortened(row.signature, 18), row.signature),
tableCell(row.instructionPath),
tableCell(row.generation, row.programId),
tableCell(row.text),
tableCell(String(row.payloadLengthBytes)),
tableCell(row.verifiedSigners.join(", ") || "—"),
tableCell(`${shortened(row.payloadSha256, 12)} · ${row.decoder}`, `${row.payloadSha256}\n${row.idempotenceKey}\ntransaction_annotations@${row.processorVersion}`),
);
body.append(tableRow);
}
element<HTMLElement>("#annotationJournalStatus").textContent = `${rows.length} annotation(s) committed chargée(s).`;
}
async function loadAnnotations(): Promise<void> {
const request: DemoTransactionAnnotationRequest = {
signatureContains: inputValue("#annotationSignatureInput") || null,
limit: integerValue("#annotationLimitInput"),
};
try {
frontendDebug("kb_app_demo.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.frontend.demo_decode_replay", `Invoke demo_decode_replay_annotations completed rows=${rows.length}`);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLElement>("#annotationJournalStatus").textContent = `Erreur : ${message}`;
frontendError("kb_app_demo.frontend.demo_decode_replay", `Annotation journal loading failed: ${message}`);
}
}
async function openWindow(command: "open_demo_sql_pg_core_window" | "open_demo_sql_replay_candidates_window"): Promise<void> {
try {
frontendDebug("kb_app_demo.frontend.demo_decode_replay", `Invoke ${command}`);
await invoke(command);
frontendDebug("kb_app_demo.frontend.demo_decode_replay", `Invoke ${command} completed`);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
frontendError("kb_app_demo.frontend.demo_decode_replay", `Window opening failed: ${message}`);
}
}
async function loadOptions(): Promise<void> {
frontendDebug("kb_app_demo.frontend.demo_decode_replay", "Invoke demo_decode_replay_options");
const options = await invoke<DemoDecodeReplayOptionsPayload>("demo_decode_replay_options");
frontendDebug("kb_app_demo.frontend.demo_decode_replay", `Invoke demo_decode_replay_options completed options=${JSON.stringify(options)}`);
element<HTMLElement>("#decodeReplayVersionBadge").textContent = `Pipeline ${options.pipelineVersion}`;
element<HTMLInputElement>("#decodeReplayLimitInput").value = String(options.defaultLimit);
element<HTMLInputElement>("#decodeReplayConcurrencyInput").value = String(options.defaultMaxConcurrentInputs);
const container = element<HTMLElement>("#decodeReplayDecoderList");
container.innerHTML = "";
for (const decoder of options.decoders) {
const wrapper = document.createElement("div");
wrapper.className = "form-check border rounded p-3 ps-5 mb-2";
const input = document.createElement("input");
input.className = "form-check-input decode-replay-decoder-input";
input.type = "checkbox";
input.value = decoder.name;
input.id = `decode-${decoder.name}`;
input.checked = true;
const label = document.createElement("label");
label.className = "form-check-label";
label.htmlFor = input.id;
label.textContent = `${decoder.name} v${decoder.version}${decoder.programIds.length} programmes`;
wrapper.append(input, label);
container.append(wrapper);
}
materializationAvailable = options.materializerNames.length > 0;
const materializeHelp = element<HTMLElement>("#decodeReplayMaterializeHelp");
materializeHelp.textContent = materializationAvailable
? `Matérialiseurs disponibles : ${options.materializerNames.join(", ")}`
: "Aucun matérialiseur enregistré : option désactivée.";
setRunning(options.running);
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge("kb_app_demo.frontend.demo_decode_replay");
frontendDebug("kb_app_demo.frontend.demo_decode_replay", "decode replay demo window loaded");
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(item => new bootstrap.Tooltip(item));
element<HTMLTextAreaElement>("#decodeReplaySignaturesTextarea").addEventListener("input", () => updateReplayScopeControls());
element<HTMLSelectElement>("#decodeReplayStateSelect").addEventListener("change", () => updateReplayScopeControls());
element<HTMLInputElement>("#decodeReplayAllSignaturesInput").addEventListener("change", event => {
const input = event.currentTarget as HTMLInputElement;
if (input.checked) {
element<HTMLInputElement>("#decodeReplayForceInput").checked = true;
}
updateReplayScopeControls();
});
element<HTMLInputElement>("#decodeReplayForceInput").addEventListener("change", event => {
const input = event.currentTarget as HTMLInputElement;
if (!input.checked) {
element<HTMLInputElement>("#decodeReplayAllSignaturesInput").checked = false;
}
updateReplayScopeControls();
});
element<HTMLButtonElement>("#startDecodeReplayButton").addEventListener("click", () => void executeDecodeReplay());
element<HTMLButtonElement>("#cancelDecodeReplayButton").addEventListener("click", () => void cancelDecodeReplay());
element<HTMLButtonElement>("#loadDecodeDiagnosticsButton").addEventListener("click", () => void loadDiagnostics());
element<HTMLButtonElement>("#loadAnnotationsButton").addEventListener("click", () => void loadAnnotations());
element<HTMLButtonElement>("#openReplayCandidatesFromDecodeButton").addEventListener("click", () => void openWindow("open_demo_sql_replay_candidates_window"));
element<HTMLButtonElement>("#openCoreDiagnosticsFromDecodeButton").addEventListener("click", () => void openWindow("open_demo_sql_pg_core_window"));
void listen<DemoDecodeReplayProgressPayload>("demo-decode-replay-progress", event => appendLog(event.payload));
void loadOptions().catch(caughtError => {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
appendLog({ timestamp: new Date().toISOString(), level: "error", message });
frontendError("kb_app_demo.frontend.demo_decode_replay", `Options loading failed: ${message}`);
});
});

View File

@@ -0,0 +1,31 @@
// file: kb_app_demo/frontend/ts/demo_execution_solana_core.ts
// version: 6
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { frontendDebug, frontendError, frontendWarn, installFrontendConsoleBridge } from "./frontend_log";
import type { DemoExecutionSolanaCoreGeneratedRecipientPayload } from "./bindings/kb_app_demo/demo_execution_solana_core/DemoExecutionSolanaCoreGeneratedRecipientPayload";
import type { DemoExecutionSolanaCoreOptionsPayload } from "./bindings/kb_app_demo/demo_execution_solana_core/DemoExecutionSolanaCoreOptionsPayload";
import type { DemoExecutionSolanaCoreProfileOption } from "./bindings/kb_app_demo/demo_execution_solana_core/DemoExecutionSolanaCoreProfileOption";
import type { DemoExecutionSolanaCoreProgressPayload } from "./bindings/kb_app_demo/demo_execution_solana_core/DemoExecutionSolanaCoreProgressPayload";
import type { DemoExecutionSolanaCoreRequest } from "./bindings/kb_app_demo/demo_execution_solana_core/DemoExecutionSolanaCoreRequest";
import type { DemoExecutionSolanaCoreSummaryPayload } from "./bindings/kb_app_demo/demo_execution_solana_core/DemoExecutionSolanaCoreSummaryPayload";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
const logLines: string[] = [];
let running = false;
let profileOptions: DemoExecutionSolanaCoreProfileOption[] = [];
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 integerValue(selector: string): number { const value=Number.parseInt(element<HTMLInputElement>(selector).value,10); if(!Number.isFinite(value)||value<0){throw new Error(`Valeur numérique invalide pour ${selector}`);} return value; }
function selectedProfile(): DemoExecutionSolanaCoreProfileOption|null { const name=element<HTMLSelectElement>("#executionProfileSelect").value; return profileOptions.find(profile=>profile.name===name)??null; }
function appendLog(payload: DemoExecutionSolanaCoreProgressPayload|{timestamp:string;level:string;stage:string;message:string;signature?:string|null}):void { const signature=payload.signature?` signature=${payload.signature}`:""; logLines.push(`${payload.timestamp} ${payload.level.toUpperCase()} [${payload.stage}] ${payload.message}${signature}`); const output=element<HTMLTextAreaElement>("#executionLogOutput"); output.value=logLines.join("\n"); output.scrollTop=output.scrollHeight; }
function setRunning(value:boolean):void { running=value; const profile=selectedProfile(); element<HTMLButtonElement>("#simulateExecutionButton").disabled=value||!profile; element<HTMLButtonElement>("#submitExecutionButton").disabled=value||!profile||!profile.sendEnabled; element<HTMLButtonElement>("#generateRecipientButton").disabled=value; element<HTMLButtonElement>("#cancelExecutionButton").disabled=!value; const badge=element<HTMLElement>("#executionStatusBadge"); badge.textContent=value?"Exécution en cours":"Prêt"; badge.className=value?"badge text-bg-warning":"badge text-bg-success"; }
function updateProfileHelp():void { const profile=selectedProfile(); const help=element<HTMLElement>("#executionProfileHelp"); if(!profile){help.textContent="Aucun profil Devnet compatible.";return;} help.textContent=`wallet=${profile.walletAlias}, spendMax=${profile.maxSpendLamports}, airdropMax=${profile.maxAirdropLamports}, send=${profile.sendEnabled?"enabled":"disabled"}`; element<HTMLInputElement>("#executionAirdropInput").max=String(profile.maxAirdropLamports); element<HTMLInputElement>("#executionLamportsInput").max=String(profile.maxSpendLamports); setRunning(running); }
function buildRequest(submit:boolean):DemoExecutionSolanaCoreRequest { const recipient=element<HTMLInputElement>("#executionRecipientInput").value.trim(); if(recipient.length===0){throw new Error("Le destinataire est obligatoire.");} return {profileName:element<HTMLSelectElement>("#executionProfileSelect").value,recipient,lamports:integerValue("#executionLamportsInput"),airdropLamports:integerValue("#executionAirdropInput"),submit,operatorConfirmed:element<HTMLInputElement>("#executionOperatorConfirmedInput").checked,forcePostValidationReplay:element<HTMLInputElement>("#executionForceReplayInput").checked,materializeAfterDecode:element<HTMLInputElement>("#executionMaterializeInput").checked}; }
function displaySummary(summary:DemoExecutionSolanaCoreSummaryPayload):void { element<HTMLTextAreaElement>("#executionSummaryOutput").value=JSON.stringify(summary,null,2); element<HTMLTextAreaElement>("#executionPlanOutput").value=summary.planJson; element<HTMLTextAreaElement>("#executionSimulationOutput").value=summary.simulationJson; element<HTMLTextAreaElement>("#executionDiagnosticsOutput").value=summary.diagnosticsJson; }
async function execute(submit:boolean):Promise<void>{if(running){return;}try{const request=buildRequest(submit);if(submit&&!request.operatorConfirmed){frontendWarn("kb_app_demo.frontend.demo_execution_solana_core","Submission blocked: operator confirmation missing");return;}setRunning(true);const summary=await invoke<DemoExecutionSolanaCoreSummaryPayload>("demo_execution_solana_core_execute",{request});displaySummary(summary);}catch(caughtError){const message=caughtError instanceof Error?caughtError.message:String(caughtError);frontendError("kb_app_demo.frontend.demo_execution_solana_core",`Execution failed: ${message}`);}finally{setRunning(false);}}
async function loadOptions():Promise<void>{const options=await invoke<DemoExecutionSolanaCoreOptionsPayload>("demo_execution_solana_core_options");profileOptions=options.profiles;const select=element<HTMLSelectElement>("#executionProfileSelect");select.replaceChildren();for(const profile of profileOptions){const option=document.createElement("option");option.value=profile.name;option.textContent=profile.name;option.selected=profile.name===options.defaultProfileName;select.append(option);}element<HTMLInputElement>("#executionLamportsInput").value=String(options.defaultTransferLamports);updateProfileHelp();setRunning(options.running);}
document.addEventListener("DOMContentLoaded",async()=>{installFrontendConsoleBridge("kb_app_demo.frontend.demo_execution_solana_core");frontendDebug("kb_app_demo.frontend.demo_execution_solana_core","Solana Core execution window loaded");await listen<DemoExecutionSolanaCoreProgressPayload>("demo-execution-solana-core-progress",event=>appendLog(event.payload));element<HTMLSelectElement>("#executionProfileSelect").addEventListener("change",updateProfileHelp);element<HTMLButtonElement>("#generateRecipientButton").addEventListener("click",async()=>{const payload=await invoke<DemoExecutionSolanaCoreGeneratedRecipientPayload>("demo_execution_solana_core_generate_recipient");element<HTMLInputElement>("#executionRecipientInput").value=payload.publicKey;});element<HTMLButtonElement>("#simulateExecutionButton").addEventListener("click",()=>void execute(false));element<HTMLButtonElement>("#submitExecutionButton").addEventListener("click",()=>void execute(true));element<HTMLButtonElement>("#cancelExecutionButton").addEventListener("click",()=>void invoke("demo_execution_solana_core_cancel"));element<HTMLButtonElement>("#clearExecutionLogButton").addEventListener("click",()=>{logLines.length=0;element<HTMLTextAreaElement>("#executionLogOutput").value="";});try{await loadOptions();}catch(caughtError){const message=caughtError instanceof Error?caughtError.message:String(caughtError);frontendError("kb_app_demo.frontend.demo_execution_solana_core",`Options loading failed: ${message}`);}});

View File

@@ -0,0 +1,881 @@
// file: kb_app_demo/frontend/ts/demo_execution_spl.ts
// version: 2
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { frontendDebug, frontendError, frontendWarn, installFrontendConsoleBridge } from "./frontend_log";
import type { DemoExecutionSolanaCoreGeneratedRecipientPayload } from "./bindings/kb_app_demo/demo_execution_solana_core/DemoExecutionSolanaCoreGeneratedRecipientPayload";
import type { DemoExecutionSolanaCoreOptionsPayload } from "./bindings/kb_app_demo/demo_execution_solana_core/DemoExecutionSolanaCoreOptionsPayload";
import type { DemoExecutionSolanaCoreProfileOption } from "./bindings/kb_app_demo/demo_execution_solana_core/DemoExecutionSolanaCoreProfileOption";
import type { DemoExecutionSolanaCoreProgressPayload } from "./bindings/kb_app_demo/demo_execution_solana_core/DemoExecutionSolanaCoreProgressPayload";
import type { DemoExecutionSolanaCoreRequest } from "./bindings/kb_app_demo/demo_execution_solana_core/DemoExecutionSolanaCoreRequest";
import type { DemoExecutionSolanaCoreSummaryPayload } from "./bindings/kb_app_demo/demo_execution_solana_core/DemoExecutionSolanaCoreSummaryPayload";
import type { DemoExecutionMemoRequest } from "./bindings/kb_app_demo/demo_execution_solana_core/DemoExecutionMemoRequest";
import type { DemoExecutionMemoSummaryPayload } from "./bindings/kb_app_demo/demo_execution_solana_core/DemoExecutionMemoSummaryPayload";
import type { DemoExecutionSplTokenRequest } from "./bindings/kb_app_demo/demo_spl_token/DemoExecutionSplTokenRequest";
import type { DemoExecutionSplTokenSummaryPayload } from "./bindings/kb_app_demo/demo_spl_token/DemoExecutionSplTokenSummaryPayload";
import type { DemoSplTokenJournalRequest } from "./bindings/kb_app_demo/demo_spl_token/DemoSplTokenJournalRequest";
import type { DemoSplTokenJournalRow } from "./bindings/kb_app_demo/demo_spl_token/DemoSplTokenJournalRow";
import type { DemoExecutionSplAtaRequest } from "./bindings/kb_app_demo/demo_spl_ata/DemoExecutionSplAtaRequest";
import type { DemoExecutionSplAtaSummaryPayload } from "./bindings/kb_app_demo/demo_spl_ata/DemoExecutionSplAtaSummaryPayload";
import type { DemoSplAtaDerivationPayload } from "./bindings/kb_app_demo/demo_spl_ata/DemoSplAtaDerivationPayload";
import type { DemoSplAtaDerivationRequest } from "./bindings/kb_app_demo/demo_spl_ata/DemoSplAtaDerivationRequest";
import type { DemoSplAtaJournalRequest } from "./bindings/kb_app_demo/demo_spl_ata/DemoSplAtaJournalRequest";
import type { DemoSplAtaJournalRow } from "./bindings/kb_app_demo/demo_spl_ata/DemoSplAtaJournalRow";
import type { DemoExecutionSplToken2022Request } from "./bindings/kb_app_demo/demo_spl_token_2022/DemoExecutionSplToken2022Request";
import type { DemoExecutionSplToken2022SummaryPayload } from "./bindings/kb_app_demo/demo_spl_token_2022/DemoExecutionSplToken2022SummaryPayload";
import type { DemoSplToken2022FixturePayload } from "./bindings/kb_app_demo/demo_spl_token_2022/DemoSplToken2022FixturePayload";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
const logLines: string[] = [];
const maximumLogLines = 1200;
let running = false;
let profileOptions: DemoExecutionSolanaCoreProfileOption[] = [];
let token2022Fixture: DemoSplToken2022FixturePayload | null = null;
function element<T extends HTMLElement>(selector: string): T {
const value = document.querySelector<T>(selector);
if (!value) {
throw new Error(`Missing UI element: ${selector}`);
}
return value;
}
function integerValue(selector: string): number {
const value = Number.parseInt(element<HTMLInputElement>(selector).value, 10);
if (!Number.isFinite(value) || value < 0) {
throw new Error(`Valeur numérique invalide pour ${selector}`);
}
return value;
}
function appendLog(payload: DemoExecutionSolanaCoreProgressPayload | { timestamp: string; level: string; stage: string; message: string; signature?: string | null }): void {
const signature = payload.signature ? ` signature=${payload.signature}` : "";
logLines.push(`${payload.timestamp} ${payload.level.toUpperCase()} [${payload.stage}] ${payload.message}${signature}`);
while (logLines.length > maximumLogLines) {
logLines.shift();
}
const output = element<HTMLTextAreaElement>("#executionLogOutput");
output.value = logLines.join("\n");
output.scrollTop = output.scrollHeight;
}
function setRunning(value: boolean): void {
running = value;
const profile = selectedProfile();
element<HTMLButtonElement>("#simulateExecutionButton").disabled = value || !profile;
element<HTMLButtonElement>("#submitExecutionButton").disabled = value || !profile || !profile.sendEnabled;
element<HTMLButtonElement>("#simulateMemoButton").disabled = value || !profile;
element<HTMLButtonElement>("#submitMemoButton").disabled = value || !profile || !profile.sendEnabled;
element<HTMLButtonElement>("#simulateTokenButton").disabled = value || !profile;
element<HTMLButtonElement>("#submitTokenButton").disabled = value || !profile || !profile.sendEnabled;
element<HTMLButtonElement>("#deriveAtaButton").disabled = value || !profile;
element<HTMLButtonElement>("#simulateAtaButton").disabled = value || !profile;
element<HTMLButtonElement>("#submitAtaButton").disabled = value || !profile || !profile.sendEnabled;
element<HTMLButtonElement>("#simulateToken2022Button").disabled = value || !profile;
element<HTMLButtonElement>("#submitToken2022Button").disabled = value || !profile || !profile.sendEnabled;
element<HTMLButtonElement>("#loadToken2022FixtureButton").disabled = value || !profile;
element<HTMLButtonElement>("#generateRecipientButton").disabled = value;
element<HTMLButtonElement>("#cancelExecutionButton").disabled = !value;
const badge = element<HTMLElement>("#executionStatusBadge");
badge.textContent = value ? "Exécution en cours" : "Prêt";
badge.className = value ? "badge text-bg-warning" : "badge text-bg-success";
}
function selectedProfile(): DemoExecutionSolanaCoreProfileOption | null {
const name = element<HTMLSelectElement>("#executionProfileSelect").value;
return profileOptions.find(profile => profile.name === name) ?? null;
}
function updateProfileHelp(): void {
const profile = selectedProfile();
const help = element<HTMLElement>("#executionProfileHelp");
if (!profile) {
help.textContent = "Aucun profil Devnet compatible.";
return;
}
help.textContent = `wallet=${profile.walletAlias}, spendMax=${profile.maxSpendLamports}, airdropMax=${profile.maxAirdropLamports}, send=${profile.sendEnabled ? "enabled" : "disabled"}`;
element<HTMLInputElement>("#executionAirdropInput").max = String(profile.maxAirdropLamports);
element<HTMLInputElement>("#executionLamportsInput").max = String(profile.maxSpendLamports);
setRunning(running);
}
function buildRequest(submit: boolean): DemoExecutionSolanaCoreRequest {
const recipient = element<HTMLInputElement>("#executionRecipientInput").value.trim();
if (recipient.length === 0) {
throw new Error("Le destinataire est obligatoire.");
}
return {
profileName: element<HTMLSelectElement>("#executionProfileSelect").value,
recipient,
lamports: integerValue("#executionLamportsInput"),
airdropLamports: integerValue("#executionAirdropInput"),
submit,
operatorConfirmed: element<HTMLInputElement>("#executionOperatorConfirmedInput").checked,
forcePostValidationReplay: element<HTMLInputElement>("#executionForceReplayInput").checked,
materializeAfterDecode: element<HTMLInputElement>("#executionMaterializeInput").checked,
};
}
function displaySummary(summary: DemoExecutionSolanaCoreSummaryPayload): void {
const concise = {
profileName: summary.profileName,
cluster: summary.cluster,
genesisHash: summary.genesisHash,
walletPublicKey: summary.walletPublicKey,
recipient: summary.recipient,
recipientExistedBefore: summary.recipientExistedBefore,
recipientBalanceBeforeLamports: summary.recipientBalanceBeforeLamports,
recipientMinimumBalanceLamports: summary.recipientMinimumBalanceLamports,
balanceBeforeLamports: summary.balanceBeforeLamports,
balanceAfterFundingLamports: summary.balanceAfterFundingLamports,
feeLamports: summary.feeLamports,
simulationSuccess: summary.simulationSuccess,
simulationError: summary.simulationError,
airdropSignature: summary.airdropSignature,
transactionSignature: summary.transactionSignature,
confirmationStatus: summary.confirmationStatus,
canonicalInserted: summary.canonicalInserted,
coreExtracted: summary.coreExtracted,
decodeCompleted: summary.decodeCompleted,
decodeFailedInputs: summary.decodeFailedInputs,
};
element<HTMLTextAreaElement>("#executionSummaryOutput").value = JSON.stringify(concise, null, 2);
element<HTMLTextAreaElement>("#executionPlanOutput").value = summary.planJson;
element<HTMLTextAreaElement>("#executionSimulationOutput").value = summary.simulationJson;
element<HTMLTextAreaElement>("#executionDiagnosticsOutput").value = summary.diagnosticsJson;
}
function buildMemoRequest(submit: boolean): DemoExecutionMemoRequest {
return {
profileName: element<HTMLSelectElement>("#executionProfileSelect").value,
message: element<HTMLTextAreaElement>("#memoMessageInput").value,
includeWalletAsMemoSigner: element<HTMLInputElement>("#memoWalletSignerInput").checked,
submit,
operatorConfirmed: element<HTMLInputElement>("#executionOperatorConfirmedInput").checked,
forcePostValidationReplay: element<HTMLInputElement>("#executionForceReplayInput").checked,
};
}
function displayMemoSummary(summary: DemoExecutionMemoSummaryPayload): void {
const concise = {
operation: "spl_memo_v4.add_memo",
profileName: summary.profileName,
cluster: summary.cluster,
genesisHash: summary.genesisHash,
walletPublicKey: summary.walletPublicKey,
balanceLamports: summary.balanceLamports,
feeLamports: summary.feeLamports,
simulationSuccess: summary.simulationSuccess,
simulationError: summary.simulationError,
transactionSignature: summary.transactionSignature,
confirmationStatus: summary.confirmationStatus,
canonicalInserted: summary.canonicalInserted,
coreExtracted: summary.coreExtracted,
decodeReplayed: summary.decodeReplayed,
materialized: summary.materialized,
annotationCount: summary.annotationCount,
idempotenceValidated: summary.idempotenceValidated,
};
element<HTMLTextAreaElement>("#executionSummaryOutput").value = JSON.stringify(concise, null, 2);
element<HTMLTextAreaElement>("#executionPlanOutput").value = summary.planJson;
element<HTMLTextAreaElement>("#executionSimulationOutput").value = summary.simulationJson;
element<HTMLTextAreaElement>("#executionDiagnosticsOutput").value = summary.diagnosticsJson;
}
function requiredText(selector: string, label: string): string {
const value = element<HTMLInputElement>(selector).value.trim();
if (value.length === 0) {
throw new Error(`${label} est obligatoire.`);
}
return value;
}
function optionalText(selector: string): string | null {
const value = element<HTMLInputElement>(selector).value.trim();
return value.length === 0 ? null : value;
}
function buildTokenRequest(submit: boolean): DemoExecutionSplTokenRequest {
const amountRaw = requiredText("#tokenAmountRawInput", "Le montant brut");
if (!/^[0-9]+$/.test(amountRaw)) {
throw new Error("Le montant brut doit être un entier décimal non signé.");
}
const decimals = integerValue("#tokenDecimalsInput");
if (decimals > 255) {
throw new Error("Les decimals doivent être compris entre 0 et 255.");
}
return {
profileName: element<HTMLSelectElement>("#executionProfileSelect").value,
source: requiredText("#tokenSourceInput", "Le compte source"),
mint: requiredText("#tokenMintInput", "Le mint"),
destination: requiredText("#tokenDestinationInput", "Le compte destination"),
authority: requiredText("#tokenAuthorityInput", "Lautorité"),
amountRaw,
decimals,
submit,
operatorConfirmed: element<HTMLInputElement>("#executionOperatorConfirmedInput").checked,
forcePostValidationReplay: element<HTMLInputElement>("#executionForceReplayInput").checked,
};
}
function displayTokenSummary(summary: DemoExecutionSplTokenSummaryPayload): void {
const concise = {
operation: summary.operation,
profileName: summary.profileName,
cluster: summary.cluster,
genesisHash: summary.genesisHash,
walletPublicKey: summary.walletPublicKey,
balanceLamports: summary.balanceLamports,
feeLamports: summary.feeLamports,
amountRaw: summary.amountRaw,
decimals: summary.decimals,
readinessStatus: summary.readinessStatus,
simulationSuccess: summary.simulationSuccess,
simulationError: summary.simulationError,
transactionSignature: summary.transactionSignature,
confirmationStatus: summary.confirmationStatus,
canonicalInserted: summary.canonicalInserted,
coreExtracted: summary.coreExtracted,
decodeReplayed: summary.decodeReplayed,
materializationCount: summary.materializationCount,
idempotenceValidated: summary.idempotenceValidated,
};
element<HTMLTextAreaElement>("#executionSummaryOutput").value = JSON.stringify(concise, null, 2);
element<HTMLTextAreaElement>("#executionPlanOutput").value = `PRÉFLIGHT\n${summary.readinessJson}\n\nPLAN\n${summary.planJson}`;
element<HTMLTextAreaElement>("#executionSimulationOutput").value = summary.simulationJson;
element<HTMLTextAreaElement>("#executionDiagnosticsOutput").value = summary.diagnosticsJson;
}
function ataDerivationRequest(): DemoSplAtaDerivationRequest {
return {
profileName: element<HTMLSelectElement>("#executionProfileSelect").value,
mint: requiredText("#ataMintInput", "Le mint ATA"),
tokenProgram: element<HTMLSelectElement>("#ataTokenProgramSelect").value,
};
}
async function deriveAta(): Promise<DemoSplAtaDerivationPayload> {
const payload = await invoke<DemoSplAtaDerivationPayload>("demo_spl_ata_derive", {
request: ataDerivationRequest(),
});
element<HTMLInputElement>("#ataPayerInput").value = payload.payer;
element<HTMLInputElement>("#ataWalletOwnerInput").value = payload.walletOwner;
element<HTMLInputElement>("#ataDerivedInput").value = payload.associatedTokenAccount;
return payload;
}
function displayAtaSummary(summary: DemoExecutionSplAtaSummaryPayload): void {
element<HTMLTextAreaElement>("#executionSummaryOutput").value = JSON.stringify({
operation: summary.operation,
profileName: summary.profileName,
walletPublicKey: summary.walletPublicKey,
tokenProgramId: summary.tokenProgramId,
associatedTokenAccount: summary.associatedTokenAccount,
balanceLamports: summary.balanceLamports,
feeLamports: summary.feeLamports,
readinessStatus: summary.readinessStatus,
simulationSuccess: summary.simulationSuccess,
simulationError: summary.simulationError,
transactionSignature: summary.transactionSignature,
confirmationStatus: summary.confirmationStatus,
materializationCount: summary.materializationCount,
idempotenceValidated: summary.idempotenceValidated,
}, null, 2);
element<HTMLTextAreaElement>("#executionPlanOutput").value = `PRÉFLIGHT\n${summary.readinessJson}\n\nPLAN\n${summary.planJson}`;
element<HTMLTextAreaElement>("#executionSimulationOutput").value = summary.simulationJson;
element<HTMLTextAreaElement>("#executionDiagnosticsOutput").value = summary.diagnosticsJson;
}
async function executeAta(submit: boolean): Promise<void> {
if (running) {
return;
}
try {
if (submit && !element<HTMLInputElement>("#executionOperatorConfirmedInput").checked) {
throw new Error("La confirmation opérateur est obligatoire avant signature ATA.");
}
const derivation = await deriveAta();
const request: DemoExecutionSplAtaRequest = {
profileName: element<HTMLSelectElement>("#executionProfileSelect").value,
walletOwner: derivation.walletOwner,
mint: requiredText("#ataMintInput", "Le mint ATA"),
tokenProgram: element<HTMLSelectElement>("#ataTokenProgramSelect").value,
mode: element<HTMLSelectElement>("#ataModeSelect").value,
submit,
operatorConfirmed: element<HTMLInputElement>("#executionOperatorConfirmedInput").checked,
forcePostValidationReplay: element<HTMLInputElement>("#executionForceReplayInput").checked,
};
setRunning(true);
appendLog({
timestamp: new Date().toISOString(),
level: "info",
stage: submit ? "ata_submit" : "ata_simulation",
message: `${request.mode} ${request.tokenProgram}, ATA=${derivation.associatedTokenAccount}`,
});
const summary = await invoke<DemoExecutionSplAtaSummaryPayload>("demo_execution_spl_ata_execute", { request });
displayAtaSummary(summary);
appendLog({
timestamp: new Date().toISOString(),
level: summary.simulationSuccess && (!submit || summary.idempotenceValidated) ? "info" : "error",
stage: "ata_completed",
message: `preflight=${summary.readinessStatus}, simulation=${summary.simulationSuccess}, confirmation=${summary.confirmationStatus ?? "none"}, materializations=${summary.materializationCount}, idempotence=${summary.idempotenceValidated}`,
signature: summary.transactionSignature,
});
if (summary.transactionSignature) {
element<HTMLInputElement>("#ataJournalSignatureInput").value = summary.transactionSignature;
element<HTMLInputElement>("#ataJournalMintInput").value = request.mint;
element<HTMLInputElement>("#ataJournalAddressInput").value = summary.associatedTokenAccount;
await loadAtaJournal();
}
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
appendLog({ timestamp: new Date().toISOString(), level: "error", stage: "ata_failed", message });
frontendError("kb_app_demo.frontend.demo_execution_spl", `ATA execution failed: ${message}`);
} finally {
setRunning(false);
}
}
function ataJournalRequest(): DemoSplAtaJournalRequest {
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> {
const button = element<HTMLButtonElement>("#refreshAtaJournalButton");
button.disabled = true;
try {
const rows = await invoke<DemoSplAtaJournalRow[]>("demo_spl_ata_journal", { request: ataJournalRequest() });
element<HTMLTextAreaElement>("#ataJournalOutput").value = JSON.stringify(rows.map(row => ({
...row,
payloadJson: JSON.parse(row.payloadJson) as unknown,
})), null, 2);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLTextAreaElement>("#ataJournalOutput").value = JSON.stringify({ error: message }, null, 2);
} finally {
button.disabled = false;
}
}
async function execute(submit: boolean): Promise<void> {
if (running) {
return;
}
try {
const request = buildRequest(submit);
if (submit && !request.operatorConfirmed) {
const message = "La confirmation opérateur doit être cochée avant signature et envoi.";
element<HTMLTextAreaElement>("#executionSummaryOutput").value = JSON.stringify({ warning: message }, null, 2);
appendLog({
timestamp: new Date().toISOString(),
level: "warn",
stage: "policy",
message,
});
frontendWarn("kb_app_demo.frontend.demo_execution_spl", `Execution blocked by policy: ${message}`);
return;
}
setRunning(true);
element<HTMLTextAreaElement>("#executionSummaryOutput").value = "Exécution en cours...";
appendLog({
timestamp: new Date().toISOString(),
level: "info",
stage: submit ? "submit" : "simulation",
message: submit ? "Démarrage du parcours Devnet complet." : "Démarrage de la simulation exacte.",
});
const summary = await invoke<DemoExecutionSolanaCoreSummaryPayload>("demo_execution_solana_core_execute", { request });
displaySummary(summary);
appendLog({
timestamp: new Date().toISOString(),
level: summary.simulationSuccess ? "info" : "error",
stage: "completed",
message: `simulation=${summary.simulationSuccess}, confirmation=${summary.confirmationStatus ?? "none"}, canonical=${summary.canonicalInserted ?? 0}, core=${summary.coreExtracted ?? 0}, decode=${summary.decodeCompleted ?? 0}`,
signature: summary.transactionSignature,
});
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLTextAreaElement>("#executionSummaryOutput").value = JSON.stringify({ error: message }, null, 2);
appendLog({ timestamp: new Date().toISOString(), level: "error", stage: "failed", message });
frontendError("kb_app_demo.frontend.demo_execution_spl", `Execution failed: ${message}`);
} finally {
setRunning(false);
}
}
async function executeMemo(submit: boolean): Promise<void> {
if (running) {
return;
}
try {
const request = buildMemoRequest(submit);
if (submit && !request.operatorConfirmed) {
const message = "La confirmation opérateur doit être cochée avant signature et envoi du Memo.";
appendLog({ timestamp: new Date().toISOString(), level: "warn", stage: "memo_policy", message });
frontendWarn("kb_app_demo.frontend.demo_execution_spl", `Memo execution blocked by policy: ${message}`);
return;
}
setRunning(true);
element<HTMLTextAreaElement>("#executionSummaryOutput").value = "Exécution Memo en cours...";
appendLog({
timestamp: new Date().toISOString(),
level: "info",
stage: submit ? "memo_submit" : "memo_simulation",
message: submit ? "Démarrage du parcours Memo v4 Devnet complet." : "Démarrage de la simulation Memo v4 exacte.",
});
const summary = await invoke<DemoExecutionMemoSummaryPayload>("demo_execution_spl_memo_execute", { request });
displayMemoSummary(summary);
appendLog({
timestamp: new Date().toISOString(),
level: summary.simulationSuccess && (!submit || summary.materialized) ? "info" : "error",
stage: "memo_completed",
message: `simulation=${summary.simulationSuccess}, confirmation=${summary.confirmationStatus ?? "none"}, annotation=${summary.annotationCount}, idempotence=${summary.idempotenceValidated}`,
signature: summary.transactionSignature,
});
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLTextAreaElement>("#executionSummaryOutput").value = JSON.stringify({ error: message }, null, 2);
appendLog({ timestamp: new Date().toISOString(), level: "error", stage: "memo_failed", message });
frontendError("kb_app_demo.frontend.demo_execution_spl", `Memo execution failed: ${message}`);
} finally {
setRunning(false);
}
}
async function executeToken(submit: boolean): Promise<void> {
if (running) {
return;
}
try {
const request = buildTokenRequest(submit);
if (submit && !request.operatorConfirmed) {
const message = "La confirmation opérateur doit être cochée avant signature et envoi Token.";
appendLog({ timestamp: new Date().toISOString(), level: "warn", stage: "spl_token_policy", message });
frontendWarn("kb_app_demo.frontend.demo_execution_spl", `Token execution blocked by policy: ${message}`);
return;
}
setRunning(true);
element<HTMLTextAreaElement>("#executionSummaryOutput").value = "Préflight et exécution SPL Token en cours...";
appendLog({
timestamp: new Date().toISOString(),
level: "info",
stage: submit ? "spl_token_submit" : "spl_token_simulation",
message: submit ? "Démarrage du TransferChecked Devnet complet." : "Démarrage du préflight et de la simulation TransferChecked.",
});
const summary = await invoke<DemoExecutionSplTokenSummaryPayload>("demo_execution_spl_token_execute", { request });
displayTokenSummary(summary);
appendLog({
timestamp: new Date().toISOString(),
level: summary.simulationSuccess && (!submit || summary.idempotenceValidated) ? "info" : "error",
stage: "spl_token_completed",
message: `preflight=${summary.readinessStatus}, simulation=${summary.simulationSuccess}, confirmation=${summary.confirmationStatus ?? "none"}, materializations=${summary.materializationCount}, idempotence=${summary.idempotenceValidated}`,
signature: summary.transactionSignature,
});
if (summary.transactionSignature) {
element<HTMLInputElement>("#tokenJournalSignatureInput").value = summary.transactionSignature;
await loadTokenJournal();
}
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLTextAreaElement>("#executionSummaryOutput").value = JSON.stringify({ error: message }, null, 2);
appendLog({ timestamp: new Date().toISOString(), level: "error", stage: "spl_token_failed", message });
frontendError("kb_app_demo.frontend.demo_execution_spl", `Token execution failed: ${message}`);
} finally {
setRunning(false);
}
}
function tokenJournalRequest(): DemoSplTokenJournalRequest {
return {
profileName: element<HTMLSelectElement>("#executionProfileSelect").value,
signatureContains: optionalText("#tokenJournalSignatureInput"),
mint: optionalText("#tokenJournalMintInput"),
account: optionalText("#tokenJournalAccountInput"),
family: element<HTMLSelectElement>("#tokenJournalFamilySelect").value || null,
operation: optionalText("#tokenJournalOperationInput"),
limit: integerValue("#tokenJournalLimitInput"),
};
}
function renderTokenJournal(rows: DemoSplTokenJournalRow[]): void {
const body = element<HTMLTableSectionElement>("#tokenJournalRows");
body.replaceChildren();
if (rows.length === 0) {
const row = document.createElement("tr");
const cell = document.createElement("td");
cell.colSpan = 5;
cell.className = "text-body-secondary";
cell.textContent = "Aucun événement correspondant.";
row.append(cell);
body.append(row);
element<HTMLTextAreaElement>("#tokenJournalDetailOutput").value = "Aucun événement sélectionné.";
return;
}
for (const item of rows) {
const row = document.createElement("tr");
row.tabIndex = 0;
row.setAttribute("role", "button");
const values = [
item.slot,
item.operation ?? "—",
item.family,
item.amountRaw ?? "—",
item.signature.length > 20 ? `${item.signature.slice(0, 10)}${item.signature.slice(-8)}` : item.signature,
];
for (const value of values) {
const cell = document.createElement("td");
cell.textContent = value;
row.append(cell);
}
row.title = item.signature;
const select = (): void => {
element<HTMLTextAreaElement>("#tokenJournalDetailOutput").value = item.payloadJson;
};
row.addEventListener("click", select);
row.addEventListener("keydown", event => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
select();
}
});
body.append(row);
}
element<HTMLTextAreaElement>("#tokenJournalDetailOutput").value = rows[0].payloadJson;
}
function token2022ScenarioAmount(fixture: DemoSplToken2022FixturePayload, scenarioId: string): string {
if (scenarioId === "token_2022_mint_to_checked") {
return "1";
}
if (scenarioId === "token_2022_approve_checked") {
return "2";
}
if (scenarioId === "token_2022_burn_checked") {
return "1";
}
return fixture.defaultAmountRaw;
}
function applyToken2022Fixture(): void {
if (!token2022Fixture) {
return;
}
const scenarioId = element<HTMLSelectElement>("#token2022ScenarioSelect").value;
element<HTMLInputElement>("#token2022SourceInput").value = scenarioId === "token_2022_close_destination"
? token2022Fixture.closeAccount
: token2022Fixture.source;
element<HTMLInputElement>("#token2022MintInput").value = token2022Fixture.mint;
element<HTMLInputElement>("#token2022DestinationInput").value = scenarioId === "token_2022_close_destination"
? token2022Fixture.authority
: token2022Fixture.destination;
element<HTMLInputElement>("#token2022DelegateInput").value = token2022Fixture.delegate;
element<HTMLInputElement>("#token2022AuthorityInput").value = token2022Fixture.authority;
element<HTMLInputElement>("#token2022FreezeAuthorityInput").value = token2022Fixture.freezeAuthority;
element<HTMLInputElement>("#token2022AmountInput").value = token2022ScenarioAmount(token2022Fixture, scenarioId);
element<HTMLInputElement>("#token2022DecimalsInput").value = String(token2022Fixture.decimals);
}
async function loadToken2022Fixture(): Promise<void> {
try {
token2022Fixture = await invoke<DemoSplToken2022FixturePayload>("demo_spl_token_2022_fixture", {
profileName: element<HTMLSelectElement>("#executionProfileSelect").value,
});
applyToken2022Fixture();
element<HTMLElement>("#token2022FixtureHelp").textContent = `Fixture: ${token2022Fixture.fixturePath} — program=${token2022Fixture.programId}`;
appendLog({
timestamp: new Date().toISOString(),
level: "info",
stage: "token_2022_fixture",
message: `fixture chargée: mint=${token2022Fixture.mint}, source=${token2022Fixture.source}, destination=${token2022Fixture.destination}`,
});
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
token2022Fixture = null;
element<HTMLElement>("#token2022FixtureHelp").textContent = message;
appendLog({ timestamp: new Date().toISOString(), level: "error", stage: "token_2022_fixture", message });
}
}
function buildToken2022Request(submit: boolean): DemoExecutionSplToken2022Request {
const scenarioId = element<HTMLSelectElement>("#token2022ScenarioSelect").value;
const source = element<HTMLInputElement>("#token2022SourceInput").value.trim();
const mint = element<HTMLInputElement>("#token2022MintInput").value.trim();
const destination = element<HTMLInputElement>("#token2022DestinationInput").value.trim();
const delegate = element<HTMLInputElement>("#token2022DelegateInput").value.trim();
const authority = element<HTMLInputElement>("#token2022AuthorityInput").value.trim();
const freezeAuthority = element<HTMLInputElement>("#token2022FreezeAuthorityInput").value.trim();
if (source.length === 0) {
throw new Error("Le compte source ou cible Token-2022 est obligatoire.");
}
if (scenarioId !== "token_2022_revoke" && scenarioId !== "token_2022_close_destination" && mint.length === 0) {
throw new Error("Le mint Token-2022 est obligatoire pour ce scénario.");
}
if (scenarioId === "token_2022_transfer_checked" && destination.length === 0) {
throw new Error("Le compte destination Token-2022 est obligatoire.");
}
if (scenarioId === "token_2022_close_destination" && destination.length === 0) {
throw new Error("La destination lamports est obligatoire pour CloseAccount.");
}
if (scenarioId === "token_2022_approve_checked" && delegate.length === 0) {
throw new Error("Le delegate est obligatoire pour ApproveChecked.");
}
if (["token_2022_freeze_account", "token_2022_thaw_account"].includes(scenarioId) && freezeAuthority.length === 0) {
throw new Error("La freeze authority est obligatoire pour FreezeAccount et ThawAccount.");
}
if (!["token_2022_freeze_account", "token_2022_thaw_account"].includes(scenarioId) && authority.length === 0) {
throw new Error("Lautorité Token-2022 est obligatoire.");
}
return {
profileName: element<HTMLSelectElement>("#executionProfileSelect").value,
scenarioId,
source,
mint,
destination,
delegate,
authority,
freezeAuthority,
amountRaw: element<HTMLInputElement>("#token2022AmountInput").value.trim(),
decimals: integerValue("#token2022DecimalsInput"),
submit,
operatorConfirmed: element<HTMLInputElement>("#executionOperatorConfirmedInput").checked,
forcePostValidationReplay: element<HTMLInputElement>("#executionForceReplayInput").checked,
};
}
function displayToken2022Summary(summary: DemoExecutionSplToken2022SummaryPayload): void {
element<HTMLTextAreaElement>("#executionSummaryOutput").value = JSON.stringify({
scenarioId: summary.scenarioId,
operation: summary.operation,
profileName: summary.profileName,
cluster: summary.cluster,
genesisHash: summary.genesisHash,
walletPublicKey: summary.walletPublicKey,
balanceLamports: summary.balanceLamports,
feeLamports: summary.feeLamports,
simulationSuccess: summary.simulationSuccess,
simulationError: summary.simulationError,
transactionSignature: summary.transactionSignature,
confirmationStatus: summary.confirmationStatus,
canonicalInserted: summary.canonicalInserted,
coreExtracted: summary.coreExtracted,
decodeReplayed: summary.decodeReplayed,
materializationCount: summary.materializationCount,
idempotenceValidated: summary.idempotenceValidated,
}, null, 2);
element<HTMLTextAreaElement>("#executionPlanOutput").value = `PRÉFLIGHT\n${summary.preflightJson}\n\nPLAN\n${summary.planJson}`;
element<HTMLTextAreaElement>("#executionSimulationOutput").value = summary.simulationJson;
element<HTMLTextAreaElement>("#executionDiagnosticsOutput").value = summary.diagnosticsJson;
}
async function executeToken2022(submit: boolean): Promise<void> {
if (running) {
return;
}
let request: DemoExecutionSplToken2022Request;
try {
request = buildToken2022Request(submit);
if (submit && !request.operatorConfirmed) {
const message = "La confirmation opérateur est obligatoire avant signature Token-2022.";
element<HTMLTextAreaElement>("#executionSummaryOutput").value = JSON.stringify({ warning: message }, null, 2);
appendLog({ timestamp: new Date().toISOString(), level: "error", stage: "token_2022_failed", message });
return;
}
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
appendLog({ timestamp: new Date().toISOString(), level: "error", stage: "token_2022_failed", message });
return;
}
setRunning(true);
element<HTMLTextAreaElement>("#executionSummaryOutput").value = "Préflight et exécution Token-2022 en cours...";
appendLog({
timestamp: new Date().toISOString(),
level: "info",
stage: submit ? "token_2022_submit" : "token_2022_simulation",
message: `${request.scenarioId}, source=${request.source}`,
});
try {
const summary = await invoke<DemoExecutionSplToken2022SummaryPayload>("demo_execution_spl_token_2022_execute", { request });
displayToken2022Summary(summary);
appendLog({
timestamp: new Date().toISOString(),
level: summary.simulationSuccess ? "info" : "error",
stage: "token_2022_completed",
message: `scenario=${summary.scenarioId}, simulation=${summary.simulationSuccess}, confirmation=${summary.confirmationStatus ?? "none"}, materializations=${summary.materializationCount}, idempotence=${summary.idempotenceValidated}`,
signature: summary.transactionSignature,
});
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLTextAreaElement>("#executionSummaryOutput").value = JSON.stringify({ error: message }, null, 2);
appendLog({ timestamp: new Date().toISOString(), level: "error", stage: "token_2022_failed", message });
frontendError("kb_app_demo.frontend.demo_execution_spl", `Token-2022 execution failed: ${message}`);
} finally {
setRunning(false);
}
}
async function loadTokenJournal(): Promise<void> {
const button = element<HTMLButtonElement>("#refreshTokenJournalButton");
button.disabled = true;
try {
const rows = await invoke<DemoSplTokenJournalRow[]>("demo_spl_token_journal", { request: tokenJournalRequest() });
renderTokenJournal(rows);
appendLog({
timestamp: new Date().toISOString(),
level: "info",
stage: "spl_token_journal",
message: `${rows.length} événement(s) matérialisé(s) chargé(s).`,
});
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLTextAreaElement>("#tokenJournalDetailOutput").value = JSON.stringify({ error: message }, null, 2);
frontendError("kb_app_demo.frontend.demo_execution_spl", `Token journal loading failed: ${message}`);
} finally {
button.disabled = false;
}
}
async function generateRecipient(): Promise<void> {
try {
const payload = await invoke<DemoExecutionSolanaCoreGeneratedRecipientPayload>("demo_execution_solana_core_generate_recipient");
element<HTMLInputElement>("#executionRecipientInput").value = payload.publicKey;
appendLog({
timestamp: new Date().toISOString(),
level: "info",
stage: "recipient",
message: `Adresse destinataire jetable générée : ${payload.publicKey}`,
});
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
frontendError("kb_app_demo.frontend.demo_execution_spl", `Recipient generation failed: ${message}`);
}
}
async function cancelExecution(): Promise<void> {
try {
const accepted = await invoke<boolean>("demo_execution_solana_core_cancel");
appendLog({
timestamp: new Date().toISOString(),
level: accepted ? "warn" : "info",
stage: "cancel",
message: accepted ? "Demande dannulation enregistrée." : "Aucune exécution active.",
});
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
frontendError("kb_app_demo.frontend.demo_execution_spl", `Cancellation failed: ${message}`);
}
}
async function loadOptions(): Promise<void> {
const options = await invoke<DemoExecutionSolanaCoreOptionsPayload>("demo_execution_solana_core_options");
profileOptions = options.profiles;
const select = element<HTMLSelectElement>("#executionProfileSelect");
select.replaceChildren();
for (const profile of profileOptions) {
const option = document.createElement("option");
option.value = profile.name;
option.textContent = profile.name;
option.selected = profile.name === options.defaultProfileName;
select.append(option);
}
element<HTMLInputElement>("#executionLamportsInput").value = String(options.defaultTransferLamports);
updateProfileHelp();
setRunning(options.running);
if (profileOptions.length === 0) {
element<HTMLButtonElement>("#simulateExecutionButton").disabled = true;
element<HTMLButtonElement>("#submitExecutionButton").disabled = true;
element<HTMLButtonElement>("#simulateMemoButton").disabled = true;
element<HTMLButtonElement>("#submitMemoButton").disabled = true;
element<HTMLButtonElement>("#simulateTokenButton").disabled = true;
element<HTMLButtonElement>("#submitTokenButton").disabled = true;
element<HTMLButtonElement>("#deriveAtaButton").disabled = true;
element<HTMLButtonElement>("#simulateAtaButton").disabled = true;
element<HTMLButtonElement>("#submitAtaButton").disabled = true;
element<HTMLButtonElement>("#simulateToken2022Button").disabled = true;
element<HTMLButtonElement>("#submitToken2022Button").disabled = true;
element<HTMLButtonElement>("#loadToken2022FixtureButton").disabled = true;
}
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge("kb_app_demo.frontend.demo_execution_spl");
frontendDebug("kb_app_demo.frontend.demo_execution_spl", "execution demo window loaded");
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(item => new bootstrap.Tooltip(item));
element<HTMLSelectElement>("#executionProfileSelect").addEventListener("change", updateProfileHelp);
element<HTMLButtonElement>("#generateRecipientButton").addEventListener("click", () => {
void generateRecipient();
});
element<HTMLButtonElement>("#simulateExecutionButton").addEventListener("click", () => {
void execute(false);
});
element<HTMLButtonElement>("#submitExecutionButton").addEventListener("click", () => {
void execute(true);
});
element<HTMLButtonElement>("#simulateMemoButton").addEventListener("click", () => {
void executeMemo(false);
});
element<HTMLButtonElement>("#submitMemoButton").addEventListener("click", () => {
void executeMemo(true);
});
element<HTMLButtonElement>("#simulateTokenButton").addEventListener("click", () => {
void executeToken(false);
});
element<HTMLButtonElement>("#submitTokenButton").addEventListener("click", () => {
void executeToken(true);
});
element<HTMLButtonElement>("#loadToken2022FixtureButton").addEventListener("click", () => {
void loadToken2022Fixture();
});
element<HTMLSelectElement>("#token2022ScenarioSelect").addEventListener("change", applyToken2022Fixture);
element<HTMLButtonElement>("#simulateToken2022Button").addEventListener("click", () => {
void executeToken2022(false);
});
element<HTMLButtonElement>("#submitToken2022Button").addEventListener("click", () => {
void executeToken2022(true);
});
element<HTMLButtonElement>("#refreshTokenJournalButton").addEventListener("click", () => {
void loadTokenJournal();
});
element<HTMLButtonElement>("#deriveAtaButton").addEventListener("click", () => {
void deriveAta();
});
element<HTMLButtonElement>("#simulateAtaButton").addEventListener("click", () => {
void executeAta(false);
});
element<HTMLButtonElement>("#submitAtaButton").addEventListener("click", () => {
void executeAta(true);
});
element<HTMLButtonElement>("#refreshAtaJournalButton").addEventListener("click", () => {
void loadAtaJournal();
});
element<HTMLButtonElement>("#cancelExecutionButton").addEventListener("click", () => {
void cancelExecution();
});
element<HTMLButtonElement>("#clearExecutionLogButton").addEventListener("click", () => {
logLines.length = 0;
element<HTMLTextAreaElement>("#executionLogOutput").value = "";
});
void listen<DemoExecutionSolanaCoreProgressPayload>("demo-execution-solana-core-progress", event => {
appendLog(event.payload);
});
void loadOptions().catch(caughtError => {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
appendLog({ timestamp: new Date().toISOString(), level: "error", stage: "options", message });
frontendError("kb_app_demo.frontend.demo_execution_spl", `Options loading failed: ${message}`);
});
});

View File

@@ -0,0 +1,181 @@
// file: kb_app_demo/frontend/ts/demo_http.ts
// version: 3
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log";
import type { DemoHttpExecutionPayload } from "./bindings/kb_app_demo/demo_http/DemoHttpExecutionPayload";
import type { DemoHttpMethodOption } from "./bindings/kb_app_demo/demo_http/DemoHttpMethodOption";
import type { DemoHttpOptionsPayload } from "./bindings/kb_app_demo/demo_http/DemoHttpOptionsPayload";
import type { DemoHttpRequest } from "./bindings/kb_app_demo/demo_http/DemoHttpRequest";
import type { DemoHttpRoleOption } from "./bindings/kb_app_demo/demo_http/DemoHttpRoleOption";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
let roleOptions: DemoHttpRoleOption[] = [];
let methodOptions: DemoHttpMethodOption[] = [];
function textInputValue(selector: string): string {
const element = document.querySelector<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>(selector);
return element ? element.value.trim() : "";
}
function writeTextarea(selector: string, value: string): void {
const element = document.querySelector<HTMLTextAreaElement>(selector);
if (element) {
element.value = value;
}
}
function jsonText(value: unknown): string {
if (typeof value === "string") {
return value;
}
return JSON.stringify(value, null, 2);
}
function roleSupportsMethod(role: DemoHttpRoleOption, method: DemoHttpMethodOption): boolean {
return role.requestKinds.includes("*") || role.requestKinds.includes(method.requestKind);
}
function selectedRole(): DemoHttpRoleOption | null {
const role = textInputValue("#httpRoleSelect");
return roleOptions.find(option => option.role === role) ?? null;
}
function populateSelect(selector: string, values: Array<{ value: string; label: string }>): void {
const select = document.querySelector<HTMLSelectElement>(selector);
if (!select) {
return;
}
select.textContent = "";
for (const item of values) {
const option = document.createElement("option");
option.value = item.value;
option.textContent = item.label;
select.appendChild(option);
}
}
function refreshMethodList(): void {
const role = selectedRole();
const filtered = role ? methodOptions.filter(method => roleSupportsMethod(role, method)) : methodOptions;
populateSelect("#httpMethodSelect", filtered.map(method => ({
value: method.method,
label: `${method.method}${method.label}`,
})));
refreshMethodFields();
}
function refreshMethodFields(): void {
const methodName = textInputValue("#httpMethodSelect");
const method = methodOptions.find(option => option.method === methodName) ?? null;
const firstArg = document.querySelector<HTMLInputElement>("#httpFirstArgInput");
const config = document.querySelector<HTMLTextAreaElement>("#httpConfigInput");
const params = document.querySelector<HTMLTextAreaElement>("#httpParamsInput");
if (firstArg) {
firstArg.disabled = method ? !method.requiresFirstArg : false;
firstArg.placeholder = method && method.requiresFirstArg ? "Argument requis" : "Non requis pour cette méthode";
}
if (config) {
config.disabled = method ? !method.supportsConfigJson : false;
}
if (params) {
params.disabled = false;
}
}
function formatHttpExecutionPayload(payload: DemoHttpExecutionPayload): string {
const header = {
endpointName: payload.endpointName,
provider: payload.provider,
endpointUrl: payload.endpointUrl,
role: payload.role,
method: payload.method,
requestKind: payload.requestKind,
methodClass: payload.methodClass,
};
return `${JSON.stringify(header, null, 2)}\n\n--- response ---\n${payload.responseJson}`;
}
async function copyTextarea(selector: string): Promise<void> {
const element = document.querySelector<HTMLTextAreaElement>(selector);
if (!element) {
return;
}
await navigator.clipboard.writeText(element.value);
}
async function refreshHttpOptions(): Promise<void> {
try {
const options = await invoke<DemoHttpOptionsPayload>("demo_http_options");
roleOptions = options.roles;
methodOptions = options.methods;
populateSelect("#httpRoleSelect", roleOptions.map(role => ({
value: role.role,
label: role.role,
})));
refreshMethodList();
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
writeTextarea("#httpResultOutput", `Erreur options HTTP : ${message}`);
frontendError("kb_app_demo.frontend.demo_http", `HTTP options loading failed: ${message}`);
}
}
async function refreshHttpPool(): Promise<void> {
try {
const snapshots = await invoke("demo_http_list_pool_clients");
writeTextarea("#httpPoolOutput", jsonText(snapshots));
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
writeTextarea("#httpPoolOutput", `Erreur : ${message}`);
frontendError("kb_app_demo.frontend.demo_http", `HTTP pool refresh failed: ${message}`);
}
}
async function executeHttpRequest(): Promise<void> {
const request: DemoHttpRequest = {
role: textInputValue("#httpRoleSelect"),
method: textInputValue("#httpMethodSelect"),
firstArg: textInputValue("#httpFirstArgInput") || null,
configJson: textInputValue("#httpConfigInput") || null,
paramsJson: textInputValue("#httpParamsInput") || null,
};
writeTextarea("#httpResultOutput", "Exécution en cours...");
try {
const response = await invoke<DemoHttpExecutionPayload>("demo_http_execute_request", { request });
writeTextarea("#httpResultOutput", formatHttpExecutionPayload(response));
frontendDebug("kb_app_demo.frontend.demo_http", `HTTP request completed: ${response.method}`);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
writeTextarea("#httpResultOutput", `Erreur : ${message}`);
frontendError("kb_app_demo.frontend.demo_http", `HTTP request failed: ${message}`);
}
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge("kb_app_demo.frontend.demo_http");
frontendDebug("kb_app_demo.frontend.demo_http", "HTTP demo window loaded");
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
document.querySelector<HTMLSelectElement>("#httpRoleSelect")?.addEventListener("change", refreshMethodList);
document.querySelector<HTMLSelectElement>("#httpMethodSelect")?.addEventListener("change", refreshMethodFields);
document.querySelector<HTMLButtonElement>("#executeHttpButton")?.addEventListener("click", () => {
void executeHttpRequest();
});
document.querySelector<HTMLButtonElement>("#refreshHttpPoolButton")?.addEventListener("click", () => {
void refreshHttpPool();
});
document.querySelector<HTMLButtonElement>("#copyHttpPoolButton")?.addEventListener("click", () => {
void copyTextarea("#httpPoolOutput");
});
document.querySelector<HTMLButtonElement>("#copyHttpResultButton")?.addEventListener("click", () => {
void copyTextarea("#httpResultOutput");
});
void refreshHttpOptions();
void refreshHttpPool();
});

View File

@@ -0,0 +1,60 @@
// file: kb_app_demo/frontend/ts/demo_sql_diag.ts
// version: 2
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log";
import { jsonText, renderJsonViewer, renderTables, setText } from "./demo_sql_tables";
import type { DemoSqlDiagPayload } from "./bindings/kb_app_demo/demo_sql_diag/DemoSqlDiagPayload";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
let latestPayload: DemoSqlDiagPayload | null = null;
async function refreshDiagnostics(): Promise<void> {
setText("#sqlDiagStatus", "Chargement...");
try {
const payload = await invoke<DemoSqlDiagPayload>("load_demo_sql_diag");
setText("#sqlDiagConfigPath", payload.configPath);
setText("#sqlDiagProfile", payload.activeProfileName);
setText("#sqlDiagBackend", payload.backend);
setText("#sqlDiagDsn", payload.maskedDsn);
setText("#sqlDiagSchema", payload.currentSchema ?? "—");
setText("#sqlDiagHealth", payload.healthStatus);
setText("#sqlDiagMigration", payload.migrationStatus);
setText("#sqlDiagStatus", "OK");
renderTables("#sqlDiagTableBody", payload.tables);
latestPayload = payload;
renderJsonViewer("#sqlDiagJson", payload);
frontendDebug("kb_app_demo.frontend.demo_sql_diag", "SQL diagnostics refreshed");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#sqlDiagStatus", `Erreur : ${message}`);
latestPayload = null;
renderJsonViewer("#sqlDiagJson", { error: message });
frontendError("kb_app_demo.frontend.demo_sql_diag", `SQL diagnostics loading failed: ${message}`);
}
}
async function copyDiagnostics(): Promise<void> {
if (latestPayload) {
await navigator.clipboard.writeText(jsonText(latestPayload));
}
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge("kb_app_demo.frontend.demo_sql_diag");
frontendDebug("kb_app_demo.frontend.demo_sql_diag", "SQL 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", () => {
void refreshDiagnostics();
});
document.querySelector<HTMLButtonElement>("#copySqlDiagButton")?.addEventListener("click", () => {
void copyDiagnostics();
});
void refreshDiagnostics();
});

View File

@@ -0,0 +1,42 @@
// file: kb_app_demo/frontend/ts/demo_sql_pg_core.ts
// version: 3
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log";
import { renderJsonViewer, renderTables, setText } from "./demo_sql_tables";
import type { DemoSqlPgCorePayload } from "./bindings/kb_app_demo/demo_sql_pg_core/DemoSqlPgCorePayload";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
async function refreshCore(): Promise<void> {
setText("#sqlCoreStatus", "Chargement...");
try {
const payload = await invoke<DemoSqlPgCorePayload>("load_demo_sql_pg_core");
setText("#sqlCoreProfile", payload.activeProfileName);
setText("#sqlCoreDsn", payload.maskedDsn);
setText("#sqlCoreStatus", "OK");
renderTables("#sqlCoreTableBody", payload.tables);
renderJsonViewer("#sqlCoreJson", payload);
frontendDebug("kb_app_demo.frontend.demo_sql_pg_core", "PostgreSQL core diagnostics refreshed");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#sqlCoreStatus", `Erreur : ${message}`);
renderJsonViewer("#sqlCoreJson", { error: message });
frontendError("kb_app_demo.frontend.demo_sql_pg_core", `PostgreSQL core diagnostics loading failed: ${message}`);
}
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge("kb_app_demo.frontend.demo_sql_pg_core");
frontendDebug("kb_app_demo.frontend.demo_sql_pg_core", "PostgreSQL core 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", () => {
void refreshCore();
});
void refreshCore();
});

View File

@@ -0,0 +1,42 @@
// file: kb_app_demo/frontend/ts/demo_sql_pg_raw.ts
// version: 3
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log";
import { renderJsonViewer, renderTables, setText } from "./demo_sql_tables";
import type { DemoSqlPgRawPayload } from "./bindings/kb_app_demo/demo_sql_pg_raw/DemoSqlPgRawPayload";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
async function refreshRaw(): Promise<void> {
setText("#sqlRawStatus", "Chargement...");
try {
const payload = await invoke<DemoSqlPgRawPayload>("load_demo_sql_pg_raw");
setText("#sqlRawProfile", payload.activeProfileName);
setText("#sqlRawDsn", payload.maskedDsn);
setText("#sqlRawStatus", "OK");
renderTables("#sqlRawTableBody", payload.tables);
renderJsonViewer("#sqlRawJson", payload);
frontendDebug("kb_app_demo.frontend.demo_sql_pg_raw", "PostgreSQL raw diagnostics refreshed");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#sqlRawStatus", `Erreur : ${message}`);
renderJsonViewer("#sqlRawJson", { error: message });
frontendError("kb_app_demo.frontend.demo_sql_pg_raw", `PostgreSQL raw diagnostics loading failed: ${message}`);
}
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge("kb_app_demo.frontend.demo_sql_pg_raw");
frontendDebug("kb_app_demo.frontend.demo_sql_pg_raw", "PostgreSQL raw 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", () => {
void refreshRaw();
});
void refreshRaw();
});

View File

@@ -0,0 +1,800 @@
// file: kb_app_demo/frontend/ts/demo_sql_replay_candidates.ts
// version: 7
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 type { DemoSqlReplayEntityRequest } from "./bindings/kb_app_demo/demo_sql_replay_candidates/DemoSqlReplayEntityRequest";
import type { DemoSqlReplayEntityRow } from "./bindings/kb_app_demo/demo_sql_replay_candidates/DemoSqlReplayEntityRow";
import type { DemoSqlReplayKnownProgramOption } from "./bindings/kb_app_demo/demo_sql_replay_candidates/DemoSqlReplayKnownProgramOption";
import type { DemoSqlReplayOptionsPayload } from "./bindings/kb_app_demo/demo_sql_replay_candidates/DemoSqlReplayOptionsPayload";
import type { DemoSqlReplayProgramRequest } from "./bindings/kb_app_demo/demo_sql_replay_candidates/DemoSqlReplayProgramRequest";
import type { DemoSqlReplayProgramRow } from "./bindings/kb_app_demo/demo_sql_replay_candidates/DemoSqlReplayProgramRow";
import type { DemoSqlReplayTransactionRequest } from "./bindings/kb_app_demo/demo_sql_replay_candidates/DemoSqlReplayTransactionRequest";
import type { DemoSqlReplayTransactionRow } from "./bindings/kb_app_demo/demo_sql_replay_candidates/DemoSqlReplayTransactionRow";
(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;
limitSelector: string;
summarySelector: string;
loadButtonSelector: string;
resetButtonSelector: string;
useButtonSelector: string;
copyButtonSelector: string;
exportButtonSelector: string;
fileName: string;
};
const tracingTarget = "kb_app_demo.frontend.demo_sql_replay_candidates";
const entityViewConfigs: EntityViewConfig[] = [
{
kind: "mint",
label: "mint",
pluralLabel: "mints",
tableSelector: "#mintCandidateTable",
containsSelector: "#mintContainsInput",
limitSelector: "#mintLimitInput",
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",
limitSelector: "#ownerLimitInput",
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",
limitSelector: "#accountLimitInput",
summarySelector: "#accountResultSummary",
loadButtonSelector: "#loadAccountsButton",
resetButtonSelector: "#resetAccountsButton",
useButtonSelector: "#useSelectedAccountsButton",
copyButtonSelector: "#copyAccountsButton",
exportButtonSelector: "#exportAccountsCsvButton",
fileName: "replay_account_keys.csv",
},
];
let transactionTable: Api<DemoSqlReplayTransactionRow> | null = null;
let programTable: Api<DemoSqlReplayProgramRow> | null = null;
let mintTable: Api<DemoSqlReplayEntityRow> | null = null;
let ownerTable: Api<DemoSqlReplayEntityRow> | null = null;
let accountTable: Api<DemoSqlReplayEntityRow> | null = null;
let busyOperationCount = 0;
let maximumReplayCandidateLimit = 5_000;
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 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) {
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: DemoSqlReplayTransactionRow): string {
if (!row.hasCoreTransaction) {
return "absent";
}
if (row.transactionFailed === true) {
return "échec on-chain";
}
return "présent";
}
function coreDisplay(_value: unknown, type: string, row: DemoSqlReplayTransactionRow): 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: DemoSqlReplayEntityRow): 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: DemoSqlReplayEntityRow): 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<DemoSqlReplayTransactionRow> {
return new DataTable<DemoSqlReplayTransactionRow>("#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: "outerInstructionCount" },
{ data: "innerInstructionCount" },
{ data: "outerProgramCount" },
{ data: "innerProgramCount" },
{ data: "updatedAt" },
],
order: [[2, "desc"]],
pageLength: 10,
lengthMenu: [10, 25, 50, 100, 250],
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<DemoSqlReplayProgramRow> {
return new DataTable<DemoSqlReplayProgramRow>("#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: "outerInstructionCount" },
{ data: "innerInstructionCount" },
{ data: "logCount" },
{ data: "minSlot" },
{ data: "maxSlot" },
],
order: [[3, "desc"]],
pageLength: 10,
lengthMenu: [10, 25, 50, 100, 250],
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<DemoSqlReplayEntityRow> {
return new DataTable<DemoSqlReplayEntityRow>(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],
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<DemoSqlReplayEntityRow> | 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 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") {
button.disabled = operationInProgress;
}
});
}
function buildTransactionRequest(): DemoSqlReplayTransactionRequest {
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,
limit: boundedPositiveIntegerValue("#transactionLimitInput"),
newestFirst: element<HTMLInputElement>("#transactionNewestFirstInput").checked,
};
}
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>("#transactionLimitInput").value = "500";
element<HTMLInputElement>("#transactionNewestFirstInput").checked = true;
resetTableState(transactionTable);
await loadTransactions();
}
async function resetProgramFilters(): Promise<void> {
element<HTMLSelectElement>("#knownProgramSelect").value = "";
element<HTMLInputElement>("#programContainsInput").value = "";
element<HTMLInputElement>("#programLimitInput").value = "500";
resetTableState(programTable);
await loadPrograms();
}
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);
}
async function loadTransactions(): Promise<void> {
if (transactionTable === null) {
return;
}
try {
setBusy(true, "Chargement transactions");
const request = buildTransactionRequest();
const rows = await invoke<DemoSqlReplayTransactionRow[]>("load_demo_sql_replay_transactions", { request });
replaceRows(transactionTable, rows);
element<HTMLElement>("#transactionResultSummary").textContent = `${rows.length} transaction(s) chargée(s) depuis PostgreSQL.`;
frontendDebug(tracingTarget, `loaded ${rows.length} replay transaction candidates`);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLElement>("#transactionResultSummary").textContent = `Erreur : ${message}`;
frontendError(tracingTarget, `transaction candidate loading failed: ${message}`);
} finally {
setBusy(false, "Prêt");
}
}
async function loadPrograms(): Promise<void> {
if (programTable === null) {
return;
}
try {
setBusy(true, "Chargement programmes");
const request: DemoSqlReplayProgramRequest = {
programIdContains: optionalInputValue("#programContainsInput"),
limit: boundedPositiveIntegerValue("#programLimitInput"),
};
const rows = await invoke<DemoSqlReplayProgramRow[]>("load_demo_sql_replay_programs", { request });
replaceRows(programTable, rows);
element<HTMLElement>("#programResultSummary").textContent = `${rows.length} programme(s) chargé(s), avec occurrences outer, inner et logs liés.`;
frontendDebug(tracingTarget, `loaded ${rows.length} replay program summaries`);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLElement>("#programResultSummary").textContent = `Erreur : ${message}`;
frontendError(tracingTarget, `program summary loading failed: ${message}`);
} finally {
setBusy(false, "Prêt");
}
}
async function loadEntity(config: EntityViewConfig): Promise<void> {
const table = entityTable(config.kind);
if (table === null) {
return;
}
try {
setBusy(true, `Chargement ${config.pluralLabel}`);
const request: DemoSqlReplayEntityRequest = {
entityKind: config.kind,
entityValueContains: optionalInputValue(config.containsSelector),
limit: boundedPositiveIntegerValue(config.limitSelector),
};
const rows = await invoke<DemoSqlReplayEntityRow[]>("load_demo_sql_replay_entities", { request });
replaceRows(table, rows);
element<HTMLElement>(config.summarySelector).textContent = `${rows.length} ${config.pluralLabel} chargé(s) depuis PostgreSQL.`;
frontendDebug(tracingTarget, `loaded ${rows.length} replay ${config.kind} summaries`);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLElement>(config.summarySelector).textContent = `Erreur : ${message}`;
frontendError(tracingTarget, `${config.kind} summary loading failed: ${message}`);
} finally {
setBusy(false, "Prêt");
}
}
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_sql_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", "outer_instruction_count", "inner_instruction_count", "outer_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.outerInstructionCount, row.innerInstructionCount, row.outerProgramCount, 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", "outer_instruction_count", "inner_instruction_count", "log_count", "min_slot", "max_slot"],
rows.map(row => [row.programCode, row.programId, row.transactionCount, row.outerInstructionCount, 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(): DemoSqlReplayTransactionRow[] {
if (transactionTable === null) {
return [];
}
return selectedOrFilteredRows(transactionTable);
}
function programRowsForExport(): DemoSqlReplayProgramRow[] {
if (programTable === null) {
return [];
}
return selectedOrFilteredRows(programTable);
}
function entityRowsForExport(kind: EntityKindCode): DemoSqlReplayEntityRow[] {
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 loadTransactions();
}
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 loadTransactions();
}
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<DemoSqlReplayOptionsPayload>("demo_sql_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.");
}
maximumReplayCandidateLimit = options.maximumLimit;
element<HTMLElement>("#replayCandidateProfileBadge").textContent = `Profil ${options.activeProfileName}`;
element<HTMLElement>("#replayCandidateDsn").textContent = options.maskedDsn;
element<HTMLElement>("#replayCandidateMaximumLimit").textContent = String(options.maximumLimit);
document.querySelectorAll<HTMLInputElement>("input[type=number][max]").forEach(input => {
input.max = String(options.maximumLimit);
});
populateKnownPrograms(options.knownPrograms);
}
function populateKnownPrograms(programs: DemoSqlReplayKnownProgramOption[]): 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 loadTransactions());
element<HTMLButtonElement>("#resetTransactionsButton").addEventListener("click", () => {
void resetTransactionFilters().catch(error => reportUiError("reset transaction filters", error));
});
element<HTMLButtonElement>("#loadProgramsButton").addEventListener("click", () => void loadPrograms());
element<HTMLButtonElement>("#resetProgramsButton").addEventListener("click", () => {
void resetProgramFilters().catch(error => reportUiError("reset program filters", error));
});
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 loadEntity(config));
element<HTMLButtonElement>(config.resetButtonSelector).addEventListener("click", () => {
void resetEntityFilters(config).catch(error => reportUiError(`reset ${config.pluralLabel} filters`, error));
});
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<HTMLSelectElement>("#knownProgramSelect").addEventListener("change", event => {
const target = event.currentTarget as HTMLSelectElement;
element<HTMLInputElement>("#programContainsInput").value = target.value;
});
document.querySelectorAll<HTMLButtonElement>('[data-bs-toggle="tab"]').forEach(button => {
button.addEventListener("shown.bs.tab", adjustVisibleTables);
});
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge(tracingTarget);
frontendDebug(tracingTarget, "SQL 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([
loadTransactions(),
loadPrograms(),
loadEntity(entityViewConfigs[0]),
loadEntity(entityViewConfigs[1]),
loadEntity(entityViewConfigs[2]),
]))
.catch(caughtError => reportUiError("initial loading", caughtError));
});

View File

@@ -0,0 +1,68 @@
// file: kb_app_demo/frontend/ts/demo_sql_tables.ts
// version: 2
import "@andypf/json-viewer";
import type { DemoSqlTableSnapshot } from "./bindings/kb_app_demo/demo_sql/DemoSqlTableSnapshot";
export function setText(selector: string, value: string): void {
const element = document.querySelector<HTMLElement>(selector);
if (element) {
element.textContent = value;
}
}
export function renderTables(selector: string, tables: DemoSqlTableSnapshot[]): void {
const tbody = document.querySelector<HTMLTableSectionElement>(selector);
if (!tbody) {
return;
}
tbody.textContent = "";
for (const table of tables) {
const row = document.createElement("tr");
row.appendChild(cell(table.tableName, true));
row.appendChild(cell(table.domain, false));
row.appendChild(cell(table.role, false));
row.appendChild(cell(table.exists ? "oui" : "non", false));
row.appendChild(cell(table.rowCount === null ? "—" : String(table.rowCount), false));
row.appendChild(cell(table.minSlot === null ? "—" : String(table.minSlot), false));
row.appendChild(cell(table.maxSlot === null ? "—" : String(table.maxSlot), false));
row.appendChild(cell(table.latestCreatedAt ?? "—", false));
tbody.appendChild(row);
}
}
export function jsonText(value: unknown): string {
return JSON.stringify(value, null, 2);
}
export function renderJsonViewer(selector: string, value: unknown): void {
const container = document.querySelector<HTMLElement>(selector);
if (!container) {
return;
}
const viewer = document.createElement("andypf-json-viewer");
viewer.setAttribute("indent", "2");
viewer.setAttribute("expanded", "1");
viewer.setAttribute("theme", "default-light");
viewer.setAttribute("show-data-types", "true");
viewer.setAttribute("show-toolbar", "false");
viewer.setAttribute("expand-icon-type", "arrow");
viewer.setAttribute("show-copy", "true");
viewer.setAttribute("show-size", "true");
viewer.setAttribute("expand-empty", "false");
viewer.setAttribute("data", jsonText(value));
container.textContent = "";
container.appendChild(viewer);
}
function cell(value: string, code: boolean): HTMLTableCellElement {
const td = document.createElement("td");
if (code) {
const codeElement = document.createElement("code");
codeElement.textContent = value;
td.appendChild(codeElement);
} else {
td.textContent = value;
}
return td;
}

View File

@@ -0,0 +1,325 @@
// file: kb_app_demo/frontend/ts/demo_ws.ts
// version: 6
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import {invoke} from "@tauri-apps/api/core";
import {listen} from "@tauri-apps/api/event";
import {frontendDebug, frontendError, installFrontendConsoleBridge} from "./frontend_log";
import type {DemoWsExecutionPayload} from "./bindings/kb_app_demo/demo_ws/DemoWsExecutionPayload";
import type {DemoWsMessagePayload} from "./bindings/kb_app_demo/demo_ws/DemoWsMessagePayload";
import type {DemoWsMethodOption} from "./bindings/kb_app_demo/demo_ws/DemoWsMethodOption";
import type {DemoWsOptionsPayload} from "./bindings/kb_app_demo/demo_ws/DemoWsOptionsPayload";
import type {DemoWsRequest} from "./bindings/kb_app_demo/demo_ws/DemoWsRequest";
import type {DemoWsRoleOption} from "./bindings/kb_app_demo/demo_ws/DemoWsRoleOption";
import type {DemoWsStatusPayload} from "./bindings/kb_app_demo/demo_ws/DemoWsStatusPayload";
import type {DemoWsUnsubscribeRequest} from "./bindings/kb_app_demo/demo_ws/DemoWsUnsubscribeRequest";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
let roleOptions: DemoWsRoleOption[] = [];
let methodOptions: DemoWsMethodOption[] = [];
const maxResultOutputCharacters = 200_000;
const maxIncomingMessageCharacters = 25_000;
function textInputValue(selector: string): string {
const element = document.querySelector<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>(selector);
return element ? element.value.trim() : "";
}
function writeTextarea(selector: string, value: string): void {
const element = document.querySelector<HTMLTextAreaElement>(selector);
if (element) {
element.value = value;
}
}
function truncateIncomingMessage(value: string): string {
if (value.length <= maxIncomingMessageCharacters) {
return value;
}
return `${value.slice(0, maxIncomingMessageCharacters)}\n... message tronqué côté UI ...`;
}
function capTextareaValue(value: string): string {
if (value.length <= maxResultOutputCharacters) {
return value;
}
return `... historique tronqué côté UI ...\n${value.slice(value.length - maxResultOutputCharacters)}`;
}
function appendTextarea(selector: string, value: string): void {
const element = document.querySelector<HTMLTextAreaElement>(selector);
if (!element) {
return;
}
const previous = element.value.trim();
const safeValue = truncateIncomingMessage(value);
const nextValue = previous.length > 0 ? `${previous}\n\n${safeValue}` : safeValue;
element.value = capTextareaValue(nextValue);
element.scrollTop = element.scrollHeight;
}
function jsonText(value: unknown): string {
if (typeof value === "string") {
return value;
}
return JSON.stringify(value, null, 2);
}
function roleSupportsMethod(role: DemoWsRoleOption, method: DemoWsMethodOption): boolean {
return role.requestKinds.includes("*") || role.requestKinds.includes(method.requestKind);
}
function selectedRole(): DemoWsRoleOption | null {
const role = textInputValue("#wsRoleSelect");
return roleOptions.find(option => option.role === role) ?? null;
}
function populateSelect(selector: string, values: Array<{ value: string; label: string }>): void {
const select = document.querySelector<HTMLSelectElement>(selector);
if (!select) {
return;
}
select.textContent = "";
for (const item of values) {
const option = document.createElement("option");
option.value = item.value;
option.textContent = item.label;
select.appendChild(option);
}
}
function refreshMethodList(): void {
const role = selectedRole();
const filtered = role ? methodOptions.filter(method => roleSupportsMethod(role, method)) : methodOptions;
populateSelect("#wsMethodSelect", filtered.map(method => ({
value: method.method,
label: `${method.method}${method.label}`,
})));
refreshMethodFields();
}
function refreshMethodFields(): void {
const methodName = textInputValue("#wsMethodSelect");
const method = methodOptions.find(option => option.method === methodName) ?? null;
const target = document.querySelector<HTMLInputElement>("#wsTargetInput");
const filter = document.querySelector<HTMLTextAreaElement>("#wsFilterInput");
const config = document.querySelector<HTMLTextAreaElement>("#wsConfigInput");
if (target) {
target.disabled = method ? !method.requiresTarget : false;
target.placeholder = method && method.requiresTarget ? "Target requis" : "Non requis pour cette méthode";
}
if (filter) {
filter.disabled = method ? !method.requiresFilterJson : false;
}
if (config) {
config.disabled = method ? !method.supportsConfigJson : false;
}
}
function refreshSubscriptionSelect(status: DemoWsStatusPayload): void {
const select = document.querySelector<HTMLSelectElement>("#wsSubscriptionSelect");
const unsubscribeButton = document.querySelector<HTMLButtonElement>("#unsubscribeWsButton");
if (!select) {
return;
}
const previous = select.value;
select.textContent = "";
for (const subscription of status.subscriptions) {
const option = document.createElement("option");
option.value = String(subscription.subscriptionId);
option.textContent = `${subscription.subscriptionId} · ${subscription.method}`;
select.appendChild(option);
}
if (previous.length > 0) {
select.value = previous;
}
if (unsubscribeButton) {
unsubscribeButton.disabled = !status.connected || status.subscriptions.length === 0;
}
}
function setConnectedUi(status: DemoWsStatusPayload): void {
const badge = document.querySelector<HTMLElement>("#wsStatusBadge");
const connectButton = document.querySelector<HTMLButtonElement>("#connectWsButton");
const disconnectButton = document.querySelector<HTMLButtonElement>("#disconnectWsButton");
if (badge) {
badge.textContent = status.connected ? `Connecté · ${status.subscriptionCount} subscription(s)` : "Déconnecté";
badge.className = status.connected ? "badge text-bg-success" : "badge text-bg-secondary";
}
if (connectButton) {
connectButton.disabled = false;
}
if (disconnectButton) {
disconnectButton.disabled = !status.connected;
}
refreshSubscriptionSelect(status);
writeTextarea("#wsStatusOutput", jsonText(status));
}
function formatWsExecutionPayload(payload: DemoWsExecutionPayload): string {
const header = {
endpointName: payload.endpointName,
provider: payload.provider,
endpointUrl: payload.endpointUrl,
role: payload.role,
method: payload.method,
requestKind: payload.requestKind,
responseKind: payload.responseKind,
subscriptionId: payload.subscriptionId,
};
return `${JSON.stringify(header, null, 2)}\n\n--- subscription response ---\n${payload.responseJson}`;
}
async function copyTextarea(selector: string): Promise<void> {
const element = document.querySelector<HTMLTextAreaElement>(selector);
if (!element) {
return;
}
await navigator.clipboard.writeText(element.value);
}
async function refreshWsOptions(): Promise<void> {
try {
const options = await invoke<DemoWsOptionsPayload>("demo_ws_options");
roleOptions = options.roles;
methodOptions = options.methods;
populateSelect("#wsRoleSelect", roleOptions.map(role => ({
value: role.role,
label: role.role,
})));
refreshMethodList();
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
writeTextarea("#wsResultOutput", `Erreur options WebSocket : ${message}`);
frontendError("kb_app_demo.frontend.demo_ws", `WebSocket options loading failed: ${message}`);
}
}
async function refreshWsPool(): Promise<void> {
try {
const snapshots = await invoke("demo_ws_list_pool_clients");
writeTextarea("#wsPoolOutput", jsonText(snapshots));
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
writeTextarea("#wsPoolOutput", `Erreur : ${message}`);
frontendError("kb_app_demo.frontend.demo_ws", `WebSocket pool refresh failed: ${message}`);
}
}
async function refreshWsStatus(): Promise<void> {
try {
const status = await invoke<DemoWsStatusPayload>("demo_ws_status");
setConnectedUi(status);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
writeTextarea("#wsStatusOutput", `Erreur statut WebSocket : ${message}`);
frontendError("kb_app_demo.frontend.demo_ws", `WebSocket status failed: ${message}`);
}
}
async function connectWsRequest(): Promise<void> {
const request: DemoWsRequest = {
role: textInputValue("#wsRoleSelect"),
method: textInputValue("#wsMethodSelect"),
target: textInputValue("#wsTargetInput") || null,
filterJson: textInputValue("#wsFilterInput") || null,
configJson: textInputValue("#wsConfigInput") || null,
};
writeTextarea("#wsResultOutput", "Souscription en cours sur la connexion WebSocket...");
try {
const response = await invoke<DemoWsExecutionPayload>("demo_ws_connect", { request });
writeTextarea("#wsResultOutput", formatWsExecutionPayload(response));
frontendDebug("kb_app_demo.frontend.demo_ws", `WebSocket subscribed: ${response.method}`);
await refreshWsStatus();
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
writeTextarea("#wsResultOutput", `Erreur : ${message}`);
frontendError("kb_app_demo.frontend.demo_ws", `WebSocket subscription failed: ${message}`);
await refreshWsStatus();
}
}
async function unsubscribeWsRequest(): Promise<void> {
const subscriptionIdText = textInputValue("#wsSubscriptionSelect");
if (subscriptionIdText.length === 0) {
appendTextarea("#wsResultOutput", "Aucune subscription sélectionnée.");
return;
}
const subscriptionId = Number(subscriptionIdText);
if (!Number.isSafeInteger(subscriptionId) || subscriptionId < 0) {
appendTextarea("#wsResultOutput", "Identifiant de subscription invalide.");
return;
}
const request: DemoWsUnsubscribeRequest = {
subscriptionId,
};
try {
const status = await invoke<DemoWsStatusPayload>("demo_ws_unsubscribe", { request });
setConnectedUi(status);
appendTextarea("#wsResultOutput", `--- unsubscribe ---\nSubscription ${request.subscriptionId} désinscrite.`);
frontendDebug("kb_app_demo.frontend.demo_ws", `WebSocket unsubscribed: ${request.subscriptionId}`);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
appendTextarea("#wsResultOutput", `Erreur unsubscribe : ${message}`);
frontendError("kb_app_demo.frontend.demo_ws", `WebSocket unsubscribe failed: ${message}`);
await refreshWsStatus();
}
}
async function disconnectWsRequest(): Promise<void> {
try {
const status = await invoke<DemoWsStatusPayload>("demo_ws_disconnect");
setConnectedUi(status);
appendTextarea("#wsResultOutput", "--- disconnect ---\nDéconnexion demandée.");
frontendDebug("kb_app_demo.frontend.demo_ws", "WebSocket disconnected");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
appendTextarea("#wsResultOutput", `Erreur déconnexion : ${message}`);
frontendError("kb_app_demo.frontend.demo_ws", `WebSocket disconnection failed: ${message}`);
}
}
async function installWsEventListeners(): Promise<void> {
await listen<DemoWsStatusPayload>("demo-ws-status", event => {
setConnectedUi(event.payload);
});
await listen<DemoWsMessagePayload>("demo-ws-message", event => {
appendTextarea("#wsResultOutput", `--- ${event.payload.kind} ---\n${event.payload.payloadJson}`);
});
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge("kb_app_demo.frontend.demo_ws");
frontendDebug("kb_app_demo.frontend.demo_ws", "WebSocket demo window loaded");
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
document.querySelector<HTMLSelectElement>("#wsRoleSelect")?.addEventListener("change", refreshMethodList);
document.querySelector<HTMLSelectElement>("#wsMethodSelect")?.addEventListener("change", refreshMethodFields);
document.querySelector<HTMLButtonElement>("#connectWsButton")?.addEventListener("click", () => {
void connectWsRequest();
});
document.querySelector<HTMLButtonElement>("#unsubscribeWsButton")?.addEventListener("click", () => {
void unsubscribeWsRequest();
});
document.querySelector<HTMLButtonElement>("#disconnectWsButton")?.addEventListener("click", () => {
void disconnectWsRequest();
});
document.querySelector<HTMLButtonElement>("#refreshWsPoolButton")?.addEventListener("click", () => {
void refreshWsPool();
});
document.querySelector<HTMLButtonElement>("#copyWsPoolButton")?.addEventListener("click", () => {
void copyTextarea("#wsPoolOutput");
});
document.querySelector<HTMLButtonElement>("#copyWsResultButton")?.addEventListener("click", () => {
void copyTextarea("#wsResultOutput");
});
void installWsEventListeners();
void refreshWsOptions();
void refreshWsPool();
void refreshWsStatus();
});

View File

@@ -0,0 +1,97 @@
// file: kb_app_demo/frontend/ts/frontend_log.ts
// version: 1
//! Frontend logging helpers that preserve explicit tracing targets.
import { invoke } from "@tauri-apps/api/core";
import type { FrontendLogPayload } from "./bindings/kb_app_demo/frontend_log/FrontendLogPayload";
type FrontendLogLevel = "trace" | "debug" | "info" | "warn" | "error";
type ConsoleMethod = (...items: unknown[]) => void;
const originalConsole = {
trace: console.trace.bind(console),
debug: console.debug.bind(console),
log: console.log.bind(console),
info: console.info.bind(console),
warn: console.warn.bind(console),
error: console.error.bind(console),
};
function stringifyItem(item: unknown): string {
if (item instanceof Error) {
return item.stack ?? item.message;
}
if (typeof item === "string") {
return item;
}
try {
return JSON.stringify(item);
} catch (caughtError) {
return String(item);
}
}
function formatMessage(items: unknown[]): string {
return items.map(item => stringifyItem(item)).join(" ");
}
async function sendFrontendLog(level: FrontendLogLevel, target: string, message: string): Promise<void> {
const payload: FrontendLogPayload = {
level,
target,
message,
};
try {
await invoke("emit_frontend_log", { payload });
} catch (caughtError) {
originalConsole.error("frontend tracing bridge failed", caughtError);
}
}
export function frontendTrace(target: string, ...items: unknown[]): void {
const message = formatMessage(items);
originalConsole.trace(message);
void sendFrontendLog("trace", target, message);
}
export function frontendDebug(target: string, ...items: unknown[]): void {
const message = formatMessage(items);
originalConsole.debug(message);
void sendFrontendLog("debug", target, message);
}
export function frontendInfo(target: string, ...items: unknown[]): void {
const message = formatMessage(items);
originalConsole.info(message);
void sendFrontendLog("info", target, message);
}
export function frontendWarn(target: string, ...items: unknown[]): void {
const message = formatMessage(items);
originalConsole.warn(message);
void sendFrontendLog("warn", target, message);
}
export function frontendError(target: string, ...items: unknown[]): void {
const message = formatMessage(items);
originalConsole.error(message);
void sendFrontendLog("error", target, message);
}
function buildConsoleBridge(level: FrontendLogLevel, target: string, original: ConsoleMethod): ConsoleMethod {
return (...items: unknown[]) => {
original(...items);
void sendFrontendLog(level, target, formatMessage(items));
};
}
export function installFrontendConsoleBridge(target: string): void {
console.trace = buildConsoleBridge("trace", target, originalConsole.trace);
console.debug = buildConsoleBridge("debug", target, originalConsole.debug);
console.log = buildConsoleBridge("info", target, originalConsole.log);
console.info = buildConsoleBridge("info", target, originalConsole.info);
console.warn = buildConsoleBridge("warn", target, originalConsole.warn);
console.error = buildConsoleBridge("error", target, originalConsole.error);
}

View File

@@ -0,0 +1,256 @@
// file: kb_app_demo/frontend/ts/main.ts
// version: 14
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import MarkdownIt from "markdown-it";
import { invoke } from "@tauri-apps/api/core";
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
const markdownRenderer = new MarkdownIt({
html: false,
linkify: true,
typographer: true,
});
function renderMarkdown(markdownText: string): string {
return markdownRenderer.render(markdownText);
}
async function loadReadme(readmeContent: HTMLElement): Promise<void> {
try {
const readme = await invoke<string>("load_project_readme");
readmeContent.innerHTML = renderMarkdown(readme);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
readmeContent.textContent = `Erreur pendant le chargement du README : ${message}`;
frontendError("kb_app_demo.frontend.main", `README loading failed: ${message}`);
}
}
async function openDemoConfigWindow(): Promise<void> {
try {
await invoke("open_demo_config_window");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
frontendError("kb_app_demo.frontend.main", `Configuration demo window opening failed: ${message}`);
}
}
async function openDemoHttpWindow(): Promise<void> {
try {
await invoke("open_demo_http_window");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
frontendError("kb_app_demo.frontend.main", `HTTP demo window opening failed: ${message}`);
}
}
async function openDemoSqlDiagWindow(): Promise<void> {
try {
await invoke("open_demo_sql_diag_window");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
frontendError("kb_app_demo.frontend.main", `SQL diagnostics demo window opening failed: ${message}`);
}
}
async function openDemoSqlPgRawWindow(): Promise<void> {
try {
await invoke("open_demo_sql_pg_raw_window");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
frontendError("kb_app_demo.frontend.main", `PostgreSQL raw demo window opening failed: ${message}`);
}
}
async function openDemoSqlPgCoreWindow(): Promise<void> {
try {
await invoke("open_demo_sql_pg_core_window");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
frontendError("kb_app_demo.frontend.main", `PostgreSQL core demo window opening failed: ${message}`);
}
}
async function openDemoSqlReplayCandidatesWindow(): Promise<void> {
try {
await invoke("open_demo_sql_replay_candidates_window");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
frontendError("kb_app_demo.frontend.main", `SQL replay candidates window opening failed: ${message}`);
}
}
async function openDemoWsWindow(): Promise<void> {
try {
await invoke("open_demo_ws_window");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
frontendError("kb_app_demo.frontend.main", `WebSocket demo window opening failed: ${message}`);
}
}
async function openDemoBackfillWindow(): Promise<void> {
try {
await invoke("open_demo_backfill_window");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
frontendError("kb_app_demo.frontend.main", `Backfill demo window opening failed: ${message}`);
}
}
async function openDemoCoreExtractionWindow(): Promise<void> {
try {
await invoke("open_demo_core_extraction_window");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
frontendError("kb_app_demo.frontend.main", `Core extraction demo window opening failed: ${message}`);
}
}
async function openDemoDecodeReplayWindow(): Promise<void> {
try {
await invoke("open_demo_decode_replay_window");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
frontendError("kb_app_demo.frontend.main", `Decode replay demo window opening failed: ${message}`);
}
}
async function openDemoExecutionSolanaCoreWindow(): Promise<void> {
try {
await invoke("open_demo_execution_solana_core_window");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
frontendError("kb_app_demo.frontend.main", `Solana Devnet execution window opening failed: ${message}`);
}
}
async function openDemoExecutionSplWindow(): Promise<void> {
try {
await invoke("open_demo_execution_spl_window");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
frontendError("kb_app_demo.frontend.main", `SPL Devnet execution window opening failed: ${message}`);
}
}
document.addEventListener("DOMContentLoaded", async () => {
installFrontendConsoleBridge("kb_app_demo.frontend.main");
frontendDebug("kb_app_demo.frontend.main", "main window loaded");
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
const toastElList = document.querySelectorAll('.toast');
Array.from(toastElList).map(toastEl => new bootstrap.Toast(toastEl));
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]');
Array.from(popoverTriggerList).map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl));
const readmeContent = document.querySelector<HTMLElement>("#readmeContent");
if (readmeContent) {
await loadReadme(readmeContent);
}
const openDemoConfigLink = document.querySelector<HTMLAnchorElement>("#openDemoConfigLink");
if (openDemoConfigLink) {
openDemoConfigLink.addEventListener("click", event => {
event.preventDefault();
void openDemoConfigWindow();
});
}
const openDemoHttpLink = document.querySelector<HTMLAnchorElement>("#openDemoHttpLink");
if (openDemoHttpLink) {
openDemoHttpLink.addEventListener("click", event => {
event.preventDefault();
void openDemoHttpWindow();
});
}
const openDemoWsLink = document.querySelector<HTMLAnchorElement>("#openDemoWsLink");
if (openDemoWsLink) {
openDemoWsLink.addEventListener("click", event => {
event.preventDefault();
void openDemoWsWindow();
});
}
const openDemoBackfillLink = document.querySelector<HTMLAnchorElement>("#openDemoBackfillLink");
if (openDemoBackfillLink) {
openDemoBackfillLink.addEventListener("click", event => {
event.preventDefault();
void openDemoBackfillWindow();
});
}
const openDemoCoreExtractionLink = document.querySelector<HTMLAnchorElement>("#openDemoCoreExtractionLink");
if (openDemoCoreExtractionLink) {
openDemoCoreExtractionLink.addEventListener("click", event => {
event.preventDefault();
void openDemoCoreExtractionWindow();
});
}
const openDemoDecodeReplayLink = document.querySelector<HTMLAnchorElement>("#openDemoDecodeReplayLink");
if (openDemoDecodeReplayLink) {
openDemoDecodeReplayLink.addEventListener("click", event => {
event.preventDefault();
void openDemoDecodeReplayWindow();
});
}
const openDemoExecutionSolanaCoreLink = document.querySelector<HTMLAnchorElement>("#openDemoExecutionSolanaCoreLink");
if (openDemoExecutionSolanaCoreLink) {
openDemoExecutionSolanaCoreLink.addEventListener("click", event => {
event.preventDefault();
void openDemoExecutionSolanaCoreWindow();
});
}
const openDemoExecutionSplLink = document.querySelector<HTMLAnchorElement>("#openDemoExecutionSplLink");
if (openDemoExecutionSplLink) {
openDemoExecutionSplLink.addEventListener("click", event => {
event.preventDefault();
void openDemoExecutionSplWindow();
});
}
const openDemoSqlDiagLink = document.querySelector<HTMLAnchorElement>("#openDemoSqlDiagLink");
if (openDemoSqlDiagLink) {
openDemoSqlDiagLink.addEventListener("click", event => {
event.preventDefault();
void openDemoSqlDiagWindow();
});
}
const openDemoSqlPgRawLink = document.querySelector<HTMLAnchorElement>("#openDemoSqlPgRawLink");
if (openDemoSqlPgRawLink) {
openDemoSqlPgRawLink.addEventListener("click", event => {
event.preventDefault();
void openDemoSqlPgRawWindow();
});
}
const openDemoSqlPgCoreLink = document.querySelector<HTMLAnchorElement>("#openDemoSqlPgCoreLink");
if (openDemoSqlPgCoreLink) {
openDemoSqlPgCoreLink.addEventListener("click", event => {
event.preventDefault();
void openDemoSqlPgCoreWindow();
});
}
const openDemoSqlReplayCandidatesLink = document.querySelector<HTMLAnchorElement>("#openDemoSqlReplayCandidatesLink");
if (openDemoSqlReplayCandidatesLink) {
openDemoSqlReplayCandidatesLink.addEventListener("click", event => {
event.preventDefault();
void openDemoSqlReplayCandidatesWindow();
});
}
});

View File

@@ -0,0 +1,130 @@
// file: kb_app_demo/frontend/ts/splash.ts
// version: 4
//! Splash-screen event handling for the Tauri startup window.
import { listen } from "@tauri-apps/api/event";
import type { SplashOrder } from "./bindings/kb_app_demo/splash/SplashOrder";
import { frontendError, frontendDebug, installFrontendConsoleBridge } from "./frontend_log";
const splashTarget = "kb_app_demo.frontend.splash";
const defaultFadeDurationMs = 3000;
let activeOpacityFrame: number | null = null;
function normalizeDurationMs(value: number | null | undefined): number {
if (typeof value !== "number") {
return defaultFadeDurationMs;
}
if (!Number.isFinite(value) || value <= 0) {
return defaultFadeDurationMs;
}
return value;
}
function easeInOut(progress: number): number {
if (progress < 0.5) {
return 2 * progress * progress;
}
return 1 - Math.pow(-2 * progress + 2, 2) / 2;
}
async function animateOpacity(element: HTMLElement, fromOpacity: number, toOpacity: number, durationMs: number): Promise<void> {
frontendDebug(splashTarget, `Animating from ${fromOpacity} to ${toOpacity} over ${durationMs}ms`);
if (activeOpacityFrame !== null) {
cancelAnimationFrame(activeOpacityFrame);
activeOpacityFrame = null;
}
element.style.opacity = fromOpacity.toString();
element.style.willChange = "opacity";
await new Promise<void>(resolve => requestAnimationFrame(() => resolve()));
await new Promise<void>(resolve => {
const startedAt = performance.now();
const opacityDelta = toOpacity - fromOpacity;
const updateOpacity = (currentTime: number): void => {
const elapsedMs = currentTime - startedAt;
const rawProgress = Math.min(elapsedMs / durationMs, 1);
const easedProgress = easeInOut(rawProgress);
const nextOpacity = fromOpacity + opacityDelta * easedProgress;
element.style.opacity = nextOpacity.toString();
if (rawProgress >= 1) {
element.style.opacity = toOpacity.toString();
element.style.willChange = "auto";
activeOpacityFrame = null;
resolve();
return;
}
activeOpacityFrame = requestAnimationFrame(updateOpacity);
};
activeOpacityFrame = requestAnimationFrame(updateOpacity);
});
}
function addLogMessage(message: string): void {
const debugInfo = document.getElementById("debug-info");
if (!debugInfo) {
return;
}
const time = new Date().toLocaleTimeString();
debugInfo.innerHTML += `${time}: ${message}<br>`;
frontendDebug(splashTarget, `addLogMessage: ${message}`);
}
function addMessage(message: string, status: string): void {
const messagesContainer = document.getElementById("messages-container");
if (!messagesContainer) {
return;
}
const messageElement = document.createElement("div");
messageElement.className = `splash-message ${status}`;
messageElement.textContent = message;
messagesContainer.appendChild(messageElement);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
frontendDebug(splashTarget, `addMessage: ${message} - ${status}`);
}
async function handleSplashOrder(order: SplashOrder): Promise<void> {
const durationMs = normalizeDurationMs(order.duration_ms);
const container = document.getElementById("splash-container");
if (order.order === "fadein" && container) {
frontendDebug(splashTarget, `fadein`);
await animateOpacity(container, 0, 1, durationMs);
return;
}
if (order.order === "fadeout" && container) {
frontendDebug(splashTarget, `fadeout`);
await animateOpacity(container, 1, 0, durationMs);
return;
}
if (order.order === "add_msg" && order.msg && order.status) {
addMessage(order.msg, order.status);
return;
}
if (order.order === "add_log" && order.msg) {
addLogMessage(order.msg);
return;
}
frontendError(splashTarget, `unknown splash order: ${order.order}`);
}
function initializeSplashDom(): void {
const container = document.getElementById("splash-container");
if (container) {
container.style.opacity = "0";
container.style.willChange = "opacity";
}
installFrontendConsoleBridge(splashTarget);
}
async function initializeSplashListeners(): Promise<void> {
await listen<SplashOrder>("splash", event => {
void handleSplashOrder(event.payload);
});
}
document.addEventListener("DOMContentLoaded", () => {
initializeSplashDom();
void initializeSplashListeners();
frontendDebug(splashTarget, "splash window loaded");
});