v0.4.7-pre.013

This commit is contained in:
2026-08-04 17:14:01 +02:00
parent 4d08c75b2b
commit 22228149a3
26 changed files with 2367 additions and 223 deletions

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/frontend/demo_execution_metadata.html -->
<!-- version: 3 -->
<!-- version: 9 -->
<!DOCTYPE html>
<html lang="fr">
@@ -24,6 +24,23 @@
<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="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>
<div class="accordion-collapse collapse" id="metadataGuideCollapse" data-bs-parent="#metadataGuideAccordion">
<div class="accordion-body">
<ol class="mb-2">
<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>
</ol>
<div class="small text-muted">Les chaînes entre chevrons sont des placeholders non exécutables. Ne jamais les soumettre telles quelles.</div>
</div>
</div>
</div>
</div>
<div class="row g-4">
<div class="col-12 col-xl-5">
<div class="accordion shadow-sm" id="metadataExecutionAccordion">
@@ -49,13 +66,41 @@
<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>
<select class="form-select" id="metadataProfileSelect"></select>
<div class="form-text mb-3" id="metadataProfileHelp">Chargement du profil Devnet.</div>
<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>
<label class="form-label mt-3" for="metadataStepSelect">Étape du parcours</label>
<select class="form-select" id="metadataStepSelect"></select>
<div class="d-flex gap-2 mt-2">
<button class="btn btn-outline-secondary flex-fill" id="previousMetadataStepButton" type="button">Étape précédente</button>
<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>
<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>
<textarea class="form-control font-monospace" id="metadataPreflightReadsJson" rows="4" spellcheck="false">[]</textarea>
<label class="form-label mt-3" for="metadataPostconditionReadsJson">Lectures stateful après confirmation</label>
<textarea class="form-control font-monospace" id="metadataPostconditionReadsJson" rows="4" spellcheck="false">[]</textarea>
<div class="form-check mt-3">
<input class="form-check-input" id="metadataMaterializeInput" type="checkbox" checked>
<label class="form-check-label" for="metadataMaterializeInput">Matérialiser les snapshots Metaplex confirmés</label>
<div class="form-text">Après confirmation, relit les comptes déclarés, les décode canoniquement et produit les projections détat. Aucun fetch URI off-chain.</div>
</div>
<div class="form-check mt-3">
<input class="form-check-input" id="metadataOperatorConfirmed" type="checkbox">
<label class="form-check-label" for="metadataOperatorConfirmed">Je confirme explicitement la soumission</label>
</div>
<div class="d-grid gap-2 mt-3">
<button class="btn btn-outline-primary" id="simulateMetadataButton" type="button">Simuler sur Devnet</button>
<button class="btn btn-primary" id="submitMetadataButton" type="button">Soumettre sur Devnet</button>
</div>
<div class="alert alert-warning mt-3 mb-0">La simulation appelle réellement le RPC Devnet. La soumission reste désactivée tant que lopérateur ne lautorise et ne la confirme pas explicitement.</div>
</div>
</div>
</div>
@@ -67,6 +112,12 @@
</div>
</div>
</div>
<div class="card shadow-sm border-0 mt-4">
<div class="card-body">
<h2 class="h5 mb-2">Journal dexécution</h2>
<textarea class="form-control font-monospace" id="metadataExecutionLogOutput" readonly rows="12" spellcheck="false"></textarea>
</div>
</div>
<div class="card shadow-sm border-0 mt-4">
<div class="card-body">
<h2 class="h5">Exigences</h2>
@@ -78,19 +129,19 @@
<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>
<textarea class="form-control font-monospace" data-json-viewer="true" id="metadataPreflightOutput" readonly rows="14">{"title":"Préflight et opérations","status":"idle","message":"Aucun résultat."}</textarea>
</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>
<textarea class="form-control font-monospace" data-json-viewer="true" id="metadataSimulationOutput" readonly rows="16">{"title":"Simulation et confirmation","status":"idle","message":"Aucune simulation."}</textarea>
</div>
</div>
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h5">Postconditions et preuves</h2>
<div id="metadataPostconditionsOutput"></div>
<textarea class="form-control font-monospace" data-json-viewer="true" id="metadataPostconditionsOutput" readonly rows="16">{"title":"Postconditions et preuves","status":"idle","message":"Aucune preuve."}</textarea>
</div>
</div>
</div>

