v0.1.0-pre.050

This commit is contained in:
2026-07-25 21:04:56 +02:00
parent 8286dc0919
commit 50fd7467f8
23 changed files with 3129 additions and 27 deletions

View File

@@ -1,5 +1,5 @@
# file: kb-app-demo-desktop/Cargo.toml
# version: 2
# version: 3
[package]
name = "kb-app-demo-desktop"
@@ -28,6 +28,7 @@ kb-core = { path = "../kb-core" }
kb-logging = { path = "../kb-logging" }
kb-store = { path = "../kb-store" }
kb-pipeline = { path = "../kb-pipeline" }
kb-program-ids = { path = "../kb-program-ids" }
kb-onchain-transport = { path = "../kb-onchain-transport" }
serde.workspace = true
serde_json.workspace = true

View File

@@ -4,7 +4,14 @@
"description": "Default desktop demo capability",
"windows": [
"splash",
"main"
"main",
"demo_backfill",
"demo_http",
"demo_ws",
"demo_sql_diag",
"demo_sql_pg_raw",
"demo_sql_pg_core",
"demo_sql_replay_candidates"
],
"permissions": [
"core:default"

View File

@@ -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-Bot3 - 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">Bot3</span>
</span>
<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">
&copy; 2026 SASEDEV
</div>
</div>
</div>
</footer>
<script type="module" src="ts/demo_sql_diag.ts" defer></script>
</body>
</html>

View File

@@ -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-Bot3 - 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">Bot3</span>
</span>
<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">
&copy; 2026 SASEDEV
</div>
</div>
</div>
</footer>
<script type="module" src="ts/demo_sql_pg_core.ts" defer></script>
</body>
</html>

View File

@@ -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-Bot3 - 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">Bot3</span>
</span>
<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">
&copy; 2026 SASEDEV
</div>
</div>
</div>
</footer>
<script type="module" src="ts/demo_sql_pg_raw.ts" defer></script>
</body>
</html>

View File

@@ -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 Bot3 - 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>
</span>
<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 lexport 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 dimport, 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 dentité</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 dabord</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 lextraction core a échoué.">Core</th>
<th>Ledger</th>
<th>Version</th>
<th>Essais</th>
<th data-bs-toggle="tooltip" title="Nombre total dinstructions top-level.">Outer inst.</th>
<th data-bs-toggle="tooltip" title="Nombre total dinstructions 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, laction 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, laction 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, laction 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, laction 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">&copy; 2026 SASEDEV</div>
</div>
</div>
</footer>
<datalist id="knownProgramDatalist"></datalist>
<script type="module" src="ts/demo_sql_replay_candidates.ts" defer></script>
</body>
</html>

View File

@@ -25,6 +25,11 @@
<li><a id="openDemoWsLink" class="dropdown-item" href="#">WebSocket standard</a></li>
<li><a id="openDemoHttpLink" class="dropdown-item" href="#">HTTP JSON-RPC</a></li>
<li><a id="openDemoBackfillLink" class="dropdown-item" href="#">Backfill HTTP</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="#">SQL PostgreSQL raw</a></li>
<li><a id="openDemoSqlPgCoreLink" class="dropdown-item" href="#">SQL PostgreSQL core</a></li>
<li><a id="openDemoSqlReplayCandidatesLink" class="dropdown-item" href="#">SQL replay candidates</a></li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,60 @@
// file: kb-app-demo-desktop/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_desktop/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();
});

View File

@@ -0,0 +1,42 @@
// file: kb-app-demo-desktop/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_desktop/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();
});

View File

@@ -0,0 +1,42 @@
// file: kb-app-demo-desktop/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_desktop/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();
});

View File

@@ -0,0 +1,800 @@
// file: kb-app-demo-desktop/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_desktop/demo_sql_replay_candidates/DemoSqlReplayEntityRequest";
import type { DemoSqlReplayEntityRow } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayEntityRow";
import type { DemoSqlReplayKnownProgramOption } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayKnownProgramOption";
import type { DemoSqlReplayOptionsPayload } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayOptionsPayload";
import type { DemoSqlReplayProgramRequest } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayProgramRequest";
import type { DemoSqlReplayProgramRow } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayProgramRow";
import type { DemoSqlReplayTransactionRequest } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayTransactionRequest";
import type { DemoSqlReplayTransactionRow } from "./bindings/kb_app_demo_desktop/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("&amp;")
.split("<").join("&lt;")
.split(">").join("&gt;")
.split('"').join("&quot;")
.split("'").join("&#039;");
}
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 dentité 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));
});

View File

