v0.1.0-pre.050
This commit is contained in:
115
kb-app-demo-desktop/frontend/demo_sql_diag.html
Normal file
115
kb-app-demo-desktop/frontend/demo_sql_diag.html
Normal 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">
|
||||
© 2026 SASEDEV
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="ts/demo_sql_diag.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
102
kb-app-demo-desktop/frontend/demo_sql_pg_core.html
Normal file
102
kb-app-demo-desktop/frontend/demo_sql_pg_core.html
Normal 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">
|
||||
© 2026 SASEDEV
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="ts/demo_sql_pg_core.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
102
kb-app-demo-desktop/frontend/demo_sql_pg_raw.html
Normal file
102
kb-app-demo-desktop/frontend/demo_sql_pg_raw.html
Normal 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">
|
||||
© 2026 SASEDEV
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="ts/demo_sql_pg_raw.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
443
kb-app-demo-desktop/frontend/demo_sql_replay_candidates.html
Normal file
443
kb-app-demo-desktop/frontend/demo_sql_replay_candidates.html
Normal 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 l’export CSV utilisent toutes les lignes filtrées. Les CSV UTF-8 avec BOM et séparateur point-virgule sont écrits par le backend Tauri dans <code>./data/exports_csv/</code>. LibreOffice Calc peut toujours afficher son dialogue d’import, car le format CSV ne décrit pas formellement son encodage ni son séparateur.</p>
|
||||
<div class="small text-body-secondary">
|
||||
<span class="fw-semibold">DSN :</span>
|
||||
<code id="replayCandidateDsn">—</code>
|
||||
<span class="ms-3 fw-semibold">Limite serveur maximale :</span>
|
||||
<code id="replayCandidateMaximumLimit">—</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="nav nav-tabs mb-3" id="replayCandidateTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="transactionsTabButton" data-bs-toggle="tab" data-bs-target="#transactionsTabPane" type="button" role="tab" aria-controls="transactionsTabPane" aria-selected="true">Transactions et signatures</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="programsTabButton" data-bs-toggle="tab" data-bs-target="#programsTabPane" type="button" role="tab" aria-controls="programsTabPane" aria-selected="false">Programmes</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="mintsTabButton" data-bs-toggle="tab" data-bs-target="#mintsTabPane" type="button" role="tab" aria-controls="mintsTabPane" aria-selected="false">Mints</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="ownersTabButton" data-bs-toggle="tab" data-bs-target="#ownersTabPane" type="button" role="tab" aria-controls="ownersTabPane" aria-selected="false">Owners</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="accountsTabButton" data-bs-toggle="tab" data-bs-target="#accountsTabPane" type="button" role="tab" aria-controls="accountsTabPane" aria-selected="false">Comptes</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="replayCandidateTabContent">
|
||||
<section class="tab-pane fade show active" id="transactionsTabPane" role="tabpanel" aria-labelledby="transactionsTabButton" tabindex="0">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 card-title">Filtres PostgreSQL bornés</h2>
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-12 col-xl-4">
|
||||
<label for="transactionSignatureContainsInput" class="form-label">Signature contient</label>
|
||||
<input id="transactionSignatureContainsInput" class="form-control font-monospace" type="text" autocomplete="off" />
|
||||
</div>
|
||||
<div class="col-6 col-md-3 col-xl-2">
|
||||
<label for="transactionMinSlotInput" class="form-label">Slot minimum</label>
|
||||
<input id="transactionMinSlotInput" class="form-control" type="number" min="0" step="1" />
|
||||
</div>
|
||||
<div class="col-6 col-md-3 col-xl-2">
|
||||
<label for="transactionMaxSlotInput" class="form-label">Slot maximum</label>
|
||||
<input id="transactionMaxSlotInput" class="form-control" type="number" min="0" step="1" />
|
||||
</div>
|
||||
<div class="col-6 col-md-3 col-xl-2">
|
||||
<label for="transactionRawStateSelect" class="form-label">État raw</label>
|
||||
<select id="transactionRawStateSelect" class="form-select">
|
||||
<option value="">Tous</option>
|
||||
<option value="received">received</option>
|
||||
<option value="core_extracted">core_extracted</option>
|
||||
<option value="decoded">decoded</option>
|
||||
<option value="materialized">materialized</option>
|
||||
<option value="failed">failed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6 col-md-3 col-xl-2">
|
||||
<label for="transactionLedgerStatusSelect" class="form-label">Ledger core</label>
|
||||
<select id="transactionLedgerStatusSelect" class="form-select">
|
||||
<option value="">Tous</option>
|
||||
<option value="not_started">not_started</option>
|
||||
<option value="running">running</option>
|
||||
<option value="succeeded">succeeded</option>
|
||||
<option value="failed">failed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-xl-5">
|
||||
<label for="transactionProgramIdInput" class="form-label">Program ID exact</label>
|
||||
<input id="transactionProgramIdInput" class="form-control font-monospace" type="text" list="knownProgramDatalist" autocomplete="off" />
|
||||
</div>
|
||||
<div class="col-6 col-md-3 col-xl-2">
|
||||
<label for="transactionProgramScopeSelect" class="form-label">Portée programme</label>
|
||||
<select id="transactionProgramScopeSelect" class="form-select">
|
||||
<option value="any">outer + inner + logs</option>
|
||||
<option value="outer">outer uniquement</option>
|
||||
<option value="inner">inner uniquement</option>
|
||||
<option value="logs">logs liés uniquement</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6 col-md-3 col-xl-2">
|
||||
<label for="transactionEntityKindSelect" class="form-label">Type d’entité</label>
|
||||
<select id="transactionEntityKindSelect" class="form-select">
|
||||
<option value="">Aucune</option>
|
||||
<option value="mint">mint</option>
|
||||
<option value="owner">owner</option>
|
||||
<option value="account_key">account_key</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-xl-3">
|
||||
<label for="transactionEntityValueInput" class="form-label">Entité exacte</label>
|
||||
<input id="transactionEntityValueInput" class="form-control font-monospace" type="text" autocomplete="off" />
|
||||
</div>
|
||||
<div class="col-6 col-md-3 col-xl-1">
|
||||
<label for="transactionLimitInput" class="form-label">Limite</label>
|
||||
<input id="transactionLimitInput" class="form-control" type="number" min="1" max="5000" step="1" value="500" />
|
||||
</div>
|
||||
<div class="col-6 col-md-3 col-xl-2">
|
||||
<div class="form-check form-switch mb-2">
|
||||
<input id="transactionNewestFirstInput" class="form-check-input" type="checkbox" role="switch" checked />
|
||||
<label class="form-check-label" for="transactionNewestFirstInput">Plus récents d’abord</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-3">
|
||||
<div class="d-grid d-sm-flex gap-2">
|
||||
<button id="resetTransactionsButton" class="btn btn-outline-secondary" type="button">Réinitialiser</button>
|
||||
<button id="loadTransactionsButton" class="btn btn-primary flex-grow-1" type="button">Charger les transactions</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
|
||||
<div>
|
||||
<h2 class="h5 card-title mb-1">Transactions candidates</h2>
|
||||
<div id="transactionResultSummary" class="small text-body-secondary">Aucune requête exécutée.</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="btn-group flex-wrap" role="group" aria-label="Actions signatures">
|
||||
<button id="copySignaturesButton" class="btn btn-sm btn-outline-primary" type="button">Copier signatures multiligne</button>
|
||||
<button id="exportTransactionsCsvButton" class="btn btn-sm btn-outline-success" type="button">Exporter CSV</button>
|
||||
</div>
|
||||
<div class="small text-body-secondary mt-1">Les identifiants longs sont tronqués à l’écran. Survolez-les pour afficher leur valeur complète ou utilisez le bouton de copie individuel.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table id="transactionCandidateTable" class="table table-striped table-hover table-sm align-middle w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="replay-select-column"><span class="visually-hidden">Sélection</span></th>
|
||||
<th>Signature</th>
|
||||
<th>Slot</th>
|
||||
<th>Raw</th>
|
||||
<th title="État de présence core ; échec on-chain signifie que la transaction Solana a échoué, pas que l’extraction core a échoué.">Core</th>
|
||||
<th>Ledger</th>
|
||||
<th>Version</th>
|
||||
<th>Essais</th>
|
||||
<th data-bs-toggle="tooltip" title="Nombre total d’instructions top-level.">Outer inst.</th>
|
||||
<th data-bs-toggle="tooltip" title="Nombre total d’instructions internes.">Inner inst.</th>
|
||||
<th data-bs-toggle="tooltip" title="Nombre de program IDs distincts dans les instructions top-level.">Outer pr.</th>
|
||||
<th data-bs-toggle="tooltip" title="Nombre de program IDs distincts dans les instructions internes.">Inner pr.</th>
|
||||
<th>Mise à jour</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="tab-pane fade" id="programsTabPane" role="tabpanel" aria-labelledby="programsTabButton" tabindex="0">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-12 col-lg-5">
|
||||
<label for="knownProgramSelect" class="form-label">Programme connu (kb_program_ids)</label>
|
||||
<select id="knownProgramSelect" class="form-select">
|
||||
<option value="">Tous les programmes connus</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4">
|
||||
<label for="programContainsInput" class="form-label">Program ID contient</label>
|
||||
<input id="programContainsInput" class="form-control font-monospace" type="text" list="knownProgramDatalist" autocomplete="off" />
|
||||
</div>
|
||||
<div class="col-6 col-lg-1">
|
||||
<label for="programLimitInput" class="form-label">Limite</label>
|
||||
<input id="programLimitInput" class="form-control" type="number" min="1" max="5000" step="1" value="500" />
|
||||
</div>
|
||||
<div class="col-12 col-lg-2">
|
||||
<div class="d-grid gap-2">
|
||||
<button id="resetProgramsButton" class="btn btn-outline-secondary" type="button">Réinitialiser</button>
|
||||
<button id="loadProgramsButton" class="btn btn-primary" type="button">Charger les programmes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
|
||||
<div>
|
||||
<h2 class="h5 card-title mb-1">Programmes indexés</h2>
|
||||
<div id="programResultSummary" class="small text-body-secondary">Aucune requête exécutée.</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="btn-group flex-wrap" role="group" aria-label="Actions programmes">
|
||||
<button id="useSelectedProgramButton" class="btn btn-sm btn-outline-secondary" type="button">Utiliser comme filtre transactions</button>
|
||||
<button id="copyProgramsButton" class="btn btn-sm btn-outline-primary" type="button">Copier IDs multiligne</button>
|
||||
<button id="exportProgramsCsvButton" class="btn btn-sm btn-outline-success" type="button">Exporter CSV</button>
|
||||
</div>
|
||||
<div class="small text-body-secondary mt-1">Cochez un programme. Sans sélection, l’action accepte une seule ligne restant après le filtre local.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table id="programCandidateTable" class="table table-striped table-hover table-sm align-middle w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="replay-select-column"><span class="visually-hidden">Sélection</span></th>
|
||||
<th>Code connu</th>
|
||||
<th>Program ID</th>
|
||||
<th>Transactions</th>
|
||||
<th>Outer</th>
|
||||
<th>Inner</th>
|
||||
<th>Logs liés</th>
|
||||
<th>Slot min</th>
|
||||
<th>Slot max</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="tab-pane fade" id="mintsTabPane" role="tabpanel" aria-labelledby="mintsTabButton" tabindex="0">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-12 col-lg-7">
|
||||
<label for="mintContainsInput" class="form-label">Mint contient</label>
|
||||
<input id="mintContainsInput" class="form-control font-monospace" type="text" autocomplete="off" />
|
||||
</div>
|
||||
<div class="col-6 col-lg-2">
|
||||
<label for="mintLimitInput" class="form-label">Limite</label>
|
||||
<input id="mintLimitInput" class="form-control" type="number" min="1" max="5000" step="1" value="500" />
|
||||
</div>
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="d-grid d-sm-flex gap-2">
|
||||
<button id="resetMintsButton" class="btn btn-outline-secondary" type="button">Réinitialiser</button>
|
||||
<button id="loadMintsButton" class="btn btn-primary flex-grow-1" type="button">Charger les mints</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
|
||||
<div>
|
||||
<h2 class="h5 card-title mb-1">Mints</h2>
|
||||
<div id="mintResultSummary" class="small text-body-secondary">Aucune requête exécutée.</div>
|
||||
<div class="small text-body-secondary">Source : <code>kb_sol_core_balance_changes.mint</code>.</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="btn-group flex-wrap" role="group" aria-label="Actions mints">
|
||||
<button id="useSelectedMintsButton" class="btn btn-sm btn-outline-secondary" type="button">Utiliser comme filtre transactions</button>
|
||||
<button id="copyMintsButton" class="btn btn-sm btn-outline-primary" type="button">Copier valeurs multiligne</button>
|
||||
<button id="exportMintsCsvButton" class="btn btn-sm btn-outline-success" type="button">Exporter CSV</button>
|
||||
</div>
|
||||
<div class="small text-body-secondary mt-1">Cochez une ligne. Sans sélection, l’action accepte une seule ligne restant après le filtre local.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table id="mintCandidateTable" class="table table-striped table-hover table-sm align-middle w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="replay-select-column"><span class="visually-hidden">Sélection</span></th>
|
||||
<th>Mint</th>
|
||||
<th data-bs-toggle="tooltip" title="Nombre de signatures distinctes dans lesquelles la valeur apparaît.">Transactions</th>
|
||||
<th data-bs-toggle="tooltip" title="Nombre total de lignes core portant cette valeur.">Occurrences</th>
|
||||
<th data-bs-toggle="tooltip" title="Occurrences divisées par le nombre de transactions distinctes.">Occ./tx</th>
|
||||
<th data-bs-toggle="tooltip" title="Premier slot observé dans le corpus core.">Slot min</th>
|
||||
<th data-bs-toggle="tooltip" title="Dernier slot observé dans le corpus core.">Slot max</th>
|
||||
<th data-bs-toggle="tooltip" title="Différence entre le slot maximal et le slot minimal.">Étendue</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="tab-pane fade" id="ownersTabPane" role="tabpanel" aria-labelledby="ownersTabButton" tabindex="0">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-12 col-lg-7">
|
||||
<label for="ownerContainsInput" class="form-label">Owner contient</label>
|
||||
<input id="ownerContainsInput" class="form-control font-monospace" type="text" autocomplete="off" />
|
||||
</div>
|
||||
<div class="col-6 col-lg-2">
|
||||
<label for="ownerLimitInput" class="form-label">Limite</label>
|
||||
<input id="ownerLimitInput" class="form-control" type="number" min="1" max="5000" step="1" value="500" />
|
||||
</div>
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="d-grid d-sm-flex gap-2">
|
||||
<button id="resetOwnersButton" class="btn btn-outline-secondary" type="button">Réinitialiser</button>
|
||||
<button id="loadOwnersButton" class="btn btn-primary flex-grow-1" type="button">Charger les owners</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
|
||||
<div>
|
||||
<h2 class="h5 card-title mb-1">Owners</h2>
|
||||
<div id="ownerResultSummary" class="small text-body-secondary">Aucune requête exécutée.</div>
|
||||
<div class="small text-body-secondary">Source : <code>kb_sol_core_balance_changes.owner</code>.</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="btn-group flex-wrap" role="group" aria-label="Actions owners">
|
||||
<button id="useSelectedOwnersButton" class="btn btn-sm btn-outline-secondary" type="button">Utiliser comme filtre transactions</button>
|
||||
<button id="copyOwnersButton" class="btn btn-sm btn-outline-primary" type="button">Copier valeurs multiligne</button>
|
||||
<button id="exportOwnersCsvButton" class="btn btn-sm btn-outline-success" type="button">Exporter CSV</button>
|
||||
</div>
|
||||
<div class="small text-body-secondary mt-1">Cochez une ligne. Sans sélection, l’action accepte une seule ligne restant après le filtre local.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table id="ownerCandidateTable" class="table table-striped table-hover table-sm align-middle w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="replay-select-column"><span class="visually-hidden">Sélection</span></th>
|
||||
<th>Owner</th>
|
||||
<th data-bs-toggle="tooltip" title="Nombre de signatures distinctes dans lesquelles la valeur apparaît.">Transactions</th>
|
||||
<th data-bs-toggle="tooltip" title="Nombre total de lignes core portant cette valeur.">Occurrences</th>
|
||||
<th data-bs-toggle="tooltip" title="Occurrences divisées par le nombre de transactions distinctes.">Occ./tx</th>
|
||||
<th data-bs-toggle="tooltip" title="Premier slot observé dans le corpus core.">Slot min</th>
|
||||
<th data-bs-toggle="tooltip" title="Dernier slot observé dans le corpus core.">Slot max</th>
|
||||
<th data-bs-toggle="tooltip" title="Différence entre le slot maximal et le slot minimal.">Étendue</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="tab-pane fade" id="accountsTabPane" role="tabpanel" aria-labelledby="accountsTabButton" tabindex="0">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-12 col-lg-7">
|
||||
<label for="accountContainsInput" class="form-label">Account key contient</label>
|
||||
<input id="accountContainsInput" class="form-control font-monospace" type="text" autocomplete="off" />
|
||||
</div>
|
||||
<div class="col-6 col-lg-2">
|
||||
<label for="accountLimitInput" class="form-label">Limite</label>
|
||||
<input id="accountLimitInput" class="form-control" type="number" min="1" max="5000" step="1" value="500" />
|
||||
</div>
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="d-grid d-sm-flex gap-2">
|
||||
<button id="resetAccountsButton" class="btn btn-outline-secondary" type="button">Réinitialiser</button>
|
||||
<button id="loadAccountsButton" class="btn btn-primary flex-grow-1" type="button">Charger les comptes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
|
||||
<div>
|
||||
<h2 class="h5 card-title mb-1">Comptes</h2>
|
||||
<div id="accountResultSummary" class="small text-body-secondary">Aucune requête exécutée.</div>
|
||||
<div class="small text-body-secondary">Source : <code>kb_sol_core_account_keys.account_key</code>.</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="btn-group flex-wrap" role="group" aria-label="Actions comptes">
|
||||
<button id="useSelectedAccountsButton" class="btn btn-sm btn-outline-secondary" type="button">Utiliser comme filtre transactions</button>
|
||||
<button id="copyAccountsButton" class="btn btn-sm btn-outline-primary" type="button">Copier valeurs multiligne</button>
|
||||
<button id="exportAccountsCsvButton" class="btn btn-sm btn-outline-success" type="button">Exporter CSV</button>
|
||||
</div>
|
||||
<div class="small text-body-secondary mt-1">Cochez une ligne. Sans sélection, l’action accepte une seule ligne restant après le filtre local.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table id="accountCandidateTable" class="table table-striped table-hover table-sm align-middle w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="replay-select-column"><span class="visually-hidden">Sélection</span></th>
|
||||
<th>Account key</th>
|
||||
<th data-bs-toggle="tooltip" title="Nombre de signatures distinctes dans lesquelles la valeur apparaît.">Transactions</th>
|
||||
<th data-bs-toggle="tooltip" title="Nombre total de lignes core portant cette valeur.">Occurrences</th>
|
||||
<th data-bs-toggle="tooltip" title="Occurrences divisées par le nombre de transactions distinctes.">Occ./tx</th>
|
||||
<th data-bs-toggle="tooltip" title="Premier slot observé dans le corpus core.">Slot min</th>
|
||||
<th data-bs-toggle="tooltip" title="Dernier slot observé dans le corpus core.">Slot max</th>
|
||||
<th data-bs-toggle="tooltip" title="Différence entre le slot maximal et le slot minimal.">Étendue</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="app-footer bg-dark text-light">
|
||||
<div class="container h-100 d-flex align-items-center">
|
||||
<div class="row flex-grow-1 align-items-center">
|
||||
<div class="col-12 text-center text-small my-1 my-md-0">© 2026 SASEDEV</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<datalist id="knownProgramDatalist"></datalist>
|
||||
<script type="module" src="ts/demo_sql_replay_candidates.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -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>
|
||||
|
||||
60
kb-app-demo-desktop/frontend/ts/demo_sql_diag.ts
Normal file
60
kb-app-demo-desktop/frontend/ts/demo_sql_diag.ts
Normal 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();
|
||||
});
|
||||
42
kb-app-demo-desktop/frontend/ts/demo_sql_pg_core.ts
Normal file
42
kb-app-demo-desktop/frontend/ts/demo_sql_pg_core.ts
Normal 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();
|
||||
});
|
||||
42
kb-app-demo-desktop/frontend/ts/demo_sql_pg_raw.ts
Normal file
42
kb-app-demo-desktop/frontend/ts/demo_sql_pg_raw.ts
Normal 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();
|
||||
});
|
||||
800
kb-app-demo-desktop/frontend/ts/demo_sql_replay_candidates.ts
Normal file
800
kb-app-demo-desktop/frontend/ts/demo_sql_replay_candidates.ts
Normal 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("&")
|
||||
.split("<").join("<")
|
||||
.split(">").join(">")
|
||||
.split('"').join(""")
|
||||
.split("'").join("'");
|
||||
}
|
||||
|
||||
function codeDisplay(value: unknown, type: string): string {
|
||||
if (type !== "display") {
|
||||
return String(value ?? "");
|
||||
}
|
||||
return `<code class="text-break">${escapeHtml(value)}</code>`;
|
||||
}
|
||||
|
||||
function compactIdentifier(value: string, leadingLength: number = 12, trailingLength: number = 10): string {
|
||||
if (value.length <= leadingLength + trailingLength + 1) {
|
||||
return value;
|
||||
}
|
||||
return `${value.slice(0, leadingLength)}…${value.slice(-trailingLength)}`;
|
||||
}
|
||||
|
||||
function copyableCodeDisplay(label: string): (value: unknown, type: string) => string {
|
||||
return (value: unknown, type: string): string => {
|
||||
const text = String(value ?? "");
|
||||
if (type !== "display") {
|
||||
return text;
|
||||
}
|
||||
const escapedText = escapeHtml(text);
|
||||
const compactText = escapeHtml(compactIdentifier(text));
|
||||
const escapedLabel = escapeHtml(label);
|
||||
return `<span class="d-inline-flex align-items-center gap-1 text-nowrap"><code data-bs-toggle="tooltip" data-bs-placement="top" title="${escapedText}">${compactText}</code><button class="btn btn-sm btn-link text-secondary p-0 replay-cell-copy" type="button" data-bs-toggle="tooltip" data-bs-placement="top" title="Copier ${escapedLabel}" aria-label="Copier ${escapedLabel}" data-copy-value="${escapedText}" data-copy-label="${escapedLabel}"><i class="fa-regular fa-copy" aria-hidden="true"></i></button></span>`;
|
||||
};
|
||||
}
|
||||
|
||||
function badgeDisplay(value: unknown, type: string): string {
|
||||
if (type !== "display") {
|
||||
return String(value ?? "");
|
||||
}
|
||||
const text = String(value ?? "—");
|
||||
const className = text === "succeeded" || text === "core_extracted"
|
||||
? "text-bg-success"
|
||||
: text === "failed"
|
||||
? "text-bg-danger"
|
||||
: text === "running" || text === "received"
|
||||
? "text-bg-warning"
|
||||
: "text-bg-secondary";
|
||||
return `<span class="badge ${className}">${escapeHtml(text)}</span>`;
|
||||
}
|
||||
|
||||
function coreStatusText(row: DemoSqlReplayTransactionRow): string {
|
||||
if (!row.hasCoreTransaction) {
|
||||
return "absent";
|
||||
}
|
||||
if (row.transactionFailed === true) {
|
||||
return "échec on-chain";
|
||||
}
|
||||
return "présent";
|
||||
}
|
||||
|
||||
function coreDisplay(_value: unknown, type: string, row: DemoSqlReplayTransactionRow): string | number {
|
||||
if (type === "display") {
|
||||
if (!row.hasCoreTransaction) {
|
||||
return '<span class="badge text-bg-secondary">absent</span>';
|
||||
}
|
||||
if (row.transactionFailed === true) {
|
||||
return '<span class="badge text-bg-danger" title="La transaction Solana a échoué on-chain, mais son graphe core a été extrait.">échec on-chain</span>';
|
||||
}
|
||||
return '<span class="badge text-bg-success">présent</span>';
|
||||
}
|
||||
if (type === "filter") {
|
||||
return coreStatusText(row);
|
||||
}
|
||||
if (row.transactionFailed === true) {
|
||||
return 0;
|
||||
}
|
||||
return row.hasCoreTransaction ? 1 : 2;
|
||||
}
|
||||
|
||||
function occurrenceRatioDisplay(_value: unknown, type: string, row: DemoSqlReplayEntityRow): string | number {
|
||||
const ratio = row.transactionCount > 0 ? row.occurrenceCount / row.transactionCount : 0;
|
||||
if (type === "display" || type === "filter") {
|
||||
return ratio.toFixed(2);
|
||||
}
|
||||
return ratio;
|
||||
}
|
||||
|
||||
function slotSpanDisplay(_value: unknown, type: string, row: DemoSqlReplayEntityRow): string | number {
|
||||
const span = Math.max(0, row.maxSlot - row.minSlot);
|
||||
if (type === "display" || type === "filter") {
|
||||
return String(span);
|
||||
}
|
||||
return span;
|
||||
}
|
||||
|
||||
function commonLanguage(emptyTable: string, infoLabel: string, zeroRecords: string): object {
|
||||
return {
|
||||
emptyTable,
|
||||
info: `_START_ à _END_ sur _TOTAL_ ${infoLabel}`,
|
||||
infoEmpty: `0 ${infoLabel}`,
|
||||
infoFiltered: "(filtré depuis _MAX_)",
|
||||
lengthMenu: "Afficher _MENU_",
|
||||
search: "Filtre local :",
|
||||
zeroRecords,
|
||||
paginate: { first: "Premier", last: "Dernier", next: "Suivant", previous: "Précédent" },
|
||||
select: { rows: { _: "%d lignes sélectionnées", 0: "", 1: "1 ligne sélectionnée" } },
|
||||
};
|
||||
}
|
||||
|
||||
function createTransactionTable(): Api<DemoSqlReplayTransactionRow> {
|
||||
return new DataTable<DemoSqlReplayTransactionRow>("#transactionCandidateTable", {
|
||||
data: [],
|
||||
columns: [
|
||||
{
|
||||
data: null,
|
||||
className: "replay-select-column text-center",
|
||||
width: "1%",
|
||||
orderable: false,
|
||||
searchable: false,
|
||||
render: DataTable.render.select(),
|
||||
},
|
||||
{ data: "signature", render: copyableCodeDisplay("la signature") },
|
||||
{ data: "slot" },
|
||||
{ data: "rawProcessingState", render: badgeDisplay },
|
||||
{ data: "hasCoreTransaction", render: coreDisplay },
|
||||
{ data: "ledgerStatus", render: badgeDisplay },
|
||||
{ data: "processorVersion", defaultContent: "—", render: codeDisplay },
|
||||
{ data: "attemptCount" },
|
||||
{ data: "outerInstructionCount" },
|
||||
{ data: "innerInstructionCount" },
|
||||
{ data: "outerProgramCount" },
|
||||
{ data: "innerProgramCount" },
|
||||
{ data: "updatedAt" },
|
||||
],
|
||||
order: [[2, "desc"]],
|
||||
pageLength: 10,
|
||||
lengthMenu: [10, 25, 50, 100, 250],
|
||||
select: { style: "multi", selector: "td:first-child", headerCheckbox: "select-all" },
|
||||
language: commonLanguage(
|
||||
"Aucune transaction chargée",
|
||||
"transactions",
|
||||
"Aucune transaction ne correspond au filtre local",
|
||||
),
|
||||
drawCallback: () => initializeTooltips(),
|
||||
});
|
||||
}
|
||||
|
||||
function createProgramTable(): Api<DemoSqlReplayProgramRow> {
|
||||
return new DataTable<DemoSqlReplayProgramRow>("#programCandidateTable", {
|
||||
data: [],
|
||||
columns: [
|
||||
{
|
||||
data: null,
|
||||
className: "replay-select-column text-center",
|
||||
width: "1%",
|
||||
orderable: false,
|
||||
searchable: false,
|
||||
render: DataTable.render.select(),
|
||||
},
|
||||
{ data: "programCode", defaultContent: "—", render: codeDisplay },
|
||||
{ data: "programId", render: copyableCodeDisplay("le program ID") },
|
||||
{ data: "transactionCount" },
|
||||
{ data: "outerInstructionCount" },
|
||||
{ data: "innerInstructionCount" },
|
||||
{ data: "logCount" },
|
||||
{ data: "minSlot" },
|
||||
{ data: "maxSlot" },
|
||||
],
|
||||
order: [[3, "desc"]],
|
||||
pageLength: 10,
|
||||
lengthMenu: [10, 25, 50, 100, 250],
|
||||
select: { style: "multi", selector: "td:first-child", headerCheckbox: "select-all" },
|
||||
language: commonLanguage(
|
||||
"Aucun programme chargé",
|
||||
"programmes",
|
||||
"Aucun programme ne correspond au filtre local",
|
||||
),
|
||||
drawCallback: () => initializeTooltips(),
|
||||
});
|
||||
}
|
||||
|
||||
function createEntityTable(config: EntityViewConfig): Api<DemoSqlReplayEntityRow> {
|
||||
return new DataTable<DemoSqlReplayEntityRow>(config.tableSelector, {
|
||||
data: [],
|
||||
columns: [
|
||||
{
|
||||
data: null,
|
||||
className: "replay-select-column text-center",
|
||||
width: "1%",
|
||||
orderable: false,
|
||||
searchable: false,
|
||||
render: DataTable.render.select(),
|
||||
},
|
||||
{ data: "entityValue", render: copyableCodeDisplay(config.label) },
|
||||
{ data: "transactionCount" },
|
||||
{ data: "occurrenceCount" },
|
||||
{ data: null, render: occurrenceRatioDisplay },
|
||||
{ data: "minSlot" },
|
||||
{ data: "maxSlot" },
|
||||
{ data: null, render: slotSpanDisplay },
|
||||
],
|
||||
order: [[2, "desc"]],
|
||||
pageLength: 10,
|
||||
lengthMenu: [10, 25, 50, 100, 250],
|
||||
select: { style: "multi", selector: "td:first-child", headerCheckbox: "select-all" },
|
||||
language: commonLanguage(
|
||||
`Aucun ${config.label} chargé`,
|
||||
config.pluralLabel,
|
||||
`Aucun ${config.label} ne correspond au filtre local`,
|
||||
),
|
||||
drawCallback: () => initializeTooltips(),
|
||||
});
|
||||
}
|
||||
|
||||
function entityTable(kind: EntityKindCode): Api<DemoSqlReplayEntityRow> | null {
|
||||
if (kind === "mint") {
|
||||
return mintTable;
|
||||
}
|
||||
if (kind === "owner") {
|
||||
return ownerTable;
|
||||
}
|
||||
return accountTable;
|
||||
}
|
||||
|
||||
function replaceRows<T>(table: Api<T>, rows: T[]): void {
|
||||
table.clear();
|
||||
table.rows.add(rows);
|
||||
table.draw();
|
||||
}
|
||||
|
||||
function resetTableState<T>(table: Api<T> | null): void {
|
||||
if (table === null) {
|
||||
return;
|
||||
}
|
||||
table.search("");
|
||||
table.columns().search("");
|
||||
table.rows().deselect();
|
||||
table.draw();
|
||||
}
|
||||
|
||||
function selectedOrFilteredRows<T>(table: Api<T>): T[] {
|
||||
const selected = table.rows({ selected: true, search: "applied" }).data().toArray();
|
||||
if (selected.length > 0) {
|
||||
return selected;
|
||||
}
|
||||
return table.rows({ search: "applied" }).data().toArray();
|
||||
}
|
||||
|
||||
function selectedOrSingleFilteredRow<T>(table: Api<T>, label: string): T {
|
||||
const selected = table.rows({ selected: true }).data().toArray();
|
||||
if (selected.length === 1) {
|
||||
return selected[0];
|
||||
}
|
||||
if (selected.length > 1) {
|
||||
throw new Error(`Plusieurs ${label} sont cochés. Conservez une seule sélection.`);
|
||||
}
|
||||
const filtered = table.rows({ search: "applied" }).data().toArray();
|
||||
if (filtered.length === 1) {
|
||||
return filtered[0];
|
||||
}
|
||||
throw new Error(`Cochez exactement un ${label}, ou réduisez le filtre local à une seule ligne.`);
|
||||
}
|
||||
|
||||
function setBusy(busy: boolean, message: string): void {
|
||||
busyOperationCount = busy ? busyOperationCount + 1 : Math.max(0, busyOperationCount - 1);
|
||||
const operationInProgress = busyOperationCount > 0;
|
||||
const badge = element<HTMLElement>("#replayCandidateStatusBadge");
|
||||
badge.textContent = operationInProgress ? (busy ? message : "Chargement en cours") : "Prêt";
|
||||
badge.className = operationInProgress ? "badge text-bg-warning" : "badge text-bg-success";
|
||||
document.querySelectorAll<HTMLButtonElement>("button").forEach(button => {
|
||||
if (button.id !== "openCoreExtractionButton") {
|
||||
button.disabled = operationInProgress;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function buildTransactionRequest(): DemoSqlReplayTransactionRequest {
|
||||
const entityKind = optionalInputValue("#transactionEntityKindSelect");
|
||||
const entityValue = optionalInputValue("#transactionEntityValueInput");
|
||||
if ((entityKind === null) !== (entityValue === null)) {
|
||||
throw new Error("Le type d’entité et sa valeur doivent être renseignés ensemble.");
|
||||
}
|
||||
return {
|
||||
signatureContains: optionalInputValue("#transactionSignatureContainsInput"),
|
||||
minSlot: optionalNonNegativeIntegerValue("#transactionMinSlotInput"),
|
||||
maxSlot: optionalNonNegativeIntegerValue("#transactionMaxSlotInput"),
|
||||
rawProcessingState: optionalInputValue("#transactionRawStateSelect"),
|
||||
ledgerStatus: optionalInputValue("#transactionLedgerStatusSelect"),
|
||||
programId: optionalInputValue("#transactionProgramIdInput"),
|
||||
programScope: requiredInputValue("#transactionProgramScopeSelect"),
|
||||
entityKind,
|
||||
entityValue,
|
||||
limit: boundedPositiveIntegerValue("#transactionLimitInput"),
|
||||
newestFirst: element<HTMLInputElement>("#transactionNewestFirstInput").checked,
|
||||
};
|
||||
}
|
||||
|
||||
async function resetTransactionFilters(): Promise<void> {
|
||||
element<HTMLInputElement>("#transactionSignatureContainsInput").value = "";
|
||||
element<HTMLInputElement>("#transactionMinSlotInput").value = "";
|
||||
element<HTMLInputElement>("#transactionMaxSlotInput").value = "";
|
||||
element<HTMLSelectElement>("#transactionRawStateSelect").value = "";
|
||||
element<HTMLSelectElement>("#transactionLedgerStatusSelect").value = "";
|
||||
element<HTMLInputElement>("#transactionProgramIdInput").value = "";
|
||||
element<HTMLSelectElement>("#transactionProgramScopeSelect").value = "any";
|
||||
element<HTMLSelectElement>("#transactionEntityKindSelect").value = "";
|
||||
element<HTMLInputElement>("#transactionEntityValueInput").value = "";
|
||||
element<HTMLInputElement>("#transactionLimitInput").value = "500";
|
||||
element<HTMLInputElement>("#transactionNewestFirstInput").checked = true;
|
||||
resetTableState(transactionTable);
|
||||
await loadTransactions();
|
||||
}
|
||||
|
||||
async function resetProgramFilters(): Promise<void> {
|
||||
element<HTMLSelectElement>("#knownProgramSelect").value = "";
|
||||
element<HTMLInputElement>("#programContainsInput").value = "";
|
||||
element<HTMLInputElement>("#programLimitInput").value = "500";
|
||||
resetTableState(programTable);
|
||||
await loadPrograms();
|
||||
}
|
||||
|
||||
async function resetEntityFilters(config: EntityViewConfig): Promise<void> {
|
||||
element<HTMLInputElement>(config.containsSelector).value = "";
|
||||
element<HTMLInputElement>(config.limitSelector).value = "500";
|
||||
resetTableState(entityTable(config.kind));
|
||||
await loadEntity(config);
|
||||
}
|
||||
|
||||
async function loadTransactions(): Promise<void> {
|
||||
if (transactionTable === null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setBusy(true, "Chargement transactions");
|
||||
const request = buildTransactionRequest();
|
||||
const rows = await invoke<DemoSqlReplayTransactionRow[]>("load_demo_sql_replay_transactions", { request });
|
||||
replaceRows(transactionTable, rows);
|
||||
element<HTMLElement>("#transactionResultSummary").textContent = `${rows.length} transaction(s) chargée(s) depuis PostgreSQL.`;
|
||||
frontendDebug(tracingTarget, `loaded ${rows.length} replay transaction candidates`);
|
||||
} catch (caughtError) {
|
||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||
element<HTMLElement>("#transactionResultSummary").textContent = `Erreur : ${message}`;
|
||||
frontendError(tracingTarget, `transaction candidate loading failed: ${message}`);
|
||||
} finally {
|
||||
setBusy(false, "Prêt");
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPrograms(): Promise<void> {
|
||||
if (programTable === null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setBusy(true, "Chargement programmes");
|
||||
const request: DemoSqlReplayProgramRequest = {
|
||||
programIdContains: optionalInputValue("#programContainsInput"),
|
||||
limit: boundedPositiveIntegerValue("#programLimitInput"),
|
||||
};
|
||||
const rows = await invoke<DemoSqlReplayProgramRow[]>("load_demo_sql_replay_programs", { request });
|
||||
replaceRows(programTable, rows);
|
||||
element<HTMLElement>("#programResultSummary").textContent = `${rows.length} programme(s) chargé(s), avec occurrences outer, inner et logs liés.`;
|
||||
frontendDebug(tracingTarget, `loaded ${rows.length} replay program summaries`);
|
||||
} catch (caughtError) {
|
||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||
element<HTMLElement>("#programResultSummary").textContent = `Erreur : ${message}`;
|
||||
frontendError(tracingTarget, `program summary loading failed: ${message}`);
|
||||
} finally {
|
||||
setBusy(false, "Prêt");
|
||||
}
|
||||
}
|
||||
|
||||
async function loadEntity(config: EntityViewConfig): Promise<void> {
|
||||
const table = entityTable(config.kind);
|
||||
if (table === null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setBusy(true, `Chargement ${config.pluralLabel}`);
|
||||
const request: DemoSqlReplayEntityRequest = {
|
||||
entityKind: config.kind,
|
||||
entityValueContains: optionalInputValue(config.containsSelector),
|
||||
limit: boundedPositiveIntegerValue(config.limitSelector),
|
||||
};
|
||||
const rows = await invoke<DemoSqlReplayEntityRow[]>("load_demo_sql_replay_entities", { request });
|
||||
replaceRows(table, rows);
|
||||
element<HTMLElement>(config.summarySelector).textContent = `${rows.length} ${config.pluralLabel} chargé(s) depuis PostgreSQL.`;
|
||||
frontendDebug(tracingTarget, `loaded ${rows.length} replay ${config.kind} summaries`);
|
||||
} catch (caughtError) {
|
||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||
element<HTMLElement>(config.summarySelector).textContent = `Erreur : ${message}`;
|
||||
frontendError(tracingTarget, `${config.kind} summary loading failed: ${message}`);
|
||||
} finally {
|
||||
setBusy(false, "Prêt");
|
||||
}
|
||||
}
|
||||
|
||||
async function copyLines(lines: string[], label: string): Promise<void> {
|
||||
if (lines.length === 0) {
|
||||
throw new Error(`Aucune ${label} à copier.`);
|
||||
}
|
||||
await navigator.clipboard.writeText(lines.join("\n"));
|
||||
element<HTMLElement>("#replayCandidateStatusBadge").textContent = `${lines.length} ${label} copiée(s)`;
|
||||
}
|
||||
|
||||
function csvCell(value: unknown): string {
|
||||
const text = value === null || value === undefined ? "" : String(value);
|
||||
return `"${text.split('"').join('""')}"`;
|
||||
}
|
||||
|
||||
function csvContent(headers: string[], rows: unknown[][]): string {
|
||||
const lines = [headers, ...rows].map(row => row.map(csvCell).join(";"));
|
||||
return `${lines.join("\r\n")}\r\n`;
|
||||
}
|
||||
|
||||
async function saveCsv(fileName: string, content: string, rowCount: number): Promise<void> {
|
||||
if (rowCount === 0) {
|
||||
throw new Error("Aucune ligne à exporter.");
|
||||
}
|
||||
const path = await invoke<string>("export_demo_sql_replay_csv", { fileName, content });
|
||||
const badge = element<HTMLElement>("#replayCandidateStatusBadge");
|
||||
badge.textContent = `${rowCount} ligne(s) exportée(s) : ${path}`;
|
||||
badge.className = "badge text-bg-success";
|
||||
frontendDebug(tracingTarget, `CSV export written: ${path}`);
|
||||
}
|
||||
|
||||
async function exportTransactionsCsv(): Promise<void> {
|
||||
const rows = transactionRowsForExport();
|
||||
const content = csvContent(
|
||||
["signature", "slot", "raw_processing_state", "retention_state", "core_status", "ledger_status", "processor_version", "attempt_count", "outer_instruction_count", "inner_instruction_count", "outer_program_count", "inner_program_count", "updated_at"],
|
||||
rows.map(row => [row.signature, row.slot, row.rawProcessingState, row.retentionState, coreStatusText(row), row.ledgerStatus, row.processorVersion, row.attemptCount, row.outerInstructionCount, row.innerInstructionCount, row.outerProgramCount, row.innerProgramCount, row.updatedAt]),
|
||||
);
|
||||
await saveCsv("replay_transactions.csv", content, rows.length);
|
||||
}
|
||||
|
||||
async function exportProgramsCsv(): Promise<void> {
|
||||
const rows = programRowsForExport();
|
||||
const content = csvContent(
|
||||
["program_code", "program_id", "transaction_count", "outer_instruction_count", "inner_instruction_count", "log_count", "min_slot", "max_slot"],
|
||||
rows.map(row => [row.programCode, row.programId, row.transactionCount, row.outerInstructionCount, row.innerInstructionCount, row.logCount, row.minSlot, row.maxSlot]),
|
||||
);
|
||||
await saveCsv("replay_programs.csv", content, rows.length);
|
||||
}
|
||||
|
||||
async function exportEntityCsv(config: EntityViewConfig): Promise<void> {
|
||||
const rows = entityRowsForExport(config.kind);
|
||||
const content = csvContent(
|
||||
["entity_kind", "entity_value", "transaction_count", "occurrence_count", "occurrences_per_transaction", "min_slot", "max_slot", "slot_span"],
|
||||
rows.map(row => [row.entityKind, row.entityValue, row.transactionCount, row.occurrenceCount, row.transactionCount > 0 ? row.occurrenceCount / row.transactionCount : 0, row.minSlot, row.maxSlot, Math.max(0, row.maxSlot - row.minSlot)]),
|
||||
);
|
||||
await saveCsv(config.fileName, content, rows.length);
|
||||
}
|
||||
|
||||
function transactionRowsForExport(): DemoSqlReplayTransactionRow[] {
|
||||
if (transactionTable === null) {
|
||||
return [];
|
||||
}
|
||||
return selectedOrFilteredRows(transactionTable);
|
||||
}
|
||||
|
||||
function programRowsForExport(): DemoSqlReplayProgramRow[] {
|
||||
if (programTable === null) {
|
||||
return [];
|
||||
}
|
||||
return selectedOrFilteredRows(programTable);
|
||||
}
|
||||
|
||||
function entityRowsForExport(kind: EntityKindCode): DemoSqlReplayEntityRow[] {
|
||||
const table = entityTable(kind);
|
||||
if (table === null) {
|
||||
return [];
|
||||
}
|
||||
return selectedOrFilteredRows(table);
|
||||
}
|
||||
|
||||
function showTransactionsTab(): void {
|
||||
bootstrap.Tab.getOrCreateInstance(element<HTMLButtonElement>("#transactionsTabButton")).show();
|
||||
}
|
||||
|
||||
async function useSelectedProgram(): Promise<void> {
|
||||
if (programTable === null) {
|
||||
return;
|
||||
}
|
||||
const row = selectedOrSingleFilteredRow(programTable, "programme");
|
||||
element<HTMLInputElement>("#transactionProgramIdInput").value = row.programId;
|
||||
element<HTMLSelectElement>("#transactionProgramScopeSelect").value = "any";
|
||||
showTransactionsTab();
|
||||
await loadTransactions();
|
||||
}
|
||||
|
||||
async function useSelectedEntity(config: EntityViewConfig): Promise<void> {
|
||||
const table = entityTable(config.kind);
|
||||
if (table === null) {
|
||||
return;
|
||||
}
|
||||
const row = selectedOrSingleFilteredRow(table, config.label);
|
||||
element<HTMLSelectElement>("#transactionEntityKindSelect").value = config.kind;
|
||||
element<HTMLInputElement>("#transactionEntityValueInput").value = row.entityValue;
|
||||
showTransactionsTab();
|
||||
await loadTransactions();
|
||||
}
|
||||
|
||||
async function openCoreExtraction(): Promise<void> {
|
||||
try {
|
||||
await invoke("open_demo_core_extraction_window");
|
||||
} catch (caughtError) {
|
||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||
frontendError(tracingTarget, `core extraction window opening failed: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOptions(): Promise<void> {
|
||||
const options = await invoke<DemoSqlReplayOptionsPayload>("demo_sql_replay_options");
|
||||
if (!Number.isSafeInteger(options.maximumLimit) || options.maximumLimit <= 0) {
|
||||
throw new Error("La limite maximale des candidats replay retournée par le backend est invalide.");
|
||||
}
|
||||
maximumReplayCandidateLimit = options.maximumLimit;
|
||||
element<HTMLElement>("#replayCandidateProfileBadge").textContent = `Profil ${options.activeProfileName}`;
|
||||
element<HTMLElement>("#replayCandidateDsn").textContent = options.maskedDsn;
|
||||
element<HTMLElement>("#replayCandidateMaximumLimit").textContent = String(options.maximumLimit);
|
||||
document.querySelectorAll<HTMLInputElement>("input[type=number][max]").forEach(input => {
|
||||
input.max = String(options.maximumLimit);
|
||||
});
|
||||
populateKnownPrograms(options.knownPrograms);
|
||||
}
|
||||
|
||||
function populateKnownPrograms(programs: DemoSqlReplayKnownProgramOption[]): void {
|
||||
const select = element<HTMLSelectElement>("#knownProgramSelect");
|
||||
const datalist = element<HTMLDataListElement>("#knownProgramDatalist");
|
||||
select.replaceChildren(new Option("Tous les programmes connus", ""));
|
||||
datalist.replaceChildren();
|
||||
for (const program of programs) {
|
||||
const label = `${program.code} — ${program.programId}`;
|
||||
select.add(new Option(label, program.programId));
|
||||
const option = document.createElement("option");
|
||||
option.value = program.programId;
|
||||
option.label = program.code;
|
||||
datalist.append(option);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeTooltips(): void {
|
||||
document.querySelectorAll<HTMLElement>('[data-bs-toggle="tooltip"]').forEach(target => {
|
||||
bootstrap.Tooltip.getOrCreateInstance(target);
|
||||
});
|
||||
}
|
||||
|
||||
function adjustVisibleTables(): void {
|
||||
transactionTable?.columns.adjust();
|
||||
programTable?.columns.adjust();
|
||||
mintTable?.columns.adjust();
|
||||
ownerTable?.columns.adjust();
|
||||
accountTable?.columns.adjust();
|
||||
}
|
||||
|
||||
function installInlineCopyHandler(): void {
|
||||
document.addEventListener("click", event => {
|
||||
const source = event.target;
|
||||
if (!(source instanceof Element)) {
|
||||
return;
|
||||
}
|
||||
const button = source.closest<HTMLButtonElement>("button[data-copy-value]");
|
||||
if (button === null) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const value = button.dataset.copyValue ?? "";
|
||||
const label = button.dataset.copyLabel ?? "valeur";
|
||||
void copyLines([value], label).catch(error => reportUiError(`copy ${label}`, error));
|
||||
});
|
||||
}
|
||||
|
||||
function reportUiError(action: string, caughtError: unknown): void {
|
||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||
element<HTMLElement>("#replayCandidateStatusBadge").textContent = `Erreur : ${message}`;
|
||||
element<HTMLElement>("#replayCandidateStatusBadge").className = "badge text-bg-danger";
|
||||
frontendError(tracingTarget, `${action} failed: ${message}`);
|
||||
}
|
||||
|
||||
function installHandlers(): void {
|
||||
element<HTMLButtonElement>("#loadTransactionsButton").addEventListener("click", () => void loadTransactions());
|
||||
element<HTMLButtonElement>("#resetTransactionsButton").addEventListener("click", () => {
|
||||
void resetTransactionFilters().catch(error => reportUiError("reset transaction filters", error));
|
||||
});
|
||||
element<HTMLButtonElement>("#loadProgramsButton").addEventListener("click", () => void loadPrograms());
|
||||
element<HTMLButtonElement>("#resetProgramsButton").addEventListener("click", () => {
|
||||
void resetProgramFilters().catch(error => reportUiError("reset program filters", error));
|
||||
});
|
||||
element<HTMLButtonElement>("#openCoreExtractionButton").addEventListener("click", () => void openCoreExtraction());
|
||||
element<HTMLButtonElement>("#copySignaturesButton").addEventListener("click", () => {
|
||||
void copyLines(transactionRowsForExport().map(row => row.signature), "signature").catch(error => reportUiError("copy signatures", error));
|
||||
});
|
||||
element<HTMLButtonElement>("#exportTransactionsCsvButton").addEventListener("click", () => {
|
||||
void exportTransactionsCsv().catch(error => reportUiError("export transactions CSV", error));
|
||||
});
|
||||
element<HTMLButtonElement>("#copyProgramsButton").addEventListener("click", () => {
|
||||
void copyLines(programRowsForExport().map(row => row.programId), "program ID").catch(error => reportUiError("copy programs", error));
|
||||
});
|
||||
element<HTMLButtonElement>("#exportProgramsCsvButton").addEventListener("click", () => {
|
||||
void exportProgramsCsv().catch(error => reportUiError("export programs CSV", error));
|
||||
});
|
||||
element<HTMLButtonElement>("#useSelectedProgramButton").addEventListener("click", () => {
|
||||
void useSelectedProgram().catch(error => reportUiError("use selected program", error));
|
||||
});
|
||||
for (const config of entityViewConfigs) {
|
||||
element<HTMLButtonElement>(config.loadButtonSelector).addEventListener("click", () => void loadEntity(config));
|
||||
element<HTMLButtonElement>(config.resetButtonSelector).addEventListener("click", () => {
|
||||
void resetEntityFilters(config).catch(error => reportUiError(`reset ${config.pluralLabel} filters`, error));
|
||||
});
|
||||
element<HTMLButtonElement>(config.copyButtonSelector).addEventListener("click", () => {
|
||||
void copyLines(entityRowsForExport(config.kind).map(row => row.entityValue), config.label).catch(error => reportUiError(`copy ${config.pluralLabel}`, error));
|
||||
});
|
||||
element<HTMLButtonElement>(config.exportButtonSelector).addEventListener("click", () => {
|
||||
void exportEntityCsv(config).catch(error => reportUiError(`export ${config.pluralLabel} CSV`, error));
|
||||
});
|
||||
element<HTMLButtonElement>(config.useButtonSelector).addEventListener("click", () => {
|
||||
void useSelectedEntity(config).catch(error => reportUiError(`use selected ${config.label}`, error));
|
||||
});
|
||||
}
|
||||
element<HTMLSelectElement>("#knownProgramSelect").addEventListener("change", event => {
|
||||
const target = event.currentTarget as HTMLSelectElement;
|
||||
element<HTMLInputElement>("#programContainsInput").value = target.value;
|
||||
});
|
||||
document.querySelectorAll<HTMLButtonElement>('[data-bs-toggle="tab"]').forEach(button => {
|
||||
button.addEventListener("shown.bs.tab", adjustVisibleTables);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
installFrontendConsoleBridge(tracingTarget);
|
||||
frontendDebug(tracingTarget, "SQL replay candidate browser loaded");
|
||||
transactionTable = createTransactionTable();
|
||||
programTable = createProgramTable();
|
||||
mintTable = createEntityTable(entityViewConfigs[0]);
|
||||
ownerTable = createEntityTable(entityViewConfigs[1]);
|
||||
accountTable = createEntityTable(entityViewConfigs[2]);
|
||||
initializeTooltips();
|
||||
installHandlers();
|
||||
installInlineCopyHandler();
|
||||
void loadOptions()
|
||||
.then(() => Promise.all([
|
||||
loadTransactions(),
|
||||
loadPrograms(),
|
||||
loadEntity(entityViewConfigs[0]),
|
||||
loadEntity(entityViewConfigs[1]),
|
||||
loadEntity(entityViewConfigs[2]),
|
||||
]))
|
||||
.catch(caughtError => reportUiError("initial loading", caughtError));
|
||||
});
|
||||
68
kb-app-demo-desktop/frontend/ts/demo_sql_tables.ts
Normal file
68
kb-app-demo-desktop/frontend/ts/demo_sql_tables.ts
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user