v0.4.7-pre.009

This commit is contained in:
2026-08-02 17:53:36 +02:00
parent d6cb4b0cce
commit ebdfd30d5d
21 changed files with 657 additions and 46 deletions

View File

@@ -0,0 +1,104 @@
<!-- file: kb-app-demo-desktop/frontend/demo_execution_metadata.html -->
<!-- version: 3 -->
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<link rel="icon" href="imgs/logo.png">
<link rel="stylesheet" href="sass/main.scss">
<title>Khadhroony Bot3 — Exécution Metadata Devnet</title>
</head>
<body>
<header class="app-header">
<nav class="navbar h-100 py-0 bg-dark text-light">
<div class="container-fluid px-4">
<div class="navbar-brand d-flex align-items-center"><img alt="Logo" src="imgs/logo.png" class="app-logo"><span class="ps-2 fs-4 fw-bold text-primary font-logo">Bot3 — <span class="badge text-bg-primary">Exécution Metadata Devnet</span></span></div>
<span class="badge text-bg-success" id="metadataStatusBadge">Prêt</span>
</div>
</nav>
</header>
<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="row g-4">
<div class="col-12 col-xl-5">
<div class="accordion shadow-sm" id="metadataExecutionAccordion">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button" data-bs-target="#metaplexMetadataCollapse" data-bs-toggle="collapse" type="button">Metaplex Token Metadata</button>
</h2>
<div class="accordion-collapse collapse show" id="metaplexMetadataCollapse" data-bs-parent="#metadataExecutionAccordion">
<div class="accordion-body">
<div class="accordion" id="metaplexScenarioModeAccordion">
<div class="accordion-item">
<h3 class="accordion-header">
<button class="accordion-button" data-bs-target="#metaplexSyntheticCollapse" data-bs-toggle="collapse" type="button">Scénarios synthétiques</button>
</h3>
<div class="accordion-collapse collapse show" id="metaplexSyntheticCollapse" data-bs-parent="#metaplexScenarioModeAccordion">
<div class="accordion-body"><select class="form-select" id="metadataSyntheticScenarioSelect"></select></div>
</div>
</div>
<div class="accordion-item">
<h3 class="accordion-header">
<button class="accordion-button collapsed" data-bs-target="#metaplexDevnetCollapse" data-bs-toggle="collapse" type="button">Scénarios Devnet</button>
</h3>
<div class="accordion-collapse collapse" id="metaplexDevnetCollapse" data-bs-parent="#metaplexScenarioModeAccordion">
<div class="accordion-body">
<label class="form-label" for="metadataProfileSelect">Profil Devnet</label>
<select class="form-select mb-3" id="metadataProfileSelect"></select>
<label class="form-label" for="metadataDevnetScenarioSelect">Contrat de scénario</label>
<select class="form-select" id="metadataDevnetScenarioSelect"></select>
<div class="d-grid mt-3"><button class="btn btn-outline-primary" id="prepareMetadataProfileButton" 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="metadataProfileStatusBadge">Non chargé</span></div>
<div class="mt-3" id="metadataProfileOutput"></div>
<div class="alert alert-warning mt-3 mb-0">Cette version prépare réellement le profil et sa base Devnet, mais nexécute pas encore de transaction Metaplex. Les contrats affichés ne constituent pas une preuve de simulation réseau.</div>
</div>
</div>
</div>
</div>
<div class="d-grid mt-3">
<button class="btn btn-primary" id="refreshMetadataScenariosButton" type="button">Recharger les contrats</button>
</div>
</div>
</div>
</div>
</div>
<div class="card shadow-sm border-0 mt-4">
<div class="card-body">
<h2 class="h5">Exigences</h2>
<dl class="row mb-0" id="metadataRequirements"></dl>
</div>
</div>
</div>
<div class="col-12 col-xl-7">
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<h2 class="h5">Préflight et opérations</h2>
<div id="metadataPreflightOutput"></div>
</div>
</div>
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<h2 class="h5">Simulation et confirmation</h2>
<div id="metadataSimulationOutput"></div>
</div>
</div>
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h5">Postconditions et preuves</h2>
<div id="metadataPostconditionsOutput"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<script type="module" src="ts/demo_execution_metadata.ts"></script>
</body>
</html>

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/frontend/main.html -->
<!-- version: 9 -->
<!-- version: 11 -->
<!DOCTYPE html>
<html lang="fr">
@@ -61,6 +61,7 @@
</li>
<li><a id="openDemoExecutionSolanaCoreLink" class="dropdown-item" href="#">Solana Core</a></li>
<li><a id="openDemoExecutionSplLink" class="dropdown-item" href="#">SPL</a></li>
<li><a id="openDemoExecutionMetadataLink" class="dropdown-item" href="#">Exécution Metadata Devnet</a></li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,166 @@
// file: kb-app-demo-desktop/frontend/ts/demo_execution_metadata.ts
// version: 3
import "bootstrap";
import "simplebar";
import { invoke } from "@tauri-apps/api/core";
import { frontendError } from "./frontend_log.ts";
import { 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 { 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";
const storageKey = "kb-app-demo-desktop.metadata.selected-scenario";
const profileStorageKey = "kb-app-demo-desktop.metadata.selected-profile";
let scenarios: DemoExecutionMetadataScenarioPayload[] = [];
let selectedProfileName: string | null = null;
function element<T extends HTMLElement>(selector: string): T {
const value = document.querySelector<T>(selector);
if (value === null) {
throw new Error(`Élément introuvable : ${selector}`);
}
return value;
}
function activeSelect(): HTMLSelectElement {
return document.querySelector("#metaplexDevnetCollapse.show") !== null ? element<HTMLSelectElement>("#metadataDevnetScenarioSelect") : element<HTMLSelectElement>("#metadataSyntheticScenarioSelect");
}
function selectedScenario(): DemoExecutionMetadataScenarioPayload | null {
const id = activeSelect().value;
return scenarios.find(scenario => scenario.id === id) ?? null;
}
function fillSelect(selector: string, mode: string): void {
const select = element<HTMLSelectElement>(selector);
select.replaceChildren();
for (const scenario of scenarios.filter(candidate => candidate.mode === mode)) {
const option = document.createElement("option");
option.value = scenario.id;
option.textContent = `${scenario.assetFamily}${scenario.id}`;
select.append(option);
}
}
function renderSelectedScenario(): void {
const scenario = selectedScenario();
if (scenario === null) {
return;
}
window.localStorage.setItem(storageKey, scenario.id);
const networkMode = scenario.mode !== "synthetic";
element<HTMLElement>("#metadataRequirements").innerHTML = `<dt class="col-6">Famille</dt><dd class="col-6"><code>${scenario.assetFamily}</code></dd><dt class="col-6">Mode</dt><dd class="col-6"><code>${scenario.mode}</code></dd><dt class="col-6">Profil</dt><dd class="col-6"><code>${networkMode ? selectedProfileName ?? "non chargé" : "non applicable"}</code></dd><dt class="col-6">Collection</dt><dd class="col-6">${scenario.requiresCollection ? "requise" : "non requise"}</dd><dt class="col-6">Rule set</dt><dd class="col-6">${scenario.requiresProgrammableRules ? "requis" : "non requis"}</dd><dt class="col-6">Validation</dt><dd class="col-6"><code>${scenario.validationStatus}</code></dd>`;
renderJsonViewer("#metadataPreflightOutput", {
scenarioId: scenario.id,
operationCodes: scenario.operationCodes,
checks: ["program_id", "owner", "pda_and_seeds", "required_accounts", "authorities", "stateful_correlations"],
networkExecutionPerformed: false,
});
renderJsonViewer("#metadataSimulationOutput", {
mode: scenario.mode,
policy: "simulation_first",
networkExecutionPerformed: false,
automaticSubmission: false,
operatorConfirmationRequired: true,
expectedState: scenario.validationStatus,
note: networkMode ? "Le contrat Devnet est sélectionné, mais aucune transaction Metaplex n'est simulée par cette commande de chargement." : "Scénario synthétique déterministe.",
});
renderJsonViewer("#metadataPostconditionsOutput", {
required: scenario.requiresPostcondition,
evidenceKinds: scenario.requiredEvidence,
networkEvidenceCollected: false,
rule: "Aucune validation réseau n'est déclarée sans simulation RPC réelle, signature éventuelle, slot et postconditions.",
});
}
async function loadProfiles(): Promise<void> {
const options = await invoke<DemoExecutionSolanaCoreOptionsPayload>("demo_execution_solana_core_options");
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);
}
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;
renderSelectedScenario();
}
async function prepareProfile(): Promise<void> {
const badge = element<HTMLElement>("#metadataProfileStatusBadge");
const profileName = element<HTMLSelectElement>("#metadataProfileSelect").value;
if (profileName.length === 0) {
badge.textContent = "Aucun profil";
badge.className = "badge text-bg-danger";
return;
}
badge.textContent = "Préparation";
badge.className = "badge text-bg-warning";
try {
const readiness = await invoke<DemoExecutionDevnetStoreReadinessPayload>("demo_execution_devnet_prepare_profile", { profileName });
selectedProfileName = profileName;
window.localStorage.setItem(profileStorageKey, profileName);
renderJsonViewer("#metadataProfileOutput", readiness);
badge.textContent = "Base prête";
badge.className = "badge text-bg-success";
renderSelectedScenario();
} catch (caught) {
const message = caught instanceof Error ? caught.message : String(caught);
badge.textContent = "Erreur";
badge.className = "badge text-bg-danger";
frontendError("kb-app-demo-desktop.frontend.execution_metadata", message);
}
}
async function loadScenarios(): Promise<void> {
const badge = element<HTMLElement>("#metadataStatusBadge");
badge.textContent = "Chargement";
badge.className = "badge text-bg-warning";
try {
scenarios = await invoke<DemoExecutionMetadataScenarioPayload[]>("demo_execution_metadata_scenarios");
fillSelect("#metadataSyntheticScenarioSelect", "synthetic");
fillSelect("#metadataDevnetScenarioSelect", "network_simulation");
const restored = window.localStorage.getItem(storageKey);
for (const selector of ["#metadataSyntheticScenarioSelect", "#metadataDevnetScenarioSelect"]) {
const select = element<HTMLSelectElement>(selector);
if (restored !== null && Array.from(select.options).some(option => option.value === restored)) {
select.value = restored;
}
}
renderSelectedScenario();
badge.textContent = `${scenarios.length} contrats`;
badge.className = "badge text-bg-success";
} catch (caught) {
const message = caught instanceof Error ? caught.message : String(caught);
badge.textContent = "Erreur";
badge.className = "badge text-bg-danger";
frontendError("kb-app-demo-desktop.frontend.execution_metadata", message);
}
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge("kb-app-demo-desktop.frontend.execution_metadata");
for (const selector of ["#metadataSyntheticScenarioSelect", "#metadataDevnetScenarioSelect"]) {
element<HTMLSelectElement>(selector).addEventListener("change", renderSelectedScenario);
}
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;
renderSelectedScenario();
});
element<HTMLButtonElement>("#prepareMetadataProfileButton").addEventListener("click", () => void prepareProfile());
element<HTMLButtonElement>("#refreshMetadataScenariosButton").addEventListener("click", () => void loadScenarios());
void loadProfiles().catch(caught => frontendError("kb-app-demo-desktop.frontend.execution_metadata", caught instanceof Error ? caught.message : String(caught)));
void loadScenarios();
});

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/main.ts
// version: 4
// version: 6
import * as bootstrap from "bootstrap";
import MarkdownIt from "markdown-it";
@@ -80,6 +80,7 @@ const pipelineDemoLinks: ReadonlyArray<readonly [string, string]> = [
["openDemoDecodeReplayLink", "open_demo_decode_replay_window"],
["openDemoExecutionSolanaCoreLink", "open_demo_execution_solana_core_window"],
["openDemoExecutionSplLink", "open_demo_execution_spl_window"],
["openDemoExecutionMetadataLink", "open_demo_execution_metadata_window"],
];
for (const [elementId, command] of pipelineDemoLinks) {
const link = document.querySelector<HTMLAnchorElement>(`#${elementId}`);