@@ -0,0 +1,68 @@
// file: kb-app-demo-desktop/frontend/ts/demo_sql_tables.ts
// version: 2
import "@andypf/json-viewer";
import type { DemoSqlTableSnapshot } from "./bindings/kb_app_demo_desktop/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;
}

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/main.ts
// version: 2
// version: 3
import * as bootstrap from "bootstrap";
import MarkdownIt from "markdown-it";
@@ -60,3 +60,19 @@ if (openDemoBackfillLink) {
void invoke("open_demo_backfill_window");
});
}
const sqlDemoLinks: ReadonlyArray<readonly [string, string]> = [
["openDemoSqlDiagLink", "open_demo_sql_diag_window"],
["openDemoSqlPgRawLink", "open_demo_sql_pg_raw_window"],
["openDemoSqlPgCoreLink", "open_demo_sql_pg_core_window"],
["openDemoSqlReplayCandidatesLink", "open_demo_sql_replay_candidates_window"],
];
for (const [elementId, command] of sqlDemoLinks) {
const link = document.querySelector<HTMLAnchorElement>(`#${elementId}`);
if (link) {
link.addEventListener("click", event => {
event.preventDefault();
void invoke(command);
});
}
}

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/app_state.rs
// version: 3
// version: 4
//! Shared Tauri application state and startup initialization.
@@ -10,7 +10,7 @@ pub(crate) struct AppState {
active_profile: kb_config::ProfileConfig,
logging_guard: std::sync::Mutex<kb_logging::LoggingGuard>,
http_pool: kb_onchain_transport::HttpEndpointPool,
ws_pool: kb_onchain_transport::WsEndpointPool,
ws_pool: std::sync::Mutex<std::option::Option<std::sync::Arc<kb_onchain_transport::WsEndpointPool>>>,
demo_ws_session: tokio::sync::Mutex<std::option::Option<std::sync::Arc<kb_onchain_transport::WsSession>>>,
demo_backfill_running: std::sync::atomic::AtomicBool,
demo_backfill_cancel_requested: std::sync::atomic::AtomicBool,
@@ -37,17 +37,13 @@ impl crate::AppState {
std::result::Result::Ok(pool) => pool,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let ws_pool = match kb_onchain_transport::WsEndpointPool::from_profile(&active_profile) {
std::result::Result::Ok(pool) => pool,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::AppState {
config_path: config_path.display().to_string(),
app_config,
active_profile,
logging_guard: std::sync::Mutex::new(logging_guard),
http_pool,
ws_pool,
ws_pool: std::sync::Mutex::new(std::option::Option::None),
demo_ws_session: tokio::sync::Mutex::new(std::option::Option::None),
demo_backfill_running: std::sync::atomic::AtomicBool::new(false),
demo_backfill_cancel_requested: std::sync::atomic::AtomicBool::new(false),
@@ -74,9 +70,28 @@ impl crate::AppState {
return &self.http_pool;
}
/// Returns the configured WebSocket endpoint pool.
pub(crate) fn ws_pool(&self) -> &kb_onchain_transport::WsEndpointPool {
return &self.ws_pool;
/// Returns the lazily initialized WebSocket endpoint pool.
pub(crate) fn demo_ws_pool(
&self,
) -> kb_core::Result<std::sync::Arc<kb_onchain_transport::WsEndpointPool>> {
let lock_result = self.ws_pool.lock();
let mut guard = match lock_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(kb_core::Error::invalid_state(
"demo WebSocket pool lock is poisoned",
));
},
};
if let std::option::Option::Some(pool) = guard.as_ref() {
return std::result::Result::Ok(std::sync::Arc::clone(pool));
}
let pool = match kb_onchain_transport::WsEndpointPool::from_profile(&self.active_profile) {
std::result::Result::Ok(value) => std::sync::Arc::new(value),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
*guard = std::option::Option::Some(std::sync::Arc::clone(&pool));
return std::result::Result::Ok(pool);
}
/// Returns the persistent WebSocket demo session slot.

View File

@@ -0,0 +1,363 @@
// file: kb-app-demo-desktop/src/demo_sql_common.rs
// version: 8
//! Shared SQL demo helpers and serializable payloads.
use tauri::Manager; // rust-rules: trait-import
use ts_rs::TS; // rust-rules: derive-import
/// UI-safe table diagnostics shown by SQL demo windows.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql/DemoSqlTableSnapshot.ts"
)]
pub(crate) struct DemoSqlTableSnapshot {
/// Table name.
pub(crate) table_name: std::string::String,
/// Logical Solana domain encoded in the table name.
pub(crate) domain: std::string::String,
/// Human-readable table role.
pub(crate) role: std::string::String,
/// Whether the table currently exists.
pub(crate) exists: bool,
/// Row count when the table exists.
#[ts(type = "number | null")]
pub(crate) row_count: std::option::Option<i64>,
/// Minimum slot when available.
#[ts(type = "number | null")]
pub(crate) min_slot: std::option::Option<i64>,
/// Maximum slot when available.
#[ts(type = "number | null")]
pub(crate) max_slot: std::option::Option<i64>,
/// Latest insertion timestamp rendered by PostgreSQL when available.
pub(crate) latest_created_at: std::option::Option<std::string::String>,
}
/// Opens or focuses a SQL demo window.
pub(crate) fn open_sql_demo_window(
app_handle: tauri::AppHandle,
label: &str,
html_path: &str,
title: &str,
) -> std::result::Result<(), std::string::String> {
tracing::info!(target: crate::TRACING_TARGET, window = label, "open SQL demo window");
let existing_window = app_handle.get_webview_window(label);
if let std::option::Option::Some(window) = existing_window {
let show_result = window.show();
if let std::result::Result::Err(error) = show_result {
return std::result::Result::Err(
kb_core::Error::tauri(format!("cannot show {label} window: {error}")).to_string(),
);
}
let focus_result = window.set_focus();
if let std::result::Result::Err(error) = focus_result {
return std::result::Result::Err(
kb_core::Error::tauri(format!("cannot focus {label} window: {error}")).to_string(),
);
}
return std::result::Result::Ok(());
}
let builder = tauri::WebviewWindowBuilder::new(
&app_handle,
label,
tauri::WebviewUrl::App(html_path.into()),
)
.title(title)
.inner_size(1220.0, 780.0)
.min_inner_size(920.0, 560.0)
.resizable(true)
.visible(true);
let build_result = builder.build();
return match build_result {
std::result::Result::Ok(window) => {
let focus_result = window.set_focus();
if let std::result::Result::Err(error) = focus_result {
return std::result::Result::Err(
kb_core::Error::tauri(format!("cannot focus created {label} window: {error}"))
.to_string(),
);
}
std::result::Result::Ok(())
},
std::result::Result::Err(error) => std::result::Result::Err(
kb_core::Error::tauri(format!("cannot create {label} window: {error}")).to_string(),
),
};
}
/// Builds PostgreSQL options from the active profile without coupling kb-store to kb-config.
pub(crate) fn postgres_store_options_from_profile(
profile: &kb_config::ProfileConfig,
auto_initialize_schema: bool,
) -> kb_core::Result<kb_store::PostgresStoreOptions> {
return kb_store::PostgresStoreOptions::new(
profile.database.postgres.url.clone(),
profile.database.postgres.max_connections,
profile.database.postgres.connect_timeout_ms,
auto_initialize_schema,
);
}
/// Connects to PostgreSQL without reapplying schemas initialized at application startup.
pub(crate) async fn connect_postgres_store(
profile: &kb_config::ProfileConfig,
) -> std::result::Result<kb_store::PostgresStore, std::string::String> {
if !profile.database.enabled {
return std::result::Result::Err(std::string::String::from(
"database configuration is disabled in the active profile",
));
}
let options_result = postgres_store_options_from_profile(profile, false);
let mut options = match options_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
options.auto_initialize_schema = false;
let store_result = kb_store::PostgresStore::connect(options).await;
return match store_result {
std::result::Result::Ok(store) => std::result::Result::Ok(store),
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
};
}
/// Converts store table diagnostics to the UI payload shape.
pub(crate) fn table_snapshot_from_pg(
value: &kb_store::PostgresTableDiagnostics,
) -> crate::DemoSqlTableSnapshot {
let row_count = match &value.statistics {
std::option::Option::Some(statistics) => std::option::Option::Some(statistics.row_count),
std::option::Option::None => std::option::Option::None,
};
let min_slot = match &value.statistics {
std::option::Option::Some(statistics) => statistics.min_slot,
std::option::Option::None => std::option::Option::None,
};
let max_slot = match &value.statistics {
std::option::Option::Some(statistics) => statistics.max_slot,
std::option::Option::None => std::option::Option::None,
};
let latest_created_at = match &value.statistics {
std::option::Option::Some(statistics) => statistics.latest_created_at.clone(),
std::option::Option::None => std::option::Option::None,
};
return crate::DemoSqlTableSnapshot {
table_name: value.table_name.clone(),
domain: value.domain.clone(),
role: value.role.clone(),
exists: value.exists,
row_count,
min_slot,
max_slot,
latest_created_at,
};
}
/// Converts many table diagnostics to UI payloads.
pub(crate) fn table_snapshots_from_pg(
values: &[kb_store::PostgresTableDiagnostics],
) -> std::vec::Vec<crate::DemoSqlTableSnapshot> {
let mut output = std::vec::Vec::new();
for value in values {
output.push(table_snapshot_from_pg(value));
}
return output;
}
/// Initializes PostgreSQL raw, core, decode and materialization schemas during Tauri startup.
pub(crate) async fn initialize_postgres_schema_for_startup(
state: &crate::AppState,
splash_window: &tauri::WebviewWindow,
) {
let profile = state.active_profile();
if !profile.database.enabled {
emit_sql_startup_splash(
splash_window,
"SQL store disabled; schema initialization skipped.",
"info",
false,
);
tracing::debug!(target: crate::TRACING_TARGET, "database is disabled; skip SQL schema initialization");
return;
}
if profile.database.backend != "postgres" {
emit_sql_startup_splash(
splash_window,
"SQL backend is not PostgreSQL; schema initialization skipped.",
"info",
false,
);
tracing::debug!(target: crate::TRACING_TARGET, backend = profile.database.backend.as_str(), "database backend is not postgres; skip SQL schema initialization");
return;
}
let options_result = postgres_store_options_from_profile(profile, false);
let mut options = match options_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
let error_message = error.to_string();
emit_sql_startup_error(splash_window, error_message.as_str());
tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "cannot build PostgreSQL store options");
return;
},
};
if !options.auto_initialize_schema {
emit_sql_startup_splash(
splash_window,
"PostgreSQL schema auto-initialization disabled.",
"info",
false,
);
let masked_dsn = options.masked_dsn();
tracing::debug!(target: crate::TRACING_TARGET, dsn = masked_dsn.as_str(), "PostgreSQL schema auto-initialization disabled");
return;
}
let masked_dsn = options.masked_dsn();
options.auto_initialize_schema = false;
emit_sql_startup_splash(
splash_window,
"Checking PostgreSQL raw/core/decode tables...",
"info",
true,
);
tracing::debug!(target: crate::TRACING_TARGET, dsn = masked_dsn.as_str(), "start PostgreSQL schema initialization");
let store_result = kb_store::PostgresStore::connect(options).await;
let store = match store_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
let error_message = error.to_string();
emit_sql_startup_error(splash_window, error_message.as_str());
tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "PostgreSQL connection failed during startup schema initialization");
return;
},
};
let before_result = store.known_table_diagnostics().await;
let before_tables = match before_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
let error_message = error.to_string();
emit_sql_startup_error(splash_window, error_message.as_str());
tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "cannot read table diagnostics before schema initialization");
return;
},
};
let initialize_result = store.initialize_store_schema().await;
if let std::result::Result::Err(error) = initialize_result {
let error_message = error.to_string();
emit_sql_startup_error(splash_window, error_message.as_str());
tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "PostgreSQL store schema initialization failed");
return;
}
let after_result = store.known_table_diagnostics().await;
let after_tables = match after_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
let error_message = error.to_string();
emit_sql_startup_error(splash_window, error_message.as_str());
tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "cannot read table diagnostics after schema initialization");
return;
},
};
emit_sql_startup_table_report(splash_window, before_tables.as_slice(), after_tables.as_slice());
}
fn emit_sql_startup_table_report(
splash_window: &tauri::WebviewWindow,
before_tables: &[kb_store::PostgresTableDiagnostics],
after_tables: &[kb_store::PostgresTableDiagnostics],
) {
let mut created_count = 0_u32;
let mut existing_count = 0_u32;
let mut missing_count = 0_u32;
for table in after_tables {
let before_exists = table_exists_in(before_tables, table.table_name.as_str());
if table.exists && before_exists == std::option::Option::Some(false) {
created_count += 1;
emit_sql_startup_splash(
splash_window,
format!("Created table {}", table.table_name).as_str(),
"success",
true,
);
tracing::debug!(target: crate::TRACING_TARGET, table = table.table_name.as_str(), "PostgreSQL table created");
} else if table.exists {
existing_count += 1;
emit_sql_startup_splash(
splash_window,
format!("Table {} already exists", table.table_name).as_str(),
"info",
true,
);
tracing::debug!(target: crate::TRACING_TARGET, table = table.table_name.as_str(), "PostgreSQL table already exists");
} else {
missing_count += 1;
emit_sql_startup_splash(
splash_window,
format!("Table {} is missing after initialization", table.table_name).as_str(),
"warning",
true,
);
tracing::error!(target: crate::TRACING_TARGET, table = table.table_name.as_str(), "PostgreSQL table missing after initialization");
}
}
emit_sql_startup_splash(
splash_window,
format!(
"PostgreSQL schema ready: {created_count} created, {existing_count} already present, {missing_count} missing."
)
.as_str(),
schema_report_status(missing_count),
false,
);
tracing::debug!(target: crate::TRACING_TARGET, created = created_count, existing = existing_count, missing = missing_count, "PostgreSQL schema initialization completed");
}
fn table_exists_in(
tables: &[kb_store::PostgresTableDiagnostics],
table_name: &str,
) -> std::option::Option<bool> {
for table in tables {
if table.table_name.as_str() == table_name {
return std::option::Option::Some(table.exists);
}
}
return std::option::Option::None;
}
fn schema_report_status(missing_count: u32) -> &'static str {
if missing_count == 0 {
return "success";
}
return "warning";
}
fn emit_sql_startup_error(splash_window: &tauri::WebviewWindow, message: &str) {
emit_sql_startup_splash(
splash_window,
format!("PostgreSQL schema initialization error: {message}").as_str(),
"danger",
false,
);
}
fn emit_sql_startup_splash(
splash_window: &tauri::WebviewWindow,
message: &str,
status: &str,
log_only: bool,
) {
let order = if log_only { "add_log" } else { "add_msg" };
crate::emit_splash_order(
splash_window,
order,
std::option::Option::Some(message),
std::option::Option::Some(status),
std::option::Option::None,
);
}
/// Formats debug enums for UI display.
pub(crate) fn debug_status<T: std::fmt::Debug>(value: T) -> std::string::String {
return format!("{value:?}");
}

