v0.4.8-pre.010

This commit is contained in:
2026-08-06 17:26:14 +02:00
parent b314bfc244
commit 3167297283
40 changed files with 2556 additions and 1125 deletions

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/frontend/demo_execution_metadata.html -->
<!-- version: 9 -->
<!-- version: 10 -->
<!DOCTYPE html>
<html lang="fr">
@@ -23,20 +23,27 @@
<main class="app-main">
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
<div class="container-fluid px-4 py-4">
<div class="alert alert-info">Cette fenêtre accueille les familles dexécution metadata. Metaplex Token Metadata est la première famille ; SPL Token Metadata sera ajouté ultérieurement sans changer la frontière de la fenêtre.</div>
<div class="alert alert-info">Cette fenêtre sépare les familles metadata. Metaplex Token Metadata conserve ses scénarios propres ; Solana Program Metadata exécute les neuf opérations stables de <code>ProgM6…</code> via deux parcours Devnet. Token-2022 Token Metadata sera ajouté dans une étape ultérieure.</div>
<div class="accordion shadow-sm mb-4" id="metadataGuideAccordion">
<div class="accordion-item">
<h2 class="accordion-header"><button class="accordion-button collapsed" data-bs-target="#metadataGuideCollapse" data-bs-toggle="collapse" type="button">Guide dexécution Metaplex Devnet</button></h2>
<h2 class="accordion-header"><button class="accordion-button collapsed" data-bs-target="#metadataGuideCollapse" data-bs-toggle="collapse" type="button">Guide dexécution Metadata Devnet</button></h2>
<div class="accordion-collapse collapse" id="metadataGuideCollapse" data-bs-parent="#metadataGuideAccordion">
<div class="accordion-body">
<ol class="mb-2">
<h3 class="h6">Metaplex Token Metadata</h3>
<ol class="mb-3">
<li>Choisir un profil Devnet puis préparer sa base PostgreSQL.</li>
<li>Choisir le parcours Devnet puis son étape. Le parcours actuellement exécutable couvre <code>create</code> puis <code>update</code> sur un NFT classique.</li>
<li>Cliquer sur <strong>Préparer létape courante</strong> : lintent, les lectures stateful et les postconditions sont générés ensemble.</li>
<li>Lancer dabord une simulation. La soumission exige les deux confirmations explicites.</li>
<li>Contrôler le journal, le plan exact, la simulation RPC, la signature éventuelle et les états avant/après.</li>
<li>Choisir le parcours Devnet puis son étape.</li>
<li>Préparer létape afin de générer ensemble lintent, les lectures stateful et les postconditions.</li>
<li>Lancer dabord une simulation. La soumission exige une confirmation explicite.</li>
</ol>
<div class="small text-muted">Les chaînes entre chevrons sont des placeholders non exécutables. Ne jamais les soumettre telles quelles.</div>
<h3 class="h6">Solana Program Metadata</h3>
<ol class="mb-2">
<li>Choisir et préparer un profil Devnet persistant, autorisé à soumettre.</li>
<li>Contrôler les deux parcours et leurs neuf étapes dans linventaire.</li>
<li>Confirmer la campagne complète : deux transferts de préfinancement puis neuf transactions <code>ProgM6…</code>.</li>
<li>Contrôler les simulations, signatures, confirmations, lectures avant/après, postconditions et snapshots matérialisés.</li>
</ol>
<div class="small text-muted">La campagne Solana Program Metadata est indivisible : elle crée des PDA frais et arrête immédiatement le parcours au premier échec ou à la première preuve manquante.</div>
</div>
</div>
</div>
@@ -80,7 +87,7 @@
<button class="btn btn-outline-secondary flex-fill" id="nextMetadataStepButton" type="button">Étape suivante</button>
</div>
<div class="d-grid mt-2"><button class="btn btn-outline-success" id="prepareMetadataScenarioStepButton" type="button">Préparer létape courante</button></div>
<div class="form-text">Le scénario remplit ensemble lintent, les lectures avant/après et les cibles de matérialisation. Les étapes non encore préparables sont refusées explicitement.</div>
<div class="form-text">Le scénario remplit ensemble lintent, les lectures avant/après et les cibles de matérialisation.</div>
<label class="form-label mt-3" for="metadataOperationJson">Intent Metaplex typé</label>
<textarea class="form-control font-monospace" id="metadataOperationJson" rows="12" spellcheck="false"></textarea>
<label class="form-label mt-3" for="metadataPreflightReadsJson">Lectures stateful avant simulation</label>
@@ -106,11 +113,36 @@
</div>
</div>
<div class="d-grid mt-3">
<button class="btn btn-primary" id="refreshMetadataScenariosButton" type="button">Recharger les contrats</button>
<button class="btn btn-primary" id="refreshMetadataScenariosButton" type="button">Recharger les contrats Metaplex</button>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" data-bs-target="#solanaProgramMetadataCollapse" data-bs-toggle="collapse" type="button">Solana Program Metadata</button>
</h2>
<div class="accordion-collapse collapse" id="solanaProgramMetadataCollapse" data-bs-parent="#metadataExecutionAccordion">
<div class="accordion-body">
<label class="form-label" for="solanaProgramMetadataProfileSelect">Profil Devnet</label>
<select class="form-select" id="solanaProgramMetadataProfileSelect"></select>
<div class="form-text mb-3" id="solanaProgramMetadataProfileHelp">Chargement du profil Devnet.</div>
<div class="d-grid"><button class="btn btn-outline-primary" id="prepareSolanaProgramMetadataProfileButton" type="button">Charger le profil et préparer la base</button></div>
<div class="d-flex justify-content-between align-items-center mt-3"><span class="small text-muted">État du profil</span><span class="badge text-bg-secondary" id="solanaProgramMetadataProfileStatusBadge">Non chargé</span></div>
<div class="mt-3" id="solanaProgramMetadataProfileOutput"></div>
<label class="form-label mt-3" for="solanaProgramMetadataScenarioSelect">Parcours</label>
<select class="form-select" id="solanaProgramMetadataScenarioSelect"></select>
<div class="mt-3" id="solanaProgramMetadataScenarioOutput"></div>
<div class="alert alert-warning mt-3">Cette campagne soumet onze transactions : deux transferts de préfinancement et les neuf opérations stables. Elle utilise des PDA frais et ne peut pas reprendre au milieu dun parcours.</div>
<div class="form-check mt-3">
<input class="form-check-input" id="solanaProgramMetadataOperatorConfirmed" type="checkbox">
<label class="form-check-label" for="solanaProgramMetadataOperatorConfirmed">Je confirme le préfinancement et les neuf mutations Devnet</label>
</div>
<div class="d-grid mt-3"><button class="btn btn-danger" id="executeSolanaProgramMetadataCampaignButton" type="button">Exécuter la campagne complète</button></div>
<div class="form-text">Le runner arrête la campagne dès quune simulation, une confirmation, une postcondition ou une matérialisation requise échoue.</div>
</div>
</div>
</div>
</div>
<div class="card shadow-sm border-0 mt-4">
<div class="card-body">
@@ -152,4 +184,4 @@
<script type="module" src="ts/demo_execution_metadata.ts"></script>
</body>
</html>
</html>

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/demo_execution_metadata.ts
// version: 10
// version: 12
import * as bootstrap from "bootstrap";
import "simplebar";
@@ -7,18 +7,24 @@ import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log.ts";
import { renderJsonViewer } from "./json_viewer.ts";
import type { DemoExecutionMetadataScenarioPayload } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataScenarioPayload.ts";
import type { DemoExecutionMetadataPreparedStepPayload } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataPreparedStepPayload.ts";
import type { DemoExecutionMetadataRequest } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataRequest.ts";
import type { DemoExecutionMetadataSummaryPayload } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataSummaryPayload.ts";
import type { DemoExecutionMetadataMetaplexTokenMetadataScenarioPayload } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataMetaplexTokenMetadataScenarioPayload.ts";
import type { DemoExecutionMetadataProgressPayload } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataProgressPayload.ts";
import type { DemoExecutionMetadataMetaplexTokenMetadataPreparedStepPayload } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataMetaplexTokenMetadataPreparedStepPayload.ts";
import type { DemoExecutionMetadataMetaplexTokenMetadataRequest } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataMetaplexTokenMetadataRequest.ts";
import type { DemoExecutionMetadataMetaplexTokenMetadataSummaryPayload } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataMetaplexTokenMetadataSummaryPayload.ts";
import type { DemoExecutionMetadataSolanaProgramCampaignRequest } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataSolanaProgramCampaignRequest.ts";
import type { DemoExecutionMetadataSolanaProgramCampaignSummaryPayload } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataSolanaProgramCampaignSummaryPayload.ts";
import type { DemoExecutionMetadataSolanaProgramScenarioPayload } from "./bindings/kb_app_demo_desktop/demo_execution_metadata/DemoExecutionMetadataSolanaProgramScenarioPayload.ts";
import type { DemoExecutionDevnetStoreReadinessPayload } from "./bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionDevnetStoreReadinessPayload.ts";
import type { DemoExecutionSolanaCoreOptionsPayload } from "./bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionSolanaCoreOptionsPayload.ts";
import type { DemoExecutionSolanaCoreProfileOption } from "./bindings/kb_app_demo_desktop/demo_execution_solana_core/DemoExecutionSolanaCoreProfileOption.ts";
const storageKey = "kb-app-demo-desktop.metadata.selected-scenario";
const solanaProgramMetadataStorageKey = "kb-app-demo-desktop.metadata.solana-program.selected-scenario";
const profileStorageKey = "kb-app-demo-desktop.metadata.selected-profile";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
let scenarios: DemoExecutionMetadataScenarioPayload[] = [];
let scenarios: DemoExecutionMetadataMetaplexTokenMetadataScenarioPayload[] = [];
let solanaProgramMetadataScenarios: DemoExecutionMetadataSolanaProgramScenarioPayload[] = [];
let selectedProfileName: string | null = null;
let executionRunning = false;
let profileStoreReady = false;
@@ -28,9 +34,9 @@ let preparedStepIndex: number | null = null;
const metadataLogLines: string[] = [];
const maximumMetadataLogLines = 1200;
type MetadataProgressPayload = { timestamp: string; level: string; stage: string; message: string; signature?: string | null };
type MetadataProgressInput = Omit<DemoExecutionMetadataProgressPayload, "signature"> & { signature?: string | null };
function appendMetadataLog(payload: MetadataProgressPayload): void {
function appendMetadataLog(payload: MetadataProgressInput): void {
const signature = payload.signature ? ` signature=${payload.signature}` : "";
metadataLogLines.push(`${payload.timestamp} ${payload.level.toUpperCase()} [${payload.stage}] ${payload.message}${signature}`);
if (metadataLogLines.length > maximumMetadataLogLines) {
@@ -51,11 +57,34 @@ function element<T extends HTMLElement>(selector: string): T {
return value;
}
const profileSelectSelectors = ["#metadataProfileSelect", "#solanaProgramMetadataProfileSelect"] as const;
const profileHelpSelectors = ["#metadataProfileHelp", "#solanaProgramMetadataProfileHelp"] as const;
const profileStatusSelectors = ["#metadataProfileStatusBadge", "#solanaProgramMetadataProfileStatusBadge"] as const;
const profileOutputSelectors = ["#metadataProfileOutput", "#solanaProgramMetadataProfileOutput"] as const;
function synchronizeProfileSelections(profileName: string): void {
for (const selector of profileSelectSelectors) {
const select = element<HTMLSelectElement>(selector);
if (Array.from(select.options).some(option => option.value === profileName)) {
select.value = profileName;
}
}
selectedProfileName = profileName.length === 0 ? null : profileName;
}
function updateProfileStatus(text: string, className: string): void {
for (const selector of profileStatusSelectors) {
const badge = element<HTMLElement>(selector);
badge.textContent = text;
badge.className = className;
}
}
function activeSelect(): HTMLSelectElement {
return document.querySelector("#metaplexDevnetCollapse.show") !== null ? element<HTMLSelectElement>("#metadataDevnetScenarioSelect") : element<HTMLSelectElement>("#metadataSyntheticScenarioSelect");
}
function selectedScenario(): DemoExecutionMetadataScenarioPayload | null {
function selectedScenario(): DemoExecutionMetadataMetaplexTokenMetadataScenarioPayload | null {
const id = activeSelect().value;
return scenarios.find(scenario => scenario.id === id) ?? null;
}
@@ -76,7 +105,7 @@ function operationName(operationCode: string): string {
return parts.length === 0 ? operationCode : parts[parts.length - 1] ?? operationCode;
}
function renderScenarioSteps(scenario: DemoExecutionMetadataScenarioPayload): void {
function renderScenarioSteps(scenario: DemoExecutionMetadataMetaplexTokenMetadataScenarioPayload): void {
const select = element<HTMLSelectElement>("#metadataStepSelect");
const previousValue = Number.parseInt(select.value, 10);
select.replaceChildren();
@@ -145,7 +174,7 @@ async function prepareScenarioStep(): Promise<void> {
if (scenario === null || scenario.mode === "synthetic") { throw new Error("Sélectionnez un scénario Devnet."); }
if (!Number.isInteger(stepIndex) || stepIndex < 0) { throw new Error("Sélectionnez une étape valide."); }
appendMetadataLog({ timestamp: new Date().toISOString(), level: "info", stage: "fixture", message: `Préparation de ${scenario.id} étape ${stepIndex + 1}` });
const prepared = await invoke<DemoExecutionMetadataPreparedStepPayload>("demo_execution_metadata_prepare_step", {
const prepared = await invoke<DemoExecutionMetadataMetaplexTokenMetadataPreparedStepPayload>("demo_execution_metadata_metaplex_token_metadata_prepare_step", {
profileName,
scenarioId: scenario.id,
stepIndex,
@@ -193,18 +222,20 @@ function parseJsonText(value: string, label: string): unknown {
}
function selectedProfile(): DemoExecutionSolanaCoreProfileOption | null {
const name = element<HTMLSelectElement>("#metadataProfileSelect").value;
return profileOptions.find(profile => profile.name === name) ?? null;
if (selectedProfileName === null) {
return null;
}
return profileOptions.find(profile => profile.name === selectedProfileName) ?? null;
}
function updateProfileHelp(): void {
const profile = selectedProfile();
const help = element<HTMLElement>("#metadataProfileHelp");
if (profile === null) {
help.textContent = "Aucun profil Devnet compatible.";
return;
const text = profile === null
? "Aucun profil Devnet compatible."
: `wallet=${profile.walletAlias}, spendMax=${profile.maxSpendLamports}, airdropMax=${profile.maxAirdropLamports}, send=${profile.sendEnabled ? "enabled" : "disabled"}`;
for (const selector of profileHelpSelectors) {
element<HTMLElement>(selector).textContent = text;
}
help.textContent = `wallet=${profile.walletAlias}, spendMax=${profile.maxSpendLamports}, airdropMax=${profile.maxAirdropLamports}, send=${profile.sendEnabled ? "enabled" : "disabled"}`;
}
function setExecutionRunning(value: boolean): void {
@@ -215,6 +246,8 @@ function setExecutionRunning(value: boolean): void {
element<HTMLButtonElement>("#simulateMetadataButton").disabled = unavailable;
element<HTMLButtonElement>("#submitMetadataButton").disabled = unavailable || !profile?.sendEnabled;
element<HTMLButtonElement>("#prepareMetadataScenarioStepButton").disabled = value || profile === null || !profileStoreReady;
const solanaProgramConfirmed = element<HTMLInputElement>("#solanaProgramMetadataOperatorConfirmed").checked;
element<HTMLButtonElement>("#executeSolanaProgramMetadataCampaignButton").disabled = value || profile === null || !profileStoreReady || !profile.sendEnabled || !solanaProgramConfirmed;
}
async function executeMetadata(submit: boolean): Promise<void> {
@@ -243,7 +276,7 @@ async function executeMetadata(submit: boolean): Promise<void> {
if (submit && !operatorConfirmed) {
throw new Error("La soumission exige la confirmation explicite de lopérateur.");
}
const request: DemoExecutionMetadataRequest = {
const request: DemoExecutionMetadataMetaplexTokenMetadataRequest = {
profileName,
intentId: `desktop-metaplex-${Date.now()}`,
operationJson,
@@ -259,7 +292,7 @@ async function executeMetadata(submit: boolean): Promise<void> {
badge.className = "badge text-bg-warning";
appendMetadataLog({ timestamp: new Date().toISOString(), level: "info", stage: submit ? "submit" : "simulation", message: submit ? "Démarrage du parcours Devnet complet." : "Démarrage de la simulation RPC exacte." });
try {
const summary = await invoke<DemoExecutionMetadataSummaryPayload>("demo_execution_metadata_execute", { request });
const summary = await invoke<DemoExecutionMetadataMetaplexTokenMetadataSummaryPayload>("demo_execution_metadata_metaplex_token_metadata_execute", { request });
renderJsonViewer("#metadataPreflightOutput", titled("Préflight et opérations", JSON.parse(summary.preflightJson)));
renderJsonViewer("#metadataSimulationOutput", titled("Simulation et confirmation", {
profileName: summary.profileName,
@@ -283,65 +316,160 @@ async function executeMetadata(submit: boolean): Promise<void> {
}
}
function selectedSolanaProgramMetadataScenario(): DemoExecutionMetadataSolanaProgramScenarioPayload | null {
const id = element<HTMLSelectElement>("#solanaProgramMetadataScenarioSelect").value;
return solanaProgramMetadataScenarios.find(scenario => scenario.id === id) ?? null;
}
function renderActiveMetadataSelection(): void {
if (document.querySelector("#solanaProgramMetadataCollapse.show") !== null) {
renderSolanaProgramMetadataScenario();
return;
}
renderSelectedScenario();
}
function renderSolanaProgramMetadataScenario(): void {
const scenario = selectedSolanaProgramMetadataScenario();
if (scenario === null) {
return;
}
window.localStorage.setItem(solanaProgramMetadataStorageKey, scenario.id);
renderJsonViewer("#solanaProgramMetadataScenarioOutput", titled("Parcours Solana Program Metadata", {
scenarioId: scenario.id,
label: scenario.label,
fixtureAccount: scenario.fixtureAccount,
steps: scenario.steps,
}));
const allSteps = solanaProgramMetadataScenarios.flatMap(candidate => candidate.steps);
element<HTMLElement>("#metadataRequirements").innerHTML = `<dt class="col-6">Famille</dt><dd class="col-6"><code>metadata.solana_program_metadata</code></dd><dt class="col-6">Program ID</dt><dd class="col-6"><code>ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S</code></dd><dt class="col-6">Parcours affiché</dt><dd class="col-6">${scenario.label}</dd><dt class="col-6">Étapes totales</dt><dd class="col-6">${allSteps.length}</dd><dt class="col-6">Transactions</dt><dd class="col-6">11 minimum</dd><dt class="col-6">Profil</dt><dd class="col-6"><code>${selectedProfileName ?? "non chargé"}</code></dd>`;
renderJsonViewer("#metadataPreflightOutput", titled("Préflight et opérations", {
family: "metadata.solana_program_metadata",
journeys: solanaProgramMetadataScenarios,
operationCount: allSteps.length,
fixtureAccounts: ["buffer", "metadata"],
prefundingTransactionCount: 2,
}));
renderJsonViewer("#metadataSimulationOutput", titled("Simulation et confirmation", {
policy: "simulation_first",
submission: "required",
operatorConfirmationRequired: true,
stopOnFirstFailure: true,
networkExecutionPerformed: false,
}));
renderJsonViewer("#metadataPostconditionsOutput", titled("Postconditions et preuves", {
requiredEvidence: ["stateful_preflight", "rpc_simulation", "confirmed_signature", "stateful_postcondition", "materialized_snapshot_or_account_absence"],
currentStatus: "not_run",
networkEvidenceCollected: false,
}));
}
async function executeSolanaProgramMetadataCampaign(): Promise<void> {
if (executionRunning) {
return;
}
const profile = selectedProfile();
if (profile === null || selectedProfileName === null || !profileStoreReady) {
throw new Error("Sélectionnez et préparez un profil Devnet.");
}
if (!profile.sendEnabled) {
throw new Error("Le profil sélectionné nautorise pas les soumissions Devnet.");
}
const operatorConfirmed = element<HTMLInputElement>("#solanaProgramMetadataOperatorConfirmed").checked;
if (!operatorConfirmed) {
throw new Error("La campagne exige la confirmation explicite du préfinancement et des neuf mutations.");
}
const request: DemoExecutionMetadataSolanaProgramCampaignRequest = {
profileName: selectedProfileName,
operatorConfirmed,
};
setExecutionRunning(true);
const badge = element<HTMLElement>("#metadataStatusBadge");
badge.textContent = "Campagne ProgM6";
badge.className = "badge text-bg-warning";
appendMetadataLog({ timestamp: new Date().toISOString(), level: "info", stage: "spm_campaign", message: "Démarrage de la campagne complète : 2 préfinancements et 9 opérations." });
try {
const summary = await invoke<DemoExecutionMetadataSolanaProgramCampaignSummaryPayload>("demo_execution_metadata_solana_program_execute_campaign", { request });
renderJsonViewer("#metadataPreflightOutput", titled("Fixture et opérations préparées", JSON.parse(summary.fixtureJson)));
renderJsonViewer("#metadataSimulationOutput", titled("Synthèse de campagne", {
profileName: summary.profileName,
authority: summary.authority,
describedProgram: summary.describedProgram,
buffer: summary.buffer,
bufferPrefundSignature: summary.bufferPrefundSignature,
metadata: summary.metadata,
metadataPrefundSignature: summary.metadataPrefundSignature,
stepCount: summary.stepCount,
confirmedStepCount: summary.confirmedStepCount,
materializedSnapshotCount: summary.materializedSnapshotCount,
completed: summary.completed,
}));
renderJsonViewer("#metadataPostconditionsOutput", titled("Preuves ordonnées des neuf opérations", JSON.parse(summary.stepsJson)));
element<HTMLElement>("#metadataRequirements").innerHTML = `<dt class="col-6">Famille</dt><dd class="col-6"><code>metadata.solana_program_metadata</code></dd><dt class="col-6">Profil</dt><dd class="col-6"><code>${summary.profileName}</code></dd><dt class="col-6">Buffer</dt><dd class="col-6"><code>${summary.buffer}</code></dd><dt class="col-6">Metadata</dt><dd class="col-6"><code>${summary.metadata}</code></dd><dt class="col-6">Confirmées</dt><dd class="col-6">${summary.confirmedStepCount}/${summary.stepCount}</dd><dt class="col-6">Snapshots</dt><dd class="col-6">${summary.materializedSnapshotCount}</dd>`;
badge.textContent = summary.completed ? "Campagne confirmée" : "Campagne incomplète";
badge.className = summary.completed ? "badge text-bg-success" : "badge text-bg-danger";
appendMetadataLog({ timestamp: new Date().toISOString(), level: summary.completed ? "info" : "error", stage: "spm_campaign", message: `Campagne terminée : confirmed=${summary.confirmedStepCount}/${summary.stepCount}, snapshots=${summary.materializedSnapshotCount}` });
} finally {
setExecutionRunning(false);
}
}
async function loadProfiles(): Promise<void> {
const options = await invoke<DemoExecutionSolanaCoreOptionsPayload>("demo_execution_metadata_options");
profileOptions = options.profiles;
const select = element<HTMLSelectElement>("#metadataProfileSelect");
select.replaceChildren();
for (const profile of options.profiles) {
const option = document.createElement("option");
option.value = profile.name;
option.textContent = `${profile.name}${profile.walletAlias}`;
select.append(option);
for (const selector of profileSelectSelectors) {
const select = element<HTMLSelectElement>(selector);
select.replaceChildren();
for (const profile of options.profiles) {
const option = document.createElement("option");
option.value = profile.name;
option.textContent = `${profile.name}${profile.walletAlias}`;
select.append(option);
}
}
const restored = window.localStorage.getItem(profileStorageKey);
if (restored !== null && Array.from(select.options).some(option => option.value === restored)) {
select.value = restored;
} else if (options.defaultProfileName !== null) {
select.value = options.defaultProfileName;
}
selectedProfileName = select.value.length === 0 ? null : select.value;
const availableNames = options.profiles.map(profile => profile.name);
const selected = restored !== null && availableNames.includes(restored)
? restored
: options.defaultProfileName ?? availableNames[0] ?? "";
synchronizeProfileSelections(selected);
updateProfileHelp();
if (options.profiles.length === 0) {
const message = "Aucun profil Devnet compatible na été retourné par la configuration active.";
appendMetadataLog({ timestamp: new Date().toISOString(), level: "error", stage: "profile_options", message });
element<HTMLElement>("#metadataProfileStatusBadge").textContent = "Aucun profil";
element<HTMLElement>("#metadataProfileStatusBadge").className = "badge text-bg-danger";
renderSelectedScenario();
updateProfileStatus("Aucun profil", "badge text-bg-danger");
renderActiveMetadataSelection();
return;
}
appendMetadataLog({ timestamp: new Date().toISOString(), level: "info", stage: "profile_options", message: `${options.profiles.length} profil(s) Devnet chargé(s).` });
renderSelectedScenario();
renderActiveMetadataSelection();
await prepareProfile();
}
async function prepareProfile(): Promise<void> {
const badge = element<HTMLElement>("#metadataProfileStatusBadge");
const profileName = element<HTMLSelectElement>("#metadataProfileSelect").value;
const profileName = selectedProfileName ?? "";
if (profileName.length === 0) {
badge.textContent = "Aucun profil";
badge.className = "badge text-bg-danger";
updateProfileStatus("Aucun profil", "badge text-bg-danger");
return;
}
profileStoreReady = false;
setExecutionRunning(executionRunning);
badge.textContent = "Préparation";
badge.className = "badge text-bg-warning";
updateProfileStatus("Préparation", "badge text-bg-warning");
try {
const readiness = await invoke<DemoExecutionDevnetStoreReadinessPayload>("demo_execution_devnet_prepare_profile", { profileName });
selectedProfileName = profileName;
synchronizeProfileSelections(profileName);
window.localStorage.setItem(profileStorageKey, profileName);
renderJsonViewer("#metadataProfileOutput", readiness);
for (const selector of profileOutputSelectors) {
renderJsonViewer(selector, readiness);
}
profileStoreReady = true;
badge.textContent = "Base prête";
badge.className = "badge text-bg-success";
updateProfileStatus("Base prête", "badge text-bg-success");
setExecutionRunning(executionRunning);
renderSelectedScenario();
renderActiveMetadataSelection();
} catch (caught) {
const message = caught instanceof Error ? caught.message : String(caught);
profileStoreReady = false;
badge.textContent = "Erreur";
badge.className = "badge text-bg-danger";
updateProfileStatus("Erreur", "badge text-bg-danger");
setExecutionRunning(executionRunning);
frontendError("kb-app-demo-desktop.frontend.execution_metadata", message);
}
@@ -352,7 +480,8 @@ async function loadScenarios(): Promise<void> {
badge.textContent = "Chargement";
badge.className = "badge text-bg-warning";
try {
scenarios = await invoke<DemoExecutionMetadataScenarioPayload[]>("demo_execution_metadata_scenarios");
scenarios = await invoke<DemoExecutionMetadataMetaplexTokenMetadataScenarioPayload[]>("demo_execution_metadata_metaplex_token_metadata_scenarios");
solanaProgramMetadataScenarios = await invoke<DemoExecutionMetadataSolanaProgramScenarioPayload[]>("demo_execution_metadata_solana_program_scenarios");
fillSelect("#metadataSyntheticScenarioSelect", "synthetic");
fillSelect("#metadataDevnetScenarioSelect", "network_simulation");
const restored = window.localStorage.getItem(storageKey);
@@ -362,8 +491,20 @@ async function loadScenarios(): Promise<void> {
select.value = restored;
}
}
renderSelectedScenario();
badge.textContent = `${scenarios.length} contrats`;
const solanaSelect = element<HTMLSelectElement>("#solanaProgramMetadataScenarioSelect");
solanaSelect.replaceChildren();
for (const scenario of solanaProgramMetadataScenarios) {
const option = document.createElement("option");
option.value = scenario.id;
option.textContent = `${scenario.fixtureAccount}${scenario.label}`;
solanaSelect.append(option);
}
const restoredSolana = window.localStorage.getItem(solanaProgramMetadataStorageKey);
if (restoredSolana !== null && Array.from(solanaSelect.options).some(option => option.value === restoredSolana)) {
solanaSelect.value = restoredSolana;
}
renderActiveMetadataSelection();
badge.textContent = `${scenarios.length + solanaProgramMetadataScenarios.length} contrats`;
badge.className = "badge text-bg-success";
} catch (caught) {
const message = caught instanceof Error ? caught.message : String(caught);
@@ -383,16 +524,21 @@ document.addEventListener("DOMContentLoaded", () => {
for (const id of ["metaplexSyntheticCollapse", "metaplexDevnetCollapse"]) {
element<HTMLElement>(`#${id}`).addEventListener("shown.bs.collapse", renderSelectedScenario);
}
element<HTMLSelectElement>("#metadataProfileSelect").addEventListener("change", event => {
const target = event.currentTarget as HTMLSelectElement;
selectedProfileName = target.value.length === 0 ? null : target.value;
profileStoreReady = false;
updateProfileHelp();
setExecutionRunning(executionRunning);
renderSelectedScenario();
void prepareProfile();
});
element<HTMLElement>("#solanaProgramMetadataCollapse").addEventListener("shown.bs.collapse", renderSolanaProgramMetadataScenario);
element<HTMLSelectElement>("#solanaProgramMetadataScenarioSelect").addEventListener("change", renderSolanaProgramMetadataScenario);
for (const selector of profileSelectSelectors) {
element<HTMLSelectElement>(selector).addEventListener("change", event => {
const target = event.currentTarget as HTMLSelectElement;
synchronizeProfileSelections(target.value);
profileStoreReady = false;
updateProfileHelp();
setExecutionRunning(executionRunning);
renderActiveMetadataSelection();
void prepareProfile();
});
}
element<HTMLButtonElement>("#prepareMetadataProfileButton").addEventListener("click", () => void prepareProfile());
element<HTMLButtonElement>("#prepareSolanaProgramMetadataProfileButton").addEventListener("click", () => void prepareProfile());
element<HTMLButtonElement>("#refreshMetadataScenariosButton").addEventListener("click", () => void loadScenarios());
element<HTMLSelectElement>("#metadataStepSelect").addEventListener("change", () => {
const scenario = selectedScenario();
@@ -407,13 +553,17 @@ document.addEventListener("DOMContentLoaded", () => {
});
element<HTMLButtonElement>("#prepareMetadataScenarioStepButton").addEventListener("click", () => { void prepareScenarioStep().catch(caught => frontendError("kb-app-demo-desktop.frontend.execution_metadata", caught instanceof Error ? caught.message : String(caught))); });
element<HTMLTextAreaElement>("#metadataExecutionLogOutput").addEventListener("app-log-clear", () => { metadataLogLines.length = 0; });
void listen<MetadataProgressPayload>("demo-execution-solana-core-progress", event => appendMetadataLog(event.payload));
void listen<DemoExecutionMetadataProgressPayload>("demo-execution-metadata-progress", event => appendMetadataLog(event.payload));
element<HTMLButtonElement>("#simulateMetadataButton").addEventListener("click", () => {
void executeMetadata(false).catch(caught => frontendError("kb-app-demo-desktop.frontend.execution_metadata", caught instanceof Error ? caught.message : String(caught)));
});
element<HTMLButtonElement>("#submitMetadataButton").addEventListener("click", () => {
void executeMetadata(true).catch(caught => frontendError("kb-app-demo-desktop.frontend.execution_metadata", caught instanceof Error ? caught.message : String(caught)));
});
element<HTMLInputElement>("#solanaProgramMetadataOperatorConfirmed").addEventListener("change", () => setExecutionRunning(executionRunning));
element<HTMLButtonElement>("#executeSolanaProgramMetadataCampaignButton").addEventListener("click", () => {
void executeSolanaProgramMetadataCampaign().catch(caught => frontendError("kb-app-demo-desktop.frontend.execution_metadata", caught instanceof Error ? caught.message : String(caught)));
});
setExecutionRunning(false);
frontendDebug("kb-app-demo-desktop.frontend.execution_metadata", "Metadata execution window loaded");
void loadProfiles().catch(caught => frontendError("kb-app-demo-desktop.frontend.execution_metadata", caught instanceof Error ? caught.message : String(caught)));