0.1.0
This commit is contained in:
@@ -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 "L’adresse est obligatoire pour ce mode de backfill.";
|
||||
}
|
||||
if (!decodesToSolanaAddress(request.address.trim())) {
|
||||
return "L’adresse 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 d’ancrage 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}`);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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}`);
|
||||
});
|
||||
});
|
||||
@@ -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 l’autorisation Toutes les signatures.";
|
||||
}
|
||||
if (request.forceReplayAllMatching && signatureCount > 0) {
|
||||
return "Le mode « Toutes les signatures » ne peut pas être combiné avec une liste de signatures explicites.";
|
||||
}
|
||||
if (request.forceReplayAllMatching && !request.forceReplay) {
|
||||
return "Le mode « Toutes les signatures » est uniquement disponible avec Force replay.";
|
||||
}
|
||||
if (request.materializeAfterDecode && !materializationAvailable) {
|
||||
return "Aucun matérialiseur n’est actuellement enregistré dans cette application.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function reportValidationWarning(message: string): void {
|
||||
element<HTMLTextAreaElement>("#decodeReplaySummaryOutput").value = JSON.stringify({ validation: message }, null, 2);
|
||||
appendLog({ timestamp: new Date().toISOString(), level: "warn", message });
|
||||
frontendDebug("kb_app_demo.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}`);
|
||||
});
|
||||
});
|
||||
@@ -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}`);}});
|
||||
@@ -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", "L’autorité"),
|
||||
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("L’autorité 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 d’annulation 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}`);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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("&")
|
||||
.split("<").join("<")
|
||||
.split(">").join(">")
|
||||
.split('"').join(""")
|
||||
.split("'").join("'");
|
||||
}
|
||||
|
||||
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 d’entité et sa valeur doivent être renseignés ensemble.");
|
||||
}
|
||||
return {
|
||||
signatureContains: optionalInputValue("#transactionSignatureContainsInput"),
|
||||
minSlot: optionalNonNegativeIntegerValue("#transactionMinSlotInput"),
|
||||
maxSlot: optionalNonNegativeIntegerValue("#transactionMaxSlotInput"),
|
||||
rawProcessingState: optionalInputValue("#transactionRawStateSelect"),
|
||||
ledgerStatus: optionalInputValue("#transactionLedgerStatusSelect"),
|
||||
programId: optionalInputValue("#transactionProgramIdInput"),
|
||||
programScope: requiredInputValue("#transactionProgramScopeSelect"),
|
||||
entityKind,
|
||||
entityValue,
|
||||
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));
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user