View File

@@ -0,0 +1,38 @@
// file: kb-app-demo-desktop/src/demo_sql_diag.rs
// version: 3
//! SQL diagnostic demo commands.
use ts_rs::TS; // rust-rules: derive-import
/// Complete SQL diagnostic payload shown by `demo_sql_diag`.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_diag/DemoSqlDiagPayload.ts"
)]
pub(crate) struct DemoSqlDiagPayload {
/// Configuration file path.
pub(crate) config_path: std::string::String,
/// Active profile name.
pub(crate) active_profile_name: std::string::String,
/// Configured database backend.
pub(crate) backend: std::string::String,
/// Masked PostgreSQL DSN.
pub(crate) masked_dsn: std::string::String,
/// PostgreSQL current schema.
pub(crate) current_schema: std::option::Option<std::string::String>,
/// PostgreSQL health status.
pub(crate) health_status: std::string::String,
/// PostgreSQL health message.
pub(crate) health_message: std::option::Option<std::string::String>,
/// Migration status.
pub(crate) migration_status: std::string::String,
/// Migration diagnostic message.
pub(crate) migration_message: std::option::Option<std::string::String>,
/// PostgreSQL server version when available.
pub(crate) server_version: std::option::Option<std::string::String>,
/// Known raw/core table diagnostics.
pub(crate) tables: std::vec::Vec<crate::DemoSqlTableSnapshot>,
}