View File

@@ -1,19 +1,47 @@
// file: kb-app-demo-desktop/frontend/ts/demo_execution_metadata.ts
// version: 3
import "bootstrap";
// version: 10
import * as bootstrap from "bootstrap";
import "simplebar";
import { invoke } from "@tauri-apps/api/core";
import { frontendError } from "./frontend_log.ts";
import { installFrontendConsoleBridge } from "./frontend_log.ts";
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 { 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 profileStorageKey = "kb-app-demo-desktop.metadata.selected-profile";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
let scenarios: DemoExecutionMetadataScenarioPayload[] = [];
let selectedProfileName: string | null = null;
let executionRunning = false;
let profileStoreReady = false;
let profileOptions: DemoExecutionSolanaCoreProfileOption[] = [];
let preparedScenarioId: string | null = null;
let preparedStepIndex: number | null = null;
const metadataLogLines: string[] = [];
const maximumMetadataLogLines = 1200;
type MetadataProgressPayload = { timestamp: string; level: string; stage: string; message: string; signature?: string | null };
function appendMetadataLog(payload: MetadataProgressPayload): void {
const signature = payload.signature ? ` signature=${payload.signature}` : "";
metadataLogLines.push(`${payload.timestamp} ${payload.level.toUpperCase()} [${payload.stage}] ${payload.message}${signature}`);
if (metadataLogLines.length > maximumMetadataLogLines) {
metadataLogLines.splice(0, metadataLogLines.length - maximumMetadataLogLines);
}
const output = element<HTMLTextAreaElement>("#metadataExecutionLogOutput");
output.value = metadataLogLines.join("\n");
output.scrollTop = output.scrollHeight;
}
function titled(title: string, value: unknown): unknown { return { title, value }; }
function element<T extends HTMLElement>(selector: string): T {
const value = document.querySelector<T>(selector);
@@ -38,11 +66,37 @@ function fillSelect(selector: string, mode: string): void {
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}`;
option.textContent = `${scenario.assetFamily}${scenario.label}`;
select.append(option);
}
}
function operationName(operationCode: string): string {
const parts = operationCode.split(".");
return parts.length === 0 ? operationCode : parts[parts.length - 1] ?? operationCode;
}
function renderScenarioSteps(scenario: DemoExecutionMetadataScenarioPayload): void {
const select = element<HTMLSelectElement>("#metadataStepSelect");
const previousValue = Number.parseInt(select.value, 10);
select.replaceChildren();
scenario.operationCodes.forEach((operationCode, index) => {
const option = document.createElement("option");
option.value = String(index);
option.textContent = `${index + 1}. ${operationName(operationCode)}`;
select.append(option);
});
if (Number.isInteger(previousValue) && previousValue >= 0 && previousValue < scenario.operationCodes.length) {
select.value = String(previousValue);
}
const index = Number.parseInt(select.value, 10);
element<HTMLButtonElement>("#previousMetadataStepButton").disabled = !Number.isInteger(index) || index <= 0;
element<HTMLButtonElement>("#nextMetadataStepButton").disabled = !Number.isInteger(index) || index >= scenario.operationCodes.length - 1;
element<HTMLButtonElement>("#prepareMetadataScenarioStepButton").textContent = Number.isInteger(index) && scenario.operationCodes[index] !== undefined
? `Préparer létape ${index + 1}${operationName(scenario.operationCodes[index])}`
: "Préparer létape courante";
}
function renderSelectedScenario(): void {
const scenario = selectedScenario();
if (scenario === null) {
@@ -50,32 +104,188 @@ function renderSelectedScenario(): void {
}
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", {
renderScenarioSteps(scenario);
preparedScenarioId = null;
preparedStepIndex = null;
element<HTMLTextAreaElement>("#metadataOperationJson").value = "";
element<HTMLTextAreaElement>("#metadataPreflightReadsJson").value = "[]";
element<HTMLTextAreaElement>("#metadataPostconditionReadsJson").value = "[]";
element<HTMLElement>("#metadataRequirements").innerHTML = `<dt class="col-6">Parcours</dt><dd class="col-6">${scenario.label}</dd><dt class="col-6">Famille</dt><dd class="col-6"><code>${scenario.assetFamily}</code></dd><dt class="col-6">Fixture</dt><dd class="col-6"><code>${scenario.fixtureKind}</code></dd><dt class="col-6">État initial</dt><dd class="col-6"><code>${scenario.initialState}</code></dd><dt class="col-6">État attendu</dt><dd class="col-6"><code>${scenario.resultingState}</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", titled("Préflight et opérations", {
scenarioId: scenario.id,
operationCodes: scenario.operationCodes,
checks: ["program_id", "owner", "pda_and_seeds", "required_accounts", "authorities", "stateful_correlations"],
networkExecutionPerformed: false,
});
renderJsonViewer("#metadataSimulationOutput", {
}));
renderJsonViewer("#metadataSimulationOutput", titled("Simulation et confirmation", {
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", {
note: networkMode ? "Renseignez un intent typé puis lancez une simulation RPC réelle." : "Scénario synthétique déterministe.",
}));
renderJsonViewer("#metadataPostconditionsOutput", titled("Postconditions et preuves", {
required: scenario.requiresPostcondition,
evidenceKinds: scenario.requiredEvidence,
networkEvidenceCollected: false,
materializationTargets: scenario.materializationTargets,
rule: "Aucune validation réseau n'est déclarée sans simulation RPC réelle, signature éventuelle, slot et postconditions.",
}));
}
async function prepareScenarioStep(): Promise<void> {
const profileName = element<HTMLSelectElement>("#metadataProfileSelect").value;
const scenario = selectedScenario();
const stepIndex = Number.parseInt(element<HTMLSelectElement>("#metadataStepSelect").value, 10);
if (profileName.length === 0) { throw new Error("Sélectionnez un profil Devnet."); }
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", {
profileName,
scenarioId: scenario.id,
stepIndex,
previousOperationJson: element<HTMLTextAreaElement>("#metadataOperationJson").value.trim().length === 0
? null
: element<HTMLTextAreaElement>("#metadataOperationJson").value,
});
element<HTMLTextAreaElement>("#metadataOperationJson").value = prepared.operationJson;
element<HTMLTextAreaElement>("#metadataPreflightReadsJson").value = prepared.preflightReadsJson;
element<HTMLTextAreaElement>("#metadataPostconditionReadsJson").value = prepared.postconditionReadsJson;
preparedScenarioId = prepared.scenarioId;
preparedStepIndex = prepared.stepIndex;
renderJsonViewer("#metadataPreflightOutput", titled("Étape de scénario préparée", {
scenarioId: prepared.scenarioId,
stepIndex: prepared.stepIndex,
operation: prepared.operation,
fixtureState: prepared.fixtureState,
preflightReads: JSON.parse(prepared.preflightReadsJson),
postconditionReads: JSON.parse(prepared.postconditionReadsJson),
simulationReady: prepared.simulationReady,
submissionReady: prepared.submissionReady,
}));
appendMetadataLog({ timestamp: new Date().toISOString(), level: "info", stage: "fixture", message: `${prepared.label} prête (${prepared.fixtureState})` });
setExecutionRunning(executionRunning);
}
function moveScenarioStep(offset: number): void {
const select = element<HTMLSelectElement>("#metadataStepSelect");
const current = Number.parseInt(select.value, 10);
const next = current + offset;
if (Number.isInteger(next) && next >= 0 && next < select.options.length) {
select.value = String(next);
const scenario = selectedScenario();
if (scenario !== null) { renderScenarioSteps(scenario); }
}
}
function parseJsonText(value: string, label: string): unknown {
try {
return JSON.parse(value);
} catch (caught) {
const message = caught instanceof Error ? caught.message : String(caught);
throw new Error(`${label} invalide : ${message}`);
}
}
function selectedProfile(): DemoExecutionSolanaCoreProfileOption | null {
const name = element<HTMLSelectElement>("#metadataProfileSelect").value;
return profileOptions.find(profile => profile.name === name) ?? null;
}
function updateProfileHelp(): void {
const profile = selectedProfile();
const help = element<HTMLElement>("#metadataProfileHelp");
if (profile === null) {
help.textContent = "Aucun profil Devnet compatible.";
return;
}
help.textContent = `wallet=${profile.walletAlias}, spendMax=${profile.maxSpendLamports}, airdropMax=${profile.maxAirdropLamports}, send=${profile.sendEnabled ? "enabled" : "disabled"}`;
}
function setExecutionRunning(value: boolean): void {
executionRunning = value;
const profile = selectedProfile();
const missingOperation = element<HTMLTextAreaElement>("#metadataOperationJson").value.trim().length === 0;
const unavailable = value || profile === null || !profileStoreReady || missingOperation;
element<HTMLButtonElement>("#simulateMetadataButton").disabled = unavailable;
element<HTMLButtonElement>("#submitMetadataButton").disabled = unavailable || !profile?.sendEnabled;
element<HTMLButtonElement>("#prepareMetadataScenarioStepButton").disabled = value || profile === null || !profileStoreReady;
}
async function executeMetadata(submit: boolean): Promise<void> {
if (executionRunning) {
return;
}
const profileName = element<HTMLSelectElement>("#metadataProfileSelect").value;
if (profileName.length === 0 || !profileStoreReady) {
throw new Error("Sélectionnez et préparez un profil Devnet.");
}
const operationJson = element<HTMLTextAreaElement>("#metadataOperationJson").value;
const scenario = selectedScenario();
const stepIndex = Number.parseInt(element<HTMLSelectElement>("#metadataStepSelect").value, 10);
if (operationJson.trim().length === 0) {
throw new Error("Préparez létape courante avant lexécution.");
}
if (scenario === null || preparedScenarioId !== scenario.id || preparedStepIndex !== stepIndex) {
throw new Error("Préparez de nouveau létape après toute modification du scénario ou de létape.");
}
const preflightReadsJson = element<HTMLTextAreaElement>("#metadataPreflightReadsJson").value;
const postconditionReadsJson = element<HTMLTextAreaElement>("#metadataPostconditionReadsJson").value;
parseJsonText(operationJson, "Intent Metaplex");
parseJsonText(preflightReadsJson, "Lectures de préflight");
parseJsonText(postconditionReadsJson, "Lectures de postcondition");
const operatorConfirmed = element<HTMLInputElement>("#metadataOperatorConfirmed").checked;
if (submit && !operatorConfirmed) {
throw new Error("La soumission exige la confirmation explicite de lopérateur.");
}
const request: DemoExecutionMetadataRequest = {
profileName,
intentId: `desktop-metaplex-${Date.now()}`,
operationJson,
preflightReadsJson,
postconditionReadsJson,
submit,
operatorConfirmed,
materializeAfterConfirmation: element<HTMLInputElement>("#metadataMaterializeInput").checked,
};
setExecutionRunning(true);
const badge = element<HTMLElement>("#metadataStatusBadge");
badge.textContent = submit ? "Soumission" : "Simulation";
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 });
renderJsonViewer("#metadataPreflightOutput", titled("Préflight et opérations", JSON.parse(summary.preflightJson)));
renderJsonViewer("#metadataSimulationOutput", titled("Simulation et confirmation", {
profileName: summary.profileName,
cluster: summary.cluster,
genesisHash: summary.genesisHash,
walletPublicKey: summary.walletPublicKey,
balanceLamports: summary.balanceLamports,
simulationSuccess: summary.simulationSuccess,
transactionSignature: summary.transactionSignature,
confirmationStatus: summary.confirmationStatus,
materializationRequested: summary.materializationRequested,
materializedSnapshotCount: summary.materializedSnapshotCount,
plan: JSON.parse(summary.planJson),
simulation: JSON.parse(summary.simulationJson),
}));
renderJsonViewer("#metadataPostconditionsOutput", titled("Postconditions et preuves", JSON.parse(summary.evidenceJson)));
badge.textContent = summary.transactionSignature === null ? "Simulé" : "Confirmé";
badge.className = summary.simulationSuccess ? "badge text-bg-success" : "badge text-bg-danger";
} finally {
setExecutionRunning(false);
}
}
async function loadProfiles(): Promise<void> {
const options = await invoke<DemoExecutionSolanaCoreOptionsPayload>("demo_execution_solana_core_options");
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) {
@@ -91,7 +301,18 @@ async function loadProfiles(): Promise<void> {
select.value = options.defaultProfileName;
}
selectedProfileName = select.value.length === 0 ? null : select.value;
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();
return;
}
appendMetadataLog({ timestamp: new Date().toISOString(), level: "info", stage: "profile_options", message: `${options.profiles.length} profil(s) Devnet chargé(s).` });
renderSelectedScenario();
await prepareProfile();
}
async function prepareProfile(): Promise<void> {
@@ -102,6 +323,8 @@ async function prepareProfile(): Promise<void> {
badge.className = "badge text-bg-danger";
return;
}
profileStoreReady = false;
setExecutionRunning(executionRunning);
badge.textContent = "Préparation";
badge.className = "badge text-bg-warning";
try {
@@ -109,13 +332,17 @@ async function prepareProfile(): Promise<void> {
selectedProfileName = profileName;
window.localStorage.setItem(profileStorageKey, profileName);
renderJsonViewer("#metadataProfileOutput", readiness);
profileStoreReady = true;
badge.textContent = "Base prête";
badge.className = "badge text-bg-success";
setExecutionRunning(executionRunning);
renderSelectedScenario();
} catch (caught) {
const message = caught instanceof Error ? caught.message : String(caught);
profileStoreReady = false;
badge.textContent = "Erreur";
badge.className = "badge text-bg-danger";
setExecutionRunning(executionRunning);
frontendError("kb-app-demo-desktop.frontend.execution_metadata", message);
}
}
@@ -140,8 +367,10 @@ async function loadScenarios(): Promise<void> {
badge.className = "badge text-bg-success";
} catch (caught) {
const message = caught instanceof Error ? caught.message : String(caught);
profileStoreReady = false;
badge.textContent = "Erreur";
badge.className = "badge text-bg-danger";
setExecutionRunning(executionRunning);
frontendError("kb-app-demo-desktop.frontend.execution_metadata", message);
}
}
@@ -157,10 +386,36 @@ document.addEventListener("DOMContentLoaded", () => {
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<HTMLButtonElement>("#prepareMetadataProfileButton").addEventListener("click", () => void prepareProfile());
element<HTMLButtonElement>("#refreshMetadataScenariosButton").addEventListener("click", () => void loadScenarios());
element<HTMLSelectElement>("#metadataStepSelect").addEventListener("change", () => {
const scenario = selectedScenario();
if (scenario !== null) { renderScenarioSteps(scenario); }
});
element<HTMLButtonElement>("#previousMetadataStepButton").addEventListener("click", () => moveScenarioStep(-1));
element<HTMLButtonElement>("#nextMetadataStepButton").addEventListener("click", () => moveScenarioStep(1));
element<HTMLTextAreaElement>("#metadataOperationJson").addEventListener("input", () => {
preparedScenarioId = null;
preparedStepIndex = null;
setExecutionRunning(executionRunning);
});
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));
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)));
});
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)));
void loadScenarios();
});