0.1.0
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
<!-- file: kb_app_demo/frontend/demo_backfill.html -->
|
||||
<!-- version: 2 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Khadhroony Bot2 - Backfill HTTP</title>
|
||||
<link rel="stylesheet" href="sass/main.scss" />
|
||||
</head>
|
||||
|
||||
<body class="bg-body-tertiary">
|
||||
<header class="app-header">
|
||||
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
|
||||
<div class="container my-0">
|
||||
<a class="navbar-brand d-flex align-items-center" href="main.html">
|
||||
<img alt="Logo" src="imgs/logo.png" class="app-logo" />
|
||||
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Backfill HTTP</span>
|
||||
</a>
|
||||
<div class="ms-auto d-flex align-items-center gap-2">
|
||||
<span id="backfillStatusBadge" class="badge text-bg-secondary">Initialisation</span>
|
||||
<button id="cancelBackfillButton" class="btn btn-sm btn-outline-danger" type="button" disabled>Arrêter</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="app-main">
|
||||
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
|
||||
<div class="container py-4">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<h1 class="h3 card-title">Backfill transactionnel canonique</h1>
|
||||
<p class="text-body-secondary mb-0">Les signatures sont découvertes avec <code>getSignaturesForAddress</code>, hydratées avec <code>getTransaction</code>, puis stockées dans les tables canoniques et d'observation. Une seule campagne peut fonctionner à la fois.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion shadow-sm" id="backfillAccordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="sharedSettingsHeading">
|
||||
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#sharedSettingsCollapse" aria-expanded="true" aria-controls="sharedSettingsCollapse">Paramètres communs</button>
|
||||
</h2>
|
||||
<div id="sharedSettingsCollapse" class="accordion-collapse collapse show" aria-labelledby="sharedSettingsHeading" data-bs-parent="#backfillAccordion">
|
||||
<div class="accordion-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label" for="backfillRoleSelect">Rôle HTTP</label>
|
||||
<select id="backfillRoleSelect" class="form-select"></select>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label" for="backfillCommitmentSelect">Commitment</label>
|
||||
<select id="backfillCommitmentSelect" class="form-select">
|
||||
<option value="confirmed">confirmed</option>
|
||||
<option value="finalized">finalized</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6 col-lg-3">
|
||||
<label class="form-label" for="backfillPageSizeInput">Taille de page</label>
|
||||
<input id="backfillPageSizeInput" class="form-control" type="number" min="1" max="1000" />
|
||||
</div>
|
||||
<div class="col-6 col-lg-3">
|
||||
<label class="form-label" for="backfillMaxPagesInput">Pages maximales</label>
|
||||
<input id="backfillMaxPagesInput" class="form-control" type="number" min="1" />
|
||||
</div>
|
||||
<div class="col-6 col-lg-3">
|
||||
<label class="form-label" for="backfillConcurrencyInput">Concurrence</label>
|
||||
<input id="backfillConcurrencyInput" class="form-control" type="number" min="1" />
|
||||
</div>
|
||||
<div class="col-6 col-lg-3">
|
||||
<label class="form-label" for="backfillRetriesInput">Retries</label>
|
||||
<input id="backfillRetriesInput" class="form-control" type="number" min="0" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="signatureBackfillHeading">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#signatureBackfillCollapse" aria-expanded="false" aria-controls="signatureBackfillCollapse">Ensemble de signatures</button>
|
||||
</h2>
|
||||
<div id="signatureBackfillCollapse" class="accordion-collapse collapse" aria-labelledby="signatureBackfillHeading" data-bs-parent="#backfillAccordion">
|
||||
<div class="accordion-body">
|
||||
<label class="form-label" for="explicitSignaturesTextarea">Signatures, une par ligne</label>
|
||||
<textarea id="explicitSignaturesTextarea" class="form-control font-monospace mb-3" rows="10" placeholder="5abc... 4def..."></textarea>
|
||||
<button class="btn btn-primary backfill-start-button" type="button" data-backfill-mode="explicit_signatures">Lancer le backfill</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="programBackfillHeading">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#programBackfillCollapse" aria-expanded="false" aria-controls="programBackfillCollapse">Programme Solana</button>
|
||||
</h2>
|
||||
<div id="programBackfillCollapse" class="accordion-collapse collapse" aria-labelledby="programBackfillHeading" data-bs-parent="#backfillAccordion">
|
||||
<div class="accordion-body">
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-12"><label class="form-label" for="programAddressInput">Program ID</label><input id="programAddressInput" class="form-control font-monospace" /></div>
|
||||
<div class="col-12"><label class="form-label" for="programAnchorInput">Signature d'ancrage</label><input id="programAnchorInput" class="form-control font-monospace" />
|
||||
<div class="form-text">Optionnelle pour « Avant, plus anciennes » : vide, la recherche commence aux transactions les plus récentes. Obligatoire pour « Après, plus récentes ».</div>
|
||||
</div>
|
||||
<div class="col-6"><label class="form-label" for="programDirectionSelect">Direction</label><select id="programDirectionSelect" class="form-select">
|
||||
<option value="before">Avant, plus anciennes</option>
|
||||
<option value="after">Après, plus récentes</option>
|
||||
</select></div>
|
||||
<div class="col-6"><label class="form-label" for="programLimitInput">Nombre de signatures</label><input id="programLimitInput" class="form-control" type="number" min="1" value="100" /></div>
|
||||
</div>
|
||||
<button class="btn btn-primary backfill-start-button" type="button" data-backfill-mode="program">Lancer le backfill programme</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="tokenBackfillHeading">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#tokenBackfillCollapse" aria-expanded="false" aria-controls="tokenBackfillCollapse">Token</button>
|
||||
</h2>
|
||||
<div id="tokenBackfillCollapse" class="accordion-collapse collapse" aria-labelledby="tokenBackfillHeading" data-bs-parent="#backfillAccordion">
|
||||
<div class="accordion-body">
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-12"><label class="form-label" for="tokenAddressInput">Mint du token</label><input id="tokenAddressInput" class="form-control font-monospace" /></div>
|
||||
<div class="col-12"><label class="form-label" for="tokenAnchorInput">Signature d'ancrage</label><input id="tokenAnchorInput" class="form-control font-monospace" />
|
||||
<div class="form-text">Optionnelle pour « Avant, plus anciennes » : vide, la recherche commence aux transactions les plus récentes. Obligatoire pour « Après, plus récentes ».</div>
|
||||
</div>
|
||||
<div class="col-6"><label class="form-label" for="tokenDirectionSelect">Direction</label><select id="tokenDirectionSelect" class="form-select">
|
||||
<option value="before">Avant, plus anciennes</option>
|
||||
<option value="after">Après, plus récentes</option>
|
||||
</select></div>
|
||||
<div class="col-6"><label class="form-label" for="tokenLimitInput">Nombre de signatures</label><input id="tokenLimitInput" class="form-control" type="number" min="1" value="100" /></div>
|
||||
</div>
|
||||
<button class="btn btn-primary backfill-start-button" type="button" data-backfill-mode="token">Lancer le backfill token</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="poolBackfillHeading">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#poolBackfillCollapse" aria-expanded="false" aria-controls="poolBackfillCollapse">Pool</button>
|
||||
</h2>
|
||||
<div id="poolBackfillCollapse" class="accordion-collapse collapse" aria-labelledby="poolBackfillHeading" data-bs-parent="#backfillAccordion">
|
||||
<div class="accordion-body">
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-12"><label class="form-label" for="poolAddressInput">Adresse du pool</label><input id="poolAddressInput" class="form-control font-monospace" /></div>
|
||||
<div class="col-12"><label class="form-label" for="poolAnchorInput">Signature d'ancrage</label><input id="poolAnchorInput" class="form-control font-monospace" />
|
||||
<div class="form-text">Optionnelle pour « Avant, plus anciennes » : vide, la recherche commence aux transactions les plus récentes. Obligatoire pour « Après, plus récentes ».</div>
|
||||
</div>
|
||||
<div class="col-6"><label class="form-label" for="poolDirectionSelect">Direction</label><select id="poolDirectionSelect" class="form-select">
|
||||
<option value="before">Avant, plus anciennes</option>
|
||||
<option value="after">Après, plus récentes</option>
|
||||
</select></div>
|
||||
<div class="col-6"><label class="form-label" for="poolLimitInput">Nombre de signatures</label><input id="poolLimitInput" class="form-control" type="number" min="1" value="100" /></div>
|
||||
</div>
|
||||
<button class="btn btn-primary backfill-start-button" type="button" data-backfill-mode="pool">Lancer le backfill pool</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="backfillLogHeading">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#backfillLogCollapse" aria-expanded="false" aria-controls="backfillLogCollapse">Journal et résultat</button>
|
||||
</h2>
|
||||
<div id="backfillLogCollapse" class="accordion-collapse collapse" aria-labelledby="backfillLogHeading" data-bs-parent="#backfillAccordion">
|
||||
<div class="accordion-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h3 class="h6 mb-0">Journal</h3><button id="clearBackfillLogButton" class="btn btn-sm btn-outline-secondary" type="button">Effacer</button>
|
||||
</div>
|
||||
<textarea id="backfillLogOutput" class="form-control font-monospace mb-3" rows="15" readonly>Aucune campagne exécutée.</textarea>
|
||||
<h3 class="h6">Résumé JSON</h3>
|
||||
<textarea id="backfillSummaryOutput" class="form-control font-monospace" rows="16" readonly>Aucun résultat.</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="app-footer bg-dark text-light">
|
||||
<div class="container h-100 d-flex align-items-center">
|
||||
<div class="row flex-grow-1 align-items-center">
|
||||
<div class="col-12 text-center text-small my-1 my-md-0">© 2026 SASEDEV</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="ts/demo_backfill.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,93 @@
|
||||
<!-- file: kb_app_demo/frontend/demo_config.html -->
|
||||
<!-- version: 2 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Khadhroony-Bot2 - Configuration</title>
|
||||
<link rel="stylesheet" href="sass/main.scss" />
|
||||
</head>
|
||||
|
||||
<body class="bg-body-tertiary">
|
||||
<header class="app-header">
|
||||
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
|
||||
<div class="container my-0">
|
||||
<a class="navbar-brand d-flex align-items-center" href="main.html">
|
||||
<img alt="Logo" src="imgs/logo.png" class="app-logo" />
|
||||
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Bot2</span>
|
||||
</a>
|
||||
|
||||
<div class="ms-auto d-flex align-items-center gap-2">
|
||||
<span class="badge text-bg-primary">Configuration</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="app-main">
|
||||
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
|
||||
<div class="container py-4">
|
||||
<div class="row g-4">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h1 class="h3 card-title mb-3">Profil actif</h1>
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-sm-3">Fichier</dt>
|
||||
<dd class="col-sm-9"><code id="configPath">Chargement...</code></dd>
|
||||
<dt class="col-sm-3">Profil</dt>
|
||||
<dd class="col-sm-9"><code id="activeProfileName">Chargement...</code></dd>
|
||||
<dt class="col-sm-3">Environnement</dt>
|
||||
<dd class="col-sm-9"><code id="activeEnvironment">Chargement...</code></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xl-6">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 card-title mb-3">Configuration active</h2>
|
||||
<div id="activeProfileJson" class="app-config-box app-json-viewer mb-0">Chargement...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xl-6">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 card-title mb-3">Configuration complète</h2>
|
||||
<div id="fullConfigJson" class="app-config-box app-json-viewer mb-0">Chargement...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 card-title mb-3">Schéma JSON embarqué</h2>
|
||||
<div id="schemaJson" class="app-config-box app-json-viewer mb-0">Chargement...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="app-footer bg-dark text-light">
|
||||
<div class="container h-100 d-flex align-items-center">
|
||||
<div class="row flex-grow-1 align-items-center">
|
||||
<div class="col-12 text-center text-small my-1 my-md-0">
|
||||
© 2026 SASEDEV
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="ts/demo_config.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,160 @@
|
||||
<!-- file: kb_app_demo/frontend/demo_core_extraction.html -->
|
||||
<!-- version: 4 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Khadhroony Bot2 - Extraction canonical vers core</title>
|
||||
<link rel="stylesheet" href="sass/main.scss" />
|
||||
</head>
|
||||
|
||||
<body class="bg-body-tertiary">
|
||||
<header class="app-header">
|
||||
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
|
||||
<div class="container my-0">
|
||||
<a class="navbar-brand d-flex align-items-center" href="main.html">
|
||||
<img alt="Logo" src="imgs/logo.png" class="app-logo" />
|
||||
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Extraction core</span>
|
||||
</a>
|
||||
<div class="ms-auto d-flex align-items-center gap-2">
|
||||
<span id="coreExtractionVersionBadge" class="badge text-bg-info">Version —</span>
|
||||
<span id="coreExtractionStatusBadge" class="badge text-bg-secondary">Initialisation</span>
|
||||
<button id="openRawDiagnosticsButton" class="btn btn-sm btn-outline-secondary" type="button">Diagnostic raw</button>
|
||||
<button id="openCoreDiagnosticsButton" class="btn btn-sm btn-outline-secondary" type="button">Diagnostic core</button>
|
||||
<button id="openReplayCandidatesButton" class="btn btn-sm btn-outline-primary" type="button">Candidats replay</button>
|
||||
<button id="cancelCoreExtractionButton" class="btn btn-sm btn-outline-danger" type="button" disabled>Arrêter</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="app-main">
|
||||
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
|
||||
<div class="container py-4">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<h1 class="h3 card-title">Extraction transactionnelle canonical vers core</h1>
|
||||
<p class="text-body-secondary mb-0">Chaque transaction canonique sélectionnée est validée, normalisée dans les tables core et enregistrée dans le ledger de traitement au sein d'une seule transaction PostgreSQL.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion shadow-sm" id="coreExtractionAccordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="coreExtractionSettingsHeading">
|
||||
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#coreExtractionSettingsCollapse" aria-expanded="true" aria-controls="coreExtractionSettingsCollapse">Paramètres communs</button>
|
||||
</h2>
|
||||
<div id="coreExtractionSettingsCollapse" class="accordion-collapse collapse show" aria-labelledby="coreExtractionSettingsHeading" data-bs-parent="#coreExtractionAccordion">
|
||||
<div class="accordion-body">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-6 col-lg-4">
|
||||
<label class="form-label" for="coreExtractionLimitInput">Transactions maximales</label>
|
||||
<input id="coreExtractionLimitInput" class="form-control" type="number" min="1" />
|
||||
</div>
|
||||
<div class="col-6 col-lg-4">
|
||||
<label class="form-label" for="coreExtractionConcurrencyInput">Concurrence</label>
|
||||
<input id="coreExtractionConcurrencyInput" class="form-control" type="number" min="1" />
|
||||
</div>
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="form-check form-switch mb-2">
|
||||
<input id="coreExtractionForceReplayInput" class="form-check-input" type="checkbox" role="switch" />
|
||||
<label class="form-check-label" for="coreExtractionForceReplayInput">Forcer le remplacement des lignes déjà à jour</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="coreExtractionSignaturesHeading">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#coreExtractionSignaturesCollapse" aria-expanded="false" aria-controls="coreExtractionSignaturesCollapse">Signatures explicites</button>
|
||||
</h2>
|
||||
<div id="coreExtractionSignaturesCollapse" class="accordion-collapse collapse" aria-labelledby="coreExtractionSignaturesHeading" data-bs-parent="#coreExtractionAccordion">
|
||||
<div class="accordion-body">
|
||||
<label class="form-label" for="coreExtractionSignaturesTextarea">Signatures canoniques déjà stockées, une par ligne</label>
|
||||
<textarea id="coreExtractionSignaturesTextarea" class="form-control font-monospace mb-3" rows="10" placeholder="5abc... 4def..."></textarea>
|
||||
<button class="btn btn-primary core-extraction-start-button" type="button" data-core-extraction-mode="signatures">Extraire les signatures</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="coreExtractionPendingHeading">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#coreExtractionPendingCollapse" aria-expanded="false" aria-controls="coreExtractionPendingCollapse">Transactions en attente</button>
|
||||
</h2>
|
||||
<div id="coreExtractionPendingCollapse" class="accordion-collapse collapse" aria-labelledby="coreExtractionPendingHeading" data-bs-parent="#coreExtractionAccordion">
|
||||
<div class="accordion-body">
|
||||
<p class="text-body-secondary">Sélectionne les transactions canoniques dont l'état de traitement est <code>received</code>, dans l'ordre des slots.</p>
|
||||
<button class="btn btn-primary core-extraction-start-button" type="button" data-core-extraction-mode="pending">Extraire les transactions en attente</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="coreExtractionSlotsHeading">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#coreExtractionSlotsCollapse" aria-expanded="false" aria-controls="coreExtractionSlotsCollapse">Plage de slots</button>
|
||||
</h2>
|
||||
<div id="coreExtractionSlotsCollapse" class="accordion-collapse collapse" aria-labelledby="coreExtractionSlotsHeading" data-bs-parent="#coreExtractionAccordion">
|
||||
<div class="accordion-body">
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label" for="coreExtractionMinSlotInput">Slot minimum inclusif</label>
|
||||
<input id="coreExtractionMinSlotInput" class="form-control" type="number" min="0" />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label" for="coreExtractionMaxSlotInput">Slot maximum inclusif</label>
|
||||
<input id="coreExtractionMaxSlotInput" class="form-control" type="number" min="0" />
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary core-extraction-start-button" type="button" data-core-extraction-mode="slot_range">Extraire la plage de slots</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="coreExtractionProgramHeading">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#coreExtractionProgramCollapse" aria-expanded="false" aria-controls="coreExtractionProgramCollapse">Programme déjà indexé dans core</button>
|
||||
</h2>
|
||||
<div id="coreExtractionProgramCollapse" class="accordion-collapse collapse" aria-labelledby="coreExtractionProgramHeading" data-bs-parent="#coreExtractionAccordion">
|
||||
<div class="accordion-body">
|
||||
<p class="text-body-secondary">Sélectionne les transactions raw ayant déjà au moins une instruction core résolue pour ce programme. Ce mode sert aux replays après changement de version ou de hash.</p>
|
||||
<label class="form-label" for="coreExtractionProgramIdInput">Program ID</label>
|
||||
<input id="coreExtractionProgramIdInput" class="form-control font-monospace mb-3" />
|
||||
<button class="btn btn-primary core-extraction-start-button" type="button" data-core-extraction-mode="program_id">Extraire par programme</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="coreExtractionLogHeading">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#coreExtractionLogCollapse" aria-expanded="false" aria-controls="coreExtractionLogCollapse">Journal et résultat</button>
|
||||
</h2>
|
||||
<div id="coreExtractionLogCollapse" class="accordion-collapse collapse" aria-labelledby="coreExtractionLogHeading" data-bs-parent="#coreExtractionAccordion">
|
||||
<div class="accordion-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h3 class="h6 mb-0">Journal</h3><button id="clearCoreExtractionLogButton" class="btn btn-sm btn-outline-secondary" type="button">Effacer</button>
|
||||
</div>
|
||||
<textarea id="coreExtractionLogOutput" class="form-control font-monospace mb-3" rows="15" readonly>Aucune campagne exécutée.</textarea>
|
||||
<h3 class="h6">Résumé JSON</h3>
|
||||
<textarea id="coreExtractionSummaryOutput" class="form-control font-monospace" rows="16" readonly>Aucun résultat.</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="app-footer bg-dark text-light">
|
||||
<div class="container h-100 d-flex align-items-center">
|
||||
<div class="row flex-grow-1 align-items-center">
|
||||
<div class="col-12 text-center text-small my-1 my-md-0">© 2026 SASEDEV</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="ts/demo_core_extraction.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,166 @@
|
||||
<!-- file: kb_app_demo/frontend/demo_decode_replay.html -->
|
||||
<!-- version: 4 -->
|
||||
<!doctype html>
|
||||
<html lang="fr" data-bs-theme="light">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Décodage et matérialisation</title>
|
||||
<link rel="icon" href="imgs/favicon.png" />
|
||||
<link rel="stylesheet" href="sass/main.scss" />
|
||||
</head>
|
||||
|
||||
<body class="bg-body-tertiary">
|
||||
<header class="app-header">
|
||||
<nav class="navbar h-100 py-0 bg-light text-dark">
|
||||
<div class="container-fluid px-4">
|
||||
<span class="navbar-brand fw-bold">Décodage contextualisé et matérialisation</span>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span id="decodeReplayVersionBadge" class="badge text-bg-secondary">Version</span>
|
||||
<span id="decodeReplayStatusBadge" class="badge text-bg-success">Prêt</span>
|
||||
<button id="cancelDecodeReplayButton" class="btn btn-sm btn-outline-danger" type="button" disabled>Arrêter</button>
|
||||
</div>
|
||||
</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="row g-4">
|
||||
<div class="col-12 col-xl-7">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<h1 class="h4 mb-3">Sélection bornée</h1>
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-lg-6">
|
||||
<label class="form-label" for="decodeReplayProgramIdInput">Program ID exact</label>
|
||||
<input id="decodeReplayProgramIdInput" class="form-control font-monospace" placeholder="Vide = tous les programmes des décodeurs sélectionnés" />
|
||||
</div>
|
||||
<div class="col-12 col-lg-3">
|
||||
<label class="form-label" for="decodeReplayStateSelect">État instruction</label>
|
||||
<select id="decodeReplayStateSelect" class="form-select">
|
||||
<option value="incomplete_signatures">signatures incomplètes (pending + failed + replay_requested + unsupported)</option>
|
||||
<option value="actionable">pending + failed + replay_requested</option>
|
||||
<option value="pending">pending</option>
|
||||
<option value="failed">failed</option>
|
||||
<option value="replay_requested">replay_requested</option>
|
||||
<option value="decoded">decoded</option>
|
||||
<option value="ignored">ignored</option>
|
||||
<option value="materialized">materialized</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6 col-lg-3">
|
||||
<label class="form-label" for="decodeReplayLimitInput">Limite</label>
|
||||
<input id="decodeReplayLimitInput" class="form-control" type="number" min="1" />
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
<label class="form-label" for="decodeReplaySignaturesTextarea">Signatures explicites, une par ligne</label>
|
||||
<textarea id="decodeReplaySignaturesTextarea" class="form-control font-monospace" rows="7" placeholder="Copier depuis demo_sql_replay_candidates"></textarea>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
<label class="form-label" for="decodeReplayPathsTextarea">Instruction paths exacts, un par ligne</label>
|
||||
<textarea id="decodeReplayPathsTextarea" class="form-control font-monospace" rows="7" placeholder="0 1/0"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 mb-3">Dispatch et replay</h2>
|
||||
<div id="decodeReplayDecoderList" class="mb-3"></div>
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-6 col-lg-3">
|
||||
<label class="form-label" for="decodeReplayConcurrencyInput">Concurrence</label>
|
||||
<input id="decodeReplayConcurrencyInput" class="form-control" type="number" min="1" />
|
||||
</div>
|
||||
<div class="col-12 col-lg-9">
|
||||
<div class="form-check form-switch">
|
||||
<input id="decodeReplayAllCompatibleInput" class="form-check-input" type="checkbox" role="switch" />
|
||||
<label class="form-check-label" for="decodeReplayAllCompatibleInput">Exécuter tous les décodeurs compatibles</label>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input id="decodeReplayForceInput" class="form-check-input" type="checkbox" role="switch" disabled />
|
||||
<label class="form-check-label" for="decodeReplayForceInput">Force replay du processor/version/input</label>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input id="decodeReplayAllSignaturesInput" class="form-check-input" type="checkbox" role="switch" />
|
||||
<label class="form-check-label" for="decodeReplayAllSignaturesInput">Toutes les signatures correspondant aux filtres, dans la limite indiquée</label>
|
||||
</div>
|
||||
<div id="decodeReplayForceScopeHelp" class="form-text mb-2">Sans signatures explicites, cocher Toutes les signatures pour autoriser un force replay borné. Le filtre d’état est alors ignoré, mais les programmes, paths et la limite restent appliqués.</div>
|
||||
<div class="form-check form-switch">
|
||||
<input id="decodeReplayMaterializeInput" class="form-check-input" type="checkbox" role="switch" disabled />
|
||||
<label class="form-check-label" for="decodeReplayMaterializeInput">Appeler les matérialiseurs disponibles après persistance du décodage</label>
|
||||
</div>
|
||||
<div id="decodeReplayMaterializeHelp" class="form-text">Chargement de la disponibilité des matérialiseurs…</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button id="startDecodeReplayButton" class="btn btn-primary" type="button">Lancer le replay de décodage</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-5">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<button id="openReplayCandidatesFromDecodeButton" class="btn btn-sm btn-outline-primary" type="button">Candidats replay</button>
|
||||
<button id="openCoreDiagnosticsFromDecodeButton" class="btn btn-sm btn-outline-secondary" type="button">Diagnostic core</button>
|
||||
<button id="loadDecodeDiagnosticsButton" class="btn btn-sm btn-outline-secondary" type="button">Decode / ledger / couverture</button>
|
||||
</div>
|
||||
<h2 class="h5">Journal</h2>
|
||||
<textarea id="decodeReplayLogOutput" class="form-control font-monospace mb-3" rows="14" readonly>Aucune campagne exécutée.</textarea>
|
||||
<h2 class="h5">Résumé</h2>
|
||||
<textarea id="decodeReplaySummaryOutput" class="form-control font-monospace mb-3" rows="15" readonly>Aucun résultat.</textarea>
|
||||
<h2 class="h5">Diagnostics</h2>
|
||||
<textarea id="decodeReplayDiagnosticsOutput" class="form-control font-monospace" rows="18" readonly>Diagnostics non chargés.</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap align-items-end gap-3 mb-3">
|
||||
<div class="flex-grow-1">
|
||||
<label class="form-label" for="annotationSignatureInput">Journal des annotations — signature contient</label>
|
||||
<input id="annotationSignatureInput" class="form-control font-monospace" placeholder="Filtre facultatif" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" for="annotationLimitInput">Limite</label>
|
||||
<input id="annotationLimitInput" class="form-control" type="number" min="1" max="500" value="100" />
|
||||
</div>
|
||||
<button id="loadAnnotationsButton" class="btn btn-outline-primary" type="button">Charger les annotations</button>
|
||||
</div>
|
||||
<p id="annotationJournalStatus" class="text-body-secondary mb-3">Annotations non chargées.</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Slot</th>
|
||||
<th>Signature</th>
|
||||
<th>Path</th>
|
||||
<th>Génération</th>
|
||||
<th>Texte</th>
|
||||
<th>Octets</th>
|
||||
<th>Signataires vérifiés</th>
|
||||
<th>SHA-256 / provenance</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="annotationJournalBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="app-footer bg-dark text-light">
|
||||
<div class="container h-100 d-flex align-items-center justify-content-center">© 2026 SASEDEV</div>
|
||||
</footer>
|
||||
<script type="module" src="ts/demo_decode_replay.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,73 @@
|
||||
<!-- file: kb_app_demo/frontend/demo_execution_solana_core.html -->
|
||||
<!-- version: 6 -->
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Exécution Solana Core Devnet</title>
|
||||
<link href="sass/main.scss" rel="stylesheet"/>
|
||||
</head>
|
||||
<body class="app-body">
|
||||
<header class="app-header bg-dark text-light">
|
||||
<nav class="container-fluid h-100 d-flex align-items-center justify-content-between px-4">
|
||||
<div>
|
||||
<h1 class="h4 mb-0">Exécution Solana Core Devnet</h1><small class="text-light-emphasis">System Program uniquement — simulation-first et replay post-exécution</small>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2"><span class="badge text-bg-success" id="executionStatusBadge">Prêt</span><button class="btn btn-sm btn-outline-warning" disabled="" id="cancelExecutionButton" type="button">Annuler</button></div>
|
||||
</nav>
|
||||
</header>
|
||||
<main class="app-main">
|
||||
<div class="osb-scrollable pt-1 pb-4" data-simplebar="">
|
||||
<div class="container-fluid py-4 px-4">
|
||||
<div class="alert alert-warning">Cette fenêtre est strictement réservée aux opérations Solana Core. Les opérations SPL sont disponibles dans « Exécution SPL Devnet ».</div>
|
||||
<div class="row g-4">
|
||||
<div class="col-12 col-xl-5">
|
||||
<div class="card shadow-sm border-0"><div class="card-body"><div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3"><h2 class="h5 mb-0">System Program</h2><span class="badge text-bg-primary">solana_native_classifier</span></div>
|
||||
|
||||
<div class="mb-3"><label class="form-label" for="executionProfileSelect">Profil Devnet</label><select class="form-select" id="executionProfileSelect"></select>
|
||||
<div class="form-text" id="executionProfileHelp"></div>
|
||||
</div>
|
||||
<div class="mb-3"><label class="form-label" for="executionRecipientInput">Destinataire</label>
|
||||
<div class="input-group"><input autocomplete="off" class="form-control font-monospace" id="executionRecipientInput" type="text"/><button class="btn btn-outline-secondary" id="generateRecipientButton" type="button">Générer</button></div>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-6"><label class="form-label" for="executionLamportsInput">Lamports</label><input class="form-control" id="executionLamportsInput" min="1" step="1" type="number"/></div>
|
||||
<div class="col-6"><label class="form-label" for="executionAirdropInput">Airdrop maximal</label><input class="form-control" id="executionAirdropInput" min="0" step="1" type="number" value="0"/></div>
|
||||
</div>
|
||||
<hr/>
|
||||
<div class="form-check form-switch mb-2"><input class="form-check-input" id="executionOperatorConfirmedInput" type="checkbox"/><label class="form-check-label" for="executionOperatorConfirmedInput">Confirmation opérateur explicite</label></div>
|
||||
<div class="form-check form-switch mb-2"><input class="form-check-input" id="executionForceReplayInput" type="checkbox"/><label class="form-check-label" for="executionForceReplayInput">Forcer le replay post-exécution</label></div>
|
||||
<div class="form-check form-switch mb-3"><input class="form-check-input" id="executionMaterializeInput" type="checkbox"/><label class="form-check-label" for="executionMaterializeInput">Matérialiser après décodage</label></div>
|
||||
<div class="d-grid gap-2 d-md-flex"><button class="btn btn-outline-primary flex-fill" id="simulateExecutionButton" type="button">Simuler uniquement</button><button class="btn btn-danger flex-fill" id="submitExecutionButton" type="button">Signer et envoyer</button></div>
|
||||
</div></div>
|
||||
<div class="card shadow-sm border-0 mt-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h2 class="h5 mb-0">Journal</h2><button class="btn btn-sm btn-outline-secondary" id="clearExecutionLogButton" type="button">Effacer</button>
|
||||
</div><textarea class="form-control font-monospace" id="executionLogOutput" readonly="" rows="18"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-7">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 mb-3">Résumé</h2><textarea class="form-control font-monospace" id="executionSummaryOutput" readonly="" rows="14">Aucun résultat.</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0 mt-4">
|
||||
<div class="card-body">
|
||||
<h2 class="h5">Plan exact</h2><textarea class="form-control font-monospace" id="executionPlanOutput" readonly="" rows="12">Aucun plan.</textarea>
|
||||
<h2 class="h5 mt-3">Simulation exacte</h2><textarea class="form-control font-monospace" id="executionSimulationOutput" readonly="" rows="12">Aucune simulation.</textarea>
|
||||
<h2 class="h5 mt-3">Confirmation et replay</h2><textarea class="form-control font-monospace" id="executionDiagnosticsOutput" readonly="" rows="12">Aucun diagnostic.</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script defer="" src="ts/demo_execution_solana_core.ts" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,427 @@
|
||||
<!-- file: kb_app_demo/frontend/demo_execution_spl.html -->
|
||||
<!-- version: 8 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
||||
<title>Exécution SPL Devnet</title>
|
||||
<link href="sass/main.scss" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body class="app-body">
|
||||
<header class="app-header bg-dark text-light">
|
||||
<nav class="container-fluid h-100 d-flex align-items-center justify-content-between px-4">
|
||||
<div>
|
||||
<h1 class="h4 mb-0">Exécution SPL Devnet</h1>
|
||||
<small class="text-light-emphasis">Memo, SPL Token classique, Token-2022 et ATA — simulation-first et replay post-exécution</small>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="badge text-bg-success" id="executionStatusBadge">Prêt</span>
|
||||
<button class="btn btn-sm btn-outline-warning" disabled="" id="cancelExecutionButton" type="button">Annuler</button>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<main class="app-main">
|
||||
<div class="osb-scrollable pt-1 pb-4" data-simplebar="">
|
||||
<div class="container-fluid py-4 px-4">
|
||||
<div class="alert alert-warning" role="alert">
|
||||
Cette fenêtre est limitée à Devnet. La clé privée du wallet source reste dans le backend Rust et n’est jamais transmise à la WebView.
|
||||
</div>
|
||||
<div class="row g-4" id="splExecutionWorkspace">
|
||||
<div class="col-12 col-xl-5" id="splExecutionControls">
|
||||
<div class="accordion shadow-sm" id="walletPreparationAccordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="walletPreparationHeading"><button aria-controls="walletPreparationCollapse" aria-expanded="true" class="accordion-button" data-bs-target="#walletPreparationCollapse" data-bs-toggle="collapse" type="button"><span class="d-flex flex-wrap align-items-center justify-content-between gap-2 w-100 me-2"><span>Préparation commune du wallet Devnet</span><span class="badge text-bg-info">Pré-requis</span></span></button></h2>
|
||||
<div aria-labelledby="walletPreparationHeading" class="accordion-collapse collapse show" id="walletPreparationCollapse">
|
||||
<div class="accordion-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="executionProfileSelect">Profil Devnet</label>
|
||||
<select class="form-select" id="executionProfileSelect"></select>
|
||||
<div class="form-text" id="executionProfileHelp"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="executionRecipientInput">Destinataire</label>
|
||||
<div class="input-group">
|
||||
<input autocomplete="off" class="form-control font-monospace" id="executionRecipientInput" type="text" />
|
||||
<button class="btn btn-outline-secondary" id="generateRecipientButton" type="button">Générer</button>
|
||||
</div>
|
||||
<div class="form-text">Une adresse générée ici est jetable : sa clé privée n’est ni conservée ni exposée.</div>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label" for="executionLamportsInput">Lamports transférés</label>
|
||||
<input class="form-control" id="executionLamportsInput" min="1" step="1" type="number" />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label" for="executionAirdropInput">Airdrop maximal</label>
|
||||
<input class="form-control" id="executionAirdropInput" min="0" step="1" type="number" value="0" />
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="form-check form-switch mb-2">
|
||||
<input class="form-check-input" id="executionOperatorConfirmedInput" type="checkbox" />
|
||||
<label class="form-check-label" for="executionOperatorConfirmedInput">Confirmation opérateur explicite</label>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-2">
|
||||
<input class="form-check-input" id="executionForceReplayInput" type="checkbox" />
|
||||
<label class="form-check-label" for="executionForceReplayInput">Forcer le replay post-exécution</label>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input class="form-check-input" id="executionMaterializeInput" type="checkbox" />
|
||||
<label class="form-check-label" for="executionMaterializeInput">Matérialiser après décodage</label>
|
||||
</div>
|
||||
<div class="d-grid gap-2 d-md-flex">
|
||||
<button class="btn btn-outline-primary flex-fill" id="simulateExecutionButton" type="button">Simuler uniquement</button>
|
||||
<button class="btn btn-danger flex-fill" id="submitExecutionButton" type="button">Signer et envoyer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center justify-content-between mt-4 mb-2">
|
||||
<h2 class="h5 mb-0">Exécutions par décodeur</h2><small class="text-body-secondary">Ouvrir une seule famille à la fois</small>
|
||||
</div>
|
||||
<div class="accordion shadow-sm mt-4" id="decoderExecutionAccordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="splMemoDecoderHeading"><button aria-controls="splMemoDecoderCollapse" aria-expanded="false" class="accordion-button collapsed" data-bs-target="#splMemoDecoderCollapse" data-bs-toggle="collapse" type="button"><span class="d-flex flex-wrap align-items-center justify-content-between gap-2 w-100 me-2"><span>Décodeur spl_memo — Memo v4</span><span class="badge text-bg-primary">Exécutable</span></span></button></h2>
|
||||
<div aria-labelledby="splMemoDecoderHeading" class="accordion-collapse collapse" data-bs-parent="#decoderExecutionAccordion" id="splMemoDecoderCollapse">
|
||||
<div class="accordion-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="memoMessageInput">Annotation UTF-8 exacte</label>
|
||||
<textarea class="form-control" id="memoMessageInput" maxlength="566" rows="5">khadhroony-bot2 Memo v4 validation</textarea>
|
||||
<div class="form-text">566 octets UTF-8 maximum. La matérialisation `transaction_annotation` et le second replay d’idempotence sont obligatoires après envoi.</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input checked="" class="form-check-input" id="memoWalletSignerInput" type="checkbox" />
|
||||
<label class="form-check-label" for="memoWalletSignerInput">Inclure le wallet comme signer Memo readonly</label>
|
||||
</div>
|
||||
<div class="alert alert-secondary py-2 small" role="note">
|
||||
Ce parcours réutilise le profil, la confirmation opérateur et l’option de force replay du panneau System ci-dessus. Aucune clé privée ne traverse la WebView.
|
||||
</div>
|
||||
<div class="d-grid gap-2 d-md-flex">
|
||||
<button class="btn btn-outline-info flex-fill" id="simulateMemoButton" type="button">Simuler Memo</button>
|
||||
<button class="btn btn-danger flex-fill" id="submitMemoButton" type="button">Signer et envoyer Memo</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="splTokenDecoderHeading"><button aria-controls="splTokenDecoderCollapse" aria-expanded="false" class="accordion-button collapsed" data-bs-target="#splTokenDecoderCollapse" data-bs-toggle="collapse" type="button"><span class="d-flex flex-wrap align-items-center justify-content-between gap-2 w-100 me-2"><span>Décodeur spl_token — TransferChecked</span><span class="badge text-bg-primary">Exécutable</span></span></button></h2>
|
||||
<div aria-labelledby="splTokenDecoderHeading" class="accordion-collapse collapse" data-bs-parent="#decoderExecutionAccordion" id="splTokenDecoderCollapse">
|
||||
<div class="accordion-body">
|
||||
<div class="alert alert-secondary py-2 small" role="note">
|
||||
Le mint et les decimals sont explicites. Le préflight vérifie les deux comptes, le solde et l’autorité avant toute simulation. L’envoi réutilise le profil et la confirmation opérateur communs.
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="tokenSourceInput">Compte source</label>
|
||||
<input autocomplete="off" class="form-control font-monospace" id="tokenSourceInput" type="text" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="tokenMintInput">Mint</label>
|
||||
<input autocomplete="off" class="form-control font-monospace" id="tokenMintInput" type="text" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="tokenDestinationInput">Compte destination</label>
|
||||
<input autocomplete="off" class="form-control font-monospace" id="tokenDestinationInput" type="text" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="tokenAuthorityInput">Autorité simple</label>
|
||||
<input autocomplete="off" class="form-control font-monospace" id="tokenAuthorityInput" type="text" />
|
||||
<div class="form-text">Doit correspondre au wallet persistant du profil pour un envoi.</div>
|
||||
</div>
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-12 col-md-7">
|
||||
<label class="form-label" for="tokenAmountRawInput">Montant brut exact</label>
|
||||
<input class="form-control font-monospace" id="tokenAmountRawInput" inputmode="numeric" type="text" value="1" />
|
||||
</div>
|
||||
<div class="col-12 col-md-5">
|
||||
<label class="form-label" for="tokenDecimalsInput">Decimals</label>
|
||||
<input class="form-control" id="tokenDecimalsInput" max="255" min="0" step="1" type="number" value="9" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-grid gap-2 d-md-flex">
|
||||
<button class="btn btn-outline-primary flex-fill" id="simulateTokenButton" type="button">Préflight et simuler</button>
|
||||
<button class="btn btn-danger flex-fill" id="submitTokenButton" type="button">Signer et envoyer Token</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="splAtaDecoderHeading"><button aria-controls="splAtaDecoderCollapse" aria-expanded="false" class="accordion-button collapsed" data-bs-target="#splAtaDecoderCollapse" data-bs-toggle="collapse" type="button"><span class="d-flex flex-wrap align-items-center justify-content-between gap-2 w-100 me-2"><span>Décodeur spl_associated_token_account — Create / CreateIdempotent</span><span class="badge text-bg-primary">Exécutable</span></span></button></h2>
|
||||
<div aria-labelledby="splAtaDecoderHeading" class="accordion-collapse collapse" data-bs-parent="#decoderExecutionAccordion" id="splAtaDecoderCollapse">
|
||||
<div class="accordion-body">
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label" for="ataTokenProgramSelect">Token Program cible</label>
|
||||
<select class="form-select" id="ataTokenProgramSelect">
|
||||
<option value="classic">SPL Token classique</option>
|
||||
<option value="token_2022">Token-2022</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label" for="ataModeSelect">Mode</label>
|
||||
<select class="form-select" id="ataModeSelect">
|
||||
<option value="create_idempotent">CreateIdempotent</option>
|
||||
<option value="create">Create</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="ataMintInput">Mint</label>
|
||||
<input autocomplete="off" class="form-control font-monospace" id="ataMintInput" type="text" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="ataPayerInput">Payer readonly</label>
|
||||
<input class="form-control font-monospace" id="ataPayerInput" readonly="" type="text" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="ataWalletOwnerInput">Wallet owner readonly</label>
|
||||
<input class="form-control font-monospace" id="ataWalletOwnerInput" readonly="" type="text" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="ataDerivedInput">ATA dérivé readonly</label>
|
||||
<input class="form-control font-monospace" id="ataDerivedInput" readonly="" type="text" />
|
||||
<div class="form-text">Actualisé depuis le backend avec les seeds wallet, Token Program et mint.</div>
|
||||
</div>
|
||||
<div class="d-grid gap-2 d-md-flex">
|
||||
<button class="btn btn-outline-secondary flex-fill" id="deriveAtaButton" type="button">Dériver</button>
|
||||
<button class="btn btn-outline-primary flex-fill" id="simulateAtaButton" type="button">Préflight et simuler</button>
|
||||
<button class="btn btn-danger flex-fill" id="submitAtaButton" type="button">Confirmer et envoyer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="splToken2022DecoderHeading"><button aria-controls="splToken2022DecoderCollapse" aria-expanded="false" class="accordion-button collapsed" data-bs-target="#splToken2022DecoderCollapse" data-bs-toggle="collapse" type="button"><span class="d-flex flex-wrap align-items-center justify-content-between gap-2 w-100 me-2"><span>Exécution spl_token_2022 — scénarios publics</span><span class="badge text-bg-success">Exécutable</span></span></button></h2>
|
||||
<div aria-labelledby="splToken2022DecoderHeading" class="accordion-collapse collapse" data-bs-parent="#decoderExecutionAccordion" id="splToken2022DecoderCollapse">
|
||||
<div class="accordion-body">
|
||||
<div class="alert alert-info">Chaque opération est simulée sur le message exact avant toute signature. Les scénarios sont indépendants et réutilisent la fixture chargée dans les deux terminaux.</div>
|
||||
<div class="d-grid mb-3"><button class="btn btn-outline-secondary" id="loadToken2022FixtureButton" type="button">Charger la fixture Token-2022</button></div>
|
||||
<div class="form-text mb-3" id="token2022FixtureHelp">Aucune fixture chargée.</div>
|
||||
<div class="mb-3"><label class="form-label" for="token2022ScenarioSelect">Scénario</label><select class="form-select" id="token2022ScenarioSelect">
|
||||
<option value="token_2022_mint_to_checked">MintToChecked</option>
|
||||
<option value="token_2022_transfer_checked">TransferChecked</option>
|
||||
<option value="token_2022_approve_checked">ApproveChecked</option>
|
||||
<option value="token_2022_revoke">Revoke</option>
|
||||
<option value="token_2022_burn_checked">BurnChecked</option>
|
||||
<option value="token_2022_freeze_account">FreezeAccount</option>
|
||||
<option value="token_2022_thaw_account">ThawAccount</option>
|
||||
<option value="token_2022_close_destination">CloseAccount</option>
|
||||
</select></div>
|
||||
<div class="mb-3"><label class="form-label" for="token2022SourceInput">Compte source / compte cible</label><input class="form-control font-monospace" id="token2022SourceInput" type="text" /></div>
|
||||
<div class="mb-3"><label class="form-label" for="token2022MintInput">Mint Token-2022</label><input class="form-control font-monospace" id="token2022MintInput" type="text" /></div>
|
||||
<div class="mb-3"><label class="form-label" for="token2022DestinationInput">Compte destination / destination lamports</label><input class="form-control font-monospace" id="token2022DestinationInput" type="text" /></div>
|
||||
<div class="mb-3"><label class="form-label" for="token2022DelegateInput">Delegate</label><input class="form-control font-monospace" id="token2022DelegateInput" type="text" /></div>
|
||||
<div class="mb-3"><label class="form-label" for="token2022AuthorityInput">Autorité</label><input class="form-control font-monospace" id="token2022AuthorityInput" type="text" /></div>
|
||||
<div class="mb-3"><label class="form-label" for="token2022FreezeAuthorityInput">Freeze authority</label><input class="form-control font-monospace" id="token2022FreezeAuthorityInput" type="text" /></div>
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-8"><label class="form-label" for="token2022AmountInput">Montant brut</label><input class="form-control" id="token2022AmountInput" type="text" value="1" /></div>
|
||||
<div class="col-4"><label class="form-label" for="token2022DecimalsInput">Décimales</label><input class="form-control" id="token2022DecimalsInput" max="255" min="0" type="number" value="9" /></div>
|
||||
</div>
|
||||
<div class="d-grid gap-2 d-md-flex">
|
||||
<button class="btn btn-outline-primary flex-fill" id="simulateToken2022Button" type="button">Préflight et simuler</button>
|
||||
<button class="btn btn-danger flex-fill" id="submitToken2022Button" type="button">Confirmer et envoyer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="splElgamalRegistryDecoderHeading"><button aria-controls="splElgamalRegistryDecoderCollapse" aria-expanded="false" class="accordion-button collapsed" data-bs-target="#splElgamalRegistryDecoderCollapse" data-bs-toggle="collapse" type="button"><span class="d-flex flex-wrap align-items-center justify-content-between gap-2 w-100 me-2"><span>Décodeur spl_elgamal_registry — ElGamal Registry</span><span class="badge text-bg-primary">Plan exact</span></span></button></h2>
|
||||
<div aria-labelledby="splElgamalRegistryDecoderHeading" class="accordion-collapse collapse" data-bs-parent="#decoderExecutionAccordion" id="splElgamalRegistryDecoderCollapse">
|
||||
<div class="accordion-body">
|
||||
<div class="alert alert-info">Première démo : dériver le Registry PDA et construire le plan exact avec un Proof Context State existant.</div>
|
||||
<div class="mb-3"><label class="form-label" for="elgamalFeePayerInput">Fee payer</label><input class="form-control font-monospace" id="elgamalFeePayerInput" type="text" /></div>
|
||||
<div class="mb-3"><label class="form-label" for="elgamalOwnerInput">Owner du registre</label><input class="form-control font-monospace" id="elgamalOwnerInput" type="text" /></div>
|
||||
<div class="mb-3"><label class="form-label" for="elgamalOperationSelect">Opération</label><select class="form-select" id="elgamalOperationSelect">
|
||||
<option value="create_registry">CreateRegistry</option>
|
||||
<option value="update_registry">UpdateRegistry</option>
|
||||
</select></div>
|
||||
<div class="mb-3"><label class="form-label" for="elgamalProofContextInput">Proof Context State account</label><input class="form-control font-monospace" id="elgamalProofContextInput" type="text" /></div><button class="btn btn-outline-primary w-100" id="buildElgamalRegistryPlanButton" type="button">Construire le plan Registry</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-secondary mt-4 mb-0">
|
||||
<strong>Scénarios avec preuve.</strong> CreateRegistry, UpdateRegistry et ConfigureAccountWithRegistry restent dans le panneau ElGamal/Confidential et exigent un Proof Context State valide. Ils ne sont pas présentés comme des scénarios publics exécutables.
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-7" id="splExecutionResults">
|
||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||
<h2 class="h5 mb-0">Résultats détaillés</h2><small class="text-body-secondary">Plan, simulation, confirmation et journaux métier</small>
|
||||
</div>
|
||||
<div class="accordion mt-4" id="executionDetailsAccordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed" data-bs-target="#executionPlanCollapse" data-bs-toggle="collapse" type="button">Plan exact</button>
|
||||
</h2>
|
||||
<div class="accordion-collapse collapse" data-bs-parent="#executionDetailsAccordion" id="executionPlanCollapse">
|
||||
<div class="accordion-body">
|
||||
<textarea class="form-control font-monospace" id="executionPlanOutput" readonly="" rows="16">Aucun plan.</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed" data-bs-target="#executionSimulationCollapse" data-bs-toggle="collapse" type="button">Simulation exacte</button>
|
||||
</h2>
|
||||
<div class="accordion-collapse collapse" data-bs-parent="#executionDetailsAccordion" id="executionSimulationCollapse">
|
||||
<div class="accordion-body">
|
||||
<textarea class="form-control font-monospace" id="executionSimulationOutput" readonly="" rows="16">Aucune simulation.</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed" data-bs-target="#executionDiagnosticsCollapse" data-bs-toggle="collapse" type="button">Confirmation et replay</button>
|
||||
</h2>
|
||||
<div class="accordion-collapse collapse" data-bs-parent="#executionDetailsAccordion" id="executionDiagnosticsCollapse">
|
||||
<div class="accordion-body">
|
||||
<textarea class="form-control font-monospace" id="executionDiagnosticsOutput" readonly="" rows="16">Aucun diagnostic.</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion shadow-sm mt-4" id="splBusinessJournalsAccordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="splTokenJournalHeading"><button aria-controls="splTokenJournalCollapse" aria-expanded="false" class="accordion-button collapsed" data-bs-target="#splTokenJournalCollapse" data-bs-toggle="collapse" type="button">Journal métier SPL Token</button></h2>
|
||||
<div aria-labelledby="splTokenJournalHeading" class="accordion-collapse collapse" data-bs-parent="#splBusinessJournalsAccordion" id="splTokenJournalCollapse">
|
||||
<div class="accordion-body p-0" id="splTokenJournalHost">
|
||||
<div class="card border-0 rounded-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
|
||||
<div>
|
||||
<h2 class="h5 mb-0">Journal métier SPL Token</h2>
|
||||
<div class="form-text">Événements commités uniquement, sans solde final reconstruit ni OHLC.</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-primary" id="refreshTokenJournalButton" type="button">Actualiser</button>
|
||||
</div>
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-12 col-lg-6">
|
||||
<label class="form-label" for="tokenJournalSignatureInput">Signature contient</label>
|
||||
<input class="form-control font-monospace" id="tokenJournalSignatureInput" type="text" />
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
<label class="form-label" for="tokenJournalMintInput">Mint exact</label>
|
||||
<input class="form-control font-monospace" id="tokenJournalMintInput" type="text" />
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
<label class="form-label" for="tokenJournalAccountInput">Compte exact</label>
|
||||
<input class="form-control font-monospace" id="tokenJournalAccountInput" type="text" />
|
||||
</div>
|
||||
<div class="col-6 col-lg-3">
|
||||
<label class="form-label" for="tokenJournalFamilySelect">Famille</label>
|
||||
<select class="form-select" id="tokenJournalFamilySelect">
|
||||
<option value="">Toutes</option>
|
||||
<option value="token_account">token_account</option>
|
||||
<option value="admin">admin</option>
|
||||
<option value="risk">risk</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6 col-lg-3">
|
||||
<label class="form-label" for="tokenJournalLimitInput">Limite</label>
|
||||
<input class="form-control" id="tokenJournalLimitInput" max="500" min="1" step="1" type="number" value="100" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="tokenJournalOperationInput">Opération exacte</label>
|
||||
<input class="form-control font-monospace" id="tokenJournalOperationInput" placeholder="transfer_checked" type="text" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Slot</th>
|
||||
<th>Opération</th>
|
||||
<th>Famille</th>
|
||||
<th>Montant brut</th>
|
||||
<th>Signature</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tokenJournalRows">
|
||||
<tr>
|
||||
<td class="text-body-secondary" colspan="5">Aucune lecture.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<label class="form-label" for="tokenJournalDetailOutput">Détail JSON de la ligne sélectionnée</label>
|
||||
<textarea class="form-control font-monospace" id="tokenJournalDetailOutput" readonly="" rows="14">Aucun événement sélectionné.</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="ataLifecycleJournalHeading"><button aria-controls="ataLifecycleJournalCollapse" aria-expanded="false" class="accordion-button collapsed" data-bs-target="#ataLifecycleJournalCollapse" data-bs-toggle="collapse" type="button">Journal lifecycle ATA</button></h2>
|
||||
<div aria-labelledby="ataLifecycleJournalHeading" class="accordion-collapse collapse" data-bs-parent="#splBusinessJournalsAccordion" id="ataLifecycleJournalCollapse">
|
||||
<div class="accordion-body p-0" id="ataLifecycleJournalHost">
|
||||
<div class="card border-0 rounded-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
|
||||
<div>
|
||||
<h2 class="h5 mb-0">Journal lifecycle ATA</h2>
|
||||
<div class="form-text">Création, réutilisation idempotente et récupération nested, sans duplication des CPI Token.</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-primary" id="refreshAtaJournalButton" type="button">Actualiser</button>
|
||||
</div>
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-12 col-lg-6">
|
||||
<label class="form-label" for="ataJournalSignatureInput">Signature contient</label>
|
||||
<input class="form-control font-monospace" id="ataJournalSignatureInput" type="text" />
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
<label class="form-label" for="ataJournalMintInput">Mint exact</label>
|
||||
<input class="form-control font-monospace" id="ataJournalMintInput" type="text" />
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
<label class="form-label" for="ataJournalAddressInput">ATA exact</label>
|
||||
<input class="form-control font-monospace" id="ataJournalAddressInput" type="text" />
|
||||
</div>
|
||||
<div class="col-8 col-lg-4">
|
||||
<label class="form-label" for="ataJournalOperationInput">Opération</label>
|
||||
<input class="form-control font-monospace" id="ataJournalOperationInput" type="text" />
|
||||
</div>
|
||||
<div class="col-4 col-lg-2">
|
||||
<label class="form-label" for="ataJournalLimitInput">Limite</label>
|
||||
<input class="form-control" id="ataJournalLimitInput" max="500" min="1" type="number" value="100" />
|
||||
</div>
|
||||
</div>
|
||||
<textarea class="form-control font-monospace" id="ataJournalOutput" readonly="" rows="14">Aucune lecture.</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0 mt-4">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 mb-3">Résumé</h2>
|
||||
<textarea class="form-control font-monospace" id="executionSummaryOutput" readonly="" rows="14">Aucun résultat.</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0 mt-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h2 class="h5 mb-0">Journal</h2>
|
||||
<button class="btn btn-sm btn-outline-secondary" id="clearExecutionLogButton" type="button">Effacer</button>
|
||||
</div>
|
||||
<textarea class="form-control font-monospace" id="executionLogOutput" readonly="" rows="18"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="app-footer bg-dark text-light">
|
||||
<div class="container h-100 d-flex align-items-center">
|
||||
<div class="row flex-grow-1 align-items-center">
|
||||
<div class="col-12 text-center text-small my-1 my-md-0">© 2026 SASEDEV</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script defer="" src="ts/demo_execution_spl.ts" type="module"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,96 @@
|
||||
<!-- file: kb_app_demo/frontend/demo_http.html -->
|
||||
<!-- version: 3 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Khadhroony Bot2 - HTTP JSON-RPC</title>
|
||||
<link rel="stylesheet" href="sass/main.scss" />
|
||||
</head>
|
||||
|
||||
<body class="bg-body-tertiary">
|
||||
<header class="app-header">
|
||||
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
|
||||
<div class="container my-0">
|
||||
<a class="navbar-brand d-flex align-items-center" href="#">
|
||||
<img alt="Logo" src="imgs/logo.png" class="app-logo" />
|
||||
<span class="ps-2 fs-4 fw-bold text-primary font-logo">HTTP JSON-RPC</span>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="app-main">
|
||||
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
|
||||
<div class="container py-4">
|
||||
<div class="row g-4">
|
||||
<div class="col-12 col-xl-5">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h1 class="h4 card-title">Requête HTTP Solana standard</h1>
|
||||
<p class="text-body-secondary">Cette démo route une requête JSON-RPC HTTP par rôle d'endpoint.</p>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="httpRoleSelect">Rôle</label>
|
||||
<select id="httpRoleSelect" class="form-select"></select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="httpMethodSelect">Méthode</label>
|
||||
<select id="httpMethodSelect" class="form-select"></select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="httpFirstArgInput">Premier argument</label>
|
||||
<input id="httpFirstArgInput" class="form-control" placeholder="Signature, pubkey ou transaction encodée" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="httpConfigInput">Config JSON</label>
|
||||
<textarea id="httpConfigInput" class="form-control font-monospace" rows="4" placeholder='{"commitment":"confirmed"}'></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="httpParamsInput">Params JSON complet</label>
|
||||
<textarea id="httpParamsInput" class="form-control font-monospace" rows="4" placeholder='["pubkey", {"encoding":"jsonParsed"}]'></textarea>
|
||||
<div class="form-text">Si renseigné, ce tableau JSON remplace le premier argument et la config JSON.</div>
|
||||
</div>
|
||||
<button id="executeHttpButton" class="btn btn-primary">Exécuter</button>
|
||||
<button id="refreshHttpPoolButton" class="btn btn-outline-secondary ms-2">Rafraîchir endpoints</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-7">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h2 class="h5 card-title mb-0">Endpoints HTTP</h2>
|
||||
<button id="copyHttpPoolButton" class="btn btn-sm btn-outline-secondary">Copier</button>
|
||||
</div>
|
||||
<textarea id="httpPoolOutput" class="form-control font-monospace" rows="10" readonly>Chargement...</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h2 class="h5 card-title mb-0">Résultat</h2>
|
||||
<button id="copyHttpResultButton" class="btn btn-sm btn-outline-secondary">Copier</button>
|
||||
</div>
|
||||
<textarea id="httpResultOutput" class="form-control font-monospace" rows="18" readonly>Aucune requête exécutée.</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="app-footer bg-dark text-light">
|
||||
<div class="container h-100 d-flex align-items-center">
|
||||
<div class="row flex-grow-1 align-items-center">
|
||||
<div class="col-12 text-center text-small my-1 my-md-0">© 2026 SASEDEV</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="ts/demo_http.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,115 @@
|
||||
<!-- file: kb_app_demo/frontend/demo_sql_diag.html -->
|
||||
<!-- version: 2 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Khadhroony-Bot2 - SQL diagnostics</title>
|
||||
<link rel="stylesheet" href="sass/main.scss" />
|
||||
</head>
|
||||
|
||||
<body class="bg-body-tertiary">
|
||||
<header class="app-header">
|
||||
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
|
||||
<div class="container my-0">
|
||||
<a class="navbar-brand d-flex align-items-center" href="main.html">
|
||||
<img alt="Logo" src="imgs/logo.png" class="app-logo" />
|
||||
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Bot2</span>
|
||||
</a>
|
||||
<div class="ms-auto d-flex align-items-center gap-2">
|
||||
<span class="badge text-bg-primary">SQL diagnostics</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="app-main">
|
||||
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
|
||||
<div class="container py-4">
|
||||
<div class="row g-4">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-center mb-3">
|
||||
<h1 class="h3 card-title mb-0">Diagnostic PostgreSQL</h1>
|
||||
<div class="d-flex gap-2">
|
||||
<button id="refreshSqlDiagButton" class="btn btn-primary" type="button">Rafraîchir</button>
|
||||
<button id="copySqlDiagButton" class="btn btn-outline-secondary" type="button">Copier JSON</button>
|
||||
</div>
|
||||
</div>
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-sm-3">Statut</dt>
|
||||
<dd class="col-sm-9"><code id="sqlDiagStatus">Chargement...</code></dd>
|
||||
<dt class="col-sm-3">Fichier config</dt>
|
||||
<dd class="col-sm-9"><code id="sqlDiagConfigPath">—</code></dd>
|
||||
<dt class="col-sm-3">Profil</dt>
|
||||
<dd class="col-sm-9"><code id="sqlDiagProfile">—</code></dd>
|
||||
<dt class="col-sm-3">Backend</dt>
|
||||
<dd class="col-sm-9"><code id="sqlDiagBackend">—</code></dd>
|
||||
<dt class="col-sm-3">DSN</dt>
|
||||
<dd class="col-sm-9"><code id="sqlDiagDsn">—</code></dd>
|
||||
<dt class="col-sm-3">Schema courant</dt>
|
||||
<dd class="col-sm-9"><code id="sqlDiagSchema">—</code></dd>
|
||||
<dt class="col-sm-3">Healthcheck</dt>
|
||||
<dd class="col-sm-9"><code id="sqlDiagHealth">—</code></dd>
|
||||
<dt class="col-sm-3">Migrations</dt>
|
||||
<dd class="col-sm-9"><code id="sqlDiagMigration">—</code></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 card-title mb-3">Tables raw/core connues</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Table</th>
|
||||
<th>Domaine</th>
|
||||
<th>Rôle</th>
|
||||
<th>Existe</th>
|
||||
<th>Lignes</th>
|
||||
<th>Slot min</th>
|
||||
<th>Slot max</th>
|
||||
<th>Dernière insertion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="sqlDiagTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 card-title mb-3">Payload JSON</h2>
|
||||
<div id="sqlDiagJson" class="app-config-box app-json-viewer mb-0">Chargement...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="app-footer bg-dark text-light">
|
||||
<div class="container h-100 d-flex align-items-center">
|
||||
<div class="row flex-grow-1 align-items-center">
|
||||
<div class="col-12 text-center text-small my-1 my-md-0">
|
||||
© 2026 SASEDEV
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="ts/demo_sql_diag.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,102 @@
|
||||
<!-- file: kb_app_demo/frontend/demo_sql_pg_core.html -->
|
||||
<!-- version: 3 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Khadhroony-Bot2 - PostgreSQL core store</title>
|
||||
<link rel="stylesheet" href="sass/main.scss" />
|
||||
</head>
|
||||
|
||||
<body class="bg-body-tertiary">
|
||||
<header class="app-header">
|
||||
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
|
||||
<div class="container my-0">
|
||||
<a class="navbar-brand d-flex align-items-center" href="main.html">
|
||||
<img alt="Logo" src="imgs/logo.png" class="app-logo" />
|
||||
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Bot2</span>
|
||||
</a>
|
||||
<div class="ms-auto d-flex align-items-center gap-2">
|
||||
<span class="badge text-bg-primary">PostgreSQL core</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="app-main">
|
||||
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
|
||||
<div class="container py-4">
|
||||
<div class="row g-4">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-center mb-3">
|
||||
<h1 class="h3 card-title mb-0">Core store Solana</h1>
|
||||
<button id="refreshSqlCoreButton" class="btn btn-primary" type="button">Rafraîchir</button>
|
||||
</div>
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-sm-3">Statut</dt>
|
||||
<dd class="col-sm-9"><code id="sqlCoreStatus">Chargement...</code></dd>
|
||||
<dt class="col-sm-3">Profil</dt>
|
||||
<dd class="col-sm-9"><code id="sqlCoreProfile">—</code></dd>
|
||||
<dt class="col-sm-3">DSN</dt>
|
||||
<dd class="col-sm-9"><code id="sqlCoreDsn">—</code></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 card-title mb-3">Tables core</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Table</th>
|
||||
<th>Domaine</th>
|
||||
<th>Rôle</th>
|
||||
<th>Existe</th>
|
||||
<th>Lignes</th>
|
||||
<th>Slot min</th>
|
||||
<th>Slot max</th>
|
||||
<th>Dernière insertion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="sqlCoreTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 card-title mb-3">Payload JSON</h2>
|
||||
<div id="sqlCoreJson" class="app-config-box app-json-viewer mb-0">Chargement...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="app-footer bg-dark text-light">
|
||||
<div class="container h-100 d-flex align-items-center">
|
||||
<div class="row flex-grow-1 align-items-center">
|
||||
<div class="col-12 text-center text-small my-1 my-md-0">
|
||||
© 2026 SASEDEV
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="ts/demo_sql_pg_core.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,102 @@
|
||||
<!-- file: kb_app_demo/frontend/demo_sql_pg_raw.html -->
|
||||
<!-- version: 4 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Khadhroony-Bot2 - PostgreSQL canonical acquisition</title>
|
||||
<link rel="stylesheet" href="sass/main.scss" />
|
||||
</head>
|
||||
|
||||
<body class="bg-body-tertiary">
|
||||
<header class="app-header">
|
||||
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
|
||||
<div class="container my-0">
|
||||
<a class="navbar-brand d-flex align-items-center" href="main.html">
|
||||
<img alt="Logo" src="imgs/logo.png" class="app-logo" />
|
||||
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Bot2</span>
|
||||
</a>
|
||||
<div class="ms-auto d-flex align-items-center gap-2">
|
||||
<span class="badge text-bg-primary">PostgreSQL acquisition</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="app-main">
|
||||
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
|
||||
<div class="container py-4">
|
||||
<div class="row g-4">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-center mb-3">
|
||||
<h1 class="h3 card-title mb-0">Acquisition transactionnelle Solana</h1>
|
||||
<button id="refreshSqlRawButton" class="btn btn-primary" type="button">Rafraîchir</button>
|
||||
</div>
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-sm-3">Statut</dt>
|
||||
<dd class="col-sm-9"><code id="sqlRawStatus">Chargement...</code></dd>
|
||||
<dt class="col-sm-3">Profil</dt>
|
||||
<dd class="col-sm-9"><code id="sqlRawProfile">—</code></dd>
|
||||
<dt class="col-sm-3">DSN</dt>
|
||||
<dd class="col-sm-9"><code id="sqlRawDsn">—</code></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 card-title mb-3">Transactions canoniques et observations</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Table</th>
|
||||
<th>Domaine</th>
|
||||
<th>Rôle</th>
|
||||
<th>Existe</th>
|
||||
<th>Lignes</th>
|
||||
<th>Slot min</th>
|
||||
<th>Slot max</th>
|
||||
<th>Dernière insertion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="sqlRawTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 card-title mb-3">Payload JSON</h2>
|
||||
<div id="sqlRawJson" class="app-config-box app-json-viewer mb-0">Chargement...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="app-footer bg-dark text-light">
|
||||
<div class="container h-100 d-flex align-items-center">
|
||||
<div class="row flex-grow-1 align-items-center">
|
||||
<div class="col-12 text-center text-small my-1 my-md-0">
|
||||
© 2026 SASEDEV
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="ts/demo_sql_pg_raw.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,443 @@
|
||||
<!-- file: kb_app_demo/frontend/demo_sql_replay_candidates.html -->
|
||||
<!-- version: 6 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Khadhroony Bot2 - Candidats de replay SQL</title>
|
||||
<link rel="stylesheet" href="sass/main.scss" />
|
||||
</head>
|
||||
|
||||
<body class="bg-body-tertiary">
|
||||
<header class="app-header">
|
||||
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
|
||||
<div class="container-fluid px-4 my-0">
|
||||
<a class="navbar-brand d-flex align-items-center" href="main.html">
|
||||
<img alt="Logo" src="imgs/logo.png" class="app-logo" />
|
||||
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Candidats de replay SQL</span>
|
||||
</a>
|
||||
<div class="ms-auto d-flex align-items-center gap-2">
|
||||
<span id="replayCandidateProfileBadge" class="badge text-bg-info">Profil —</span>
|
||||
<span id="replayCandidateStatusBadge" class="badge text-bg-secondary">Initialisation</span>
|
||||
<button id="openCoreExtractionButton" class="btn btn-sm btn-outline-primary" type="button">Ouvrir extraction core</button>
|
||||
</div>
|
||||
</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="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<h1 class="h3 card-title">Exploration read-only des candidats</h1>
|
||||
<p class="text-body-secondary mb-2">Les requêtes sont paramétrées, bornées et sans SQL libre. Utilisez les cases à cocher pour sélectionner les lignes. La case du header sélectionne ou désélectionne toutes les lignes correspondant au filtre local. Sans sélection explicite, la copie et l’export CSV utilisent toutes les lignes filtrées. Les CSV UTF-8 avec BOM et séparateur point-virgule sont écrits par le backend Tauri dans <code>./data/exports_csv/</code>. LibreOffice Calc peut toujours afficher son dialogue d’import, car le format CSV ne décrit pas formellement son encodage ni son séparateur.</p>
|
||||
<div class="small text-body-secondary">
|
||||
<span class="fw-semibold">DSN :</span>
|
||||
<code id="replayCandidateDsn">—</code>
|
||||
<span class="ms-3 fw-semibold">Limite serveur maximale :</span>
|
||||
<code id="replayCandidateMaximumLimit">—</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="nav nav-tabs mb-3" id="replayCandidateTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="transactionsTabButton" data-bs-toggle="tab" data-bs-target="#transactionsTabPane" type="button" role="tab" aria-controls="transactionsTabPane" aria-selected="true">Transactions et signatures</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="programsTabButton" data-bs-toggle="tab" data-bs-target="#programsTabPane" type="button" role="tab" aria-controls="programsTabPane" aria-selected="false">Programmes</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="mintsTabButton" data-bs-toggle="tab" data-bs-target="#mintsTabPane" type="button" role="tab" aria-controls="mintsTabPane" aria-selected="false">Mints</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="ownersTabButton" data-bs-toggle="tab" data-bs-target="#ownersTabPane" type="button" role="tab" aria-controls="ownersTabPane" aria-selected="false">Owners</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="accountsTabButton" data-bs-toggle="tab" data-bs-target="#accountsTabPane" type="button" role="tab" aria-controls="accountsTabPane" aria-selected="false">Comptes</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="replayCandidateTabContent">
|
||||
<section class="tab-pane fade show active" id="transactionsTabPane" role="tabpanel" aria-labelledby="transactionsTabButton" tabindex="0">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 card-title">Filtres PostgreSQL bornés</h2>
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-12 col-xl-4">
|
||||
<label for="transactionSignatureContainsInput" class="form-label">Signature contient</label>
|
||||
<input id="transactionSignatureContainsInput" class="form-control font-monospace" type="text" autocomplete="off" />
|
||||
</div>
|
||||
<div class="col-6 col-md-3 col-xl-2">
|
||||
<label for="transactionMinSlotInput" class="form-label">Slot minimum</label>
|
||||
<input id="transactionMinSlotInput" class="form-control" type="number" min="0" step="1" />
|
||||
</div>
|
||||
<div class="col-6 col-md-3 col-xl-2">
|
||||
<label for="transactionMaxSlotInput" class="form-label">Slot maximum</label>
|
||||
<input id="transactionMaxSlotInput" class="form-control" type="number" min="0" step="1" />
|
||||
</div>
|
||||
<div class="col-6 col-md-3 col-xl-2">
|
||||
<label for="transactionRawStateSelect" class="form-label">État raw</label>
|
||||
<select id="transactionRawStateSelect" class="form-select">
|
||||
<option value="">Tous</option>
|
||||
<option value="received">received</option>
|
||||
<option value="core_extracted">core_extracted</option>
|
||||
<option value="decoded">decoded</option>
|
||||
<option value="materialized">materialized</option>
|
||||
<option value="failed">failed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6 col-md-3 col-xl-2">
|
||||
<label for="transactionLedgerStatusSelect" class="form-label">Ledger core</label>
|
||||
<select id="transactionLedgerStatusSelect" class="form-select">
|
||||
<option value="">Tous</option>
|
||||
<option value="not_started">not_started</option>
|
||||
<option value="running">running</option>
|
||||
<option value="succeeded">succeeded</option>
|
||||
<option value="failed">failed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-xl-5">
|
||||
<label for="transactionProgramIdInput" class="form-label">Program ID exact</label>
|
||||
<input id="transactionProgramIdInput" class="form-control font-monospace" type="text" list="knownProgramDatalist" autocomplete="off" />
|
||||
</div>
|
||||
<div class="col-6 col-md-3 col-xl-2">
|
||||
<label for="transactionProgramScopeSelect" class="form-label">Portée programme</label>
|
||||
<select id="transactionProgramScopeSelect" class="form-select">
|
||||
<option value="any">outer + inner + logs</option>
|
||||
<option value="outer">outer uniquement</option>
|
||||
<option value="inner">inner uniquement</option>
|
||||
<option value="logs">logs liés uniquement</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6 col-md-3 col-xl-2">
|
||||
<label for="transactionEntityKindSelect" class="form-label">Type d’entité</label>
|
||||
<select id="transactionEntityKindSelect" class="form-select">
|
||||
<option value="">Aucune</option>
|
||||
<option value="mint">mint</option>
|
||||
<option value="owner">owner</option>
|
||||
<option value="account_key">account_key</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-xl-3">
|
||||
<label for="transactionEntityValueInput" class="form-label">Entité exacte</label>
|
||||
<input id="transactionEntityValueInput" class="form-control font-monospace" type="text" autocomplete="off" />
|
||||
</div>
|
||||
<div class="col-6 col-md-3 col-xl-1">
|
||||
<label for="transactionLimitInput" class="form-label">Limite</label>
|
||||
<input id="transactionLimitInput" class="form-control" type="number" min="1" max="5000" step="1" value="500" />
|
||||
</div>
|
||||
<div class="col-6 col-md-3 col-xl-2">
|
||||
<div class="form-check form-switch mb-2">
|
||||
<input id="transactionNewestFirstInput" class="form-check-input" type="checkbox" role="switch" checked />
|
||||
<label class="form-check-label" for="transactionNewestFirstInput">Plus récents d’abord</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-3">
|
||||
<div class="d-grid d-sm-flex gap-2">
|
||||
<button id="resetTransactionsButton" class="btn btn-outline-secondary" type="button">Réinitialiser</button>
|
||||
<button id="loadTransactionsButton" class="btn btn-primary flex-grow-1" type="button">Charger les transactions</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
|
||||
<div>
|
||||
<h2 class="h5 card-title mb-1">Transactions candidates</h2>
|
||||
<div id="transactionResultSummary" class="small text-body-secondary">Aucune requête exécutée.</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="btn-group flex-wrap" role="group" aria-label="Actions signatures">
|
||||
<button id="copySignaturesButton" class="btn btn-sm btn-outline-primary" type="button">Copier signatures multiligne</button>
|
||||
<button id="exportTransactionsCsvButton" class="btn btn-sm btn-outline-success" type="button">Exporter CSV</button>
|
||||
</div>
|
||||
<div class="small text-body-secondary mt-1">Les identifiants longs sont tronqués à l’écran. Survolez-les pour afficher leur valeur complète ou utilisez le bouton de copie individuel.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table id="transactionCandidateTable" class="table table-striped table-hover table-sm align-middle w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="replay-select-column"><span class="visually-hidden">Sélection</span></th>
|
||||
<th>Signature</th>
|
||||
<th>Slot</th>
|
||||
<th>Raw</th>
|
||||
<th title="État de présence core ; échec on-chain signifie que la transaction Solana a échoué, pas que l’extraction core a échoué.">Core</th>
|
||||
<th>Ledger</th>
|
||||
<th>Version</th>
|
||||
<th>Essais</th>
|
||||
<th data-bs-toggle="tooltip" title="Nombre total d’instructions top-level.">Outer inst.</th>
|
||||
<th data-bs-toggle="tooltip" title="Nombre total d’instructions internes.">Inner inst.</th>
|
||||
<th data-bs-toggle="tooltip" title="Nombre de program IDs distincts dans les instructions top-level.">Outer pr.</th>
|
||||
<th data-bs-toggle="tooltip" title="Nombre de program IDs distincts dans les instructions internes.">Inner pr.</th>
|
||||
<th>Mise à jour</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="tab-pane fade" id="programsTabPane" role="tabpanel" aria-labelledby="programsTabButton" tabindex="0">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-12 col-lg-5">
|
||||
<label for="knownProgramSelect" class="form-label">Programme connu (kb_program_ids)</label>
|
||||
<select id="knownProgramSelect" class="form-select">
|
||||
<option value="">Tous les programmes connus</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4">
|
||||
<label for="programContainsInput" class="form-label">Program ID contient</label>
|
||||
<input id="programContainsInput" class="form-control font-monospace" type="text" list="knownProgramDatalist" autocomplete="off" />
|
||||
</div>
|
||||
<div class="col-6 col-lg-1">
|
||||
<label for="programLimitInput" class="form-label">Limite</label>
|
||||
<input id="programLimitInput" class="form-control" type="number" min="1" max="5000" step="1" value="500" />
|
||||
</div>
|
||||
<div class="col-12 col-lg-2">
|
||||
<div class="d-grid gap-2">
|
||||
<button id="resetProgramsButton" class="btn btn-outline-secondary" type="button">Réinitialiser</button>
|
||||
<button id="loadProgramsButton" class="btn btn-primary" type="button">Charger les programmes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
|
||||
<div>
|
||||
<h2 class="h5 card-title mb-1">Programmes indexés</h2>
|
||||
<div id="programResultSummary" class="small text-body-secondary">Aucune requête exécutée.</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="btn-group flex-wrap" role="group" aria-label="Actions programmes">
|
||||
<button id="useSelectedProgramButton" class="btn btn-sm btn-outline-secondary" type="button">Utiliser comme filtre transactions</button>
|
||||
<button id="copyProgramsButton" class="btn btn-sm btn-outline-primary" type="button">Copier IDs multiligne</button>
|
||||
<button id="exportProgramsCsvButton" class="btn btn-sm btn-outline-success" type="button">Exporter CSV</button>
|
||||
</div>
|
||||
<div class="small text-body-secondary mt-1">Cochez un programme. Sans sélection, l’action accepte une seule ligne restant après le filtre local.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table id="programCandidateTable" class="table table-striped table-hover table-sm align-middle w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="replay-select-column"><span class="visually-hidden">Sélection</span></th>
|
||||
<th>Code connu</th>
|
||||
<th>Program ID</th>
|
||||
<th>Transactions</th>
|
||||
<th>Outer</th>
|
||||
<th>Inner</th>
|
||||
<th>Logs liés</th>
|
||||
<th>Slot min</th>
|
||||
<th>Slot max</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="tab-pane fade" id="mintsTabPane" role="tabpanel" aria-labelledby="mintsTabButton" tabindex="0">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-12 col-lg-7">
|
||||
<label for="mintContainsInput" class="form-label">Mint contient</label>
|
||||
<input id="mintContainsInput" class="form-control font-monospace" type="text" autocomplete="off" />
|
||||
</div>
|
||||
<div class="col-6 col-lg-2">
|
||||
<label for="mintLimitInput" class="form-label">Limite</label>
|
||||
<input id="mintLimitInput" class="form-control" type="number" min="1" max="5000" step="1" value="500" />
|
||||
</div>
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="d-grid d-sm-flex gap-2">
|
||||
<button id="resetMintsButton" class="btn btn-outline-secondary" type="button">Réinitialiser</button>
|
||||
<button id="loadMintsButton" class="btn btn-primary flex-grow-1" type="button">Charger les mints</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
|
||||
<div>
|
||||
<h2 class="h5 card-title mb-1">Mints</h2>
|
||||
<div id="mintResultSummary" class="small text-body-secondary">Aucune requête exécutée.</div>
|
||||
<div class="small text-body-secondary">Source : <code>kb_sol_core_balance_changes.mint</code>.</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="btn-group flex-wrap" role="group" aria-label="Actions mints">
|
||||
<button id="useSelectedMintsButton" class="btn btn-sm btn-outline-secondary" type="button">Utiliser comme filtre transactions</button>
|
||||
<button id="copyMintsButton" class="btn btn-sm btn-outline-primary" type="button">Copier valeurs multiligne</button>
|
||||
<button id="exportMintsCsvButton" class="btn btn-sm btn-outline-success" type="button">Exporter CSV</button>
|
||||
</div>
|
||||
<div class="small text-body-secondary mt-1">Cochez une ligne. Sans sélection, l’action accepte une seule ligne restant après le filtre local.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table id="mintCandidateTable" class="table table-striped table-hover table-sm align-middle w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="replay-select-column"><span class="visually-hidden">Sélection</span></th>
|
||||
<th>Mint</th>
|
||||
<th data-bs-toggle="tooltip" title="Nombre de signatures distinctes dans lesquelles la valeur apparaît.">Transactions</th>
|
||||
<th data-bs-toggle="tooltip" title="Nombre total de lignes core portant cette valeur.">Occurrences</th>
|
||||
<th data-bs-toggle="tooltip" title="Occurrences divisées par le nombre de transactions distinctes.">Occ./tx</th>
|
||||
<th data-bs-toggle="tooltip" title="Premier slot observé dans le corpus core.">Slot min</th>
|
||||
<th data-bs-toggle="tooltip" title="Dernier slot observé dans le corpus core.">Slot max</th>
|
||||
<th data-bs-toggle="tooltip" title="Différence entre le slot maximal et le slot minimal.">Étendue</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="tab-pane fade" id="ownersTabPane" role="tabpanel" aria-labelledby="ownersTabButton" tabindex="0">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-12 col-lg-7">
|
||||
<label for="ownerContainsInput" class="form-label">Owner contient</label>
|
||||
<input id="ownerContainsInput" class="form-control font-monospace" type="text" autocomplete="off" />
|
||||
</div>
|
||||
<div class="col-6 col-lg-2">
|
||||
<label for="ownerLimitInput" class="form-label">Limite</label>
|
||||
<input id="ownerLimitInput" class="form-control" type="number" min="1" max="5000" step="1" value="500" />
|
||||
</div>
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="d-grid d-sm-flex gap-2">
|
||||
<button id="resetOwnersButton" class="btn btn-outline-secondary" type="button">Réinitialiser</button>
|
||||
<button id="loadOwnersButton" class="btn btn-primary flex-grow-1" type="button">Charger les owners</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
|
||||
<div>
|
||||
<h2 class="h5 card-title mb-1">Owners</h2>
|
||||
<div id="ownerResultSummary" class="small text-body-secondary">Aucune requête exécutée.</div>
|
||||
<div class="small text-body-secondary">Source : <code>kb_sol_core_balance_changes.owner</code>.</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="btn-group flex-wrap" role="group" aria-label="Actions owners">
|
||||
<button id="useSelectedOwnersButton" class="btn btn-sm btn-outline-secondary" type="button">Utiliser comme filtre transactions</button>
|
||||
<button id="copyOwnersButton" class="btn btn-sm btn-outline-primary" type="button">Copier valeurs multiligne</button>
|
||||
<button id="exportOwnersCsvButton" class="btn btn-sm btn-outline-success" type="button">Exporter CSV</button>
|
||||
</div>
|
||||
<div class="small text-body-secondary mt-1">Cochez une ligne. Sans sélection, l’action accepte une seule ligne restant après le filtre local.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table id="ownerCandidateTable" class="table table-striped table-hover table-sm align-middle w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="replay-select-column"><span class="visually-hidden">Sélection</span></th>
|
||||
<th>Owner</th>
|
||||
<th data-bs-toggle="tooltip" title="Nombre de signatures distinctes dans lesquelles la valeur apparaît.">Transactions</th>
|
||||
<th data-bs-toggle="tooltip" title="Nombre total de lignes core portant cette valeur.">Occurrences</th>
|
||||
<th data-bs-toggle="tooltip" title="Occurrences divisées par le nombre de transactions distinctes.">Occ./tx</th>
|
||||
<th data-bs-toggle="tooltip" title="Premier slot observé dans le corpus core.">Slot min</th>
|
||||
<th data-bs-toggle="tooltip" title="Dernier slot observé dans le corpus core.">Slot max</th>
|
||||
<th data-bs-toggle="tooltip" title="Différence entre le slot maximal et le slot minimal.">Étendue</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="tab-pane fade" id="accountsTabPane" role="tabpanel" aria-labelledby="accountsTabButton" tabindex="0">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-12 col-lg-7">
|
||||
<label for="accountContainsInput" class="form-label">Account key contient</label>
|
||||
<input id="accountContainsInput" class="form-control font-monospace" type="text" autocomplete="off" />
|
||||
</div>
|
||||
<div class="col-6 col-lg-2">
|
||||
<label for="accountLimitInput" class="form-label">Limite</label>
|
||||
<input id="accountLimitInput" class="form-control" type="number" min="1" max="5000" step="1" value="500" />
|
||||
</div>
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="d-grid d-sm-flex gap-2">
|
||||
<button id="resetAccountsButton" class="btn btn-outline-secondary" type="button">Réinitialiser</button>
|
||||
<button id="loadAccountsButton" class="btn btn-primary flex-grow-1" type="button">Charger les comptes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
|
||||
<div>
|
||||
<h2 class="h5 card-title mb-1">Comptes</h2>
|
||||
<div id="accountResultSummary" class="small text-body-secondary">Aucune requête exécutée.</div>
|
||||
<div class="small text-body-secondary">Source : <code>kb_sol_core_account_keys.account_key</code>.</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="btn-group flex-wrap" role="group" aria-label="Actions comptes">
|
||||
<button id="useSelectedAccountsButton" class="btn btn-sm btn-outline-secondary" type="button">Utiliser comme filtre transactions</button>
|
||||
<button id="copyAccountsButton" class="btn btn-sm btn-outline-primary" type="button">Copier valeurs multiligne</button>
|
||||
<button id="exportAccountsCsvButton" class="btn btn-sm btn-outline-success" type="button">Exporter CSV</button>
|
||||
</div>
|
||||
<div class="small text-body-secondary mt-1">Cochez une ligne. Sans sélection, l’action accepte une seule ligne restant après le filtre local.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table id="accountCandidateTable" class="table table-striped table-hover table-sm align-middle w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="replay-select-column"><span class="visually-hidden">Sélection</span></th>
|
||||
<th>Account key</th>
|
||||
<th data-bs-toggle="tooltip" title="Nombre de signatures distinctes dans lesquelles la valeur apparaît.">Transactions</th>
|
||||
<th data-bs-toggle="tooltip" title="Nombre total de lignes core portant cette valeur.">Occurrences</th>
|
||||
<th data-bs-toggle="tooltip" title="Occurrences divisées par le nombre de transactions distinctes.">Occ./tx</th>
|
||||
<th data-bs-toggle="tooltip" title="Premier slot observé dans le corpus core.">Slot min</th>
|
||||
<th data-bs-toggle="tooltip" title="Dernier slot observé dans le corpus core.">Slot max</th>
|
||||
<th data-bs-toggle="tooltip" title="Différence entre le slot maximal et le slot minimal.">Étendue</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="app-footer bg-dark text-light">
|
||||
<div class="container h-100 d-flex align-items-center">
|
||||
<div class="row flex-grow-1 align-items-center">
|
||||
<div class="col-12 text-center text-small my-1 my-md-0">© 2026 SASEDEV</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<datalist id="knownProgramDatalist"></datalist>
|
||||
<script type="module" src="ts/demo_sql_replay_candidates.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,113 @@
|
||||
<!-- file: kb_app_demo/frontend/demo_ws.html -->
|
||||
<!-- version: 5 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Khadhroony Bot2 - WebSocket standard</title>
|
||||
<link rel="stylesheet" href="sass/main.scss" />
|
||||
</head>
|
||||
|
||||
<body class="bg-body-tertiary">
|
||||
<header class="app-header">
|
||||
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
|
||||
<div class="container my-0">
|
||||
<a class="navbar-brand d-flex align-items-center" href="#">
|
||||
<img alt="Logo" src="imgs/logo.png" class="app-logo" />
|
||||
<span class="ps-2 fs-4 fw-bold text-primary font-logo">WebSocket standard</span>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="app-main">
|
||||
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
|
||||
<div class="container py-4">
|
||||
<div class="row g-4">
|
||||
<div class="col-12 col-xl-5">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h1 class="h4 card-title">Souscription WebSocket Solana standard</h1>
|
||||
<p class="text-body-secondary">Cette démo garde une connexion WebSocket ouverte dans AppState. Le bouton Souscrire réutilise la même connexion tant que l’endpoint reste identique ; la déconnexion est explicite ou réalisée à la fermeture globale de l’application.</p>
|
||||
<p class="small text-warning mb-3">Les notifications très fréquentes sont limitées côté Rust puis bornées côté interface pour éviter de bloquer la WebView.</p>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="wsRoleSelect">Rôle</label>
|
||||
<select id="wsRoleSelect" class="form-select"></select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="wsMethodSelect">Méthode</label>
|
||||
<select id="wsMethodSelect" class="form-select"></select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="wsTargetInput">Target</label>
|
||||
<input id="wsTargetInput" class="form-control" placeholder="Pubkey, program id ou signature" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="wsFilterInput">Filter JSON</label>
|
||||
<textarea id="wsFilterInput" class="form-control font-monospace" rows="4" placeholder='{"mentions":["..."]}'></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="wsConfigInput">Config JSON</label>
|
||||
<textarea id="wsConfigInput" class="form-control font-monospace" rows="4" placeholder='{"commitment":"confirmed"}'></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="wsSubscriptionSelect">Subscriptions actives</label>
|
||||
<select id="wsSubscriptionSelect" class="form-select"></select>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<button id="connectWsButton" class="btn btn-primary">Souscrire</button>
|
||||
<button id="unsubscribeWsButton" class="btn btn-outline-warning">Unsubscribe sélectionnée</button>
|
||||
<button id="disconnectWsButton" class="btn btn-outline-danger">Déconnecter socket</button>
|
||||
<button id="refreshWsPoolButton" class="btn btn-outline-secondary">Rafraîchir endpoints</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-7">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h2 class="h5 card-title mb-0">Statut</h2>
|
||||
<span id="wsStatusBadge" class="badge text-bg-secondary">Déconnecté</span>
|
||||
</div>
|
||||
<textarea id="wsStatusOutput" class="form-control font-monospace" rows="6" readonly>Déconnecté.</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h2 class="h5 card-title mb-0">Endpoints WebSocket</h2>
|
||||
<button id="copyWsPoolButton" class="btn btn-sm btn-outline-secondary">Copier</button>
|
||||
</div>
|
||||
<textarea id="wsPoolOutput" class="form-control font-monospace" rows="8" readonly>Chargement...</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h2 class="h5 card-title mb-0">Messages</h2>
|
||||
<button id="copyWsResultButton" class="btn btn-sm btn-outline-secondary">Copier</button>
|
||||
</div>
|
||||
<textarea id="wsResultOutput" class="form-control font-monospace" rows="16" readonly>Aucune connexion active.</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="app-footer bg-dark text-light">
|
||||
<div class="container h-100 d-flex align-items-center">
|
||||
<div class="row flex-grow-1 align-items-center">
|
||||
<div class="col-12 text-center text-small my-1 my-md-0">© 2026 SASEDEV</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="ts/demo_ws.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 611 KiB |
@@ -0,0 +1,131 @@
|
||||
<!-- file: kb_app_demo/frontend/main.html -->
|
||||
<!-- version: 13 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Khadhroony-Bot2</title>
|
||||
<link rel="stylesheet" href="sass/main.scss" />
|
||||
</head>
|
||||
|
||||
<body class="bg-body-tertiary">
|
||||
<header class="app-header">
|
||||
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
|
||||
<div class="container my-0">
|
||||
<a class="navbar-brand d-flex align-items-center" href="/">
|
||||
<img alt="Logo" src="imgs/logo.png" class="app-logo" />
|
||||
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Bot2</span>
|
||||
</a>
|
||||
|
||||
<div class="ms-auto d-flex align-items-center gap-2">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Démos
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<a id="openDemoConfigLink" class="dropdown-item" href="#">
|
||||
Configuration et logging
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a id="openDemoHttpLink" class="dropdown-item" href="#">
|
||||
HTTP JSON-RPC standard
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a id="openDemoWsLink" class="dropdown-item" href="#">
|
||||
WebSocket standard
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a id="openDemoBackfillLink" class="dropdown-item" href="#">
|
||||
Backfill HTTP
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a id="openDemoCoreExtractionLink" class="dropdown-item" href="#">
|
||||
Extraction canonical vers core
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a id="openDemoDecodeReplayLink" class="dropdown-item" href="#">
|
||||
Décodage et matérialisation
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a id="openDemoExecutionSolanaCoreLink" class="dropdown-item" href="#">
|
||||
Exécution Solana Core Devnet
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a id="openDemoExecutionSplLink" class="dropdown-item" href="#">
|
||||
Exécution SPL Devnet
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<hr class="dropdown-divider" />
|
||||
</li>
|
||||
<li>
|
||||
<a id="openDemoSqlDiagLink" class="dropdown-item" href="#">
|
||||
SQL diagnostics
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a id="openDemoSqlPgRawLink" class="dropdown-item" href="#">
|
||||
PostgreSQL raw store
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a id="openDemoSqlPgCoreLink" class="dropdown-item" href="#">
|
||||
PostgreSQL core store
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a id="openDemoSqlReplayCandidatesLink" class="dropdown-item" href="#">
|
||||
SQL replay candidates
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="app-main">
|
||||
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
|
||||
<div class="container py-4">
|
||||
<div class="row g-4 justify-content-center">
|
||||
<div class="col-12 col-xl-10">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body text-start">
|
||||
<h1 class="h3 card-title mb-3">README du projet</h1>
|
||||
<p class="text-body-secondary mb-3">
|
||||
Cette fenêtre d'accueil charge automatiquement le fichier README depuis le backend Rust.
|
||||
</p>
|
||||
<article id="readmeContent" class="app-readme-box app-markdown-body mb-0">Chargement du README...</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="app-footer bg-dark text-light">
|
||||
<div class="container h-100 d-flex align-items-center">
|
||||
<div class="row flex-grow-1 align-items-center">
|
||||
<div class="col-12 text-center text-small my-1 my-md-0">
|
||||
© 2026 SASEDEV
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="ts/main.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,385 @@
|
||||
// file: kb_app_demo/frontend/sass/_app.scss
|
||||
// version: 5
|
||||
|
||||
$app-header-height: 48px;
|
||||
$app-footer-height: 48px;
|
||||
$sidenav-base-width: 260px;
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#ibackground {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
z-index: 1;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#vbackground {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
z-index: 2;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
video#vbackground:not([playsinline]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: $app-header-height;
|
||||
z-index: 1030;
|
||||
|
||||
& nav {
|
||||
border-top: none !important;
|
||||
border-left: none !important;
|
||||
border-right: none !important;
|
||||
border-bottom: 3px $primary solid !important;
|
||||
border-top-left-radius: 0 !important;
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-left-radius: 15px 150px;
|
||||
border-bottom-right-radius: 75px 25px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.app-footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: $app-footer-height;
|
||||
z-index: 1020;
|
||||
border-style: solid;
|
||||
border-width: 2px 0 0;
|
||||
border-top-left-radius: 255px 25px;
|
||||
border-top-right-radius: 25px 225px;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
position: relative;
|
||||
height: calc(100vh - $app-header-height - $app-footer-height); // plein viewport
|
||||
margin-top: $app-header-height;
|
||||
margin-bottom: $app-footer-height;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden; // pas de scroll ici
|
||||
z-index: 500;
|
||||
}
|
||||
|
||||
.app-main-error {
|
||||
position: relative;
|
||||
height: 100vh; // plein viewport
|
||||
padding-top: $app-header-height;
|
||||
padding-bottom: $app-footer-height;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden; // pas de scroll ici
|
||||
background: linear-gradient(135deg, #9945ff 0%, #764ba2 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
z-index: 501;
|
||||
}
|
||||
|
||||
.app-main-error-content {
|
||||
width: 100%;
|
||||
|
||||
& h1 {
|
||||
font-size: 4rem;
|
||||
margin: 0;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
& p {
|
||||
font-size: 1.5rem;
|
||||
margin: 1rem 0 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.app-logo {
|
||||
height: 42px;
|
||||
width: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#app-dashbord {
|
||||
display: flex;
|
||||
height: 100%; //height: calc(100vh - var(--app-header-height) - var(--app-footer-height));
|
||||
}
|
||||
|
||||
#app-dashbord-sidenav {
|
||||
flex-basis: $sidenav-base-width;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.15s ease-in-out;
|
||||
|
||||
&[dir="ltr"] {
|
||||
transform: translateX(-$sidenav-base-width);
|
||||
}
|
||||
|
||||
&[dir="rtl"] {
|
||||
transform: translateX($sidenav-base-width);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#app-dashbord-main {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
flex-grow: 1;
|
||||
min-height: 100%;
|
||||
|
||||
&[dir="ltr"] {
|
||||
margin-left: -$sidenav-base-width;
|
||||
}
|
||||
|
||||
&[dir="rtl"] {
|
||||
margin-right: -$sidenav-base-width;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// quand body a la classe .sidenav-toggled
|
||||
.sidenav-toggled {
|
||||
#app-dashbord-sidenav {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
#app-dashbord-main {
|
||||
&:before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #000;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// breakpoint desktop : sidebar visible par défaut
|
||||
@include media-breakpoint-up(lg) {
|
||||
#app-dashbord-sidenav {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
#app-dashbord-main {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
transition: margin 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.sidenav-toggled {
|
||||
#app-dashbord-sidenav {
|
||||
&[dir="ltr"] {
|
||||
transform: translateX(-$sidenav-base-width);
|
||||
}
|
||||
|
||||
&[dir="rtl"] {
|
||||
transform: translateX($sidenav-base-width);
|
||||
}
|
||||
}
|
||||
|
||||
#app-dashbord-main {
|
||||
&[dir="ltr"] {
|
||||
margin-left: -$sidenav-base-width;
|
||||
}
|
||||
|
||||
&[dir="rtl"] {
|
||||
margin-right: -$sidenav-base-width;
|
||||
}
|
||||
|
||||
&:before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Sidebar (style SBAdmin-ish)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.app-sidebar.sidenav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
flex-wrap: nowrap;
|
||||
background-color: rgb(97, 53, 131, 0.7);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.sidenav-menu {
|
||||
flex-grow: 1;
|
||||
|
||||
.nav {
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.sidenav-menu-heading {
|
||||
padding: 1.5rem 1rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1.25rem;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
position: relative;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.nav-link-icon {
|
||||
font-size: 0.9rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.sidenav-collapse-arrow {
|
||||
margin-left: auto;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
.sidenav-collapse-arrow {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidenav-menu-nested {
|
||||
margin-left: 1.5rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.osb-scrollable {
|
||||
height: 100%; // occupe toute la hauteur de .app-main
|
||||
max-height: 100%; // évite de dépasser
|
||||
// Pour vérifier sans OverlayScrollbars :
|
||||
//
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 2rem;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.modal {
|
||||
z-index: 1080;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.voice-output-textarea {
|
||||
min-height: 320px;
|
||||
resize: vertical;
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
#logs-card {
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.app-readme-box,
|
||||
.app-config-box {
|
||||
max-height: 60vh;
|
||||
overflow: auto;
|
||||
word-break: break-word;
|
||||
border: 1px solid var(--bs-border-color);
|
||||
border-radius: 0.5rem;
|
||||
background: var(--bs-body-bg);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.app-config-box {
|
||||
font-size: 0.875rem;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.app-json-viewer {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.app-json-viewer andypf-json-viewer {
|
||||
display: block;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.app-markdown-body {
|
||||
line-height: 1.6;
|
||||
|
||||
& h1,
|
||||
& h2,
|
||||
& h3,
|
||||
& h4,
|
||||
& h5,
|
||||
& h6 {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
& h1:first-child,
|
||||
& h2:first-child,
|
||||
& h3:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
& pre {
|
||||
padding: 0.75rem;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--bs-border-color);
|
||||
border-radius: 0.5rem;
|
||||
background: var(--bs-tertiary-bg);
|
||||
}
|
||||
|
||||
& code {
|
||||
color: var(--bs-code-color);
|
||||
}
|
||||
}
|
||||
.app-json-box {
|
||||
max-height: 420px;
|
||||
overflow: auto;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
// file: kb_demo_app/frontend/sass/_bootswatch.scss
|
||||
// version: 1
|
||||
|
||||
// Pulse 5.3.8
|
||||
// Bootswatch
|
||||
|
||||
|
||||
// Variables
|
||||
|
||||
// Buttons
|
||||
|
||||
.btn {
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&:active:focus,
|
||||
&.active:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&-secondary {
|
||||
color: $gray-900;
|
||||
background-color: $white;
|
||||
border-color: #ccc;
|
||||
|
||||
&:hover {
|
||||
color: $gray-900;
|
||||
background-color: $gray-300;
|
||||
border-color: $gray-500;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: tint-color($gray-900, 5%);
|
||||
background-color: $white;
|
||||
border-color: tint-color(#ccc, 5%);
|
||||
}
|
||||
}
|
||||
|
||||
&-warning {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&-primary:focus {
|
||||
box-shadow: 0 0 5px tint-color($primary, 10%);
|
||||
}
|
||||
|
||||
&-secondary:focus {
|
||||
box-shadow: 0 0 5px $gray-400;
|
||||
}
|
||||
|
||||
&-success:focus {
|
||||
box-shadow: 0 0 5px tint-color($success, 10%);
|
||||
}
|
||||
|
||||
&-info:focus {
|
||||
box-shadow: 0 0 5px tint-color($info, 10%);
|
||||
}
|
||||
|
||||
&-warning:focus {
|
||||
box-shadow: 0 0 5px tint-color($warning, 10%);
|
||||
}
|
||||
|
||||
&-danger:focus {
|
||||
box-shadow: 0 0 5px tint-color($danger, 10%);
|
||||
}
|
||||
|
||||
&.disabled:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Tables
|
||||
|
||||
.table .thead-dark th {
|
||||
background-color: $secondary;
|
||||
border-color: $table-border-color;
|
||||
}
|
||||
|
||||
.table-primary,
|
||||
.table-secondary,
|
||||
.table-success,
|
||||
.table-warning,
|
||||
.table-danger,
|
||||
.table-info,
|
||||
.table-light {
|
||||
--#{$prefix}table-color: #{$body-color};
|
||||
}
|
||||
|
||||
// Forms
|
||||
|
||||
.form-control:focus {
|
||||
box-shadow: 0 0 5px rgba(100, 65, 164, .4);
|
||||
}
|
||||
|
||||
// Navs
|
||||
|
||||
.nav-tabs {
|
||||
|
||||
.nav-link,
|
||||
.nav-link.active {
|
||||
border-width: 0 0 1px;
|
||||
}
|
||||
|
||||
.nav-link:hover,
|
||||
.nav-link.active,
|
||||
.nav-link.active:hover,
|
||||
.nav-link.active:focus {
|
||||
border-bottom: 1px solid $primary;
|
||||
}
|
||||
|
||||
.nav-item+.nav-item {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
&-item.active {
|
||||
color: $gray-700;
|
||||
}
|
||||
}
|
||||
|
||||
// Indicators
|
||||
|
||||
.badge {
|
||||
&.bg-light {
|
||||
color: $dark;
|
||||
}
|
||||
}
|
||||
|
||||
// Progress bars
|
||||
|
||||
.progress {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
// Containers
|
||||
|
||||
.list-group {
|
||||
&-item {
|
||||
color: rgba(255, 255, 255, .8);
|
||||
|
||||
&.active,
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: 700;
|
||||
|
||||
&:hover {
|
||||
background-color: $list-group-hover-bg;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled:hover {
|
||||
color: $list-group-disabled-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// file: kb_demo_app/frontend/sass/_fontawesome.scss
|
||||
// version: 1
|
||||
|
||||
//@use '@fortawesome/fontawesome-free/scss/variables' with (
|
||||
// // customizing $font-path - make sure it points to where your webfonts are stored in your project
|
||||
// $font-path: '../webfonts',
|
||||
//);
|
||||
@use '@fortawesome/fontawesome-free/scss/variables' with (
|
||||
// use fonts from @fortawesome/fontawesome-free
|
||||
$font-path: '@fortawesome/fontawesome-free/webfonts',
|
||||
);
|
||||
// load Font Awesome core
|
||||
@use '@fortawesome/fontawesome-free/scss/fontawesome';
|
||||
|
||||
// load and make available Font Awesome helpers (mixins, functions, and variables)
|
||||
@use '@fortawesome/fontawesome-free/scss/fa' as fa;
|
||||
@use '@fortawesome/fontawesome-free/scss/brands' as fa-brands;
|
||||
@use '@fortawesome/fontawesome-free/scss/regular' as fa-regular;
|
||||
@use '@fortawesome/fontawesome-free/scss/solid' as fa-solid;
|
||||
@@ -0,0 +1,248 @@
|
||||
// file: kb_demo_app/frontend/sass/_simplebar.scss
|
||||
// version: 1
|
||||
|
||||
/* Rtl support */
|
||||
[data-simplebar] {
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
align-content: flex-start;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.simplebar-wrapper {
|
||||
overflow: hidden;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
max-width: inherit;
|
||||
max-height: inherit;
|
||||
}
|
||||
|
||||
.simplebar-mask {
|
||||
direction: inherit;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
// z-index: 0;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.simplebar-offset {
|
||||
direction: inherit !important;
|
||||
box-sizing: inherit !important;
|
||||
resize: none !important;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.simplebar-content-wrapper {
|
||||
direction: inherit;
|
||||
box-sizing: border-box !important;
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-hide-scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
position: fixed;
|
||||
left: 0;
|
||||
visibility: hidden;
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.simplebar-content {
|
||||
&:before {
|
||||
content: ' ';
|
||||
display: table;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: ' ';
|
||||
display: table;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-placeholder {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.simplebar-height-auto-observer-wrapper {
|
||||
box-sizing: inherit !important;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: 1px;
|
||||
position: relative;
|
||||
float: left;
|
||||
max-height: 1px;
|
||||
overflow: hidden;
|
||||
// z-index: -1;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
pointer-events: none;
|
||||
flex-grow: inherit;
|
||||
flex-shrink: 0;
|
||||
flex-basis: 0;
|
||||
}
|
||||
|
||||
.simplebar-height-auto-observer {
|
||||
box-sizing: inherit;
|
||||
display: block;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 1000%;
|
||||
width: 1000%;
|
||||
min-height: 1px;
|
||||
min-width: 1px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
// z-index: -1;
|
||||
}
|
||||
|
||||
.simplebar-track {
|
||||
// z-index: 1;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-simplebar].simplebar-dragging {
|
||||
pointer-events: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
.simplebar-content {
|
||||
pointer-events: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.simplebar-track {
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-scrollbar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
min-height: 10px;
|
||||
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
background: black;
|
||||
border-radius: 7px;
|
||||
left: 2px;
|
||||
right: 2px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s 0.5s linear;
|
||||
top: 2px;
|
||||
bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-scrollbar.simplebar-visible {
|
||||
&:before {
|
||||
opacity: 0.5;
|
||||
transition-delay: 0s;
|
||||
transition-duration: 0s;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-track.simplebar-vertical {
|
||||
top: 0;
|
||||
width: 11px;
|
||||
}
|
||||
|
||||
.simplebar-track.simplebar-horizontal {
|
||||
left: 0;
|
||||
height: 11px;
|
||||
|
||||
.simplebar-scrollbar {
|
||||
right: auto;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
min-height: 0;
|
||||
min-width: 10px;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
[data-simplebar-direction='rtl'] {
|
||||
.simplebar-track.simplebar-vertical {
|
||||
right: auto;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-dummy-scrollbar-size {
|
||||
direction: rtl;
|
||||
position: fixed;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
height: 500px;
|
||||
width: 500px;
|
||||
overflow-y: hidden;
|
||||
overflow-x: scroll;
|
||||
-ms-overflow-style: scrollbar !important;
|
||||
|
||||
>div {
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// file: kb_demo_app/frontend/sass/_variables.scss
|
||||
// version: 1
|
||||
|
||||
// Pulse 5.3.8
|
||||
// Bootswatch
|
||||
|
||||
$theme: "pulse" !default;
|
||||
|
||||
//
|
||||
// Color system
|
||||
//
|
||||
|
||||
$white: #fff !default;
|
||||
$gray-100: #fafafa !default;
|
||||
$gray-200: #f9f8fc !default;
|
||||
$gray-300: #ededed !default;
|
||||
$gray-400: #cbc8d0 !default;
|
||||
$gray-500: #adb5bd !default;
|
||||
$gray-600: #868e96 !default;
|
||||
$gray-700: #444 !default;
|
||||
$gray-800: #343a40 !default;
|
||||
$gray-900: #17141f !default;
|
||||
$black: #000 !default;
|
||||
|
||||
$blue: #007bff !default;
|
||||
$indigo: #6610f2 !default;
|
||||
$purple: #593196 !default;
|
||||
$pink: #e83e8c !default;
|
||||
$red: #fc3939 !default;
|
||||
$orange: #fd7e14 !default;
|
||||
$yellow: #efa31d !default;
|
||||
$green: #13b955 !default;
|
||||
$teal: #20c997 !default;
|
||||
$cyan: #009cdc !default;
|
||||
|
||||
$primary: $purple !default;
|
||||
$secondary: #a991d4 !default;
|
||||
$success: $green !default;
|
||||
$info: $cyan !default;
|
||||
$warning: $yellow !default;
|
||||
$danger: $red !default;
|
||||
$light: $gray-200 !default;
|
||||
$dark: $gray-900 !default;
|
||||
|
||||
$min-contrast-ratio: 2.1 !default;
|
||||
|
||||
// Options
|
||||
|
||||
$enable-rounded: false !default;
|
||||
|
||||
// Body
|
||||
|
||||
$body-color: $gray-700 !default;
|
||||
|
||||
// Links
|
||||
|
||||
$link-hover-color: $primary !default;
|
||||
|
||||
// Tables
|
||||
|
||||
$table-color: initial !default;
|
||||
|
||||
$table-border-color: rgba(0, 0, 0, .05) !default;
|
||||
|
||||
// Forms
|
||||
|
||||
$input-focus-border-color: $primary !default;
|
||||
|
||||
// Dropdowns
|
||||
|
||||
$dropdown-link-hover-color: $white !default;
|
||||
$dropdown-link-hover-bg: $primary !default;
|
||||
|
||||
// Navs
|
||||
|
||||
$nav-tabs-border-color: $gray-300 !default;
|
||||
$nav-tabs-link-hover-border-color: $primary !default;
|
||||
|
||||
// Navbar
|
||||
|
||||
$navbar-padding-y: 1.2rem !default;
|
||||
|
||||
// Progress bars
|
||||
|
||||
$progress-bg: $gray-300 !default;
|
||||
$progress-bar-bg: $primary !default;
|
||||
|
||||
// List group
|
||||
|
||||
$list-group-bg: $gray-900 !default;
|
||||
$list-group-border-color: transparent !default;
|
||||
$list-group-hover-bg: lighten($list-group-bg, 10%) !default;
|
||||
$list-group-active-color: $white !default;
|
||||
$list-group-active-bg: $list-group-bg !default;
|
||||
$list-group-disabled-color: lighten($list-group-bg, 30%) !default;
|
||||
@@ -0,0 +1,10 @@
|
||||
// file: kb_demo_app/frontend/sass/main.scss
|
||||
// version: 1
|
||||
|
||||
@import "bootstrap/scss/functions";
|
||||
@import "variables";
|
||||
@import "fontawesome";
|
||||
@import "simplebar";
|
||||
@import "bootstrap/scss/bootstrap";
|
||||
@import "bootswatch";
|
||||
@import "app";
|
||||
@@ -0,0 +1,126 @@
|
||||
// file: kb_app_demo/frontend/sass/splash.scss
|
||||
// version: 3
|
||||
|
||||
@font-face {
|
||||
font-family: 'Dos Amazigh';
|
||||
src: url('../fonts/DOS_Amazigh.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
/* Assure un bon rendu pendant le chargement de la police */
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
#splash-container {
|
||||
position: relative;
|
||||
width: 960px;
|
||||
/* Largeur exacte de votre image */
|
||||
height: 637px;
|
||||
/* Hauteur exacte de votre image */
|
||||
opacity: 0;
|
||||
/* Sera animé par JavaScript. */
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
#splash-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#app-name {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-family: 'Dos Amazigh', sans-serif;
|
||||
/* Application de la police */
|
||||
font-size: 96px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
/* Blanc pour contraste */
|
||||
text-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||
/* Ombre pour meilleure lisibilité */
|
||||
z-index: 2;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#debug-info {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 50%;
|
||||
max-height: 30%;
|
||||
overflow-y: auto;
|
||||
color: white;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
padding: 8px;
|
||||
z-index: 3;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#messages-container {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
max-height: 30%;
|
||||
overflow-y: auto;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
padding: 10px 10px 40px;
|
||||
z-index: 2;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.splash-message {
|
||||
margin-bottom: 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.splash-message.info {
|
||||
background-color: rgba(33, 150, 243, 0.2);
|
||||
}
|
||||
|
||||
.splash-message.warning {
|
||||
background-color: rgba(255, 152, 0, 0.2);
|
||||
}
|
||||
|
||||
.splash-message.error {
|
||||
background-color: rgba(244, 67, 54, 0.2);
|
||||
}
|
||||
|
||||
.splash-message.success {
|
||||
background-color: rgba(76, 175, 80, 0.2);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<!-- file: kb_app_demo/frontend/splash.html -->
|
||||
<!-- version: 2 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Loading ... Khadhroony Bot2</title>
|
||||
<link rel="stylesheet" href="sass/splash.scss" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="splash-container" style="opacity: 0;">
|
||||
<img id="splash-image" src="imgs/splash.png" alt="Application Loading" />
|
||||
<div id="app-name">Khadhroony</div>
|
||||
<div id="debug-info"></div>
|
||||
<div id="messages-container"></div>
|
||||
</div>
|
||||
<script type="module" src="ts/splash.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -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