View File

@@ -0,0 +1,22 @@
// file: kb-app-demo-desktop/src/demo_sql_pg_core.rs
// version: 4
//! PostgreSQL core store demo commands.
use ts_rs::TS; // rust-rules: derive-import
/// PostgreSQL core store payload shown by `demo_sql_pg_core`.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_pg_core/DemoSqlPgCorePayload.ts"
)]
pub(crate) struct DemoSqlPgCorePayload {
/// Active profile name.
pub(crate) active_profile_name: std::string::String,
/// Masked PostgreSQL DSN.
pub(crate) masked_dsn: std::string::String,
/// Core table diagnostics.
pub(crate) tables: std::vec::Vec<crate::DemoSqlTableSnapshot>,
}

View File

@@ -0,0 +1,22 @@
// file: kb-app-demo-desktop/src/demo_sql_pg_raw.rs
// version: 5
//! PostgreSQL canonical acquisition store demo commands.
use ts_rs::TS; // rust-rules: derive-import
/// PostgreSQL canonical acquisition payload shown by `demo_sql_pg_raw`.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_pg_raw/DemoSqlPgRawPayload.ts"
)]
pub(crate) struct DemoSqlPgRawPayload {
/// Active profile name.
pub(crate) active_profile_name: std::string::String,
/// Masked PostgreSQL DSN.
pub(crate) masked_dsn: std::string::String,
/// Raw table diagnostics.
pub(crate) tables: std::vec::Vec<crate::DemoSqlTableSnapshot>,
}

View File

@@ -0,0 +1,409 @@
// file: kb-app-demo-desktop/src/demo_sql_replay_candidates.rs
// version: 7
//! Read-only SQL replay candidate browser commands.
use ts_rs::TS; // rust-rules: derive-import
/// One program identifier exposed by the runtime `kb_program_ids` registry.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayKnownProgramOption.ts"
)]
pub(crate) struct DemoSqlReplayKnownProgramOption {
/// Stable lower snake case program code.
pub(crate) code: std::string::String,
/// Base58 Solana program identifier.
pub(crate) program_id: std::string::String,
}
/// Static options for the replay candidate browser.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayOptionsPayload.ts"
)]
pub(crate) struct DemoSqlReplayOptionsPayload {
/// Active profile name.
pub(crate) active_profile_name: std::string::String,
/// Masked PostgreSQL DSN.
pub(crate) masked_dsn: std::string::String,
/// Maximum rows accepted by one query.
pub(crate) maximum_limit: u32,
/// Program identifiers enumerable from `kb_program_ids`.
pub(crate) known_programs: std::vec::Vec<crate::DemoSqlReplayKnownProgramOption>,
}
/// UI request for bounded transaction replay candidates.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayTransactionRequest.ts"
)]
pub(crate) struct DemoSqlReplayTransactionRequest {
/// Optional partial signature search.
pub(crate) signature_contains: std::option::Option<std::string::String>,
/// Optional inclusive minimum slot.
#[ts(type = "number | null")]
pub(crate) min_slot: std::option::Option<u64>,
/// Optional inclusive maximum slot.
#[ts(type = "number | null")]
pub(crate) max_slot: std::option::Option<u64>,
/// Optional raw processing state.
pub(crate) raw_processing_state: std::option::Option<std::string::String>,
/// Optional latest ledger status.
pub(crate) ledger_status: std::option::Option<std::string::String>,
/// Optional exact program id.
pub(crate) program_id: std::option::Option<std::string::String>,
/// Program scope code: any, outer, inner or logs.
pub(crate) program_scope: std::string::String,
/// Optional entity kind: mint, owner or account_key.
pub(crate) entity_kind: std::option::Option<std::string::String>,
/// Optional exact entity value.
pub(crate) entity_value: std::option::Option<std::string::String>,
/// Maximum returned rows.
pub(crate) limit: u32,
/// Orders newest slots first when true.
pub(crate) newest_first: bool,
}
/// One transaction row shown by the replay candidate browser.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayTransactionRow.ts"
)]
pub(crate) struct DemoSqlReplayTransactionRow {
/// Canonical transaction signature.
pub(crate) signature: std::string::String,
/// Transaction slot.
#[ts(type = "number")]
pub(crate) slot: i64,
/// Current raw processing state.
pub(crate) raw_processing_state: std::string::String,
/// Current raw retention state.
pub(crate) retention_state: std::string::String,
/// Whether a core transaction exists.
pub(crate) has_core_transaction: bool,
/// Core transaction failure flag when available.
pub(crate) transaction_failed: std::option::Option<bool>,
/// Latest core extraction ledger status.
pub(crate) ledger_status: std::string::String,
/// Latest processor version when available.
pub(crate) processor_version: std::option::Option<std::string::String>,
/// Latest attempt count.
pub(crate) attempt_count: i32,
/// Number of top-level instructions.
#[ts(type = "number")]
pub(crate) outer_instruction_count: i64,
/// Number of inner instructions.
#[ts(type = "number")]
pub(crate) inner_instruction_count: i64,
/// Number of distinct top-level programs.
#[ts(type = "number")]
pub(crate) outer_program_count: i64,
/// Number of distinct inner programs.
#[ts(type = "number")]
pub(crate) inner_program_count: i64,
/// Raw row update timestamp.
pub(crate) updated_at: std::string::String,
}
/// UI request for bounded program summaries.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayProgramRequest.ts"
)]
pub(crate) struct DemoSqlReplayProgramRequest {
/// Optional partial program id search.
pub(crate) program_id_contains: std::option::Option<std::string::String>,
/// Maximum returned rows.
pub(crate) limit: u32,
}
/// Aggregated program row shown by the replay candidate browser.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayProgramRow.ts"
)]
pub(crate) struct DemoSqlReplayProgramRow {
/// Optional stable code from `kb_program_ids`.
pub(crate) program_code: std::option::Option<std::string::String>,
/// Program id.
pub(crate) program_id: std::string::String,
/// Number of distinct transactions.
#[ts(type = "number")]
pub(crate) transaction_count: i64,
/// Number of top-level instruction occurrences.
#[ts(type = "number")]
pub(crate) outer_instruction_count: i64,
/// Number of inner instruction occurrences.
#[ts(type = "number")]
pub(crate) inner_instruction_count: i64,
/// Number of reliably linked log occurrences.
#[ts(type = "number")]
pub(crate) log_count: i64,
/// Lowest observed slot.
#[ts(type = "number")]
pub(crate) min_slot: i64,
/// Highest observed slot.
#[ts(type = "number")]
pub(crate) max_slot: i64,
}
/// UI request for bounded core entity summaries.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayEntityRequest.ts"
)]
pub(crate) struct DemoSqlReplayEntityRequest {
/// Entity kind: mint, owner or account_key.
pub(crate) entity_kind: std::string::String,
/// Optional partial entity value search.
pub(crate) entity_value_contains: std::option::Option<std::string::String>,
/// Maximum returned rows.
pub(crate) limit: u32,
}
/// Aggregated core entity row shown by the replay candidate browser.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayEntityRow.ts"
)]
pub(crate) struct DemoSqlReplayEntityRow {
/// Stable entity kind code.
pub(crate) entity_kind: std::string::String,
/// Mint, owner or account-key address.
pub(crate) entity_value: std::string::String,
/// Number of distinct transactions.
#[ts(type = "number")]
pub(crate) transaction_count: i64,
/// Total number of core-table occurrences.
#[ts(type = "number")]
pub(crate) occurrence_count: i64,
/// Lowest observed slot.
#[ts(type = "number")]
pub(crate) min_slot: i64,
/// Highest observed slot.
#[ts(type = "number")]
pub(crate) max_slot: i64,
}
pub(crate) fn transaction_row_from_pg(
row: kb_store::PostgresReplayTransactionCandidate,
) -> crate::DemoSqlReplayTransactionRow {
return crate::DemoSqlReplayTransactionRow {
signature: row.signature,
slot: row.slot,
raw_processing_state: row.raw_processing_state,
retention_state: row.retention_state,
has_core_transaction: row.has_core_transaction,
transaction_failed: row.transaction_failed,
ledger_status: row.ledger_status,
processor_version: row.processor_version,
attempt_count: row.attempt_count,
outer_instruction_count: row.outer_instruction_count,
inner_instruction_count: row.inner_instruction_count,
outer_program_count: row.outer_program_count,
inner_program_count: row.inner_program_count,
updated_at: row.updated_at,
};
}
pub(crate) fn program_row_from_pg(
row: kb_store::PostgresReplayProgramSummary,
) -> crate::DemoSqlReplayProgramRow {
let program_code = kb_program_ids::find_registered_program_id(&row.program_id)
.map(|entry| return entry.code().to_owned());
return crate::DemoSqlReplayProgramRow {
program_code,
program_id: row.program_id,
transaction_count: row.transaction_count,
outer_instruction_count: row.outer_instruction_count,
inner_instruction_count: row.inner_instruction_count,
log_count: row.log_count,
min_slot: row.min_slot,
max_slot: row.max_slot,
};
}
pub(crate) fn entity_row_from_pg(
row: kb_store::PostgresReplayEntitySummary,
) -> crate::DemoSqlReplayEntityRow {
return crate::DemoSqlReplayEntityRow {
entity_kind: row.entity_kind,
entity_value: row.entity_value,
transaction_count: row.transaction_count,
occurrence_count: row.occurrence_count,
min_slot: row.min_slot,
max_slot: row.max_slot,
};
}
pub(crate) fn validated_csv_file_name(
file_name: &str,
) -> std::result::Result<&'static str, std::string::String> {
return match file_name {
"replay_transactions.csv" => std::result::Result::Ok("replay_transactions.csv"),
"replay_programs.csv" => std::result::Result::Ok("replay_programs.csv"),
"replay_mints.csv" => std::result::Result::Ok("replay_mints.csv"),
"replay_owners.csv" => std::result::Result::Ok("replay_owners.csv"),
"replay_account_keys.csv" => std::result::Result::Ok("replay_account_keys.csv"),
_ => std::result::Result::Err(format!("unsupported replay CSV file name: {file_name}")),
};
}
pub(crate) fn csv_export_directory_from_current_dir(
current_dir: &std::path::Path,
) -> std::path::PathBuf {
let current_name = current_dir.file_name().and_then(std::ffi::OsStr::to_str);
if let (std::option::Option::Some("kb_app_demo_desktop"), std::option::Option::Some(parent)) =
(current_name, current_dir.parent())
{
return parent.join("data").join("exports_csv");
}
return current_dir.join("data").join("exports_csv");
}
pub(crate) async fn available_csv_export_path(
export_dir: &std::path::Path,
file_name: &str,
) -> std::result::Result<std::path::PathBuf, std::string::String> {
let direct_path = export_dir.join(file_name);
let direct_exists_result = tokio::fs::try_exists(&direct_path).await;
let direct_exists = match direct_exists_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(format!("cannot inspect CSV export path: {error}"));
},
};
if !direct_exists {
return std::result::Result::Ok(direct_path);
}
let stem = file_name.trim_end_matches(".csv");
for suffix in 1_u16..=999_u16 {
let candidate = export_dir.join(format!("{stem}_{suffix}.csv"));
let exists_result = tokio::fs::try_exists(&candidate).await;
let exists = match exists_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(format!(
"cannot inspect CSV export path: {error}"
));
},
};
if !exists {
return std::result::Result::Ok(candidate);
}
}
return std::result::Result::Err("cannot allocate a unique CSV export path".to_owned());
}
pub(crate) fn program_scope_from_code(
code: &str,
) -> std::result::Result<kb_store::PostgresReplayProgramScope, std::string::String> {
return match code {
"any" => std::result::Result::Ok(kb_store::PostgresReplayProgramScope::Any),
"outer" => std::result::Result::Ok(kb_store::PostgresReplayProgramScope::Outer),
"inner" => std::result::Result::Ok(kb_store::PostgresReplayProgramScope::Inner),
"logs" => std::result::Result::Ok(kb_store::PostgresReplayProgramScope::Logs),
_ => std::result::Result::Err(format!("unsupported replay program scope: {code}")),
};
}
pub(crate) fn optional_entity_kind_from_code(
code: std::option::Option<&str>,
) -> std::result::Result<
std::option::Option<kb_store::PostgresReplayEntityKind>,
std::string::String,
> {
return match code {
std::option::Option::Some(value) => {
let result = entity_kind_from_code(value);
match result {
std::result::Result::Ok(kind) => {
std::result::Result::Ok(std::option::Option::Some(kind))
},
std::result::Result::Err(error) => std::result::Result::Err(error),
}
},
std::option::Option::None => std::result::Result::Ok(std::option::Option::None),
};
}
pub(crate) fn entity_kind_from_code(
code: &str,
) -> std::result::Result<kb_store::PostgresReplayEntityKind, std::string::String> {
return match code {
"mint" => std::result::Result::Ok(kb_store::PostgresReplayEntityKind::Mint),
"owner" => std::result::Result::Ok(kb_store::PostgresReplayEntityKind::Owner),
"account_key" => std::result::Result::Ok(kb_store::PostgresReplayEntityKind::AccountKey),
_ => std::result::Result::Err(format!("unsupported replay entity kind: {code}")),
};
}
#[cfg(test)]
mod tests {
#[test]
fn program_scope_parser_accepts_logs() {
let result = crate::program_scope_from_code("logs");
assert_eq!(result, std::result::Result::Ok(kb_store::PostgresReplayProgramScope::Logs));
}
#[test]
fn entity_kind_parser_rejects_unknown_code() {
let result = crate::entity_kind_from_code("unknown");
assert!(result.is_err());
}
#[test]
fn csv_file_name_rejects_unknown_name() {
let result = crate::validated_csv_file_name("arbitrary.csv");
assert!(result.is_err());
}
#[test]
fn csv_export_directory_uses_workspace_root_from_demo_crate() {
let current_dir = std::path::Path::new("/tmp/khadhroony-bot2/kb_app_demo_desktop");
let result = crate::csv_export_directory_from_current_dir(current_dir);
assert_eq!(result, std::path::Path::new("/tmp/khadhroony-bot2/data/exports_csv"));
}
#[test]
fn csv_file_name_accepts_split_entity_exports() {
let mint_result = crate::validated_csv_file_name("replay_mints.csv");
let owner_result = crate::validated_csv_file_name("replay_owners.csv");
let account_result = crate::validated_csv_file_name("replay_account_keys.csv");
assert!(mint_result.is_ok());
assert!(owner_result.is_ok());
assert!(account_result.is_ok());
}
#[test]
fn program_row_exposes_registered_code() {
let row = kb_store::PostgresReplayProgramSummary {
program_id: kb_program_ids::SYSTEM_PROGRAM_ID.to_owned(),
transaction_count: 1,
outer_instruction_count: 1,
inner_instruction_count: 0,
log_count: 1,
min_slot: 1,
max_slot: 1,
};
let converted = crate::program_row_from_pg(row);
assert_eq!(converted.program_code.as_deref(), std::option::Option::Some("system"));
}
}

View File

@@ -266,7 +266,11 @@ pub(crate) async fn demo_ws_connect_inner(
std::result::Result::Ok(request) => request,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let selected_client = match state.ws_pool().select_client_for_role_and_method(&role, &method) {
let pool = match state.demo_ws_pool() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let selected_client = match pool.select_client_for_role_and_method(&role, &method) {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};

View File

@@ -12,6 +12,11 @@ mod constants;
mod demo_backfill;
mod demo_http;
mod demo_ws;
mod demo_sql_common;
mod demo_sql_diag;
mod demo_sql_pg_core;
mod demo_sql_pg_raw;
mod demo_sql_replay_candidates;
mod frontend_log;
mod main_window;
mod splash;
@@ -90,6 +95,60 @@ pub(crate) use self::demo_ws::demo_ws_status_inner;
pub(crate) use self::demo_ws::demo_ws_unsubscribe_inner;
/// Disconnects the application WebSocket session during shutdown.
pub(crate) use self::demo_ws::disconnect_demo_ws_app_state;
/// UI-safe SQL table diagnostic snapshot.
pub(crate) use self::demo_sql_common::DemoSqlTableSnapshot;
/// Connects to the configured PostgreSQL store.
pub(crate) use self::demo_sql_common::connect_postgres_store;
/// Formats diagnostic statuses.
pub(crate) use self::demo_sql_common::debug_status;
/// Opens or focuses one SQL demo window.
pub(crate) use self::demo_sql_common::open_sql_demo_window;
/// Builds PostgreSQL options from the active profile.
pub(crate) use self::demo_sql_common::postgres_store_options_from_profile;
/// Converts one PostgreSQL table diagnostic.
pub(crate) use self::demo_sql_common::table_snapshot_from_pg;
/// Converts PostgreSQL table diagnostics.
pub(crate) use self::demo_sql_common::table_snapshots_from_pg;
/// SQL diagnostic payload.
pub(crate) use self::demo_sql_diag::DemoSqlDiagPayload;
/// PostgreSQL core-table payload.
pub(crate) use self::demo_sql_pg_core::DemoSqlPgCorePayload;
/// PostgreSQL raw-table payload.
pub(crate) use self::demo_sql_pg_raw::DemoSqlPgRawPayload;
/// SQL replay entity request.
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayEntityRequest;
/// SQL replay entity row.
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayEntityRow;
/// Known program option.
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayKnownProgramOption;
/// SQL replay options payload.
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayOptionsPayload;
/// SQL replay program request.
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayProgramRequest;
/// SQL replay program row.
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayProgramRow;
/// SQL replay transaction request.
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayTransactionRequest;
/// SQL replay transaction row.
pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayTransactionRow;
/// Resolves an available CSV path.
pub(crate) use self::demo_sql_replay_candidates::available_csv_export_path;
/// Resolves the CSV export directory.
pub(crate) use self::demo_sql_replay_candidates::csv_export_directory_from_current_dir;
/// Parses an entity-kind code.
pub(crate) use self::demo_sql_replay_candidates::entity_kind_from_code;
/// Converts a PostgreSQL entity row.
pub(crate) use self::demo_sql_replay_candidates::entity_row_from_pg;
/// Parses an optional entity-kind code.
pub(crate) use self::demo_sql_replay_candidates::optional_entity_kind_from_code;
/// Converts a PostgreSQL program row.
pub(crate) use self::demo_sql_replay_candidates::program_row_from_pg;
/// Parses a program-scope code.
pub(crate) use self::demo_sql_replay_candidates::program_scope_from_code;
/// Converts a PostgreSQL transaction row.
pub(crate) use self::demo_sql_replay_candidates::transaction_row_from_pg;
/// Validates a CSV filename.
pub(crate) use self::demo_sql_replay_candidates::validated_csv_file_name;
/// Frontend logging payload.
pub(crate) use self::frontend_log::FrontendLogPayload;
/// Emits one normalized frontend log event.

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/tauri.rs
// version: 6
// version: 7
//! Tauri runtime assembly and private command wrappers.
@@ -52,8 +52,33 @@ pub fn run() -> kb_core::Result<()> {
demo_ws_connect,
demo_ws_unsubscribe,
demo_ws_disconnect,
open_demo_sql_diag_window,
load_demo_sql_diag,
open_demo_sql_pg_raw_window,
load_demo_sql_pg_raw,
open_demo_sql_pg_core_window,
load_demo_sql_pg_core,
open_demo_sql_replay_candidates_window,
demo_sql_replay_options,
load_demo_sql_replay_transactions,
load_demo_sql_replay_programs,
load_demo_sql_replay_entities,
export_demo_sql_replay_csv,
]);
builder = builder.plugin(tracing_builder.build::<tauri::Wry>());
builder = builder.on_window_event(|window, event| {
if window.label() != "demo_ws" {
return;
}
if !matches!(event, tauri::WindowEvent::Destroyed) {
return;
}
let app_handle = window.app_handle().clone();
tauri::async_runtime::spawn(async move {
let state = app_handle.state::<crate::AppState>();
crate::disconnect_demo_ws_app_state(&state, false).await;
});
});
builder = builder.setup(|app| {
let splash_window = match app.get_webview_window("splash") {
std::option::Option::Some(window) => window,
@@ -96,13 +121,6 @@ pub fn run() -> kb_core::Result<()> {
std::option::Option::None,
std::option::Option::None,
);
crate::emit_splash_order(
&splash_window,
"add_log",
std::option::Option::Some("Pool WebSocket initialisé"),
std::option::Option::None,
std::option::Option::None,
);
crate::emit_splash_order(
&splash_window,
"fadein",
@@ -434,7 +452,11 @@ fn open_demo_ws_window(
fn demo_ws_list_pool_clients(
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<std::vec::Vec<kb_onchain_transport::WsPoolClientSnapshot>, std::string::String> {
return std::result::Result::Ok(state.ws_pool().snapshot());
let pool = match state.demo_ws_pool() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
return std::result::Result::Ok(pool.snapshot());
}
/// Lists selectable WebSocket roles and methods for the demo UI.
@@ -443,7 +465,10 @@ fn demo_ws_options(
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::DemoWsOptionsPayload, std::string::String> {
return std::result::Result::Ok(crate::DemoWsOptionsPayload {
roles: crate::build_ws_role_options(state.ws_pool().snapshot()),
roles: crate::build_ws_role_options(match state.demo_ws_pool() {
std::result::Result::Ok(pool) => pool.snapshot(),
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
}),
methods: crate::build_ws_method_options(),
});
}
@@ -488,3 +513,338 @@ async fn demo_ws_disconnect(
) -> std::result::Result<crate::DemoWsStatusPayload, std::string::String> {
return crate::demo_ws_disconnect_inner(state.inner()).await;
}
/// Opens or focuses the SQL diagnostic demo window.
#[tauri::command]
fn open_demo_sql_diag_window(
app_handle: tauri::AppHandle,
) -> std::result::Result<(), std::string::String> {
return crate::open_sql_demo_window(
app_handle,
"demo_sql_diag",
"demo_sql_diag.html",
"Khadhroony Bot3 - SQL diagnostics",
);
}
/// Loads read-only SQL diagnostics from the active profile.
#[tauri::command]
#[allow(clippy::question_mark_used)]
async fn load_demo_sql_diag(
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::DemoSqlDiagPayload, std::string::String> {
let profile = state.active_profile().clone();
let store_result = crate::connect_postgres_store(&profile).await;
let store = match store_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let diagnostics_result = store.backend_diagnostics().await;
let diagnostics = match diagnostics_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let tables_result = store.known_table_diagnostics().await;
let tables = match tables_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
return std::result::Result::Ok(crate::DemoSqlDiagPayload {
config_path: state.config_path().to_string(),
active_profile_name: profile.name.clone(),
backend: profile.database.backend.clone(),
masked_dsn: store.options().masked_dsn(),
current_schema: diagnostics.descriptor.current_schema,
health_status: crate::debug_status(diagnostics.health.status),
health_message: diagnostics.health.message,
migration_status: crate::debug_status(diagnostics.migrations.status),
migration_message: diagnostics.migrations.message,
server_version: diagnostics.server_version,
tables: crate::table_snapshots_from_pg(tables.as_slice()),
});
}
/// Opens or focuses the PostgreSQL canonical acquisition demo window.
#[tauri::command]
fn open_demo_sql_pg_raw_window(
app_handle: tauri::AppHandle,
) -> std::result::Result<(), std::string::String> {
return crate::open_sql_demo_window(
app_handle,
"demo_sql_pg_raw",
"demo_sql_pg_raw.html",
"Khadhroony Bot3 - PostgreSQL canonical acquisition",
);
}
/// Loads read-only raw table diagnostics from PostgreSQL.
#[tauri::command]
#[allow(clippy::question_mark_used)]
async fn load_demo_sql_pg_raw(
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::DemoSqlPgRawPayload, std::string::String> {
let profile = state.active_profile().clone();
let store_result = crate::connect_postgres_store(&profile).await;
let store = match store_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let tables_result = store.raw_table_diagnostics().await;
let tables = match tables_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
return std::result::Result::Ok(crate::DemoSqlPgRawPayload {
active_profile_name: profile.name.clone(),
masked_dsn: store.options().masked_dsn(),
tables: crate::table_snapshots_from_pg(tables.as_slice()),
});
}
/// Opens or focuses the PostgreSQL core store demo window.
#[tauri::command]
fn open_demo_sql_pg_core_window(
app_handle: tauri::AppHandle,
) -> std::result::Result<(), std::string::String> {
return crate::open_sql_demo_window(
app_handle,
"demo_sql_pg_core",
"demo_sql_pg_core.html",
"Khadhroony Bot3 - PostgreSQL core store",
);
}
/// Loads read-only core table diagnostics from PostgreSQL.
#[tauri::command]
#[allow(clippy::question_mark_used)]
async fn load_demo_sql_pg_core(
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::DemoSqlPgCorePayload, std::string::String> {
let profile = state.active_profile().clone();
let store_result = crate::connect_postgres_store(&profile).await;
let store = match store_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let tables_result = store.core_table_diagnostics().await;
let tables = match tables_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
return std::result::Result::Ok(crate::DemoSqlPgCorePayload {
active_profile_name: profile.name.clone(),
masked_dsn: store.options().masked_dsn(),
tables: crate::table_snapshots_from_pg(tables.as_slice()),
});
}
/// Opens or focuses the SQL replay candidate browser.
#[tauri::command]
fn open_demo_sql_replay_candidates_window(
app_handle: tauri::AppHandle,
) -> std::result::Result<(), std::string::String> {
return crate::open_sql_demo_window(
app_handle,
"demo_sql_replay_candidates",
"demo_sql_replay_candidates.html",
"Khadhroony Bot3 - SQL replay candidates",
);
}
/// Writes a bounded replay-candidate CSV file into `./data/exports_csv/`.
#[tauri::command]
#[allow(clippy::question_mark_used)]
async fn export_demo_sql_replay_csv(
file_name: std::string::String,
content: std::string::String,
) -> std::result::Result<std::string::String, std::string::String> {
if content.is_empty() {
return std::result::Result::Err("CSV export content must not be empty".to_owned());
}
if content.len() > 16 * 1024 * 1024 {
return std::result::Result::Err("CSV export content exceeds 16 MiB".to_owned());
}
let normalized_name_result = crate::validated_csv_file_name(&file_name);
let normalized_name = match normalized_name_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let current_dir_result = std::env::current_dir();
let current_dir = match current_dir_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(format!("cannot resolve current directory: {error}"));
},
};
let export_dir = crate::csv_export_directory_from_current_dir(&current_dir);
let create_result = tokio::fs::create_dir_all(&export_dir).await;
if let std::result::Result::Err(error) = create_result {
return std::result::Result::Err(format!("cannot create CSV export directory: {error}"));
}
let export_path_result = crate::available_csv_export_path(&export_dir, normalized_name).await;
let export_path = match export_path_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut bytes = std::vec::Vec::with_capacity(content.len() + 3);
bytes.extend_from_slice(&[0xEF, 0xBB, 0xBF]);
bytes.extend_from_slice(content.as_bytes());
let write_result = tokio::fs::write(&export_path, bytes).await;
if let std::result::Result::Err(error) = write_result {
return std::result::Result::Err(format!("cannot write CSV export: {error}"));
}
let display_path = export_path.to_string_lossy().into_owned();
tracing::info!(target: crate::TRACING_TARGET, path = %display_path, "CSV replay candidate export written");
return std::result::Result::Ok(display_path);
}
/// Loads static browser options from the active PostgreSQL profile.
#[tauri::command]
#[allow(clippy::question_mark_used)]
async fn demo_sql_replay_options(
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::DemoSqlReplayOptionsPayload, std::string::String> {
let profile = state.active_profile().clone();
let options_result = crate::postgres_store_options_from_profile(&profile, false);
let options = match options_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let mut known_programs =
std::vec::Vec::with_capacity(kb_program_ids::registered_program_ids().len());
for entry in kb_program_ids::registered_program_ids() {
known_programs.push(crate::DemoSqlReplayKnownProgramOption {
code: entry.code().to_owned(),
program_id: entry.program_id().to_owned(),
});
}
return std::result::Result::Ok(crate::DemoSqlReplayOptionsPayload {
active_profile_name: profile.name,
masked_dsn: options.masked_dsn(),
maximum_limit: kb_store::MAX_REPLAY_CANDIDATE_ROWS,
known_programs,
});
}
/// Loads bounded transaction replay candidates from PostgreSQL.
#[tauri::command]
#[allow(clippy::question_mark_used)]
async fn load_demo_sql_replay_transactions(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoSqlReplayTransactionRequest,
) -> std::result::Result<std::vec::Vec<crate::DemoSqlReplayTransactionRow>, std::string::String> {
let program_scope_result = crate::program_scope_from_code(request.program_scope.as_str());
let program_scope = match program_scope_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let entity_kind_result = crate::optional_entity_kind_from_code(request.entity_kind.as_deref());
let entity_kind = match entity_kind_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let filter_result = kb_store::PostgresReplayTransactionFilter::new(
request.signature_contains,
request.min_slot,
request.max_slot,
request.raw_processing_state,
request.ledger_status,
request.program_id,
program_scope,
entity_kind,
request.entity_value,
request.limit,
request.newest_first,
);
let filter = match filter_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let store_result = crate::connect_postgres_store(state.active_profile()).await;
let store = match store_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let rows_result = store.replay_transaction_candidates(&filter).await;
let rows = match rows_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
tracing::debug!(target: crate::TRACING_TARGET, rows = rows.len(), "loaded replay transaction candidates");
let mut output = std::vec::Vec::with_capacity(rows.len());
for row in rows {
output.push(crate::transaction_row_from_pg(row));
}
return std::result::Result::Ok(output);
}
/// Loads bounded program summaries from PostgreSQL.
#[tauri::command]
#[allow(clippy::question_mark_used)]
async fn load_demo_sql_replay_programs(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoSqlReplayProgramRequest,
) -> std::result::Result<std::vec::Vec<crate::DemoSqlReplayProgramRow>, std::string::String> {
let filter_result =
kb_store::PostgresReplayProgramFilter::new(request.program_id_contains, request.limit);
let filter = match filter_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let store_result = crate::connect_postgres_store(state.active_profile()).await;
let store = match store_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let rows_result = store.replay_program_summaries(&filter).await;
let rows = match rows_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
tracing::debug!(target: crate::TRACING_TARGET, rows = rows.len(), "loaded replay program summaries");
let mut output = std::vec::Vec::with_capacity(rows.len());
for row in rows {
output.push(crate::program_row_from_pg(row));
}
return std::result::Result::Ok(output);
}
/// Loads bounded mint, owner or account-key summaries from PostgreSQL core tables.
#[tauri::command]
#[allow(clippy::question_mark_used)]
async fn load_demo_sql_replay_entities(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoSqlReplayEntityRequest,
) -> std::result::Result<std::vec::Vec<crate::DemoSqlReplayEntityRow>, std::string::String> {
let entity_kind_result = crate::entity_kind_from_code(request.entity_kind.as_str());
let entity_kind = match entity_kind_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let filter_result = kb_store::PostgresReplayEntityFilter::new(
entity_kind,
request.entity_value_contains,
request.limit,
);
let filter = match filter_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let store_result = crate::connect_postgres_store(state.active_profile()).await;
let store = match store_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let rows_result = store.replay_entity_summaries(&filter).await;
let rows = match rows_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
tracing::debug!(target: crate::TRACING_TARGET, rows = rows.len(), "loaded replay entity summaries");
let mut output = std::vec::Vec::with_capacity(rows.len());
for row in rows {
output.push(crate::entity_row_from_pg(row));
}
return std::result::Result::Ok(output);
}

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/vite.config.ts
// version: 2
// version: 3
import { defineConfig, normalizePath } from "vite";
import { NodePackageImporter } from "sass-embedded";
@@ -22,7 +22,14 @@ export default defineConfig(() => ({
rollupOptions: {
input: {
"splash": normalizePath(resolve(__dirname, 'frontend/splash.html')),
"main": normalizePath(resolve(__dirname, 'frontend/main.html'))
"main": normalizePath(resolve(__dirname, 'frontend/main.html')),
"demo_backfill": normalizePath(resolve(__dirname, 'frontend/demo_backfill.html')),
"demo_http": normalizePath(resolve(__dirname, 'frontend/demo_http.html')),
"demo_ws": normalizePath(resolve(__dirname, 'frontend/demo_ws.html')),
"demo_sql_diag": normalizePath(resolve(__dirname, 'frontend/demo_sql_diag.html')),
"demo_sql_pg_raw": normalizePath(resolve(__dirname, 'frontend/demo_sql_pg_raw.html')),
"demo_sql_pg_core": normalizePath(resolve(__dirname, 'frontend/demo_sql_pg_core.html')),
"demo_sql_replay_candidates": normalizePath(resolve(__dirname, 'frontend/demo_sql_replay_candidates.html')),
},
output: {
entryFileNames: 'js/[name]-[hash].js',