0.7.27 +Refactor
This commit is contained in:
166
kb_demo_app/frontend/demo_http.html
Normal file
166
kb_demo_app/frontend/demo_http.html
Normal file
@@ -0,0 +1,166 @@
|
||||
<!-- file: kb_demo_app/frontend/demo_http.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Demo Http</title>
|
||||
<link rel="stylesheet" href="sass/main.scss" />
|
||||
</head>
|
||||
|
||||
<body class="bg-body-tertiary">
|
||||
<header class="app-header">
|
||||
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
|
||||
<div class="container my-0">
|
||||
<a class="navbar-brand d-flex align-items-center" href="/">
|
||||
<img alt="Logo" src="imgs/logo.png" class="app-logo" />
|
||||
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Khadhroony-BoBoBot</span>
|
||||
</a>
|
||||
<div class="ms-auto">
|
||||
<span id="demoHttpStatusBadge" class="badge text-bg-secondary">Ready</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="app-main">
|
||||
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
|
||||
<div class="container py-4">
|
||||
<div class="row g-4">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h1 class="h4 mb-3">Demo Http</h1>
|
||||
<p class="text-body-secondary mb-0">
|
||||
Fenêtre de test manuelle pour les requêtes HTTP Solana via le pool d’endpoints configurés.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xxl-4">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 mb-3">Requête</h2>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="demoHttpRoleSelect" class="form-label">Role</label>
|
||||
<select id="demoHttpRoleSelect" class="form-select">
|
||||
<option value="http_queries">http_queries</option>
|
||||
<option value="http_transactions">http_transactions</option>
|
||||
<option value="http_heavy">http_heavy</option>
|
||||
<option value="http_fallback">http_fallback</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
Le rôle détermine quel endpoint HTTP du pool sera sélectionné.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="demoHttpMethodSelect" class="form-label">Méthode</label>
|
||||
<select id="demoHttpMethodSelect" class="form-select">
|
||||
<option value="getHealth">getHealth</option>
|
||||
<option value="getVersion">getVersion</option>
|
||||
<option value="getSlot">getSlot</option>
|
||||
<option value="getBlockHeight">getBlockHeight</option>
|
||||
<option value="getLatestBlockhash">getLatestBlockhash</option>
|
||||
<option value="getBalance">getBalance</option>
|
||||
<option value="getAccountInfo">getAccountInfo</option>
|
||||
<option value="getProgramAccounts">getProgramAccounts</option>
|
||||
<option value="getSignaturesForAddress">getSignaturesForAddress</option>
|
||||
<option value="getTransaction">getTransaction</option>
|
||||
<option value="sendTransaction">sendTransaction</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="demoHttpFirstArgGroup" class="mb-3">
|
||||
<label for="demoHttpFirstArgInput" id="demoHttpFirstArgLabel" class="form-label">First arg</label>
|
||||
<input id="demoHttpFirstArgInput" type="text" class="form-control" spellcheck="false" />
|
||||
<div id="demoHttpFirstArgHelp" class="form-text">
|
||||
Adresse, program id, signature ou transaction encodée selon la méthode.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="demoHttpConfigGroup" class="mb-3">
|
||||
<label for="demoHttpConfigTextarea" class="form-label">Config JSON</label>
|
||||
<textarea id="demoHttpConfigTextarea" class="form-control font-monospace" rows="8" spellcheck="false"></textarea>
|
||||
<div id="demoHttpConfigHelp" class="form-text">
|
||||
Configuration JSON optionnelle.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<button id="demoHttpExecuteButton" type="button" class="btn btn-primary">Execute</button>
|
||||
<button id="demoHttpRefreshPoolButton" type="button" class="btn btn-outline-primary">Refresh pool</button>
|
||||
<button id="demoHttpCopyResponseButton" type="button" class="btn btn-outline-secondary">Copy response</button>
|
||||
<button id="demoHttpClearLogButton" type="button" class="btn btn-outline-secondary">Clear log</button>
|
||||
</div>
|
||||
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="demoHttpPrettyToggle" checked />
|
||||
<label class="form-check-label" for="demoHttpPrettyToggle">Pretty JSON</label>
|
||||
</div>
|
||||
|
||||
<div class="small text-body-secondary">
|
||||
<div><strong>Endpoint:</strong> <span id="demoHttpLastEndpointText">-</span></div>
|
||||
<div><strong>Provider:</strong> <span id="demoHttpLastProviderText">-</span></div>
|
||||
<div><strong>Method class:</strong> <span id="demoHttpLastMethodClassText">-</span></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xxl-8">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 mb-3">Pool HTTP</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Endpoint</th>
|
||||
<th>Status</th>
|
||||
<th>Paused</th>
|
||||
<th>Conc.</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="demoHttpPoolTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 mb-3">Latest response</h2>
|
||||
<textarea id="demoHttpResponseTextarea" class="form-control font-monospace" rows="12" readonly spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 mb-3">Logs</h2>
|
||||
<textarea id="demoHttpLogTextarea" class="form-control font-monospace" rows="14" readonly spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="app-footer bg-dark text-light">
|
||||
<div class="container h-100 d-flex align-items-center">
|
||||
<div class="row flex-grow-1 align-items-center">
|
||||
<div class="col-12 col-md-6 text-center text-small my-1 my-md-0">
|
||||
© 2026 SASEDEV — Demo Http
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="ts/demo_http.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
326
kb_demo_app/frontend/demo_pipeline.html
Normal file
326
kb_demo_app/frontend/demo_pipeline.html
Normal file
@@ -0,0 +1,326 @@
|
||||
<!-- file: kb_demo_app/frontend/demo_pipeline.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Khadhroony-BoBoBot — Demo Pipeline</title>
|
||||
<link rel="stylesheet" href="sass/main.scss" />
|
||||
</head>
|
||||
|
||||
<body class="bg-body-tertiary">
|
||||
<header class="app-header">
|
||||
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
|
||||
<div class="container my-0">
|
||||
<a class="navbar-brand d-flex align-items-center" href="/">
|
||||
<img alt="Logo" src="imgs/logo.png" class="app-logo" />
|
||||
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Demo Pipeline</span>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="app-main">
|
||||
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
|
||||
<div class="container vcentered sketchy-translucid py-4">
|
||||
<div class="row g-4">
|
||||
<div class="col-12 col-xxl-4">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-body">
|
||||
<h1 class="h4 mb-3">Inspection par signature</h1>
|
||||
<p class="text-body-secondary mb-3">
|
||||
Cette fenêtre inspecte la donnée déjà persistée dans <code>kb_lib</code> pour une
|
||||
signature donnée et affiche l’état du pipeline <code>0.7.x</code>.
|
||||
</p>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="demoPipelineSignatureInput" class="form-label">Signature</label>
|
||||
<input id="demoPipelineSignatureInput" type="text" class="form-control font-monospace" spellcheck="false" placeholder="Signature Solana déjà présente dans la base locale" />
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="demoPipelineCustomTimeframeInput" class="form-label">
|
||||
Timeframe custom optionnel (secondes)
|
||||
</label>
|
||||
<input id="demoPipelineCustomTimeframeInput" type="number" min="1" step="1" class="form-control" placeholder="Ex: 120" />
|
||||
<div class="form-text">
|
||||
Les timeframes matérialisés restent chargés depuis la base. Une valeur custom déclenche
|
||||
une régénération à la demande.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mb-4">
|
||||
<button id="demoPipelineInspectButton" type="button" class="btn btn-primary">
|
||||
Inspecter
|
||||
</button>
|
||||
<button id="demoPipelineClearButton" type="button" class="btn btn-outline-secondary">
|
||||
Vider
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr class="my-4" />
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="demoPipelineTokenMintInput" class="form-label">Token mint</label>
|
||||
<input id="demoPipelineTokenMintInput" type="text" class="form-control font-monospace" spellcheck="false" placeholder="Mint SPL déjà présent dans la base locale" />
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mb-4">
|
||||
<button id="demoPipelineInspectTokenButton" type="button" class="btn btn-outline-primary">
|
||||
Inspecter token
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr class="my-4" />
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="demoPipelinePairIdInput" class="form-label">Pair id</label>
|
||||
<input id="demoPipelinePairIdInput" type="number" min="1" step="1" class="form-control" placeholder="Identifiant interne de la paire" />
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<button id="demoPipelineInspectPairButton" type="button" class="btn btn-outline-primary">
|
||||
Inspecter pair
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="demoPipelinePoolAddressInput" class="form-label">Pool address</label>
|
||||
<input id="demoPipelinePoolAddressInput" type="text" class="form-control font-monospace" spellcheck="false" placeholder="Adresse du pool déjà présent dans la base locale" />
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mb-4">
|
||||
<button id="demoPipelineInspectPoolButton" type="button" class="btn btn-outline-primary">
|
||||
Inspecter pool
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr class="my-4" />
|
||||
|
||||
<h2 class="h5 mb-3">Backfill token</h2>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="demoPipelineBackfillTokenMintInput" class="form-label">Token mint à backfill</label>
|
||||
<input id="demoPipelineBackfillTokenMintInput" type="text" class="form-control font-monospace" spellcheck="false" placeholder="Mint SPL à reconstruire depuis le RPC HTTP" />
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="demoPipelineBackfillHttpRoleInput" class="form-label">HTTP role</label>
|
||||
<input id="demoPipelineBackfillHttpRoleInput" type="text" class="form-control" spellcheck="false" value="history_backfill" placeholder="Ex: history_backfill" />
|
||||
</div>
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<label for="demoPipelineBackfillMintLimitInput" class="form-label">Mint signatures</label>
|
||||
<input id="demoPipelineBackfillMintLimitInput" type="number" min="1" step="1" class="form-control" value="50" />
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="demoPipelineBackfillPoolLimitInput" class="form-label">Pool signatures</label>
|
||||
<input id="demoPipelineBackfillPoolLimitInput" type="number" min="1" step="1" class="form-control" value="50" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mt-3 mb-4">
|
||||
<button id="demoPipelineBackfillTokenButton" type="button" class="btn btn-outline-primary">
|
||||
Backfill token
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="demoPipelineBackfillPoolAddressInput" class="form-label">Pool address à backfill</label>
|
||||
<input id="demoPipelineBackfillPoolAddressInput" type="text" class="form-control font-monospace" spellcheck="false" placeholder="Adresse du pool / pair on-chain" />
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="demoPipelineBackfillPoolOnlyLimitInput" class="form-label">Pool signatures</label>
|
||||
<input id="demoPipelineBackfillPoolOnlyLimitInput" type="number" min="1" step="1" class="form-control" value="50" />
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mt-3 mb-4">
|
||||
<button id="demoPipelineBackfillPoolButton" type="button" class="btn btn-outline-primary">
|
||||
Backfill pool
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr class="my-4" />
|
||||
|
||||
<div class="small text-body-secondary">
|
||||
<div><strong>But :</strong> vérifier rapidement la cohérence du pipeline <code>0.7.x</code>.</div>
|
||||
<div><strong>Portée :</strong> lecture seule depuis SQLite via <code>kb_lib</code>.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xxl-8">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 mb-3">Résumé</h2>
|
||||
<textarea id="demoPipelineSummaryTextarea" class="form-control font-monospace" rows="10" readonly spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 mb-3">Dernier backfill token</h2>
|
||||
<textarea id="demoPipelineBackfillTextarea" class="form-control font-monospace" rows="10" readonly spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-3 mb-3">
|
||||
<div>
|
||||
<h2 class="h5 mb-1">Candles / OHLCV</h2>
|
||||
<div id="demoPipelineCandlesChartMeta" class="small text-body-secondary">
|
||||
Aucun jeu de candles chargé.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<div>
|
||||
<label for="demoPipelineChartPairSelect" class="form-label mb-1">Pair</label>
|
||||
<select id="demoPipelineChartPairSelect" class="form-select form-select-sm">
|
||||
<option value="">Aucune</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="demoPipelineChartTimeframeSelect" class="form-label mb-1">Timeframe</label>
|
||||
<select id="demoPipelineChartTimeframeSelect" class="form-select form-select-sm">
|
||||
<option value="">Aucun</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="demoPipelineCandlesChart" class="w-100 border rounded bg-body" style="height: 520px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion" id="demoPipelineAccordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="headingTransaction">
|
||||
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTransaction" aria-expanded="true" aria-controls="collapseTransaction">
|
||||
Transaction résolue
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseTransaction" class="accordion-collapse collapse show" aria-labelledby="headingTransaction" data-bs-parent="#demoPipelineAccordion">
|
||||
<div class="accordion-body">
|
||||
<textarea id="demoPipelineTransactionTextarea" class="form-control font-monospace" rows="14" readonly spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="headingDecodedEvents">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseDecodedEvents" aria-expanded="false" aria-controls="collapseDecodedEvents">
|
||||
Decoded events
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseDecodedEvents" class="accordion-collapse collapse" aria-labelledby="headingDecodedEvents" data-bs-parent="#demoPipelineAccordion">
|
||||
<div class="accordion-body">
|
||||
<textarea id="demoPipelineDecodedEventsTextarea" class="form-control font-monospace" rows="14" readonly spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="headingPools">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapsePools" aria-expanded="false" aria-controls="collapsePools">
|
||||
Pools / pairs / origins
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapsePools" class="accordion-collapse collapse" aria-labelledby="headingPools" data-bs-parent="#demoPipelineAccordion">
|
||||
<div class="accordion-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Pools</label>
|
||||
<textarea id="demoPipelinePoolsTextarea" class="form-control font-monospace" rows="10" readonly spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Pairs</label>
|
||||
<textarea id="demoPipelinePairsTextarea" class="form-control font-monospace" rows="10" readonly spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Launch attributions</label>
|
||||
<textarea id="demoPipelineLaunchAttributionsTextarea" class="form-control font-monospace" rows="10" readonly spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Pool origins</label>
|
||||
<textarea id="demoPipelinePoolOriginsTextarea" class="form-control font-monospace" rows="10" readonly spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="headingWallets">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseWallets" aria-expanded="false" aria-controls="collapseWallets">
|
||||
Wallets / participations / holdings
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseWallets" class="accordion-collapse collapse" aria-labelledby="headingWallets" data-bs-parent="#demoPipelineAccordion">
|
||||
<div class="accordion-body">
|
||||
<textarea id="demoPipelineWalletsTextarea" class="form-control font-monospace" rows="16" readonly spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="headingTrades">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTrades" aria-expanded="false" aria-controls="collapseTrades">
|
||||
Trades / metrics / candles / analytic signals
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseTrades" class="accordion-collapse collapse" aria-labelledby="headingTrades" data-bs-parent="#demoPipelineAccordion">
|
||||
<div class="accordion-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Trade events</label>
|
||||
<textarea id="demoPipelineTradeEventsTextarea" class="form-control font-monospace" rows="10" readonly spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Pair metrics</label>
|
||||
<textarea id="demoPipelinePairMetricsTextarea" class="form-control font-monospace" rows="10" readonly spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Pair candles</label>
|
||||
<textarea id="demoPipelinePairCandlesTextarea" class="form-control font-monospace" rows="12" readonly spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Pair analytic signals</label>
|
||||
<textarea id="demoPipelinePairAnalyticSignalsTextarea" class="form-control font-monospace" rows="12" readonly spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0 mt-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2 class="h5 mb-0">Log UI</h2>
|
||||
<button id="demoPipelineClearLogButton" type="button" class="btn btn-outline-secondary btn-sm">Clear log</button>
|
||||
</div>
|
||||
<textarea id="demoPipelineLogTextarea" class="form-control font-monospace" rows="10" readonly spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="app-footer bg-dark text-light">
|
||||
<div class="container h-100 d-flex align-items-center">
|
||||
<div class="row flex-grow-1 align-items-center">
|
||||
<div class="col-12 col-md-6 text-center text-small my-1 my-md-0">
|
||||
© 2026 SASEDEV — Demo Pipeline
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="ts/demo_pipeline.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
323
kb_demo_app/frontend/demo_pipeline2.html
Normal file
323
kb_demo_app/frontend/demo_pipeline2.html
Normal file
@@ -0,0 +1,323 @@
|
||||
<!-- file: kb_demo_app/frontend/demo_pipeline2.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Khadhroony-BoBoBot — Demo Pipeline 2</title>
|
||||
<link rel="stylesheet" href="sass/main.scss" />
|
||||
</head>
|
||||
|
||||
<body class="bg-body-tertiary">
|
||||
<header class="app-header">
|
||||
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
|
||||
<div class="container my-0">
|
||||
<a class="navbar-brand d-flex align-items-center" href="/">
|
||||
<img alt="Logo" src="imgs/logo.png" class="app-logo" />
|
||||
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Demo Pipeline 2</span>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="app-main">
|
||||
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
|
||||
<div class="container vcentered sketchy-translucid py-4">
|
||||
<div class="row g-4">
|
||||
<div class="col-12 col-xxl-4">
|
||||
<div class="accordion" id="demoPipeline2LeftAccordion">
|
||||
<div class="accordion-item border-0 shadow-sm mb-3">
|
||||
<h1 class="accordion-header" id="demoPipeline2CatalogHeading">
|
||||
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#demoPipeline2CatalogCollapse" aria-expanded="true" aria-controls="demoPipeline2CatalogCollapse">
|
||||
Catalogue local
|
||||
</button>
|
||||
</h1>
|
||||
<div id="demoPipeline2CatalogCollapse" class="accordion-collapse collapse show" aria-labelledby="demoPipeline2CatalogHeading" data-bs-parent="#demoPipeline2LeftAccordion">
|
||||
<div class="accordion-body">
|
||||
<div class="d-flex gap-2 mb-3">
|
||||
<button id="demoPipeline2RefreshCatalogButton" type="button" class="btn btn-primary">
|
||||
Refresh catalog
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Mints</label>
|
||||
<textarea id="demoPipeline2TokensTextarea" class="form-control font-monospace" rows="8" readonly spellcheck="false"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Pools</label>
|
||||
<textarea id="demoPipeline2PoolsTextarea" class="form-control font-monospace" rows="8" readonly spellcheck="false"></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="form-label">Pairs</label>
|
||||
<textarea id="demoPipeline2PairsTextarea" class="form-control font-monospace" rows="8" readonly spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item border-0 shadow-sm mb-3">
|
||||
<h2 class="accordion-header" id="demoPipeline2BackfillHeading">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#demoPipeline2BackfillCollapse" aria-expanded="false" aria-controls="demoPipeline2BackfillCollapse">
|
||||
Backfill ciblé
|
||||
</button>
|
||||
</h2>
|
||||
<div id="demoPipeline2BackfillCollapse" class="accordion-collapse collapse" aria-labelledby="demoPipeline2BackfillHeading" data-bs-parent="#demoPipeline2LeftAccordion">
|
||||
<div class="accordion-body">
|
||||
<div class="mb-3">
|
||||
<label for="demoPipeline2HttpRoleInput" class="form-label">HTTP role</label>
|
||||
<input id="demoPipeline2HttpRoleInput" type="text" class="form-control" value="history_backfill" spellcheck="false" />
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="demoPipeline2MintInput" class="form-label">Token mint</label>
|
||||
<input id="demoPipeline2MintInput" type="text" class="form-control font-monospace" spellcheck="false" placeholder="Mint SPL" />
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6">
|
||||
<label for="demoPipeline2MintSignatureLimitInput" class="form-label">Mint sigs</label>
|
||||
<input id="demoPipeline2MintSignatureLimitInput" type="number" min="1" step="1" class="form-control" value="20" />
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="demoPipeline2MintPoolLimitInput" class="form-label">Pool sigs</label>
|
||||
<input id="demoPipeline2MintPoolLimitInput" type="number" min="1" step="1" class="form-control" value="20" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 mb-4">
|
||||
<button id="demoPipeline2BackfillMintButton" type="button" class="btn btn-outline-primary">
|
||||
Backfill mint
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="demoPipeline2PoolInput" class="form-label">Pool address</label>
|
||||
<input id="demoPipeline2PoolInput" type="text" class="form-control font-monospace" spellcheck="false" placeholder="Pool / pair on-chain" />
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="demoPipeline2PoolSignatureLimitInput" class="form-label">Pool sigs</label>
|
||||
<input id="demoPipeline2PoolSignatureLimitInput" type="number" min="1" step="1" class="form-control" value="20" />
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button id="demoPipeline2BackfillPoolButton" type="button" class="btn btn-outline-primary">
|
||||
Backfill pool
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item border-0 shadow-sm mb-3">
|
||||
<h2 class="accordion-header" id="demoPipeline2ReplayHeading">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#demoPipeline2ReplayCollapse" aria-expanded="false" aria-controls="demoPipeline2ReplayCollapse">
|
||||
Replay local
|
||||
</button>
|
||||
</h2>
|
||||
<div id="demoPipeline2ReplayCollapse" class="accordion-collapse collapse" aria-labelledby="demoPipeline2ReplayHeading" data-bs-parent="#demoPipeline2LeftAccordion">
|
||||
<div class="accordion-body">
|
||||
<p class="small text-body-secondary mb-3">
|
||||
Rejoue le pipeline depuis les transactions déjà stockées en base sans refaire de getTransaction.
|
||||
</p>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="demoPipeline2ReplayLimitInput" class="form-label">Transaction limit</label>
|
||||
<input id="demoPipeline2ReplayLimitInput" type="number" min="1" step="1" class="form-control" value="10000" />
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="demoPipeline2ReplayMetadataCheckbox" />
|
||||
<label class="form-check-label" for="demoPipeline2ReplayMetadataCheckbox">
|
||||
Refresh missing token metadata
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="demoPipeline2ReplayMetadataLimitInput" class="form-label">Metadata limit</label>
|
||||
<input id="demoPipeline2ReplayMetadataLimitInput" type="number" min="1" step="1" class="form-control" value="250" />
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button id="demoPipeline2ReplayLocalPipelineButton" type="button" class="btn btn-outline-primary">
|
||||
Replay local pipeline
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item border-0 shadow-sm mb-3">
|
||||
<h2 class="accordion-header" id="demoPipeline2DiagnosticsHeading">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#demoPipeline2DiagnosticsCollapse" aria-expanded="false" aria-controls="demoPipeline2DiagnosticsCollapse">
|
||||
Diagnostics locaux
|
||||
</button>
|
||||
</h2>
|
||||
<div id="demoPipeline2DiagnosticsCollapse" class="accordion-collapse collapse" aria-labelledby="demoPipeline2DiagnosticsHeading" data-bs-parent="#demoPipeline2LeftAccordion">
|
||||
<div class="accordion-body">
|
||||
<p class="small text-body-secondary mb-3">
|
||||
Analyse les données déjà persistées : transactions, decoded events, trades, candles, tokens, pools et pairs.
|
||||
</p>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button id="demoPipeline2DiagnoseLocalPipelineButton" type="button" class="btn btn-outline-primary">
|
||||
Diagnose local pipeline
|
||||
</button>
|
||||
<button id="demoPipeline2ValidateLocalPipelineButton" type="button" class="btn btn-outline-success">
|
||||
Validate 0.7.27
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item border-0 shadow-sm">
|
||||
<h2 class="accordion-header" id="demoPipeline2CandlesControlHeading">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#demoPipeline2CandlesControlCollapse" aria-expanded="false" aria-controls="demoPipeline2CandlesControlCollapse">
|
||||
Chargement candles
|
||||
</button>
|
||||
</h2>
|
||||
<div id="demoPipeline2CandlesControlCollapse" class="accordion-collapse collapse" aria-labelledby="demoPipeline2CandlesControlHeading" data-bs-parent="#demoPipeline2LeftAccordion">
|
||||
<div class="accordion-body">
|
||||
<div class="mb-3">
|
||||
<label for="demoPipeline2PairSelect" class="form-label">Pair</label>
|
||||
<select id="demoPipeline2PairSelect" class="form-select">
|
||||
<option value="">Aucune</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-7">
|
||||
<label for="demoPipeline2TimeframeSelect" class="form-label">Timeframe</label>
|
||||
<select id="demoPipeline2TimeframeSelect" class="form-select">
|
||||
<option value="60">1m</option>
|
||||
<option value="300">5m</option>
|
||||
<option value="900">15m</option>
|
||||
<option value="3600">1h</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<label for="demoPipeline2CustomTimeframeInput" class="form-label">Custom</label>
|
||||
<input id="demoPipeline2CustomTimeframeInput" type="number" min="1" step="1" class="form-control" placeholder="120" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="demoPipeline2PreferMaterializedInput" />
|
||||
<label class="form-check-label" for="demoPipeline2PreferMaterializedInput">
|
||||
Prefer materialized candles
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button id="demoPipeline2LoadCandlesButton" type="button" class="btn btn-primary">
|
||||
Load candles
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xxl-8">
|
||||
<div class="accordion" id="demoPipeline2ContentAccordion">
|
||||
<div class="accordion-item border-0 shadow-sm mb-3">
|
||||
<h2 class="accordion-header" id="demoPipeline2SummaryHeading">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#demoPipeline2SummaryCollapse" aria-expanded="false" aria-controls="demoPipeline2SummaryCollapse">
|
||||
Backfill / Replay summary
|
||||
</button>
|
||||
</h2>
|
||||
<div id="demoPipeline2SummaryCollapse" class="accordion-collapse collapse" aria-labelledby="demoPipeline2SummaryHeading" data-bs-parent="#demoPipeline2ContentAccordion">
|
||||
<div class="accordion-body">
|
||||
<textarea id="demoPipeline2BackfillSummaryTextarea" class="form-control font-monospace" rows="12" readonly spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item border-0 shadow-sm mb-3">
|
||||
<h2 class="accordion-header" id="demoPipeline2LocalDiagnosticsHeading">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#demoPipeline2LocalDiagnosticsCollapse" aria-expanded="false" aria-controls="demoPipeline2LocalDiagnosticsCollapse">
|
||||
Local pipeline diagnostics
|
||||
</button>
|
||||
</h2>
|
||||
<div id="demoPipeline2LocalDiagnosticsCollapse" class="accordion-collapse collapse" aria-labelledby="demoPipeline2LocalDiagnosticsHeading" data-bs-parent="#demoPipeline2ContentAccordion">
|
||||
<div class="accordion-body">
|
||||
<textarea id="demoPipeline2LocalDiagnosticsTextarea" class="form-control font-monospace" rows="18" readonly spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item border-0 shadow-sm mb-3">
|
||||
<h2 class="accordion-header" id="demoPipeline2LocalValidationHeading">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#demoPipeline2LocalValidationCollapse" aria-expanded="false" aria-controls="demoPipeline2LocalValidationCollapse">
|
||||
Local pipeline Validation
|
||||
</button>
|
||||
</h2>
|
||||
<div id="demoPipeline2LocalValidationCollapse" class="accordion-collapse collapse" aria-labelledby="demoPipeline2LocalValidationHeading" data-bs-parent="#demoPipeline2ContentAccordion">
|
||||
<div class="accordion-body">
|
||||
<textarea id="demoPipeline2LocalValidationTextarea" class="form-control font-monospace" rows="16" readonly spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item border-0 shadow-sm mb-3">
|
||||
<h2 class="accordion-header" id="demoPipeline2ChartHeading">
|
||||
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#demoPipeline2ChartCollapse" aria-expanded="true" aria-controls="demoPipeline2ChartCollapse">
|
||||
Candles / OHLCV
|
||||
</button>
|
||||
</h2>
|
||||
<div id="demoPipeline2ChartCollapse" class="accordion-collapse collapse show" aria-labelledby="demoPipeline2ChartHeading" data-bs-parent="#demoPipeline2ContentAccordion">
|
||||
<div class="accordion-body">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-3 mb-3">
|
||||
<div id="demoPipeline2ChartMeta" class="small text-body-secondary">
|
||||
Aucun jeu de candles chargé.
|
||||
</div>
|
||||
</div>
|
||||
<div id="demoPipeline2Chart" class="w-100 border rounded bg-body" style="height: 560px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item border-0 shadow-sm">
|
||||
<h2 class="accordion-header" id="demoPipeline2LogHeading">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#demoPipeline2LogCollapse" aria-expanded="false" aria-controls="demoPipeline2LogCollapse">
|
||||
Log UI
|
||||
</button>
|
||||
</h2>
|
||||
<div id="demoPipeline2LogCollapse" class="accordion-collapse collapse" aria-labelledby="demoPipeline2LogHeading" data-bs-parent="#demoPipeline2ContentAccordion">
|
||||
<div class="accordion-body">
|
||||
<div class="d-flex justify-content-end align-items-center mb-3">
|
||||
<button id="demoPipeline2ClearLogButton" type="button" class="btn btn-outline-secondary btn-sm">
|
||||
Clear log
|
||||
</button>
|
||||
</div>
|
||||
<textarea id="demoPipeline2LogTextarea" class="form-control font-monospace" rows="14" readonly spellcheck="false"></textarea>
|
||||
</div>
|
||||
</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 col-md-6 text-center text-small my-1 my-md-0">
|
||||
© 2026 SASEDEV — Demo Pipeline 2
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="ts/demo_pipeline2.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
157
kb_demo_app/frontend/demo_ws.html
Normal file
157
kb_demo_app/frontend/demo_ws.html
Normal file
@@ -0,0 +1,157 @@
|
||||
<!-- file: kb_demo_app/frontend/demo_ws.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Demo Ws Subscribe</title>
|
||||
<link rel="stylesheet" href="sass/main.scss" />
|
||||
</head>
|
||||
|
||||
<body class="bg-body-tertiary">
|
||||
<header class="app-header">
|
||||
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
|
||||
<div class="container my-0">
|
||||
<a class="navbar-brand d-flex align-items-center" href="/">
|
||||
<img alt="Logo" src="imgs/logo.png" class="app-logo" />
|
||||
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Khadhroony-BoBoBot</span>
|
||||
</a>
|
||||
<div class="ms-auto">
|
||||
<span id="demoWsStatusBadge" class="badge text-bg-secondary">Disconnected</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="app-main">
|
||||
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
|
||||
<div class="container py-4">
|
||||
<div class="row g-4">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h1 class="h4 mb-3">Demo Ws Subscribe</h1>
|
||||
<p class="text-body-secondary mb-0">
|
||||
Fenêtre de test complète pour les souscriptions WebSocket Solana en mode raw ou typed.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xxl-4">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 mb-3">Connexion</h2>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="demoWsEndpointSelect" class="form-label">Endpoint WS activé</label>
|
||||
<select id="demoWsEndpointSelect" class="form-select"></select>
|
||||
<div class="form-text">
|
||||
Seuls les endpoints définis dans <code>config.solana.ws_endpoints</code> et marqués
|
||||
<code>enabled: true</code> apparaissent ici. Les endpoints HTTP ne sont pas utilisés
|
||||
par cette fenêtre.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<button id="demoWsConnectButton" type="button" class="btn btn-success">Connect</button>
|
||||
<button id="demoWsDisconnectButton" type="button" class="btn btn-danger">Disconnect</button>
|
||||
</div>
|
||||
|
||||
<div class="small text-body-secondary">
|
||||
<div><strong>State:</strong> <span id="demoWsStateText">Disconnected</span></div>
|
||||
<div><strong>Endpoint:</strong> <span id="demoWsEndpointText">-</span></div>
|
||||
</div>
|
||||
|
||||
<div class="small text-body-secondary mt-2">
|
||||
<div><strong>Events:</strong> <span id="demoWsEventCountText">0</span></div>
|
||||
<div><strong>Notifications:</strong> <span id="demoWsNotificationCountText">0</span></div>
|
||||
<div><strong>UI logs:</strong> <span id="demoWsUiLogCountText">0</span></div>
|
||||
<div><strong>Suppressed:</strong> <span id="demoWsSuppressedLogCountText">0</span></div>
|
||||
<div><strong>Last event:</strong> <span id="demoWsLastEventKindText">-</span></div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<h2 class="h5 mb-3">Souscription</h2>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="demoWsMethodSelect" class="form-label">Méthode</label>
|
||||
<select id="demoWsMethodSelect" class="form-select">
|
||||
<option value="account">account</option>
|
||||
<option value="block">block</option>
|
||||
<option value="logs">logs</option>
|
||||
<option value="program">program</option>
|
||||
<option value="root">root</option>
|
||||
<option value="signature">signature</option>
|
||||
<option value="slot" selected>slot</option>
|
||||
<option value="slotsUpdates">slotsUpdates</option>
|
||||
<option value="vote">vote</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="demoWsModeSelect" class="form-label">Mode</label>
|
||||
<select id="demoWsModeSelect" class="form-select">
|
||||
<option value="typed" selected>typed</option>
|
||||
<option value="raw">raw</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="demoWsTargetGroup" class="mb-3">
|
||||
<label for="demoWsTargetInput" id="demoWsTargetLabel" class="form-label">Target</label>
|
||||
<input id="demoWsTargetInput" type="text" class="form-control" spellcheck="false" />
|
||||
</div>
|
||||
|
||||
<div id="demoWsFilterGroup" class="mb-3">
|
||||
<label for="demoWsFilterTextarea" class="form-label">Filter JSON</label>
|
||||
<textarea id="demoWsFilterTextarea" class="form-control font-monospace" rows="5" spellcheck="false"></textarea>
|
||||
</div>
|
||||
|
||||
<div id="demoWsConfigGroup" class="mb-3">
|
||||
<label for="demoWsConfigTextarea" class="form-label">Config JSON</label>
|
||||
<textarea id="demoWsConfigTextarea" class="form-control font-monospace" rows="6" spellcheck="false"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<button id="demoWsSubscribeButton" type="button" class="btn btn-primary">Subscribe</button>
|
||||
<button id="demoWsUnsubscribeButton" type="button" class="btn btn-warning">Unsubscribe current</button>
|
||||
<button id="demoWsClearLogButton" type="button" class="btn btn-outline-secondary">Clear log</button>
|
||||
</div>
|
||||
|
||||
<div class="small text-body-secondary">
|
||||
<div><strong>Current subscription:</strong> <span id="demoWsSubscriptionText">-</span></div>
|
||||
<div><strong>Request:</strong> <span id="demoWsRequestText">-</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xxl-8">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 mb-3">Logs</h2>
|
||||
<textarea id="demoWsLogTextarea" class="form-control font-monospace" rows="28" readonly spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="app-footer bg-dark text-light">
|
||||
<div class="container h-100 d-flex align-items-center">
|
||||
<div class="row flex-grow-1 align-items-center">
|
||||
<div class="col-12 col-md-6 text-center text-small my-1 my-md-0">
|
||||
© 2026 SASEDEV — Demo Ws
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="ts/demo_ws.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
109
kb_demo_app/frontend/demo_ws_manager.html
Normal file
109
kb_demo_app/frontend/demo_ws_manager.html
Normal file
@@ -0,0 +1,109 @@
|
||||
<!-- file: kb_demo_app/frontend/demo_ws_manager.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Khadhroony-BoBoBot — Demo Ws Manager</title>
|
||||
<link rel="stylesheet" href="sass/main.scss" />
|
||||
</head>
|
||||
|
||||
<body class="bg-body-tertiary">
|
||||
<header class="app-header">
|
||||
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
|
||||
<div class="container my-0">
|
||||
<a class="navbar-brand d-flex align-items-center" href="/">
|
||||
<img alt="Logo" src="imgs/logo.png" class="app-logo" />
|
||||
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Demo Ws Manager</span>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="app-main">
|
||||
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
|
||||
<div class="container vcentered sketchy-translucid py-4">
|
||||
<div class="row g-4">
|
||||
<div class="col-12 col-xxl-4">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-body">
|
||||
<h1 class="h4 mb-3">Pilotage</h1>
|
||||
<p class="text-body-secondary mb-3">
|
||||
Démo légère du <code>WsManager</code> : démarrage/arrêt groupé, pilotage par rôle et bus unifié d’événements.
|
||||
</p>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mb-4">
|
||||
<button id="demoWsManagerStartAllButton" type="button" class="btn btn-primary">Start all</button>
|
||||
<button id="demoWsManagerStopAllButton" type="button" class="btn btn-outline-primary">Stop all</button>
|
||||
<button id="demoWsManagerRefreshButton" type="button" class="btn btn-outline-secondary">Refresh snapshot</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="demoWsManagerRoleSelect" class="form-label">Rôle</label>
|
||||
<select id="demoWsManagerRoleSelect" class="form-select"></select>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mb-4">
|
||||
<button id="demoWsManagerStartRoleButton" type="button" class="btn btn-primary">Start role</button>
|
||||
<button id="demoWsManagerStopRoleButton" type="button" class="btn btn-outline-primary">Stop role</button>
|
||||
</div>
|
||||
|
||||
<div class="small text-body-secondary">
|
||||
<div><strong>Managed endpoints:</strong> <span id="demoWsManagerEndpointCountText">0</span></div>
|
||||
<div><strong>Started endpoints:</strong> <span id="demoWsManagerStartedCountText">0</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xxl-8">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 mb-3">Snapshot</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Endpoint</th>
|
||||
<th>Provider</th>
|
||||
<th>Roles</th>
|
||||
<th>State</th>
|
||||
<th>Subs.</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="demoWsManagerTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2 class="h5 mb-0">Unified event log</h2>
|
||||
<button id="demoWsManagerClearLogButton" type="button" class="btn btn-outline-secondary btn-sm">Clear log</button>
|
||||
</div>
|
||||
<textarea id="demoWsManagerLogTextarea" class="form-control font-monospace" rows="18" readonly spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="app-footer bg-dark text-light">
|
||||
<div class="container h-100 d-flex align-items-center">
|
||||
<div class="row flex-grow-1 align-items-center">
|
||||
<div class="col-12 col-md-6 text-center text-small my-1 my-md-0">
|
||||
© 2026 SASEDEV — Demo Ws Manager
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="ts/demo_ws_manager.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
BIN
kb_demo_app/frontend/fonts/DOS_Amazigh.ttf
Normal file
BIN
kb_demo_app/frontend/fonts/DOS_Amazigh.ttf
Normal file
Binary file not shown.
BIN
kb_demo_app/frontend/imgs/logo.png
Normal file
BIN
kb_demo_app/frontend/imgs/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
BIN
kb_demo_app/frontend/imgs/splash.png
Normal file
BIN
kb_demo_app/frontend/imgs/splash.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 611 KiB |
117
kb_demo_app/frontend/main.html
Normal file
117
kb_demo_app/frontend/main.html
Normal file
@@ -0,0 +1,117 @@
|
||||
<!-- file: kb_demo_app/frontend/index.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Khadhroony-BoBoBot</title>
|
||||
<link rel="stylesheet" href="sass/main.scss" />
|
||||
</head>
|
||||
|
||||
<body class="bg-body-tertiary">
|
||||
<header class="app-header">
|
||||
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
|
||||
<div class="container my-0">
|
||||
<a class="navbar-brand d-flex align-items-center" href="/">
|
||||
<img alt="Logo" src="imgs/logo.png" class="app-logo" />
|
||||
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Khadhroony-BoBoBot</span>
|
||||
</a>
|
||||
|
||||
<div class="ms-auto d-flex align-items-center gap-2">
|
||||
<button id="openDemoWsButton" type="button" class="btn btn-outline-primary">
|
||||
Demo Ws
|
||||
</button>
|
||||
<button id="openDemoHttpButton" class="btn btn-outline-primary">
|
||||
Demo Http
|
||||
</button>
|
||||
<button id="openDemoWsManagerButton" type="button" class="btn btn-outline-primary">
|
||||
Demo Ws Manager
|
||||
</button>
|
||||
<button id="openDemoPipelineButton" type="button" class="btn btn-outline-primary">
|
||||
Ouvrir Demo Pipeline
|
||||
</button>
|
||||
<button id="openDemoPipeline2Button" type="button" class="btn btn-outline-primary">
|
||||
Ouvrir Demo Pipeline 2
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="app-main">
|
||||
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
|
||||
<div class="container vcentered sketchy-translucid">
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h1>Khadhroony-BoBoBot</h1>
|
||||
|
||||
<div class="container py-4">
|
||||
<div class="row g-4 justify-content-center">
|
||||
<div class="col-12 col-xl-8">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-body text-start">
|
||||
<h3 class="h5 card-title mb-3">Desktop shell</h3>
|
||||
<p class="text-body-secondary mb-3">
|
||||
La fenêtre principale reste volontairement légère.
|
||||
</p>
|
||||
<p class="text-body-secondary mb-3">
|
||||
Les tests WebSocket manuels sont disponibles dans la fenêtre dédiée
|
||||
<strong>Demo Ws</strong>.
|
||||
</p>
|
||||
<p class="text-body-secondary mb-3">
|
||||
Les tests PRC Http manuels sont disponibles dans la fenêtre dédiée
|
||||
<strong>Demo Http</strong>.
|
||||
</p>
|
||||
<p class="text-body-secondary mb-3">
|
||||
La démonstration légère de pilotage multi-clients est disponible dans la fenêtre
|
||||
<strong>Demo Ws Manager</strong>.
|
||||
</p>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<button id="openDemoWsButtonSecondary" type="button" class="btn btn-primary">
|
||||
Ouvrir Demo Ws
|
||||
</button>
|
||||
<button id="openDemoHttpButtonSecondary" type="button" class="btn btn-primary">
|
||||
Ouvrir Demo Http
|
||||
</button>
|
||||
<button id="openDemoWsManagerButtonSecondary" type="button" class="btn btn-primary">
|
||||
Ouvrir Demo Ws Manager
|
||||
</button>
|
||||
<button id="openDemoPipelineButtonSecondary" type="button" class="btn btn-primary">
|
||||
Ouvrir Demo Pipeline
|
||||
</button>
|
||||
<button id="openDemoPipeline2ButtonSecondary" type="button" class="btn btn-primary">
|
||||
Ouvrir Demo Pipeline 2
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<p class="mb-0 text-body-secondary">
|
||||
Cette fenêtre sert de point d’entrée applicatif. Les démonstrations de
|
||||
souscriptions Solana live sont isolées dans la fenêtre de test dédiée.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</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 col-md-4 text-center text-small my-1 my-md-0">© 2026 SASEDEV</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="ts/main.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
319
kb_demo_app/frontend/sass/_app.scss
Normal file
319
kb_demo_app/frontend/sass/_app.scss
Normal file
@@ -0,0 +1,319 @@
|
||||
// file: kb_demo_app/frontend/sass/_app.scss
|
||||
|
||||
$app-header-height: 48px;
|
||||
$app-footer-height: 48px;
|
||||
$sidenav-base-width: 260px;
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#ibackground {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
z-index: 1;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#vbackground {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
z-index: 2;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
video#vbackground:not([playsinline]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: $app-header-height;
|
||||
z-index: 1030;
|
||||
|
||||
& nav {
|
||||
border-top: none !important;
|
||||
border-left: none !important;
|
||||
border-right: none !important;
|
||||
border-bottom: 3px $primary solid !important;
|
||||
border-top-left-radius: 0 !important;
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-left-radius: 15px 150px;
|
||||
border-bottom-right-radius: 75px 25px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.app-footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: $app-footer-height;
|
||||
z-index: 1020;
|
||||
border-style: solid;
|
||||
border-width: 2px 0 0;
|
||||
border-top-left-radius: 255px 25px;
|
||||
border-top-right-radius: 25px 225px;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
position: relative;
|
||||
height: calc(100vh - $app-header-height - $app-footer-height); // plein viewport
|
||||
margin-top: $app-header-height;
|
||||
margin-bottom: $app-footer-height;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden; // pas de scroll ici
|
||||
z-index: 500;
|
||||
}
|
||||
|
||||
.app-main-error {
|
||||
position: relative;
|
||||
height: 100vh; // plein viewport
|
||||
padding-top: $app-header-height;
|
||||
padding-bottom: $app-footer-height;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden; // pas de scroll ici
|
||||
background: linear-gradient(135deg, #9945ff 0%, #764ba2 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
z-index: 501;
|
||||
}
|
||||
|
||||
.app-main-error-content {
|
||||
width: 100%;
|
||||
|
||||
& h1 {
|
||||
font-size: 4rem;
|
||||
margin: 0;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
& p {
|
||||
font-size: 1.5rem;
|
||||
margin: 1rem 0 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.app-logo {
|
||||
height: 42px;
|
||||
width: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#app-dashbord {
|
||||
display: flex;
|
||||
height: 100%; //height: calc(100vh - var(--app-header-height) - var(--app-footer-height));
|
||||
}
|
||||
|
||||
#app-dashbord-sidenav {
|
||||
flex-basis: $sidenav-base-width;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.15s ease-in-out;
|
||||
|
||||
&[dir="ltr"] {
|
||||
transform: translateX(-$sidenav-base-width);
|
||||
}
|
||||
|
||||
&[dir="rtl"] {
|
||||
transform: translateX($sidenav-base-width);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#app-dashbord-main {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
flex-grow: 1;
|
||||
min-height: 100%;
|
||||
|
||||
&[dir="ltr"] {
|
||||
margin-left: -$sidenav-base-width;
|
||||
}
|
||||
|
||||
&[dir="rtl"] {
|
||||
margin-right: -$sidenav-base-width;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// quand body a la classe .sidenav-toggled
|
||||
.sidenav-toggled {
|
||||
#app-dashbord-sidenav {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
#app-dashbord-main {
|
||||
&:before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #000;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// breakpoint desktop : sidebar visible par défaut
|
||||
@include media-breakpoint-up(lg) {
|
||||
#app-dashbord-sidenav {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
#app-dashbord-main {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
transition: margin 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.sidenav-toggled {
|
||||
#app-dashbord-sidenav {
|
||||
&[dir="ltr"] {
|
||||
transform: translateX(-$sidenav-base-width);
|
||||
}
|
||||
|
||||
&[dir="rtl"] {
|
||||
transform: translateX($sidenav-base-width);
|
||||
}
|
||||
}
|
||||
|
||||
#app-dashbord-main {
|
||||
&[dir="ltr"] {
|
||||
margin-left: -$sidenav-base-width;
|
||||
}
|
||||
|
||||
&[dir="rtl"] {
|
||||
margin-right: -$sidenav-base-width;
|
||||
}
|
||||
|
||||
&:before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Sidebar (style SBAdmin-ish)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.app-sidebar.sidenav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
flex-wrap: nowrap;
|
||||
background-color: rgb(97, 53, 131, 0.7);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.sidenav-menu {
|
||||
flex-grow: 1;
|
||||
|
||||
.nav {
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.sidenav-menu-heading {
|
||||
padding: 1.5rem 1rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1.25rem;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
position: relative;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.nav-link-icon {
|
||||
font-size: 0.9rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.sidenav-collapse-arrow {
|
||||
margin-left: auto;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
.sidenav-collapse-arrow {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidenav-menu-nested {
|
||||
margin-left: 1.5rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.osb-scrollable {
|
||||
height: 100%; // occupe toute la hauteur de .app-main
|
||||
max-height: 100%; // évite de dépasser
|
||||
// Pour vérifier sans OverlayScrollbars :
|
||||
//
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 2rem;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.modal {
|
||||
z-index: 1080;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.voice-output-textarea {
|
||||
min-height: 320px;
|
||||
resize: vertical;
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
#logs-card {
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
}
|
||||
159
kb_demo_app/frontend/sass/_bootswatch.scss
Normal file
159
kb_demo_app/frontend/sass/_bootswatch.scss
Normal file
@@ -0,0 +1,159 @@
|
||||
// file: kb_demo_app/frontend/sass/_bootswatch.scss
|
||||
|
||||
// Pulse 5.3.8
|
||||
// Bootswatch
|
||||
|
||||
|
||||
// Variables
|
||||
|
||||
// Buttons
|
||||
|
||||
.btn {
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&:active:focus,
|
||||
&.active:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&-secondary {
|
||||
color: $gray-900;
|
||||
background-color: $white;
|
||||
border-color: #ccc;
|
||||
|
||||
&:hover {
|
||||
color: $gray-900;
|
||||
background-color: $gray-300;
|
||||
border-color: $gray-500;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: tint-color($gray-900, 5%);
|
||||
background-color: $white;
|
||||
border-color: tint-color(#ccc, 5%);
|
||||
}
|
||||
}
|
||||
|
||||
&-warning {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&-primary:focus {
|
||||
box-shadow: 0 0 5px tint-color($primary, 10%);
|
||||
}
|
||||
|
||||
&-secondary:focus {
|
||||
box-shadow: 0 0 5px $gray-400;
|
||||
}
|
||||
|
||||
&-success:focus {
|
||||
box-shadow: 0 0 5px tint-color($success, 10%);
|
||||
}
|
||||
|
||||
&-info:focus {
|
||||
box-shadow: 0 0 5px tint-color($info, 10%);
|
||||
}
|
||||
|
||||
&-warning:focus {
|
||||
box-shadow: 0 0 5px tint-color($warning, 10%);
|
||||
}
|
||||
|
||||
&-danger:focus {
|
||||
box-shadow: 0 0 5px tint-color($danger, 10%);
|
||||
}
|
||||
|
||||
&.disabled:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Tables
|
||||
|
||||
.table .thead-dark th {
|
||||
background-color: $secondary;
|
||||
border-color: $table-border-color;
|
||||
}
|
||||
|
||||
.table-primary,
|
||||
.table-secondary,
|
||||
.table-success,
|
||||
.table-warning,
|
||||
.table-danger,
|
||||
.table-info,
|
||||
.table-light {
|
||||
--#{$prefix}table-color: #{$body-color};
|
||||
}
|
||||
|
||||
// Forms
|
||||
|
||||
.form-control:focus {
|
||||
box-shadow: 0 0 5px rgba(100, 65, 164, .4);
|
||||
}
|
||||
|
||||
// Navs
|
||||
|
||||
.nav-tabs {
|
||||
|
||||
.nav-link,
|
||||
.nav-link.active {
|
||||
border-width: 0 0 1px;
|
||||
}
|
||||
|
||||
.nav-link:hover,
|
||||
.nav-link.active,
|
||||
.nav-link.active:hover,
|
||||
.nav-link.active:focus {
|
||||
border-bottom: 1px solid $primary;
|
||||
}
|
||||
|
||||
.nav-item+.nav-item {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
&-item.active {
|
||||
color: $gray-700;
|
||||
}
|
||||
}
|
||||
|
||||
// Indicators
|
||||
|
||||
.badge {
|
||||
&.bg-light {
|
||||
color: $dark;
|
||||
}
|
||||
}
|
||||
|
||||
// Progress bars
|
||||
|
||||
.progress {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
// Containers
|
||||
|
||||
.list-group {
|
||||
&-item {
|
||||
color: rgba(255, 255, 255, .8);
|
||||
|
||||
&.active,
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: 700;
|
||||
|
||||
&:hover {
|
||||
background-color: $list-group-hover-bg;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled:hover {
|
||||
color: $list-group-disabled-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
kb_demo_app/frontend/sass/_fontawesome.scss
Normal file
18
kb_demo_app/frontend/sass/_fontawesome.scss
Normal file
@@ -0,0 +1,18 @@
|
||||
// file: kb_demo_app/frontend/sass/_fontawesome.scss
|
||||
|
||||
//@use '@fortawesome/fontawesome-free/scss/variables' with (
|
||||
// // customizing $font-path - make sure it points to where your webfonts are stored in your project
|
||||
// $font-path: '../webfonts',
|
||||
//);
|
||||
@use '@fortawesome/fontawesome-free/scss/variables' with (
|
||||
// use fonts from @fortawesome/fontawesome-free
|
||||
$font-path: '@fortawesome/fontawesome-free/webfonts',
|
||||
);
|
||||
// load Font Awesome core
|
||||
@use '@fortawesome/fontawesome-free/scss/fontawesome';
|
||||
|
||||
// load and make available Font Awesome helpers (mixins, functions, and variables)
|
||||
@use '@fortawesome/fontawesome-free/scss/fa' as fa;
|
||||
@use '@fortawesome/fontawesome-free/scss/brands' as fa-brands;
|
||||
@use '@fortawesome/fontawesome-free/scss/regular' as fa-regular;
|
||||
@use '@fortawesome/fontawesome-free/scss/solid' as fa-solid;
|
||||
247
kb_demo_app/frontend/sass/_simplebar.scss
Normal file
247
kb_demo_app/frontend/sass/_simplebar.scss
Normal file
@@ -0,0 +1,247 @@
|
||||
// file: kb_demo_app/frontend/sass/_simplebar.scss
|
||||
|
||||
/* Rtl support */
|
||||
[data-simplebar] {
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
align-content: flex-start;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.simplebar-wrapper {
|
||||
overflow: hidden;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
max-width: inherit;
|
||||
max-height: inherit;
|
||||
}
|
||||
|
||||
.simplebar-mask {
|
||||
direction: inherit;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
// z-index: 0;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.simplebar-offset {
|
||||
direction: inherit !important;
|
||||
box-sizing: inherit !important;
|
||||
resize: none !important;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.simplebar-content-wrapper {
|
||||
direction: inherit;
|
||||
box-sizing: border-box !important;
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-hide-scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
position: fixed;
|
||||
left: 0;
|
||||
visibility: hidden;
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.simplebar-content {
|
||||
&:before {
|
||||
content: ' ';
|
||||
display: table;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: ' ';
|
||||
display: table;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-placeholder {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.simplebar-height-auto-observer-wrapper {
|
||||
box-sizing: inherit !important;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: 1px;
|
||||
position: relative;
|
||||
float: left;
|
||||
max-height: 1px;
|
||||
overflow: hidden;
|
||||
// z-index: -1;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
pointer-events: none;
|
||||
flex-grow: inherit;
|
||||
flex-shrink: 0;
|
||||
flex-basis: 0;
|
||||
}
|
||||
|
||||
.simplebar-height-auto-observer {
|
||||
box-sizing: inherit;
|
||||
display: block;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 1000%;
|
||||
width: 1000%;
|
||||
min-height: 1px;
|
||||
min-width: 1px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
// z-index: -1;
|
||||
}
|
||||
|
||||
.simplebar-track {
|
||||
// z-index: 1;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-simplebar].simplebar-dragging {
|
||||
pointer-events: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
.simplebar-content {
|
||||
pointer-events: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.simplebar-track {
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-scrollbar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
min-height: 10px;
|
||||
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
background: black;
|
||||
border-radius: 7px;
|
||||
left: 2px;
|
||||
right: 2px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s 0.5s linear;
|
||||
top: 2px;
|
||||
bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-scrollbar.simplebar-visible {
|
||||
&:before {
|
||||
opacity: 0.5;
|
||||
transition-delay: 0s;
|
||||
transition-duration: 0s;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-track.simplebar-vertical {
|
||||
top: 0;
|
||||
width: 11px;
|
||||
}
|
||||
|
||||
.simplebar-track.simplebar-horizontal {
|
||||
left: 0;
|
||||
height: 11px;
|
||||
|
||||
.simplebar-scrollbar {
|
||||
right: auto;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
min-height: 0;
|
||||
min-width: 10px;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
[data-simplebar-direction='rtl'] {
|
||||
.simplebar-track.simplebar-vertical {
|
||||
right: auto;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-dummy-scrollbar-size {
|
||||
direction: rtl;
|
||||
position: fixed;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
height: 500px;
|
||||
width: 500px;
|
||||
overflow-y: hidden;
|
||||
overflow-x: scroll;
|
||||
-ms-overflow-style: scrollbar !important;
|
||||
|
||||
>div {
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
94
kb_demo_app/frontend/sass/_variables.scss
Normal file
94
kb_demo_app/frontend/sass/_variables.scss
Normal file
@@ -0,0 +1,94 @@
|
||||
// file: kb_demo_app/frontend/sass/_variables.scss
|
||||
|
||||
// Pulse 5.3.8
|
||||
// Bootswatch
|
||||
|
||||
$theme: "pulse" !default;
|
||||
|
||||
//
|
||||
// Color system
|
||||
//
|
||||
|
||||
$white: #fff !default;
|
||||
$gray-100: #fafafa !default;
|
||||
$gray-200: #f9f8fc !default;
|
||||
$gray-300: #ededed !default;
|
||||
$gray-400: #cbc8d0 !default;
|
||||
$gray-500: #adb5bd !default;
|
||||
$gray-600: #868e96 !default;
|
||||
$gray-700: #444 !default;
|
||||
$gray-800: #343a40 !default;
|
||||
$gray-900: #17141f !default;
|
||||
$black: #000 !default;
|
||||
|
||||
$blue: #007bff !default;
|
||||
$indigo: #6610f2 !default;
|
||||
$purple: #593196 !default;
|
||||
$pink: #e83e8c !default;
|
||||
$red: #fc3939 !default;
|
||||
$orange: #fd7e14 !default;
|
||||
$yellow: #efa31d !default;
|
||||
$green: #13b955 !default;
|
||||
$teal: #20c997 !default;
|
||||
$cyan: #009cdc !default;
|
||||
|
||||
$primary: $purple !default;
|
||||
$secondary: #a991d4 !default;
|
||||
$success: $green !default;
|
||||
$info: $cyan !default;
|
||||
$warning: $yellow !default;
|
||||
$danger: $red !default;
|
||||
$light: $gray-200 !default;
|
||||
$dark: $gray-900 !default;
|
||||
|
||||
$min-contrast-ratio: 2.1 !default;
|
||||
|
||||
// Options
|
||||
|
||||
$enable-rounded: false !default;
|
||||
|
||||
// Body
|
||||
|
||||
$body-color: $gray-700 !default;
|
||||
|
||||
// Links
|
||||
|
||||
$link-hover-color: $primary !default;
|
||||
|
||||
// Tables
|
||||
|
||||
$table-color: initial !default;
|
||||
|
||||
$table-border-color: rgba(0, 0, 0, .05) !default;
|
||||
|
||||
// Forms
|
||||
|
||||
$input-focus-border-color: $primary !default;
|
||||
|
||||
// Dropdowns
|
||||
|
||||
$dropdown-link-hover-color: $white !default;
|
||||
$dropdown-link-hover-bg: $primary !default;
|
||||
|
||||
// Navs
|
||||
|
||||
$nav-tabs-border-color: $gray-300 !default;
|
||||
$nav-tabs-link-hover-border-color: $primary !default;
|
||||
|
||||
// Navbar
|
||||
|
||||
$navbar-padding-y: 1.2rem !default;
|
||||
|
||||
// Progress bars
|
||||
|
||||
$progress-bg: $gray-300 !default;
|
||||
$progress-bar-bg: $primary !default;
|
||||
|
||||
// List group
|
||||
|
||||
$list-group-bg: $gray-900 !default;
|
||||
$list-group-border-color: transparent !default;
|
||||
$list-group-hover-bg: lighten($list-group-bg, 10%) !default;
|
||||
$list-group-active-color: $white !default;
|
||||
$list-group-active-bg: $list-group-bg !default;
|
||||
$list-group-disabled-color: lighten($list-group-bg, 30%) !default;
|
||||
9
kb_demo_app/frontend/sass/main.scss
Normal file
9
kb_demo_app/frontend/sass/main.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
// file: kb_demo_app/frontend/sass/main.scss
|
||||
|
||||
@import "bootstrap/scss/functions";
|
||||
@import "variables";
|
||||
@import "fontawesome";
|
||||
@import "simplebar";
|
||||
@import "bootstrap/scss/bootstrap";
|
||||
@import "bootswatch";
|
||||
@import "app";
|
||||
111
kb_demo_app/frontend/sass/splash.scss
Normal file
111
kb_demo_app/frontend/sass/splash.scss
Normal file
@@ -0,0 +1,111 @@
|
||||
// file: kb_demo_app/frontend/sass/splash.scss
|
||||
|
||||
@font-face {
|
||||
font-family: 'Dos Amazigh';
|
||||
src: url('../fonts/DOS_Amazigh.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap; /* Assure un bon rendu pendant le chargement de la police */
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
#splash-container {
|
||||
position: relative;
|
||||
width: 960px; /* Largeur exacte de votre image */
|
||||
height: 637px; /* Hauteur exacte de votre image */
|
||||
opacity: 0; /* Sera animé par JavaScript */
|
||||
}
|
||||
|
||||
#splash-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#app-name {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-family: 'Dos Amazigh', sans-serif; /* Application de la police */
|
||||
font-size: 96px;
|
||||
font-weight: bold;
|
||||
color: #fff; /* Blanc pour contraste */
|
||||
text-shadow: 0 0 10px rgba(0, 0, 0, 0.5); /* Ombre pour meilleure lisibilité */
|
||||
z-index: 2;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#debug-info {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 50%;
|
||||
max-height: 30%;
|
||||
overflow-y: auto;
|
||||
color: white;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
padding: 8px;
|
||||
z-index: 3;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#messages-container {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
max-height: 30%;
|
||||
overflow-y: auto;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
padding: 10px;
|
||||
padding-bottom: 40px;
|
||||
z-index: 2;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.splash-message {
|
||||
margin-bottom: 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.splash-message.info {
|
||||
background-color: rgba(33, 150, 243, 0.2);
|
||||
}
|
||||
|
||||
.splash-message.warning {
|
||||
background-color: rgba(255, 152, 0, 0.2);
|
||||
}
|
||||
|
||||
.splash-message.error {
|
||||
background-color: rgba(244, 67, 54, 0.2);
|
||||
}
|
||||
|
||||
.splash-message.success {
|
||||
background-color: rgba(76, 175, 80, 0.2);
|
||||
}
|
||||
22
kb_demo_app/frontend/splash.html
Normal file
22
kb_demo_app/frontend/splash.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<!-- file: kb_demo_app/frontend/splash.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Loading ... Khadhroony Solana BoBot</title>
|
||||
<link rel="stylesheet" href="sass/splash.scss" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="splash-container">
|
||||
<img id="splash-image" src="imgs/splash.png" alt="Application Loading" />
|
||||
<div id="app-name">Khadhroony</div>
|
||||
<div id="debug-info"></div>
|
||||
<div id="messages-container"></div>
|
||||
</div>
|
||||
<script type="module" src="ts/splash.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
34
kb_demo_app/frontend/ts/bindings/DemoHttpExecutionPayload.ts
Normal file
34
kb_demo_app/frontend/ts/bindings/DemoHttpExecutionPayload.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Response payload for one demo HTTP execution.
|
||||
*/
|
||||
export type DemoHttpExecutionPayload = {
|
||||
/**
|
||||
* Selected endpoint name.
|
||||
*/
|
||||
endpointName: string,
|
||||
/**
|
||||
* Selected endpoint provider.
|
||||
*/
|
||||
provider: string,
|
||||
/**
|
||||
* Selected endpoint URL.
|
||||
*/
|
||||
endpointUrl: string,
|
||||
/**
|
||||
* Requested role.
|
||||
*/
|
||||
role: string,
|
||||
/**
|
||||
* Executed method name.
|
||||
*/
|
||||
method: string,
|
||||
/**
|
||||
* Classified method family.
|
||||
*/
|
||||
methodClass: string,
|
||||
/**
|
||||
* Pretty-printed JSON response payload.
|
||||
*/
|
||||
responseJson: string, };
|
||||
24
kb_demo_app/frontend/ts/bindings/DemoHttpRequest.ts
Normal file
24
kb_demo_app/frontend/ts/bindings/DemoHttpRequest.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Request payload for one demo HTTP execution.
|
||||
*/
|
||||
export type DemoHttpRequest = {
|
||||
/**
|
||||
* Logical role used to select one endpoint from the pool.
|
||||
*/
|
||||
role: string,
|
||||
/**
|
||||
* JSON-RPC HTTP method name.
|
||||
*/
|
||||
method: string,
|
||||
/**
|
||||
* Optional first string argument, used by methods such as
|
||||
* `getBalance`, `getAccountInfo`, `getProgramAccounts`,
|
||||
* `getSignaturesForAddress`, `getTransaction`, `sendTransaction`.
|
||||
*/
|
||||
firstArg: string | null,
|
||||
/**
|
||||
* Optional JSON config payload encoded as a string.
|
||||
*/
|
||||
configJson: string | null, };
|
||||
@@ -0,0 +1,27 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DemoPipeline2CatalogPayload } from "./DemoPipeline2CatalogPayload";
|
||||
|
||||
/**
|
||||
* Shared backfill response payload.
|
||||
*/
|
||||
export type DemoPipeline2BackfillPayload = {
|
||||
/**
|
||||
* Object key used by the backfill.
|
||||
*/
|
||||
objectKey: string,
|
||||
/**
|
||||
* Mode: `tokenMint` or `poolAddress`.
|
||||
*/
|
||||
mode: string,
|
||||
/**
|
||||
* HTTP role used.
|
||||
*/
|
||||
httpRole: string,
|
||||
/**
|
||||
* Pretty JSON summary.
|
||||
*/
|
||||
summaryJson: string,
|
||||
/**
|
||||
* Refreshed local catalog after backfill.
|
||||
*/
|
||||
catalog: DemoPipeline2CatalogPayload, };
|
||||
@@ -0,0 +1,18 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Request payload for pool backfill.
|
||||
*/
|
||||
export type DemoPipeline2BackfillPoolRequest = {
|
||||
/**
|
||||
* Pool address to backfill.
|
||||
*/
|
||||
poolAddress: string,
|
||||
/**
|
||||
* Optional HTTP role.
|
||||
*/
|
||||
httpRole: string | null,
|
||||
/**
|
||||
* Limit for signatures fetched from the pool.
|
||||
*/
|
||||
poolSignatureLimit: number, };
|
||||
@@ -0,0 +1,22 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Request payload for token backfill.
|
||||
*/
|
||||
export type DemoPipeline2BackfillTokenRequest = {
|
||||
/**
|
||||
* Token mint to backfill.
|
||||
*/
|
||||
tokenMint: string,
|
||||
/**
|
||||
* Optional HTTP role.
|
||||
*/
|
||||
httpRole: string | null,
|
||||
/**
|
||||
* Limit for signatures fetched from the mint.
|
||||
*/
|
||||
mintSignatureLimit: number,
|
||||
/**
|
||||
* Limit for signatures fetched from each discovered pool.
|
||||
*/
|
||||
poolSignatureLimit: number, };
|
||||
@@ -0,0 +1,25 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DemoPipeline2PairItem } from "./DemoPipeline2PairItem";
|
||||
import type { DemoPipeline2PoolItem } from "./DemoPipeline2PoolItem";
|
||||
import type { DemoPipeline2TokenItem } from "./DemoPipeline2TokenItem";
|
||||
|
||||
/**
|
||||
* Full local catalog payload.
|
||||
*/
|
||||
export type DemoPipeline2CatalogPayload = {
|
||||
/**
|
||||
* Open database URL.
|
||||
*/
|
||||
databaseUrl: string,
|
||||
/**
|
||||
* Observed token list.
|
||||
*/
|
||||
tokens: Array<DemoPipeline2TokenItem>,
|
||||
/**
|
||||
* Known pool list.
|
||||
*/
|
||||
pools: Array<DemoPipeline2PoolItem>,
|
||||
/**
|
||||
* Known pair list.
|
||||
*/
|
||||
pairs: Array<DemoPipeline2PairItem>, };
|
||||
@@ -0,0 +1,34 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Local decoded-event diagnostics summary for the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalDecodedEventDiagnosticSummary = {
|
||||
/**
|
||||
* Protocol name.
|
||||
*/
|
||||
protocolName: string,
|
||||
/**
|
||||
* Event kind.
|
||||
*/
|
||||
eventKind: string,
|
||||
/**
|
||||
* Event category.
|
||||
*/
|
||||
eventCategory: string | null,
|
||||
/**
|
||||
* Trade candidate flag.
|
||||
*/
|
||||
tradeCandidate: boolean | null,
|
||||
/**
|
||||
* Candle candidate flag.
|
||||
*/
|
||||
candleCandidate: boolean | null,
|
||||
/**
|
||||
* Event count.
|
||||
*/
|
||||
eventCount: number,
|
||||
/**
|
||||
* Linked trade-event count.
|
||||
*/
|
||||
tradeEventCount: number, };
|
||||
@@ -0,0 +1,38 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Local DEX diagnostics summary for the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalDexDiagnosticSummary = {
|
||||
/**
|
||||
* DEX code.
|
||||
*/
|
||||
dexCode: string,
|
||||
/**
|
||||
* Pool count.
|
||||
*/
|
||||
poolCount: number,
|
||||
/**
|
||||
* Pair count.
|
||||
*/
|
||||
pairCount: number,
|
||||
/**
|
||||
* Decoded event count.
|
||||
*/
|
||||
decodedEventCount: number,
|
||||
/**
|
||||
* Decoded trade candidate count.
|
||||
*/
|
||||
decodedTradeCandidateCount: number,
|
||||
/**
|
||||
* Decoded candle candidate count.
|
||||
*/
|
||||
decodedCandleCandidateCount: number,
|
||||
/**
|
||||
* Trade event count.
|
||||
*/
|
||||
tradeEventCount: number,
|
||||
/**
|
||||
* Pair candle count.
|
||||
*/
|
||||
pairCandleCount: number, };
|
||||
@@ -0,0 +1,19 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DemoPipeline2LocalPipelineDiagnosticSummary } from "./DemoPipeline2LocalPipelineDiagnosticSummary";
|
||||
|
||||
/**
|
||||
* Local diagnostics payload returned to the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalDiagnosticsPayload = {
|
||||
/**
|
||||
* Open database URL.
|
||||
*/
|
||||
databaseUrl: string,
|
||||
/**
|
||||
* Pretty JSON diagnostics summary.
|
||||
*/
|
||||
summaryJson: string,
|
||||
/**
|
||||
* Structured diagnostics summary.
|
||||
*/
|
||||
summary: DemoPipeline2LocalPipelineDiagnosticSummary, };
|
||||
@@ -0,0 +1,34 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Local duplicate decoded-event trade diagnostic sample for the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalDuplicateDecodedEventTradeDiagnosticSample = {
|
||||
/**
|
||||
* Decoded event id.
|
||||
*/
|
||||
decodedEventId: number,
|
||||
/**
|
||||
* Protocol name.
|
||||
*/
|
||||
protocolName: string | null,
|
||||
/**
|
||||
* Event kind.
|
||||
*/
|
||||
eventKind: string | null,
|
||||
/**
|
||||
* Pool account.
|
||||
*/
|
||||
poolAccount: string | null,
|
||||
/**
|
||||
* Trade event count.
|
||||
*/
|
||||
tradeEventCount: number,
|
||||
/**
|
||||
* Trade event ids.
|
||||
*/
|
||||
tradeEventIds: string | null,
|
||||
/**
|
||||
* Signatures.
|
||||
*/
|
||||
signatures: string | null, };
|
||||
@@ -0,0 +1,54 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Local missing-trade-event diagnostic sample for the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalMissingTradeEventDiagnosticSample = {
|
||||
/**
|
||||
* Decoded event id.
|
||||
*/
|
||||
decodedEventId: number,
|
||||
/**
|
||||
* Chain transaction id.
|
||||
*/
|
||||
transactionId: number | null,
|
||||
/**
|
||||
* Transaction signature.
|
||||
*/
|
||||
signature: string | null,
|
||||
/**
|
||||
* Protocol name.
|
||||
*/
|
||||
protocolName: string,
|
||||
/**
|
||||
* Event kind.
|
||||
*/
|
||||
eventKind: string,
|
||||
/**
|
||||
* Pool account.
|
||||
*/
|
||||
poolAccount: string | null,
|
||||
/**
|
||||
* Whether the source transaction failed.
|
||||
*/
|
||||
transactionFailed: boolean,
|
||||
/**
|
||||
* Whether this missing trade event is actionable for validation.
|
||||
*/
|
||||
actionable: boolean,
|
||||
/**
|
||||
* Diagnostic reason explaining why no trade event was linked.
|
||||
*/
|
||||
reason: string,
|
||||
/**
|
||||
* Whether payload has an explicit base amount.
|
||||
*/
|
||||
hasBaseAmountPayload: boolean,
|
||||
/**
|
||||
* Whether payload has an explicit quote amount.
|
||||
*/
|
||||
hasQuoteAmountPayload: boolean,
|
||||
/**
|
||||
* Whether payload has an explicit price.
|
||||
*/
|
||||
hasPricePayload: boolean, };
|
||||
@@ -0,0 +1,34 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Local missing-trade-event reason summary for the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalMissingTradeEventReasonSummary = {
|
||||
/**
|
||||
* Diagnostic reason.
|
||||
*/
|
||||
reason: string,
|
||||
/**
|
||||
* Whether grouped source transactions failed.
|
||||
*/
|
||||
transactionFailed: boolean,
|
||||
/**
|
||||
* Whether grouped missing trade events are actionable.
|
||||
*/
|
||||
actionable: boolean,
|
||||
/**
|
||||
* Total missing trade events in this group.
|
||||
*/
|
||||
eventCount: number,
|
||||
/**
|
||||
* Total events in this group with an explicit base amount payload.
|
||||
*/
|
||||
hasBaseAmountPayloadCount: number,
|
||||
/**
|
||||
* Total events in this group with an explicit quote amount payload.
|
||||
*/
|
||||
hasQuoteAmountPayloadCount: number,
|
||||
/**
|
||||
* Total events in this group with an explicit price payload.
|
||||
*/
|
||||
hasPricePayloadCount: number, };
|
||||
@@ -0,0 +1,38 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Local multi-trade signature/pair diagnostic sample for the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalMultiTradeSignaturePairDiagnosticSample = {
|
||||
/**
|
||||
* Transaction signature.
|
||||
*/
|
||||
signature: string,
|
||||
/**
|
||||
* Pair id.
|
||||
*/
|
||||
pairId: number,
|
||||
/**
|
||||
* Pool address.
|
||||
*/
|
||||
poolAddress: string | null,
|
||||
/**
|
||||
* DEX code.
|
||||
*/
|
||||
dexCode: string | null,
|
||||
/**
|
||||
* Trade event count.
|
||||
*/
|
||||
tradeEventCount: number,
|
||||
/**
|
||||
* Distinct decoded event count.
|
||||
*/
|
||||
decodedEventCount: number,
|
||||
/**
|
||||
* Trade event ids.
|
||||
*/
|
||||
tradeEventIds: string | null,
|
||||
/**
|
||||
* Decoded event ids.
|
||||
*/
|
||||
decodedEventIds: string | null, };
|
||||
@@ -0,0 +1,70 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Local non-actionable pair diagnostic summary for the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalNonActionablePairDiagnosticSummary = {
|
||||
/**
|
||||
* Pair id.
|
||||
*/
|
||||
pairId: number,
|
||||
/**
|
||||
* Pool address.
|
||||
*/
|
||||
poolAddress: string,
|
||||
/**
|
||||
* DEX code.
|
||||
*/
|
||||
dexCode: string,
|
||||
/**
|
||||
* Base token mint.
|
||||
*/
|
||||
baseMint: string,
|
||||
/**
|
||||
* Base token symbol.
|
||||
*/
|
||||
baseSymbol: string | null,
|
||||
/**
|
||||
* Quote token mint.
|
||||
*/
|
||||
quoteMint: string,
|
||||
/**
|
||||
* Quote token symbol.
|
||||
*/
|
||||
quoteSymbol: string | null,
|
||||
/**
|
||||
* Pair symbol.
|
||||
*/
|
||||
pairSymbol: string | null,
|
||||
/**
|
||||
* Total decoded trade candidates attached to the pool.
|
||||
*/
|
||||
decodedTradeCandidateCount: number,
|
||||
/**
|
||||
* Total non-actionable missing trade events attached to the pair.
|
||||
*/
|
||||
nonActionableMissingTradeEventCount: number,
|
||||
/**
|
||||
* Total missing trade candidates caused by failed transactions.
|
||||
*/
|
||||
failedTransactionCandidateCount: number,
|
||||
/**
|
||||
* Total successful missing trade candidates without token mints.
|
||||
*/
|
||||
okTransactionWithoutTokenMintsCount: number,
|
||||
/**
|
||||
* Total successful missing trade candidates without amount payload.
|
||||
*/
|
||||
okTransactionWithoutAmountPayloadCount: number,
|
||||
/**
|
||||
* Distinct non-actionable diagnostic reasons seen for this pair.
|
||||
*/
|
||||
reasonSummary: string | null,
|
||||
/**
|
||||
* Total trade events attached to the pair.
|
||||
*/
|
||||
tradeEventCount: number,
|
||||
/**
|
||||
* Total candle buckets attached to the pair.
|
||||
*/
|
||||
pairCandleCount: number, };
|
||||
@@ -0,0 +1,66 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Local pair diagnostics summary for the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalPairDiagnosticSummary = {
|
||||
/**
|
||||
* Pair id.
|
||||
*/
|
||||
pairId: number,
|
||||
/**
|
||||
* Pool address.
|
||||
*/
|
||||
poolAddress: string,
|
||||
/**
|
||||
* DEX code.
|
||||
*/
|
||||
dexCode: string,
|
||||
/**
|
||||
* Base mint.
|
||||
*/
|
||||
baseMint: string,
|
||||
/**
|
||||
* Base symbol.
|
||||
*/
|
||||
baseSymbol: string | null,
|
||||
/**
|
||||
* Quote mint.
|
||||
*/
|
||||
quoteMint: string,
|
||||
/**
|
||||
* Quote symbol.
|
||||
*/
|
||||
quoteSymbol: string | null,
|
||||
/**
|
||||
* Pair symbol.
|
||||
*/
|
||||
pairSymbol: string | null,
|
||||
/**
|
||||
* Decoded event count.
|
||||
*/
|
||||
decodedEventCount: number,
|
||||
/**
|
||||
* Decoded trade candidate count.
|
||||
*/
|
||||
decodedTradeCandidateCount: number,
|
||||
/**
|
||||
* Decoded candle candidate count.
|
||||
*/
|
||||
decodedCandleCandidateCount: number,
|
||||
/**
|
||||
* Trade event count.
|
||||
*/
|
||||
tradeEventCount: number,
|
||||
/**
|
||||
* Invalid trade event count.
|
||||
*/
|
||||
invalidTradeEventCount: number,
|
||||
/**
|
||||
* Pair candle count.
|
||||
*/
|
||||
pairCandleCount: number,
|
||||
/**
|
||||
* Last known price.
|
||||
*/
|
||||
lastPriceQuotePerBase: number | null, };
|
||||
@@ -0,0 +1,54 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Local pair gap diagnostic sample for the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalPairGapDiagnosticSample = {
|
||||
/**
|
||||
* Pair id.
|
||||
*/
|
||||
pairId: number,
|
||||
/**
|
||||
* Pool address.
|
||||
*/
|
||||
poolAddress: string,
|
||||
/**
|
||||
* DEX code.
|
||||
*/
|
||||
dexCode: string,
|
||||
/**
|
||||
* Base mint.
|
||||
*/
|
||||
baseMint: string,
|
||||
/**
|
||||
* Base symbol.
|
||||
*/
|
||||
baseSymbol: string | null,
|
||||
/**
|
||||
* Quote mint.
|
||||
*/
|
||||
quoteMint: string,
|
||||
/**
|
||||
* Quote symbol.
|
||||
*/
|
||||
quoteSymbol: string | null,
|
||||
/**
|
||||
* Pair symbol.
|
||||
*/
|
||||
pairSymbol: string | null,
|
||||
/**
|
||||
* Decoded event count.
|
||||
*/
|
||||
decodedEventCount: number,
|
||||
/**
|
||||
* Decoded trade candidate count.
|
||||
*/
|
||||
decodedTradeCandidateCount: number,
|
||||
/**
|
||||
* Trade event count.
|
||||
*/
|
||||
tradeEventCount: number,
|
||||
/**
|
||||
* Pair candle count.
|
||||
*/
|
||||
pairCandleCount: number, };
|
||||
@@ -0,0 +1,164 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DemoPipeline2LocalDecodedEventDiagnosticSummary } from "./DemoPipeline2LocalDecodedEventDiagnosticSummary";
|
||||
import type { DemoPipeline2LocalDexDiagnosticSummary } from "./DemoPipeline2LocalDexDiagnosticSummary";
|
||||
import type { DemoPipeline2LocalDuplicateDecodedEventTradeDiagnosticSample } from "./DemoPipeline2LocalDuplicateDecodedEventTradeDiagnosticSample";
|
||||
import type { DemoPipeline2LocalMissingTradeEventDiagnosticSample } from "./DemoPipeline2LocalMissingTradeEventDiagnosticSample";
|
||||
import type { DemoPipeline2LocalMissingTradeEventReasonSummary } from "./DemoPipeline2LocalMissingTradeEventReasonSummary";
|
||||
import type { DemoPipeline2LocalMultiTradeSignaturePairDiagnosticSample } from "./DemoPipeline2LocalMultiTradeSignaturePairDiagnosticSample";
|
||||
import type { DemoPipeline2LocalNonActionablePairDiagnosticSummary } from "./DemoPipeline2LocalNonActionablePairDiagnosticSummary";
|
||||
import type { DemoPipeline2LocalPairDiagnosticSummary } from "./DemoPipeline2LocalPairDiagnosticSummary";
|
||||
import type { DemoPipeline2LocalPairGapDiagnosticSample } from "./DemoPipeline2LocalPairGapDiagnosticSample";
|
||||
|
||||
/**
|
||||
* Local pipeline diagnostics summary for the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalPipelineDiagnosticSummary = {
|
||||
/**
|
||||
* Total persisted chain transactions.
|
||||
*/
|
||||
transactionCount: number,
|
||||
/**
|
||||
* Total successful chain transactions.
|
||||
*/
|
||||
okTransactionCount: number,
|
||||
/**
|
||||
* Total failed chain transactions.
|
||||
*/
|
||||
failedTransactionCount: number,
|
||||
/**
|
||||
* Total decoded DEX events.
|
||||
*/
|
||||
decodedEventCount: number,
|
||||
/**
|
||||
* Total decoded DEX trade candidates.
|
||||
*/
|
||||
decodedTradeCandidateCount: number,
|
||||
/**
|
||||
* Total decoded DEX candle candidates.
|
||||
*/
|
||||
decodedCandleCandidateCount: number,
|
||||
/**
|
||||
* Whether the local persisted pipeline has no blocking diagnostic issue.
|
||||
*/
|
||||
diagnosticsClean: boolean,
|
||||
/**
|
||||
* Number of blocking diagnostic issues.
|
||||
*/
|
||||
blockingIssueCount: number,
|
||||
/**
|
||||
* Total trade candidates without trade event, including ignored failed transactions.
|
||||
*/
|
||||
missingTradeEventCount: number,
|
||||
/**
|
||||
* Explicit alias for decoded trade candidates without linked trade event.
|
||||
*/
|
||||
decodedTradeCandidateWithoutTradeEventCount: number,
|
||||
/**
|
||||
* Trade candidates without linked trade event on successful transactions.
|
||||
*/
|
||||
decodedTradeCandidateWithoutTradeEventOnOkTransactionCount: number,
|
||||
/**
|
||||
* Trade candidates without linked trade event on failed transactions.
|
||||
*/
|
||||
decodedTradeCandidateWithoutTradeEventOnFailedTransactionCount: number,
|
||||
/**
|
||||
* Trade candidates without linked trade event and without explicit base/quote payload amounts.
|
||||
* Actionable missing trade events on successful transactions.
|
||||
*/
|
||||
actionableMissingTradeEventCount: number,
|
||||
/**
|
||||
* Ignored missing trade events caused by failed transactions.
|
||||
*/
|
||||
ignoredFailedTransactionTradeCandidateCount: number, decodedTradeCandidateWithoutAmountPayloadCount: number,
|
||||
/**
|
||||
* Total persisted trade events.
|
||||
*/
|
||||
tradeEventCount: number,
|
||||
/**
|
||||
* Total invalid trade events.
|
||||
*/
|
||||
invalidTradeEventCount: number,
|
||||
/**
|
||||
* Total persisted pair candles.
|
||||
*/
|
||||
pairCandleCount: number,
|
||||
/**
|
||||
* Real duplicate trade rows grouped by decoded event id.
|
||||
*/
|
||||
duplicateDecodedEventTradeCount: number,
|
||||
/**
|
||||
* Multi-trade groups sharing the same signature and pair id.
|
||||
*/
|
||||
multiTradeSignaturePairCount: number,
|
||||
/**
|
||||
* Total duplicate candle buckets.
|
||||
*/
|
||||
duplicateCandleBucketCount: number,
|
||||
/**
|
||||
* Total known tokens.
|
||||
*/
|
||||
tokenCount: number,
|
||||
/**
|
||||
* Total tokens missing symbol or name.
|
||||
*/
|
||||
tokenMetadataMissingCount: number,
|
||||
/**
|
||||
* Total known pools.
|
||||
*/
|
||||
poolCount: number,
|
||||
/**
|
||||
* Total known pairs.
|
||||
*/
|
||||
pairCount: number,
|
||||
/**
|
||||
* Total pairs without trade.
|
||||
*/
|
||||
pairWithoutTradeCount: number,
|
||||
/**
|
||||
* Total pairs without candle.
|
||||
*/
|
||||
pairWithoutCandleCount: number,
|
||||
/**
|
||||
* Diagnostics grouped by DEX.
|
||||
*/
|
||||
dexSummaries: Array<DemoPipeline2LocalDexDiagnosticSummary>,
|
||||
/**
|
||||
* Diagnostics grouped by pair.
|
||||
*/
|
||||
pairSummaries: Array<DemoPipeline2LocalPairDiagnosticSummary>,
|
||||
/**
|
||||
* Diagnostics grouped by decoded event kind.
|
||||
*/
|
||||
decodedEventSummaries: Array<DemoPipeline2LocalDecodedEventDiagnosticSummary>,
|
||||
/**
|
||||
* Missing trade events grouped by diagnostic reason.
|
||||
*/
|
||||
missingTradeEventReasonSummaries: Array<DemoPipeline2LocalMissingTradeEventReasonSummary>,
|
||||
/**
|
||||
* Total pairs with only non-actionable missing trade events.
|
||||
*/
|
||||
nonActionablePairCount: number,
|
||||
/**
|
||||
* Pair summaries for non-actionable missing trade events.
|
||||
*/
|
||||
nonActionablePairSummaries: Array<DemoPipeline2LocalNonActionablePairDiagnosticSummary>,
|
||||
/**
|
||||
* Samples of decoded trade candidates without linked trade event.
|
||||
*/
|
||||
missingTradeEventSamples: Array<DemoPipeline2LocalMissingTradeEventDiagnosticSample>,
|
||||
/**
|
||||
* Samples of duplicated trade rows by decoded event id.
|
||||
*/
|
||||
duplicateDecodedEventTradeSamples: Array<DemoPipeline2LocalDuplicateDecodedEventTradeDiagnosticSample>,
|
||||
/**
|
||||
* Samples of multi-trade signature/pair groups.
|
||||
*/
|
||||
multiTradeSignaturePairSamples: Array<DemoPipeline2LocalMultiTradeSignaturePairDiagnosticSample>,
|
||||
/**
|
||||
* Samples of pairs without trade.
|
||||
*/
|
||||
pairWithoutTradeSamples: Array<DemoPipeline2LocalPairGapDiagnosticSample>,
|
||||
/**
|
||||
* Samples of pairs without candle.
|
||||
*/
|
||||
pairWithoutCandleSamples: Array<DemoPipeline2LocalPairGapDiagnosticSample>, };
|
||||
@@ -0,0 +1,22 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* One local pipeline validation issue for the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalPipelineValidationIssue = {
|
||||
/**
|
||||
* Stable machine-readable issue code.
|
||||
*/
|
||||
code: string,
|
||||
/**
|
||||
* Human-readable issue message.
|
||||
*/
|
||||
message: string,
|
||||
/**
|
||||
* Optional subject associated with the issue.
|
||||
*/
|
||||
subject: string | null,
|
||||
/**
|
||||
* Whether the issue blocks the validation report.
|
||||
*/
|
||||
blocking: boolean, };
|
||||
@@ -0,0 +1,35 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DemoPipeline2LocalPipelineValidationIssue } from "./DemoPipeline2LocalPipelineValidationIssue";
|
||||
|
||||
/**
|
||||
* Local pipeline validation report for the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalPipelineValidationReport = {
|
||||
/**
|
||||
* Stable validation profile code used to produce the report.
|
||||
*/
|
||||
validationProfileCode: string,
|
||||
/**
|
||||
* Whether the validation passed without blocking issues.
|
||||
*/
|
||||
validationPassed: boolean,
|
||||
/**
|
||||
* Number of blocking issues found by validation.
|
||||
*/
|
||||
blockingIssueCount: number,
|
||||
/**
|
||||
* Number of non-blocking warnings found by validation.
|
||||
*/
|
||||
warningCount: number,
|
||||
/**
|
||||
* Expected DEX codes used by the validation run.
|
||||
*/
|
||||
expectedDexCodes: Array<string>,
|
||||
/**
|
||||
* Observed DEX codes found in diagnostics.
|
||||
*/
|
||||
observedDexCodes: Array<string>,
|
||||
/**
|
||||
* Issues produced by validation.
|
||||
*/
|
||||
issues: Array<DemoPipeline2LocalPipelineValidationIssue>, };
|
||||
@@ -0,0 +1,32 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DemoPipeline2LocalPipelineDiagnosticSummary } from "./DemoPipeline2LocalPipelineDiagnosticSummary";
|
||||
import type { DemoPipeline2LocalPipelineValidationReport } from "./DemoPipeline2LocalPipelineValidationReport";
|
||||
|
||||
/**
|
||||
* Local pipeline validation run for the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalPipelineValidationRun = {
|
||||
/**
|
||||
* Stable validation profile code used to produce the run.
|
||||
*/
|
||||
validationProfileCode: string,
|
||||
/**
|
||||
* Whether the validation passed without blocking issues.
|
||||
*/
|
||||
validationPassed: boolean,
|
||||
/**
|
||||
* Number of blocking issues found by validation.
|
||||
*/
|
||||
blockingIssueCount: number,
|
||||
/**
|
||||
* Number of non-blocking warnings found by validation.
|
||||
*/
|
||||
warningCount: number,
|
||||
/**
|
||||
* Diagnostics summary that was validated.
|
||||
*/
|
||||
summary: DemoPipeline2LocalPipelineDiagnosticSummary,
|
||||
/**
|
||||
* Detailed validation report produced from the diagnostics summary.
|
||||
*/
|
||||
report: DemoPipeline2LocalPipelineValidationReport, };
|
||||
@@ -0,0 +1,23 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DemoPipeline2LocalPipelineValidationRun } from "./DemoPipeline2LocalPipelineValidationRun";
|
||||
|
||||
/**
|
||||
* Local validation payload returned to the UI.
|
||||
*/
|
||||
export type DemoPipeline2LocalValidationPayload = {
|
||||
/**
|
||||
* Open database URL.
|
||||
*/
|
||||
databaseUrl: string,
|
||||
/**
|
||||
* Pretty JSON diagnostics summary used by validation.
|
||||
*/
|
||||
summaryJson: string,
|
||||
/**
|
||||
* Pretty JSON validation run.
|
||||
*/
|
||||
validationJson: string,
|
||||
/**
|
||||
* Structured validation run.
|
||||
*/
|
||||
run: DemoPipeline2LocalPipelineValidationRun, };
|
||||
@@ -0,0 +1,18 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Candle payload returned to the UI.
|
||||
*/
|
||||
export type DemoPipeline2PairCandlesPayload = {
|
||||
/**
|
||||
* Pair id.
|
||||
*/
|
||||
pairId: number,
|
||||
/**
|
||||
* Timeframe in seconds.
|
||||
*/
|
||||
timeframeSeconds: number,
|
||||
/**
|
||||
* Pretty JSON array of candles.
|
||||
*/
|
||||
candlesJson: string, };
|
||||
@@ -0,0 +1,18 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Request payload for pair candles.
|
||||
*/
|
||||
export type DemoPipeline2PairCandlesRequest = {
|
||||
/**
|
||||
* Pair id to load.
|
||||
*/
|
||||
pairId: number,
|
||||
/**
|
||||
* Timeframe in seconds.
|
||||
*/
|
||||
timeframeSeconds: number,
|
||||
/**
|
||||
* Whether materialized candles should be preferred when available.
|
||||
*/
|
||||
preferMaterialized: boolean, };
|
||||
30
kb_demo_app/frontend/ts/bindings/DemoPipeline2PairItem.ts
Normal file
30
kb_demo_app/frontend/ts/bindings/DemoPipeline2PairItem.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* One pair item for the local catalog.
|
||||
*/
|
||||
export type DemoPipeline2PairItem = {
|
||||
/**
|
||||
* Internal pair id.
|
||||
*/
|
||||
pairId: number,
|
||||
/**
|
||||
* Related pool address.
|
||||
*/
|
||||
poolAddress: string,
|
||||
/**
|
||||
* Optional pair symbol.
|
||||
*/
|
||||
symbol: string | null,
|
||||
/**
|
||||
* Optional DEX code.
|
||||
*/
|
||||
dexCode: string | null,
|
||||
/**
|
||||
* Optional local trade count.
|
||||
*/
|
||||
tradeCount: number | null,
|
||||
/**
|
||||
* Optional local last price.
|
||||
*/
|
||||
lastPriceQuotePerBase: number | null, };
|
||||
18
kb_demo_app/frontend/ts/bindings/DemoPipeline2PoolItem.ts
Normal file
18
kb_demo_app/frontend/ts/bindings/DemoPipeline2PoolItem.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* One pool item for the local catalog.
|
||||
*/
|
||||
export type DemoPipeline2PoolItem = {
|
||||
/**
|
||||
* Pool address.
|
||||
*/
|
||||
poolAddress: string,
|
||||
/**
|
||||
* Optional internal pair id when known.
|
||||
*/
|
||||
pairId: number | null,
|
||||
/**
|
||||
* Optional DEX code.
|
||||
*/
|
||||
dexCode: string | null, };
|
||||
18
kb_demo_app/frontend/ts/bindings/DemoPipeline2TokenItem.ts
Normal file
18
kb_demo_app/frontend/ts/bindings/DemoPipeline2TokenItem.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* One token item for the local catalog.
|
||||
*/
|
||||
export type DemoPipeline2TokenItem = {
|
||||
/**
|
||||
* Token mint.
|
||||
*/
|
||||
mint: string,
|
||||
/**
|
||||
* Optional token symbol.
|
||||
*/
|
||||
symbol: string | null,
|
||||
/**
|
||||
* Optional token name.
|
||||
*/
|
||||
name: string | null, };
|
||||
@@ -0,0 +1,22 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Response payload for one pool backfill launched from `kb_demo_app`.
|
||||
*/
|
||||
export type DemoPipelineBackfillPoolPayload = {
|
||||
/**
|
||||
* Backfilled pool address.
|
||||
*/
|
||||
poolAddress: string,
|
||||
/**
|
||||
* HTTP role used during backfill.
|
||||
*/
|
||||
httpRole: string,
|
||||
/**
|
||||
* Pretty JSON summary returned by `TokenBackfillService::backfill_pool_by_address`.
|
||||
*/
|
||||
backfillJson: string,
|
||||
/**
|
||||
* Whether the pool exists in persisted pool objects after backfill.
|
||||
*/
|
||||
poolPersistedAfterBackfill: boolean, };
|
||||
@@ -0,0 +1,18 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Request payload for one pool backfill launched from `kb_demo_app`.
|
||||
*/
|
||||
export type DemoPipelineBackfillPoolRequest = {
|
||||
/**
|
||||
* Pool address to backfill.
|
||||
*/
|
||||
poolAddress: string,
|
||||
/**
|
||||
* HTTP role used to select one endpoint in the pool.
|
||||
*/
|
||||
httpRole: string | null,
|
||||
/**
|
||||
* Maximum number of signatures fetched from the pool address.
|
||||
*/
|
||||
poolSignatureLimit: number, };
|
||||
@@ -0,0 +1,22 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Response payload for one token backfill launched from `kb_demo_app`.
|
||||
*/
|
||||
export type DemoPipelineBackfillTokenPayload = {
|
||||
/**
|
||||
* Backfilled token mint.
|
||||
*/
|
||||
tokenMint: string,
|
||||
/**
|
||||
* HTTP role used during backfill.
|
||||
*/
|
||||
httpRole: string,
|
||||
/**
|
||||
* Pretty JSON summary returned by `TokenBackfillService`.
|
||||
*/
|
||||
backfillJson: string,
|
||||
/**
|
||||
* Whether the token exists in persisted token objects after backfill.
|
||||
*/
|
||||
tokenPersistedAfterBackfill: boolean, };
|
||||
@@ -0,0 +1,22 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Request payload for one token backfill launched from `kb_demo_app`.
|
||||
*/
|
||||
export type DemoPipelineBackfillTokenRequest = {
|
||||
/**
|
||||
* Token mint to backfill.
|
||||
*/
|
||||
tokenMint: string,
|
||||
/**
|
||||
* HTTP role used to select one endpoint in the pool.
|
||||
*/
|
||||
httpRole: string | null,
|
||||
/**
|
||||
* Maximum number of signatures fetched directly from the mint.
|
||||
*/
|
||||
mintSignatureLimit: number,
|
||||
/**
|
||||
* Maximum number of signatures fetched from each discovered pool.
|
||||
*/
|
||||
poolSignatureLimit: number, };
|
||||
@@ -0,0 +1,14 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Request payload for one pipeline inspection by pair id.
|
||||
*/
|
||||
export type DemoPipelineInspectPairRequest = {
|
||||
/**
|
||||
* Pair id to inspect.
|
||||
*/
|
||||
pairId: bigint,
|
||||
/**
|
||||
* Optional custom timeframe in seconds for on-demand candle rebuild.
|
||||
*/
|
||||
customTimeframeSeconds: bigint | null, };
|
||||
@@ -0,0 +1,58 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Response payload for one pipeline inspection.
|
||||
*/
|
||||
export type DemoPipelineInspectPayload = {
|
||||
/**
|
||||
* Inspected signature.
|
||||
*/
|
||||
signature: string,
|
||||
/**
|
||||
* Summary JSON block.
|
||||
*/
|
||||
summaryJson: string,
|
||||
/**
|
||||
* Resolved transaction JSON block.
|
||||
*/
|
||||
transactionJson: string,
|
||||
/**
|
||||
* Decoded events JSON block.
|
||||
*/
|
||||
decodedEventsJson: string,
|
||||
/**
|
||||
* Pools JSON block.
|
||||
*/
|
||||
poolsJson: string,
|
||||
/**
|
||||
* Pairs JSON block.
|
||||
*/
|
||||
pairsJson: string,
|
||||
/**
|
||||
* Launch attributions JSON block.
|
||||
*/
|
||||
launchAttributionsJson: string,
|
||||
/**
|
||||
* Pool origins JSON block.
|
||||
*/
|
||||
poolOriginsJson: string,
|
||||
/**
|
||||
* Wallet inspection JSON block.
|
||||
*/
|
||||
walletsJson: string,
|
||||
/**
|
||||
* Trade events JSON block.
|
||||
*/
|
||||
tradeEventsJson: string,
|
||||
/**
|
||||
* Pair metrics JSON block.
|
||||
*/
|
||||
pairMetricsJson: string,
|
||||
/**
|
||||
* Pair candles JSON block.
|
||||
*/
|
||||
pairCandlesJson: string,
|
||||
/**
|
||||
* Pair analytic signals JSON block.
|
||||
*/
|
||||
pairAnalyticSignalsJson: string, };
|
||||
@@ -0,0 +1,14 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Request payload for one pipeline inspection by pool address.
|
||||
*/
|
||||
export type DemoPipelineInspectPoolRequest = {
|
||||
/**
|
||||
* Pool address to inspect.
|
||||
*/
|
||||
poolAddress: string,
|
||||
/**
|
||||
* Optional custom timeframe in seconds for on-demand candle rebuild.
|
||||
*/
|
||||
customTimeframeSeconds: bigint | null, };
|
||||
@@ -0,0 +1,14 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Request payload for one pipeline inspection by signature.
|
||||
*/
|
||||
export type DemoPipelineInspectRequest = {
|
||||
/**
|
||||
* Transaction signature to inspect.
|
||||
*/
|
||||
signature: string,
|
||||
/**
|
||||
* Optional custom timeframe in seconds for on-demand candle rebuild.
|
||||
*/
|
||||
customTimeframeSeconds: bigint | null, };
|
||||
@@ -0,0 +1,14 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Request payload for one pipeline inspection by token mint.
|
||||
*/
|
||||
export type DemoPipelineInspectTokenRequest = {
|
||||
/**
|
||||
* Token mint to inspect.
|
||||
*/
|
||||
tokenMint: string,
|
||||
/**
|
||||
* Optional custom timeframe in seconds for on-demand candle rebuild.
|
||||
*/
|
||||
customTimeframeSeconds: bigint | null, };
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Endpoint summary sent to the demo frontend.
|
||||
*/
|
||||
export type DemoWsEndpointSummary = { name: string, resolvedUrl: string, provider: string, enabled: boolean, roles: Array<string>, };
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Static endpoint summary enriched with current manager state.
|
||||
*/
|
||||
export type DemoWsManagerEndpointSummary = { name: string, resolvedUrl: string, provider: string, roles: Array<string>, connectionState: string, activeSubscriptionCount: number, };
|
||||
@@ -0,0 +1,7 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DemoWsManagerEndpointSummary } from "./DemoWsManagerEndpointSummary";
|
||||
|
||||
/**
|
||||
* Global demo manager snapshot payload.
|
||||
*/
|
||||
export type DemoWsManagerSnapshotPayload = { endpointCount: number, startedCount: number, endpoints: Array<DemoWsManagerEndpointSummary>, };
|
||||
6
kb_demo_app/frontend/ts/bindings/DemoWsStatusPayload.ts
Normal file
6
kb_demo_app/frontend/ts/bindings/DemoWsStatusPayload.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Current demo window runtime status.
|
||||
*/
|
||||
export type DemoWsStatusPayload = { connectionState: string, endpointName: string | null, endpointUrl: string | null, currentSubscriptionId: bigint | null, currentSubscribeMethod: string | null, currentUnsubscribeMethod: string | null, currentNotificationMethod: string | null, eventCountTotal: bigint, notificationCountTotal: bigint, uiLogCount: bigint, suppressedLogCount: bigint, lastEventKind: string | null, };
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Subscribe request sent by the demo frontend.
|
||||
*/
|
||||
export type DemoWsSubscribeRequest = { method: string, mode: string, target: string | null, filterJson: string | null, configJson: string | null, };
|
||||
18
kb_demo_app/frontend/ts/bindings/SplashOrder.ts
Normal file
18
kb_demo_app/frontend/ts/bindings/SplashOrder.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Command payload sent from Rust to the splash frontend.
|
||||
*/
|
||||
export type SplashOrder = {
|
||||
/**
|
||||
* Splash command name such as `fadein`, `fadeout`, `add_msg`, or `add_log`.
|
||||
*/
|
||||
order: string,
|
||||
/**
|
||||
* Optional message payload attached to the command.
|
||||
*/
|
||||
msg: string | null,
|
||||
/**
|
||||
* Optional status payload attached to the command.
|
||||
*/
|
||||
status: string | null, };
|
||||
422
kb_demo_app/frontend/ts/demo_http.ts
Normal file
422
kb_demo_app/frontend/ts/demo_http.ts
Normal file
@@ -0,0 +1,422 @@
|
||||
import * as bootstrap from "bootstrap";
|
||||
import "simplebar";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { debug, takeoverConsole } from "@fltsci/tauri-plugin-tracing";
|
||||
import { DemoHttpRequest } from './bindings/DemoHttpRequest.ts';
|
||||
import { DemoHttpExecutionPayload } from './bindings/DemoHttpExecutionPayload.ts';
|
||||
|
||||
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
|
||||
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
||||
|
||||
interface DemoHttpPoolClientSnapshot {
|
||||
endpointName: string;
|
||||
provider: string;
|
||||
endpointUrl: string;
|
||||
roles: string[];
|
||||
status: string;
|
||||
pausedRemainingMs: number | null;
|
||||
availableConcurrencySlots: number;
|
||||
}
|
||||
|
||||
let demoHttpLastResponseRawText = "";
|
||||
|
||||
function appendLogLine(textarea: HTMLTextAreaElement, line: string): void {
|
||||
const now = new Date();
|
||||
const timestamp = now.toLocaleTimeString("fr-CH", { hour12: false });
|
||||
|
||||
const lines = textarea.value === "" ? [] : textarea.value.split("\n");
|
||||
lines.push(`[${timestamp}] ${line}`);
|
||||
|
||||
const maxLines = 400;
|
||||
textarea.value = lines.slice(-maxLines).join("\n");
|
||||
textarea.scrollTop = textarea.scrollHeight;
|
||||
}
|
||||
|
||||
function methodNeedsFirstArg(method: string): boolean {
|
||||
return [
|
||||
"getBalance",
|
||||
"getAccountInfo",
|
||||
"getProgramAccounts",
|
||||
"getSignaturesForAddress",
|
||||
"getTransaction",
|
||||
"sendTransaction",
|
||||
].includes(method);
|
||||
}
|
||||
|
||||
function firstArgLabelForMethod(method: string): string {
|
||||
if (method === "getBalance") return "Address";
|
||||
if (method === "getAccountInfo") return "Address";
|
||||
if (method === "getProgramAccounts") return "Program id";
|
||||
if (method === "getSignaturesForAddress") return "Address";
|
||||
if (method === "getTransaction") return "Signature";
|
||||
if (method === "sendTransaction") return "Encoded transaction (base64)";
|
||||
return "First arg";
|
||||
}
|
||||
|
||||
function firstArgHelpForMethod(method: string): string {
|
||||
if (method === "getBalance") return "Adresse Solana dont on veut lire le solde.";
|
||||
if (method === "getAccountInfo") return "Adresse Solana du compte à lire.";
|
||||
if (method === "getProgramAccounts") return "Program id dont on veut lister les comptes.";
|
||||
if (method === "getSignaturesForAddress") return "Adresse Solana dont on veut lire les signatures récentes.";
|
||||
if (method === "getTransaction") return "Signature exacte d’une transaction.";
|
||||
if (method === "sendTransaction") {
|
||||
return "Transaction signée encodée en base64. Le preset fourni est volontairement invalide et sert seulement à tester la gestion d’erreur.";
|
||||
}
|
||||
return "Adresse, program id, signature ou transaction encodée selon la méthode.";
|
||||
}
|
||||
|
||||
function configHelpForMethod(method: string): string {
|
||||
if (method === "getProgramAccounts") {
|
||||
return "Preset volontairement filtré pour éviter une réponse trop large.";
|
||||
}
|
||||
|
||||
if (method === "sendTransaction") {
|
||||
return "Configuration d’envoi. Avec le preset de transaction invalide, la requête doit échouer côté RPC.";
|
||||
}
|
||||
|
||||
return "Configuration JSON optionnelle.";
|
||||
}
|
||||
|
||||
function defaultRoleForMethod(method: string): string {
|
||||
if (method === "sendTransaction") return "http_transactions";
|
||||
if (method === "getProgramAccounts") return "http_heavy";
|
||||
return "http_queries";
|
||||
}
|
||||
|
||||
function defaultFirstArgForMethod(method: string): string {
|
||||
if (method === "getBalance") return "11111111111111111111111111111111";
|
||||
if (method === "getAccountInfo") return "11111111111111111111111111111111";
|
||||
if (method === "getProgramAccounts") return "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
||||
if (method === "getSignaturesForAddress") return "11111111111111111111111111111111";
|
||||
if (method === "getTransaction") {
|
||||
return "5h6xBEauJ3PK6SWC7r7J2W8mE1D7aQj4J6Jg8n1SmWnVqSg9H6gq2K7xwJkL2GZ2RZ6n9wYk9cW1b2V3a4d5e6f7";
|
||||
}
|
||||
if (method === "sendTransaction") return "AQ==";
|
||||
return "";
|
||||
}
|
||||
|
||||
function defaultConfigForMethod(method: string): string {
|
||||
if (method === "getSlot" || method === "getBlockHeight" || method === "getLatestBlockhash") {
|
||||
return `{"commitment":"confirmed"}`;
|
||||
}
|
||||
|
||||
if (method === "getBalance") {
|
||||
return `{"commitment":"confirmed"}`;
|
||||
}
|
||||
|
||||
if (method === "getAccountInfo") {
|
||||
return `{"encoding":"base64","commitment":"confirmed"}`;
|
||||
}
|
||||
|
||||
if (method === "getProgramAccounts") {
|
||||
return `{
|
||||
"encoding":"base64",
|
||||
"commitment":"confirmed",
|
||||
"dataSlice":{"offset":0,"length":0},
|
||||
"filters":[
|
||||
{"dataSize":165},
|
||||
{"memcmp":{"offset":32,"bytes":"11111111111111111111111111111111"}}
|
||||
]
|
||||
}`;
|
||||
}
|
||||
|
||||
if (method === "getSignaturesForAddress") {
|
||||
return `{"limit":10,"commitment":"confirmed"}`;
|
||||
}
|
||||
|
||||
if (method === "getTransaction") {
|
||||
return `{"encoding":"json","commitment":"confirmed","maxSupportedTransactionVersion":0}`;
|
||||
}
|
||||
|
||||
if (method === "sendTransaction") {
|
||||
return `{"encoding":"base64","skipPreflight":true,"maxRetries":0}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function renderPoolSnapshots(
|
||||
snapshots: DemoHttpPoolClientSnapshot[],
|
||||
tableBody: HTMLTableSectionElement,
|
||||
): void {
|
||||
tableBody.innerHTML = "";
|
||||
|
||||
for (const snapshot of snapshots) {
|
||||
const row = document.createElement("tr");
|
||||
|
||||
const endpointCell = document.createElement("td");
|
||||
endpointCell.innerHTML = `<div class="fw-semibold">${snapshot.endpointName}</div><div class="small text-body-secondary">${snapshot.endpointUrl}</div>`;
|
||||
|
||||
const providerCell = document.createElement("td");
|
||||
providerCell.textContent = snapshot.provider;
|
||||
|
||||
const statusCell = document.createElement("td");
|
||||
statusCell.textContent = snapshot.status;
|
||||
|
||||
const pausedCell = document.createElement("td");
|
||||
pausedCell.textContent = snapshot.pausedRemainingMs === null ? "-" : String(snapshot.pausedRemainingMs);
|
||||
|
||||
const concurrencyCell = document.createElement("td");
|
||||
concurrencyCell.textContent = String(snapshot.availableConcurrencySlots);
|
||||
|
||||
const rolesCell = document.createElement("td");
|
||||
rolesCell.textContent = snapshot.roles.join(", ");
|
||||
|
||||
row.appendChild(endpointCell);
|
||||
row.appendChild(providerCell);
|
||||
row.appendChild(statusCell);
|
||||
row.appendChild(pausedCell);
|
||||
row.appendChild(concurrencyCell);
|
||||
row.appendChild(rolesCell);
|
||||
|
||||
tableBody.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
function renderResponse(
|
||||
responseTextarea: HTMLTextAreaElement,
|
||||
prettyToggle: HTMLInputElement,
|
||||
): void {
|
||||
if (demoHttpLastResponseRawText.trim() === "") {
|
||||
responseTextarea.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!prettyToggle.checked) {
|
||||
responseTextarea.value = demoHttpLastResponseRawText;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(demoHttpLastResponseRawText);
|
||||
responseTextarea.value = JSON.stringify(parsed, null, 2);
|
||||
} catch {
|
||||
responseTextarea.value = demoHttpLastResponseRawText;
|
||||
}
|
||||
}
|
||||
|
||||
async function copyTextToClipboard(text: string): Promise<void> {
|
||||
if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return;
|
||||
}
|
||||
|
||||
const tempTextarea = document.createElement("textarea");
|
||||
tempTextarea.value = text;
|
||||
tempTextarea.style.position = "fixed";
|
||||
tempTextarea.style.left = "-9999px";
|
||||
document.body.appendChild(tempTextarea);
|
||||
tempTextarea.focus();
|
||||
tempTextarea.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(tempTextarea);
|
||||
}
|
||||
|
||||
function updateMethodForm(
|
||||
roleSelect: HTMLSelectElement,
|
||||
methodSelect: HTMLSelectElement,
|
||||
firstArgGroup: HTMLDivElement,
|
||||
firstArgLabel: HTMLLabelElement,
|
||||
firstArgHelp: HTMLDivElement,
|
||||
firstArgInput: HTMLInputElement,
|
||||
configTextarea: HTMLTextAreaElement,
|
||||
configHelp: HTMLDivElement,
|
||||
): void {
|
||||
const method = methodSelect.value;
|
||||
|
||||
firstArgGroup.style.display = methodNeedsFirstArg(method) ? "" : "none";
|
||||
firstArgLabel.textContent = firstArgLabelForMethod(method);
|
||||
firstArgHelp.textContent = firstArgHelpForMethod(method);
|
||||
firstArgInput.value = defaultFirstArgForMethod(method);
|
||||
configTextarea.value = defaultConfigForMethod(method);
|
||||
configHelp.textContent = configHelpForMethod(method);
|
||||
roleSelect.value = defaultRoleForMethod(method);
|
||||
}
|
||||
|
||||
async function refreshPoolSnapshot(
|
||||
tableBody: HTMLTableSectionElement,
|
||||
logTextarea: HTMLTextAreaElement,
|
||||
shouldLog: boolean,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const snapshots = await invoke<DemoHttpPoolClientSnapshot[]>("demo_http_list_pool_clients");
|
||||
renderPoolSnapshots(snapshots, tableBody);
|
||||
|
||||
if (shouldLog) {
|
||||
appendLogLine(logTextarea, `[ui] refreshed http pool snapshot (${snapshots.length} endpoint(s))`);
|
||||
}
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] refresh pool error: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
void takeoverConsole();
|
||||
debug("demo_http window loaded");
|
||||
|
||||
const sidebarToggle = document.querySelector<HTMLButtonElement>('#sidebarToggle');
|
||||
if (sidebarToggle) {
|
||||
// restaurer l’état depuis localStorage
|
||||
if (localStorage.getItem('sidebar-toggle') === 'true') {
|
||||
document.body.classList.add('sidenav-toggled');
|
||||
}
|
||||
|
||||
sidebarToggle.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
document.body.classList.toggle('sidenav-toggled');
|
||||
localStorage.setItem('sidebar-toggle', document.body.classList.contains('sidenav-toggled') ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
|
||||
const toastElList = document.querySelectorAll('.toast');
|
||||
Array.from(toastElList).map(toastEl => new bootstrap.Toast(toastEl));
|
||||
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]');
|
||||
Array.from(popoverTriggerList).map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl));
|
||||
|
||||
const gobackto = location.pathname + location.search;
|
||||
|
||||
document.querySelectorAll<HTMLAnchorElement>('a[data-setlang]').forEach((a) => {
|
||||
const href = a.getAttribute("href");
|
||||
if (!href) return; // pas de href => on ignore
|
||||
|
||||
const url = new URL(href, location.origin);
|
||||
url.searchParams.set("gobackto", gobackto);
|
||||
|
||||
// conserve une URL relative (path + query)
|
||||
a.setAttribute("href", url.pathname + "?" + url.searchParams.toString());
|
||||
});
|
||||
|
||||
const roleSelect = document.querySelector<HTMLSelectElement>("#demoHttpRoleSelect");
|
||||
const methodSelect = document.querySelector<HTMLSelectElement>("#demoHttpMethodSelect");
|
||||
const firstArgGroup = document.querySelector<HTMLDivElement>("#demoHttpFirstArgGroup");
|
||||
const firstArgLabel = document.querySelector<HTMLLabelElement>("#demoHttpFirstArgLabel");
|
||||
const firstArgHelp = document.querySelector<HTMLDivElement>("#demoHttpFirstArgHelp");
|
||||
const firstArgInput = document.querySelector<HTMLInputElement>("#demoHttpFirstArgInput");
|
||||
const configTextarea = document.querySelector<HTMLTextAreaElement>("#demoHttpConfigTextarea");
|
||||
const configHelp = document.querySelector<HTMLDivElement>("#demoHttpConfigHelp");
|
||||
const executeButton = document.querySelector<HTMLButtonElement>("#demoHttpExecuteButton");
|
||||
const refreshPoolButton = document.querySelector<HTMLButtonElement>("#demoHttpRefreshPoolButton");
|
||||
const clearLogButton = document.querySelector<HTMLButtonElement>("#demoHttpClearLogButton");
|
||||
const copyResponseButton = document.querySelector<HTMLButtonElement>("#demoHttpCopyResponseButton");
|
||||
const prettyToggle = document.querySelector<HTMLInputElement>("#demoHttpPrettyToggle");
|
||||
const responseTextarea = document.querySelector<HTMLTextAreaElement>("#demoHttpResponseTextarea");
|
||||
const logTextarea = document.querySelector<HTMLTextAreaElement>("#demoHttpLogTextarea");
|
||||
const poolTableBody = document.querySelector<HTMLTableSectionElement>("#demoHttpPoolTableBody");
|
||||
const lastEndpointText = document.querySelector<HTMLSpanElement>("#demoHttpLastEndpointText");
|
||||
const lastProviderText = document.querySelector<HTMLSpanElement>("#demoHttpLastProviderText");
|
||||
const lastMethodClassText = document.querySelector<HTMLSpanElement>("#demoHttpLastMethodClassText");
|
||||
|
||||
if (
|
||||
!roleSelect ||
|
||||
!methodSelect ||
|
||||
!firstArgGroup ||
|
||||
!firstArgLabel ||
|
||||
!firstArgHelp ||
|
||||
!firstArgInput ||
|
||||
!configTextarea ||
|
||||
!configHelp ||
|
||||
!executeButton ||
|
||||
!refreshPoolButton ||
|
||||
!clearLogButton ||
|
||||
!copyResponseButton ||
|
||||
!prettyToggle ||
|
||||
!responseTextarea ||
|
||||
!logTextarea ||
|
||||
!poolTableBody ||
|
||||
!lastEndpointText ||
|
||||
!lastProviderText ||
|
||||
!lastMethodClassText
|
||||
) {
|
||||
debug("demo_http UI controls not found");
|
||||
return;
|
||||
}
|
||||
|
||||
updateMethodForm(
|
||||
roleSelect,
|
||||
methodSelect,
|
||||
firstArgGroup,
|
||||
firstArgLabel,
|
||||
firstArgHelp,
|
||||
firstArgInput,
|
||||
configTextarea,
|
||||
configHelp,
|
||||
);
|
||||
|
||||
methodSelect.addEventListener("change", () => {
|
||||
updateMethodForm(
|
||||
roleSelect,
|
||||
methodSelect,
|
||||
firstArgGroup,
|
||||
firstArgLabel,
|
||||
firstArgHelp,
|
||||
firstArgInput,
|
||||
configTextarea,
|
||||
configHelp,
|
||||
);
|
||||
});
|
||||
|
||||
prettyToggle.addEventListener("change", () => {
|
||||
renderResponse(responseTextarea, prettyToggle);
|
||||
});
|
||||
|
||||
refreshPoolButton.addEventListener("click", async () => {
|
||||
await refreshPoolSnapshot(poolTableBody, logTextarea, true);
|
||||
});
|
||||
|
||||
clearLogButton.addEventListener("click", () => {
|
||||
logTextarea.value = "";
|
||||
});
|
||||
|
||||
copyResponseButton.addEventListener("click", async () => {
|
||||
if (responseTextarea.value.trim() === "") {
|
||||
appendLogLine(logTextarea, "[ui] copy skipped: no response available");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await copyTextToClipboard(responseTextarea.value);
|
||||
appendLogLine(logTextarea, "[ui] response copied to clipboard");
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] copy response error: ${String(error)}`);
|
||||
}
|
||||
});
|
||||
|
||||
executeButton.addEventListener("click", async () => {
|
||||
const request: DemoHttpRequest = {
|
||||
role: roleSelect.value,
|
||||
method: methodSelect.value,
|
||||
firstArg: firstArgInput.value.trim() === "" ? null : firstArgInput.value.trim(),
|
||||
configJson: configTextarea.value.trim() === "" ? null : configTextarea.value.trim(),
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await invoke<DemoHttpExecutionPayload>("demo_http_execute_request", { request });
|
||||
|
||||
lastEndpointText.textContent = `${response.endpointName} (${response.endpointUrl})`;
|
||||
lastProviderText.textContent = response.provider;
|
||||
lastMethodClassText.textContent = response.methodClass;
|
||||
|
||||
demoHttpLastResponseRawText = response.responseJson;
|
||||
renderResponse(responseTextarea, prettyToggle);
|
||||
|
||||
appendLogLine(
|
||||
logTextarea,
|
||||
`[ui] ${response.method} via ${response.endpointName} / role=${response.role} / class=${response.methodClass}`,
|
||||
);
|
||||
|
||||
await refreshPoolSnapshot(poolTableBody, logTextarea, true);
|
||||
} catch (error) {
|
||||
demoHttpLastResponseRawText = String(error);
|
||||
renderResponse(responseTextarea, prettyToggle);
|
||||
|
||||
appendLogLine(logTextarea, `[ui] execute error: ${String(error)}`);
|
||||
await refreshPoolSnapshot(poolTableBody, logTextarea, true);
|
||||
}
|
||||
});
|
||||
|
||||
appendLogLine(logTextarea, "[ui] demo_http window loaded");
|
||||
await refreshPoolSnapshot(poolTableBody, logTextarea, true);
|
||||
});
|
||||
1087
kb_demo_app/frontend/ts/demo_pipeline.ts
Normal file
1087
kb_demo_app/frontend/ts/demo_pipeline.ts
Normal file
File diff suppressed because it is too large
Load Diff
712
kb_demo_app/frontend/ts/demo_pipeline2.ts
Normal file
712
kb_demo_app/frontend/ts/demo_pipeline2.ts
Normal file
@@ -0,0 +1,712 @@
|
||||
// file: kb_demo_app/frontend/ts/demo_pipeline2.ts
|
||||
|
||||
import * as bootstrap from "bootstrap";
|
||||
import "simplebar";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
import * as echarts from "echarts";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { debug, takeoverConsole } from "@fltsci/tauri-plugin-tracing";
|
||||
|
||||
import type { DemoPipeline2CatalogPayload } from "./bindings/DemoPipeline2CatalogPayload.ts";
|
||||
import type { DemoPipeline2BackfillTokenRequest } from "./bindings/DemoPipeline2BackfillTokenRequest.ts";
|
||||
import type { DemoPipeline2BackfillPoolRequest } from "./bindings/DemoPipeline2BackfillPoolRequest.ts";
|
||||
import type { DemoPipeline2BackfillPayload } from "./bindings/DemoPipeline2BackfillPayload.ts";
|
||||
import type { DemoPipeline2PairCandlesRequest } from "./bindings/DemoPipeline2PairCandlesRequest.ts";
|
||||
import type { DemoPipeline2PairCandlesPayload } from "./bindings/DemoPipeline2PairCandlesPayload.ts";
|
||||
import type { DemoPipeline2LocalDiagnosticsPayload } from "./bindings/DemoPipeline2LocalDiagnosticsPayload.ts";
|
||||
import type { DemoPipeline2LocalValidationPayload } from "./bindings/DemoPipeline2LocalValidationPayload.ts";
|
||||
|
||||
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
|
||||
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
||||
|
||||
interface PairCandle {
|
||||
id: number | null;
|
||||
pair_id: number;
|
||||
timeframe_seconds: number;
|
||||
bucket_start_unix: number;
|
||||
bucket_end_unix: number;
|
||||
open_price_quote_per_base: number;
|
||||
high_price_quote_per_base: number;
|
||||
low_price_quote_per_base: number;
|
||||
close_price_quote_per_base: number;
|
||||
trade_count: number;
|
||||
buy_count: number;
|
||||
sell_count: number;
|
||||
base_volume_raw: string | null;
|
||||
quote_volume_raw: string | null;
|
||||
first_trade_signature: string | null;
|
||||
last_trade_signature: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
|
||||
interface LocalPipelineReplayResult {
|
||||
selectedTransactionCount: number;
|
||||
replayedTransactionCount: number;
|
||||
decodeErrorCount: number;
|
||||
detectErrorCount: number;
|
||||
tradeAggregationErrorCount: number;
|
||||
pairCandleErrorCount: number;
|
||||
analyticSignalErrorCount: number;
|
||||
decodedEventCount: number;
|
||||
detectionCount: number;
|
||||
tradeEventCount: number;
|
||||
pairCandleUpsertCount: number;
|
||||
analyticSignalUpsertCount: number;
|
||||
tokenMetadataUpdatedCount: number;
|
||||
pairSymbolUpdatedCount: number;
|
||||
globalErrorCount: number;
|
||||
}
|
||||
function appendLogLine(textarea: HTMLTextAreaElement, line: string): void {
|
||||
const now = new Date();
|
||||
const timestamp = now.toLocaleTimeString("fr-CH", { hour12: false });
|
||||
|
||||
const lines = textarea.value === "" ? [] : textarea.value.split("\n");
|
||||
lines.push(`[${timestamp}] ${line}`);
|
||||
|
||||
textarea.value = lines.slice(-300).join("\n");
|
||||
textarea.scrollTop = textarea.scrollHeight;
|
||||
}
|
||||
|
||||
function setEmptyChart(
|
||||
chart: echarts.ECharts,
|
||||
chartMeta: HTMLElement,
|
||||
message: string,
|
||||
): void {
|
||||
chartMeta.textContent = message;
|
||||
|
||||
chart.setOption({
|
||||
animation: false,
|
||||
title: {
|
||||
text: message,
|
||||
left: "center",
|
||||
top: "middle",
|
||||
textStyle: {
|
||||
fontSize: 14,
|
||||
fontWeight: "normal",
|
||||
},
|
||||
},
|
||||
tooltip: {},
|
||||
xAxis: { show: false, type: "category", data: [] },
|
||||
yAxis: { show: false, type: "value" },
|
||||
series: [],
|
||||
}, true);
|
||||
}
|
||||
|
||||
function readPositiveIntegerInput(
|
||||
input: HTMLInputElement,
|
||||
logTextarea: HTMLTextAreaElement,
|
||||
label: string,
|
||||
): number | undefined {
|
||||
const text = input.value.trim();
|
||||
if (text === "") {
|
||||
appendLogLine(logTextarea, `[ui] ${label} is required`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parsed = Number.parseInt(text, 10);
|
||||
if (Number.isNaN(parsed) || parsed <= 0) {
|
||||
appendLogLine(logTextarea, `[ui] invalid ${label} '${text}'`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
|
||||
function readOptionalPositiveIntegerInput(
|
||||
input: HTMLInputElement,
|
||||
logTextarea: HTMLTextAreaElement,
|
||||
label: string,
|
||||
): number | null | undefined {
|
||||
const text = input.value.trim();
|
||||
if (text === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = Number.parseInt(text, 10);
|
||||
if (Number.isNaN(parsed) || parsed <= 0) {
|
||||
appendLogLine(logTextarea, `[ui] invalid ${label} '${text}'`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
function refreshPairSelect(
|
||||
catalog: DemoPipeline2CatalogPayload,
|
||||
select: HTMLSelectElement,
|
||||
): void {
|
||||
const previousValue = select.value;
|
||||
|
||||
select.innerHTML = "";
|
||||
const emptyOption = document.createElement("option");
|
||||
emptyOption.value = "";
|
||||
emptyOption.textContent = "Aucune";
|
||||
select.appendChild(emptyOption);
|
||||
|
||||
for (const pair of catalog.pairs) {
|
||||
const option = document.createElement("option");
|
||||
option.value = pair.pairId.toString();
|
||||
option.textContent = `#${pair.pairId.toString()} ${pair.symbol ?? ""} ${pair.poolAddress}`.trim();
|
||||
if (option.value === previousValue) {
|
||||
option.selected = true;
|
||||
}
|
||||
select.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
function renderCatalogTextareas(
|
||||
catalog: DemoPipeline2CatalogPayload,
|
||||
tokensTextarea: HTMLTextAreaElement,
|
||||
poolsTextarea: HTMLTextAreaElement,
|
||||
pairsTextarea: HTMLTextAreaElement,
|
||||
): void {
|
||||
tokensTextarea.value = JSON.stringify(catalog.tokens, null, 2);
|
||||
poolsTextarea.value = JSON.stringify(catalog.pools, null, 2);
|
||||
pairsTextarea.value = JSON.stringify(catalog.pairs, null, 2);
|
||||
}
|
||||
|
||||
function parseCandlesJson(raw: string): PairCandle[] {
|
||||
if (raw.trim() === "") {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(raw) as PairCandle[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function parseVolume(text: string | null, fallback: number): number {
|
||||
if (text === null || text.trim() === "") {
|
||||
return Number(fallback);
|
||||
}
|
||||
|
||||
const parsed = Number.parseFloat(text);
|
||||
if (Number.isNaN(parsed)) {
|
||||
return Number(fallback);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function renderCandlesChart(
|
||||
chart: echarts.ECharts,
|
||||
chartMeta: HTMLElement,
|
||||
pairId: number,
|
||||
timeframeSeconds: number,
|
||||
candles: PairCandle[],
|
||||
): void {
|
||||
if (candles.length === 0) {
|
||||
setEmptyChart(chart, chartMeta, "Aucune candle disponible.");
|
||||
return;
|
||||
}
|
||||
|
||||
const sorted = [...candles].sort(
|
||||
(left, right) => left.bucket_start_unix - right.bucket_start_unix,
|
||||
);
|
||||
|
||||
const categoryData = sorted.map((candle) =>
|
||||
new Date(Number(candle.bucket_start_unix) * 1000).toLocaleString("fr-CH", {
|
||||
hour12: false,
|
||||
year: "2-digit",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}),
|
||||
);
|
||||
|
||||
const ohlcData = sorted.map((candle) => [
|
||||
candle.open_price_quote_per_base,
|
||||
candle.close_price_quote_per_base,
|
||||
candle.low_price_quote_per_base,
|
||||
candle.high_price_quote_per_base,
|
||||
]);
|
||||
|
||||
const volumeData = sorted.map((candle) =>
|
||||
parseVolume(candle.quote_volume_raw, candle.trade_count),
|
||||
);
|
||||
|
||||
chartMeta.textContent =
|
||||
`Pair #${pairId.toString()} • timeframe ${timeframeSeconds.toString()}s • ${sorted.length} candles`;
|
||||
|
||||
chart.setOption({
|
||||
animation: false,
|
||||
legend: {
|
||||
data: ["OHLC", "Volume"],
|
||||
top: 0,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: {
|
||||
type: "cross",
|
||||
},
|
||||
},
|
||||
axisPointer: {
|
||||
link: [{ xAxisIndex: "all" }],
|
||||
},
|
||||
grid: [
|
||||
{ left: 60, right: 24, top: 40, height: "58%" },
|
||||
{ left: 60, right: 24, top: "74%", height: "16%" },
|
||||
],
|
||||
xAxis: [
|
||||
{
|
||||
type: "category",
|
||||
data: categoryData,
|
||||
boundaryGap: true,
|
||||
axisLine: { onZero: false },
|
||||
splitLine: { show: false },
|
||||
min: "dataMin",
|
||||
max: "dataMax",
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
gridIndex: 1,
|
||||
data: categoryData,
|
||||
boundaryGap: true,
|
||||
axisLine: { onZero: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { show: false },
|
||||
axisLabel: { show: false },
|
||||
min: "dataMin",
|
||||
max: "dataMax",
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
scale: true,
|
||||
splitArea: { show: false },
|
||||
},
|
||||
{
|
||||
gridIndex: 1,
|
||||
scale: true,
|
||||
splitNumber: 2,
|
||||
},
|
||||
],
|
||||
dataZoom: [
|
||||
{
|
||||
type: "inside",
|
||||
xAxisIndex: [0, 1],
|
||||
start: 0,
|
||||
end: 100,
|
||||
},
|
||||
{
|
||||
show: true,
|
||||
type: "slider",
|
||||
xAxisIndex: [0, 1],
|
||||
bottom: 6,
|
||||
start: 0,
|
||||
end: 100,
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: "OHLC",
|
||||
type: "candlestick",
|
||||
data: ohlcData,
|
||||
itemStyle: {
|
||||
color: "#16a34a",
|
||||
color0: "#dc2626",
|
||||
borderColor: "#15803d",
|
||||
borderColor0: "#b91c1c",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Volume",
|
||||
type: "bar",
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 1,
|
||||
data: volumeData,
|
||||
},
|
||||
],
|
||||
}, true);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
void takeoverConsole();
|
||||
debug("demo_pipeline2 window loaded");
|
||||
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
Array.from(tooltipTriggerList).map((tooltipTriggerEl) => new bootstrap.Tooltip(tooltipTriggerEl));
|
||||
|
||||
const refreshCatalogButton = document.querySelector<HTMLButtonElement>("#demoPipeline2RefreshCatalogButton");
|
||||
const tokensTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipeline2TokensTextarea");
|
||||
const poolsTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipeline2PoolsTextarea");
|
||||
const pairsTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipeline2PairsTextarea");
|
||||
|
||||
const httpRoleInput = document.querySelector<HTMLInputElement>("#demoPipeline2HttpRoleInput");
|
||||
const mintInput = document.querySelector<HTMLInputElement>("#demoPipeline2MintInput");
|
||||
const mintSignatureLimitInput = document.querySelector<HTMLInputElement>("#demoPipeline2MintSignatureLimitInput");
|
||||
const mintPoolLimitInput = document.querySelector<HTMLInputElement>("#demoPipeline2MintPoolLimitInput");
|
||||
const backfillMintButton = document.querySelector<HTMLButtonElement>("#demoPipeline2BackfillMintButton");
|
||||
|
||||
const poolInput = document.querySelector<HTMLInputElement>("#demoPipeline2PoolInput");
|
||||
const poolSignatureLimitInput = document.querySelector<HTMLInputElement>("#demoPipeline2PoolSignatureLimitInput");
|
||||
const backfillPoolButton = document.querySelector<HTMLButtonElement>("#demoPipeline2BackfillPoolButton");
|
||||
|
||||
const replayLimitInput = document.querySelector<HTMLInputElement>("#demoPipeline2ReplayLimitInput");
|
||||
const replayMetadataCheckbox = document.querySelector<HTMLInputElement>("#demoPipeline2ReplayMetadataCheckbox");
|
||||
const replayMetadataLimitInput = document.querySelector<HTMLInputElement>("#demoPipeline2ReplayMetadataLimitInput");
|
||||
const replayLocalPipelineButton = document.querySelector<HTMLButtonElement>("#demoPipeline2ReplayLocalPipelineButton");
|
||||
const diagnoseLocalPipelineButton = document.querySelector<HTMLButtonElement>("#demoPipeline2DiagnoseLocalPipelineButton");
|
||||
const validateLocalPipelineButton = document.querySelector<HTMLButtonElement>("#demoPipeline2ValidateLocalPipelineButton");
|
||||
|
||||
const pairSelect = document.querySelector<HTMLSelectElement>("#demoPipeline2PairSelect");
|
||||
const timeframeSelect = document.querySelector<HTMLSelectElement>("#demoPipeline2TimeframeSelect");
|
||||
const customTimeframeInput = document.querySelector<HTMLInputElement>("#demoPipeline2CustomTimeframeInput");
|
||||
const preferMaterializedInput = document.querySelector<HTMLInputElement>("#demoPipeline2PreferMaterializedInput");
|
||||
const loadCandlesButton = document.querySelector<HTMLButtonElement>("#demoPipeline2LoadCandlesButton");
|
||||
|
||||
const backfillSummaryTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipeline2BackfillSummaryTextarea");
|
||||
const chartElement = document.querySelector<HTMLDivElement>("#demoPipeline2Chart");
|
||||
const chartMeta = document.querySelector<HTMLDivElement>("#demoPipeline2ChartMeta");
|
||||
const localDiagnosticsTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipeline2LocalDiagnosticsTextarea");
|
||||
const localValidationTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipeline2LocalValidationTextarea");
|
||||
|
||||
const clearLogButton = document.querySelector<HTMLButtonElement>("#demoPipeline2ClearLogButton");
|
||||
const logTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipeline2LogTextarea");
|
||||
|
||||
if (
|
||||
!refreshCatalogButton ||
|
||||
!tokensTextarea ||
|
||||
!poolsTextarea ||
|
||||
!pairsTextarea ||
|
||||
!httpRoleInput ||
|
||||
!mintInput ||
|
||||
!mintSignatureLimitInput ||
|
||||
!mintPoolLimitInput ||
|
||||
!backfillMintButton ||
|
||||
!poolInput ||
|
||||
!poolSignatureLimitInput ||
|
||||
!backfillPoolButton ||
|
||||
!replayLimitInput ||
|
||||
!replayMetadataCheckbox ||
|
||||
!replayMetadataLimitInput ||
|
||||
!replayLocalPipelineButton ||
|
||||
!diagnoseLocalPipelineButton ||
|
||||
!validateLocalPipelineButton ||
|
||||
!pairSelect ||
|
||||
!timeframeSelect ||
|
||||
!customTimeframeInput ||
|
||||
!preferMaterializedInput ||
|
||||
!loadCandlesButton ||
|
||||
!backfillSummaryTextarea ||
|
||||
!localDiagnosticsTextarea ||
|
||||
!localValidationTextarea ||
|
||||
!chartElement ||
|
||||
!chartMeta ||
|
||||
!clearLogButton ||
|
||||
!logTextarea
|
||||
) {
|
||||
console.error("demo_pipeline2 DOM is incomplete");
|
||||
return;
|
||||
}
|
||||
|
||||
const safeTokensTextarea = tokensTextarea;
|
||||
const safePoolsTextarea = poolsTextarea;
|
||||
const safePairsTextarea = pairsTextarea;
|
||||
|
||||
|
||||
const safePairSelect = pairSelect;
|
||||
const safeChartElement = chartElement;
|
||||
const safeChartMeta = chartMeta;
|
||||
|
||||
const safeLogTextarea = logTextarea;
|
||||
const safeLocalDiagnosticsTextarea = localDiagnosticsTextarea;
|
||||
|
||||
const chart = echarts.init(safeChartElement);
|
||||
setEmptyChart(chart, safeChartMeta, "Aucune candle disponible.");
|
||||
window.addEventListener("resize", () => chart.resize());
|
||||
|
||||
clearLogButton.addEventListener("click", () => {
|
||||
logTextarea.value = "";
|
||||
});
|
||||
|
||||
let currentCatalog: DemoPipeline2CatalogPayload | null = null;
|
||||
|
||||
async function refreshCatalog(): Promise<void> {
|
||||
appendLogLine(safeLogTextarea, "[ui] refreshing local catalog");
|
||||
|
||||
try {
|
||||
const catalog = await invoke<DemoPipeline2CatalogPayload>("demo_pipeline2_get_catalog");
|
||||
currentCatalog = catalog;
|
||||
|
||||
renderCatalogTextareas(catalog, safeTokensTextarea, safePoolsTextarea, safePairsTextarea);
|
||||
refreshPairSelect(catalog, safePairSelect);
|
||||
|
||||
appendLogLine(
|
||||
safeLogTextarea,
|
||||
`[ui] catalog refreshed: ${catalog.tokens.length} tokens, ${catalog.pools.length} pools, ${catalog.pairs.length} pairs`,
|
||||
);
|
||||
} catch (error) {
|
||||
appendLogLine(safeLogTextarea, `[ui] catalog refresh error: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
refreshCatalogButton.addEventListener("click", () => {
|
||||
void refreshCatalog();
|
||||
});
|
||||
|
||||
backfillMintButton.addEventListener("click", async () => {
|
||||
const tokenMint = mintInput.value.trim();
|
||||
if (tokenMint === "") {
|
||||
appendLogLine(logTextarea, "[ui] token mint is required");
|
||||
return;
|
||||
}
|
||||
|
||||
const mintSignatureLimit = readPositiveIntegerInput(
|
||||
mintSignatureLimitInput,
|
||||
logTextarea,
|
||||
"mintSignatureLimit",
|
||||
);
|
||||
if (mintSignatureLimit === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const poolSignatureLimit = readPositiveIntegerInput(
|
||||
mintPoolLimitInput,
|
||||
logTextarea,
|
||||
"poolSignatureLimit",
|
||||
);
|
||||
if (poolSignatureLimit === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const httpRoleText = httpRoleInput.value.trim();
|
||||
const httpRole = httpRoleText === "" ? null : httpRoleText;
|
||||
|
||||
appendLogLine(
|
||||
logTextarea,
|
||||
`[ui] launching token backfill for '${tokenMint}' with role '${httpRole ?? "history_backfill"}'`,
|
||||
);
|
||||
|
||||
const request: DemoPipeline2BackfillTokenRequest = {
|
||||
tokenMint,
|
||||
httpRole,
|
||||
mintSignatureLimit,
|
||||
poolSignatureLimit,
|
||||
};
|
||||
|
||||
try {
|
||||
const payload = await invoke<DemoPipeline2BackfillPayload>(
|
||||
"demo_pipeline2_backfill_token_mint",
|
||||
{ request },
|
||||
);
|
||||
|
||||
backfillSummaryTextarea.value = payload.summaryJson;
|
||||
currentCatalog = payload.catalog;
|
||||
renderCatalogTextareas(payload.catalog, tokensTextarea, poolsTextarea, pairsTextarea);
|
||||
refreshPairSelect(payload.catalog, pairSelect);
|
||||
|
||||
appendLogLine(logTextarea, `[ui] token backfill completed for '${payload.objectKey}'`);
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] token backfill error: ${String(error)}`);
|
||||
}
|
||||
});
|
||||
|
||||
backfillPoolButton.addEventListener("click", async () => {
|
||||
const poolAddress = poolInput.value.trim();
|
||||
if (poolAddress === "") {
|
||||
appendLogLine(logTextarea, "[ui] pool address is required");
|
||||
return;
|
||||
}
|
||||
|
||||
const poolSignatureLimit = readPositiveIntegerInput(
|
||||
poolSignatureLimitInput,
|
||||
logTextarea,
|
||||
"poolSignatureLimit",
|
||||
);
|
||||
if (poolSignatureLimit === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const httpRoleText = httpRoleInput.value.trim();
|
||||
const httpRole = httpRoleText === "" ? null : httpRoleText;
|
||||
|
||||
appendLogLine(
|
||||
logTextarea,
|
||||
`[ui] launching pool backfill for '${poolAddress}' with role '${httpRole ?? "history_backfill"}'`,
|
||||
);
|
||||
|
||||
const request: DemoPipeline2BackfillPoolRequest = {
|
||||
poolAddress,
|
||||
httpRole,
|
||||
poolSignatureLimit,
|
||||
};
|
||||
|
||||
try {
|
||||
const payload = await invoke<DemoPipeline2BackfillPayload>(
|
||||
"demo_pipeline2_backfill_pool_address",
|
||||
{ request },
|
||||
);
|
||||
|
||||
backfillSummaryTextarea.value = payload.summaryJson;
|
||||
currentCatalog = payload.catalog;
|
||||
renderCatalogTextareas(payload.catalog, tokensTextarea, poolsTextarea, pairsTextarea);
|
||||
refreshPairSelect(payload.catalog, pairSelect);
|
||||
|
||||
appendLogLine(logTextarea, `[ui] pool backfill completed for '${payload.objectKey}'`);
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] pool backfill error: ${String(error)}`);
|
||||
}
|
||||
});
|
||||
|
||||
replayLocalPipelineButton.addEventListener("click", async () => {
|
||||
const replayLimit = readOptionalPositiveIntegerInput(
|
||||
replayLimitInput,
|
||||
logTextarea,
|
||||
"replayLimit",
|
||||
);
|
||||
if (replayLimit === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenMetadataLimit = readOptionalPositiveIntegerInput(
|
||||
replayMetadataLimitInput,
|
||||
logTextarea,
|
||||
"tokenMetadataLimit",
|
||||
);
|
||||
if (tokenMetadataLimit === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
appendLogLine(
|
||||
logTextarea,
|
||||
`[ui] launching local pipeline replay limit='${replayLimit ?? "none"}' metadata='${replayMetadataCheckbox.checked ? "yes" : "no"}'`,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await invoke<LocalPipelineReplayResult>(
|
||||
"demo_pipeline2_replay_local_pipeline",
|
||||
{
|
||||
limit: replayLimit,
|
||||
refreshMissingTokenMetadata: replayMetadataCheckbox.checked,
|
||||
tokenMetadataLimit,
|
||||
},
|
||||
);
|
||||
|
||||
backfillSummaryTextarea.value = JSON.stringify(result, null, 2);
|
||||
|
||||
appendLogLine(
|
||||
logTextarea,
|
||||
`[ui] local pipeline replay completed: ${result.replayedTransactionCount.toString()} replayed, ${result.tradeEventCount.toString()} trades, ${result.pairCandleUpsertCount.toString()} candle upserts`,
|
||||
);
|
||||
|
||||
await refreshCatalog();
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] local pipeline replay error: ${String(error)}`);
|
||||
}
|
||||
});
|
||||
|
||||
diagnoseLocalPipelineButton.addEventListener("click", async () => {
|
||||
appendLogLine(logTextarea, "[ui] diagnosing local pipeline");
|
||||
|
||||
diagnoseLocalPipelineButton.disabled = true;
|
||||
|
||||
try {
|
||||
const payload = await invoke<DemoPipeline2LocalDiagnosticsPayload>(
|
||||
"demo_pipeline2_diagnose_local_pipeline",
|
||||
);
|
||||
|
||||
safeLocalDiagnosticsTextarea.value = payload.summaryJson;
|
||||
|
||||
appendLogLine(
|
||||
logTextarea,
|
||||
`[ui] local pipeline diagnostics completed: ${payload.summary.decodedEventCount.toString()} decoded, ${payload.summary.tradeEventCount.toString()} trades, ${payload.summary.pairCandleCount.toString()} candles, actionableMissing='${payload.summary.actionableMissingTradeEventCount.toString()}', nonActionablePairs='${payload.summary.nonActionablePairCount.toString()}', blocking='${payload.summary.blockingIssueCount.toString()}'`,
|
||||
);
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] local pipeline diagnostics error: ${String(error)}`);
|
||||
} finally {
|
||||
diagnoseLocalPipelineButton.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
validateLocalPipelineButton.addEventListener("click", async () => {
|
||||
appendLogLine(logTextarea, "[ui] validating local pipeline with 0.7.27 profile");
|
||||
|
||||
try {
|
||||
const payload = await invoke<DemoPipeline2LocalValidationPayload>(
|
||||
"demo_pipeline2_validate_local_pipeline",
|
||||
);
|
||||
|
||||
localValidationTextarea.value = payload.validationJson;
|
||||
localDiagnosticsTextarea.value = payload.summaryJson;
|
||||
|
||||
appendLogLine(
|
||||
logTextarea,
|
||||
`[ui] local pipeline validation completed: profile='${payload.run.validationProfileCode}' passed='${payload.run.validationPassed ? "yes" : "no"}' blocking='${payload.run.blockingIssueCount.toString()}' warnings='${payload.run.warningCount.toString()}'`,
|
||||
);
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] local pipeline validation error: ${String(error)}`);
|
||||
}
|
||||
});
|
||||
|
||||
loadCandlesButton.addEventListener("click", async () => {
|
||||
const pairIdText = pairSelect.value.trim();
|
||||
if (pairIdText === "") {
|
||||
appendLogLine(logTextarea, "[ui] pair selection is required");
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedPairId = Number.parseInt(pairIdText, 10);
|
||||
if (Number.isNaN(parsedPairId) || parsedPairId <= 0) {
|
||||
appendLogLine(logTextarea, `[ui] invalid pair id '${pairIdText}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
let timeframeSeconds = Number.parseInt(timeframeSelect.value.trim(), 10);
|
||||
const customTimeframeText = customTimeframeInput.value.trim();
|
||||
if (customTimeframeText !== "") {
|
||||
const parsedCustom = Number.parseInt(customTimeframeText, 10);
|
||||
if (Number.isNaN(parsedCustom) || parsedCustom <= 0) {
|
||||
appendLogLine(logTextarea, `[ui] invalid custom timeframe '${customTimeframeText}'`);
|
||||
return;
|
||||
}
|
||||
timeframeSeconds = parsedCustom;
|
||||
}
|
||||
|
||||
appendLogLine(
|
||||
logTextarea,
|
||||
`[ui] loading candles for pair '${parsedPairId}' timeframe '${timeframeSeconds}s'`,
|
||||
);
|
||||
|
||||
const request: DemoPipeline2PairCandlesRequest = {
|
||||
pairId: parsedPairId,
|
||||
timeframeSeconds,
|
||||
preferMaterialized: preferMaterializedInput.checked,
|
||||
};
|
||||
|
||||
try {
|
||||
const payload = await invoke<DemoPipeline2PairCandlesPayload>(
|
||||
"demo_pipeline2_get_pair_candles",
|
||||
{ request },
|
||||
);
|
||||
|
||||
const candles = parseCandlesJson(payload.candlesJson);
|
||||
renderCandlesChart(
|
||||
chart,
|
||||
chartMeta,
|
||||
payload.pairId,
|
||||
payload.timeframeSeconds,
|
||||
candles,
|
||||
);
|
||||
|
||||
appendLogLine(
|
||||
logTextarea,
|
||||
`[ui] loaded ${candles.length} candles for pair '${payload.pairId.toString()}'`,
|
||||
);
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] load candles error: ${String(error)}`);
|
||||
setEmptyChart(chart, chartMeta, "Erreur lors du chargement des candles.");
|
||||
}
|
||||
});
|
||||
|
||||
await refreshCatalog();
|
||||
|
||||
if (currentCatalog !== null && currentCatalog.pairs.length > 0) {
|
||||
pairSelect.value = currentCatalog.pairs[0].pairId.toString();
|
||||
}
|
||||
});
|
||||
493
kb_demo_app/frontend/ts/demo_ws.ts
Normal file
493
kb_demo_app/frontend/ts/demo_ws.ts
Normal file
@@ -0,0 +1,493 @@
|
||||
// file: kb_demo_app/frontend/ts/demo_ws.ts
|
||||
|
||||
import * as bootstrap from "bootstrap";
|
||||
import "simplebar";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { debug, takeoverConsole } from "@fltsci/tauri-plugin-tracing";
|
||||
import { DemoWsEndpointSummary } from './bindings/DemoWsEndpointSummary.ts';
|
||||
import { DemoWsStatusPayload } from './bindings/DemoWsStatusPayload.ts';
|
||||
import { DemoWsSubscribeRequest } from './bindings/DemoWsSubscribeRequest.ts';
|
||||
|
||||
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
|
||||
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
||||
|
||||
|
||||
function shortenLine(line: string, maxChars = 3000): string {
|
||||
if (line.length <= maxChars) {
|
||||
return line;
|
||||
}
|
||||
|
||||
return `${line.slice(0, maxChars)} …[truncated ${line.length - maxChars} chars]`;
|
||||
}
|
||||
|
||||
function appendLogLine(textarea: HTMLTextAreaElement, line: string): void {
|
||||
const now = new Date();
|
||||
const timestamp = now.toLocaleTimeString("fr-CH", { hour12: false });
|
||||
const safeLine = shortenLine(line, 3000);
|
||||
|
||||
const existingLines = textarea.value === "" ? [] : textarea.value.split("\n");
|
||||
existingLines.push(`[${timestamp}] ${safeLine}`);
|
||||
|
||||
const maxLines = 800;
|
||||
const trimmedLines = existingLines.length > maxLines
|
||||
? existingLines.slice(existingLines.length - maxLines)
|
||||
: existingLines;
|
||||
|
||||
textarea.value = trimmedLines.join("\n");
|
||||
textarea.scrollTop = textarea.scrollHeight;
|
||||
}
|
||||
|
||||
function setStatusBadge(badge: HTMLSpanElement, state: string): void {
|
||||
badge.textContent = state;
|
||||
|
||||
if (state === "Connected") {
|
||||
badge.className = "badge text-bg-success";
|
||||
return;
|
||||
}
|
||||
|
||||
if (state === "Connecting" || state === "Disconnecting") {
|
||||
badge.className = "badge text-bg-warning";
|
||||
return;
|
||||
}
|
||||
|
||||
badge.className = "badge text-bg-secondary";
|
||||
}
|
||||
|
||||
function methodSupportsTypedMode(method: string): boolean {
|
||||
return ["account", "block", "logs", "program", "signature"].includes(method);
|
||||
}
|
||||
|
||||
function methodNeedsTarget(method: string): boolean {
|
||||
return ["account", "program", "signature"].includes(method);
|
||||
}
|
||||
|
||||
function methodNeedsFilter(method: string): boolean {
|
||||
return ["block", "logs"].includes(method);
|
||||
}
|
||||
|
||||
function methodNeedsConfig(method: string): boolean {
|
||||
return ["account", "block", "logs", "program", "signature"].includes(method);
|
||||
}
|
||||
|
||||
function targetLabelForMethod(method: string): string {
|
||||
if (method === "account") return "Account pubkey";
|
||||
if (method === "program") return "Program id";
|
||||
if (method === "signature") return "Signature";
|
||||
return "Target";
|
||||
}
|
||||
|
||||
function methodPreset(method: string, mode: string): {
|
||||
target: string;
|
||||
filterJson: string;
|
||||
configJson: string;
|
||||
} {
|
||||
if (method === "account") {
|
||||
return {
|
||||
target: "11111111111111111111111111111111",
|
||||
filterJson: "",
|
||||
configJson: mode === "typed"
|
||||
? `{"encoding":"base64","commitment":"confirmed"}`
|
||||
: `{"encoding":"base64","commitment":"confirmed"}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (method === "block") {
|
||||
return {
|
||||
target: "",
|
||||
filterJson: `{"mentionsAccountOrProgram":"11111111111111111111111111111111"}`,
|
||||
configJson: mode === "typed"
|
||||
? `{"commitment":"confirmed","encoding":"base64","transactionDetails":"signatures","showRewards":false,"maxSupportedTransactionVersion":0}`
|
||||
: `{"commitment":"confirmed","encoding":"base64","transactionDetails":"signatures","showRewards":false,"maxSupportedTransactionVersion":0}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (method === "logs") {
|
||||
return {
|
||||
target: "",
|
||||
filterJson: `{"mentions":["11111111111111111111111111111111"]}`,
|
||||
configJson: `{"commitment":"confirmed"}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (method === "program") {
|
||||
return {
|
||||
target: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
|
||||
filterJson: "",
|
||||
configJson: mode === "typed"
|
||||
? `{"encoding":"base64","commitment":"confirmed","filters":[]}`
|
||||
: `{"encoding":"base64","commitment":"confirmed","filters":[]}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (method === "signature") {
|
||||
return {
|
||||
target: "2EBVM6cB8vAAD93Ktr6Vd8p67XPbQzCJX47MpReuiCXJAtcjaxpvWpcg9Ege1Nr5Tk3a2GFrByT7WPBjdsTycY9b",
|
||||
filterJson: "",
|
||||
configJson: `{"commitment":"confirmed","enableReceivedNotification":true}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
target: "",
|
||||
filterJson: "",
|
||||
configJson: "",
|
||||
};
|
||||
}
|
||||
|
||||
function updateFormVisibility(
|
||||
methodSelect: HTMLSelectElement,
|
||||
modeSelect: HTMLSelectElement,
|
||||
targetGroup: HTMLDivElement,
|
||||
targetLabel: HTMLLabelElement,
|
||||
targetInput: HTMLInputElement,
|
||||
filterGroup: HTMLDivElement,
|
||||
filterTextarea: HTMLTextAreaElement,
|
||||
configGroup: HTMLDivElement,
|
||||
configTextarea: HTMLTextAreaElement,
|
||||
): void {
|
||||
const method = methodSelect.value;
|
||||
const supportsTypedMode = methodSupportsTypedMode(method);
|
||||
|
||||
modeSelect.disabled = !supportsTypedMode;
|
||||
if (!supportsTypedMode) {
|
||||
modeSelect.value = "typed";
|
||||
}
|
||||
|
||||
targetGroup.style.display = methodNeedsTarget(method) ? "" : "none";
|
||||
filterGroup.style.display = methodNeedsFilter(method) ? "" : "none";
|
||||
configGroup.style.display = methodNeedsConfig(method) ? "" : "none";
|
||||
|
||||
targetLabel.textContent = targetLabelForMethod(method);
|
||||
|
||||
const preset = methodPreset(method, modeSelect.value);
|
||||
targetInput.value = preset.target;
|
||||
filterTextarea.value = preset.filterJson;
|
||||
configTextarea.value = preset.configJson;
|
||||
}
|
||||
|
||||
function applyStatusToUi(
|
||||
status: DemoWsStatusPayload,
|
||||
statusBadge: HTMLSpanElement,
|
||||
stateText: HTMLSpanElement,
|
||||
endpointText: HTMLSpanElement,
|
||||
subscriptionText: HTMLSpanElement,
|
||||
eventCountText: HTMLSpanElement,
|
||||
notificationCountText: HTMLSpanElement,
|
||||
uiLogCountText: HTMLSpanElement,
|
||||
suppressedLogCountText: HTMLSpanElement,
|
||||
lastEventKindText: HTMLSpanElement,
|
||||
connectButton: HTMLButtonElement,
|
||||
disconnectButton: HTMLButtonElement,
|
||||
subscribeButton: HTMLButtonElement,
|
||||
unsubscribeButton: HTMLButtonElement,
|
||||
): void {
|
||||
setStatusBadge(statusBadge, status.connectionState);
|
||||
stateText.textContent = status.connectionState;
|
||||
endpointText.textContent = status.endpointName && status.endpointUrl
|
||||
? `${status.endpointName} (${status.endpointUrl})`
|
||||
: "-";
|
||||
|
||||
if (status.currentSubscriptionId !== null) {
|
||||
subscriptionText.textContent =
|
||||
`${status.currentSubscribeMethod ?? "?"} / #${status.currentSubscriptionId} / ${status.currentNotificationMethod ?? "?"}`;
|
||||
} else {
|
||||
subscriptionText.textContent = "-";
|
||||
}
|
||||
|
||||
eventCountText.textContent = String(status.eventCountTotal);
|
||||
notificationCountText.textContent = String(status.notificationCountTotal);
|
||||
uiLogCountText.textContent = String(status.uiLogCount);
|
||||
suppressedLogCountText.textContent = String(status.suppressedLogCount);
|
||||
lastEventKindText.textContent = status.lastEventKind ?? "-";
|
||||
|
||||
const isConnected = status.connectionState === "Connected";
|
||||
const isBusy = status.connectionState === "Connecting" || status.connectionState === "Disconnecting";
|
||||
|
||||
connectButton.disabled = isConnected || isBusy;
|
||||
disconnectButton.disabled = !isConnected && status.connectionState !== "Disconnecting";
|
||||
subscribeButton.disabled = !isConnected || isBusy;
|
||||
unsubscribeButton.disabled = !isConnected || isBusy || status.currentSubscriptionId === null;
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
void takeoverConsole();
|
||||
|
||||
debug("demo_ws window loaded");
|
||||
|
||||
const sidebarToggle = document.querySelector<HTMLButtonElement>('#sidebarToggle');
|
||||
if (sidebarToggle) {
|
||||
// restaurer l’état depuis localStorage
|
||||
if (localStorage.getItem('sidebar-toggle') === 'true') {
|
||||
document.body.classList.add('sidenav-toggled');
|
||||
}
|
||||
|
||||
sidebarToggle.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
document.body.classList.toggle('sidenav-toggled');
|
||||
localStorage.setItem('sidebar-toggle', document.body.classList.contains('sidenav-toggled') ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
|
||||
const toastElList = document.querySelectorAll('.toast');
|
||||
Array.from(toastElList).map(toastEl => new bootstrap.Toast(toastEl));
|
||||
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]');
|
||||
Array.from(popoverTriggerList).map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl));
|
||||
|
||||
const gobackto = location.pathname + location.search;
|
||||
|
||||
document.querySelectorAll<HTMLAnchorElement>('a[data-setlang]').forEach((a) => {
|
||||
const href = a.getAttribute("href");
|
||||
if (!href) return; // pas de href => on ignore
|
||||
|
||||
const url = new URL(href, location.origin);
|
||||
url.searchParams.set("gobackto", gobackto);
|
||||
|
||||
// conserve une URL relative (path + query)
|
||||
a.setAttribute("href", url.pathname + "?" + url.searchParams.toString());
|
||||
});
|
||||
|
||||
const endpointSelect = document.querySelector<HTMLSelectElement>("#demoWsEndpointSelect");
|
||||
const methodSelect = document.querySelector<HTMLSelectElement>("#demoWsMethodSelect");
|
||||
const modeSelect = document.querySelector<HTMLSelectElement>("#demoWsModeSelect");
|
||||
const targetGroup = document.querySelector<HTMLDivElement>("#demoWsTargetGroup");
|
||||
const targetLabel = document.querySelector<HTMLLabelElement>("#demoWsTargetLabel");
|
||||
const targetInput = document.querySelector<HTMLInputElement>("#demoWsTargetInput");
|
||||
const filterGroup = document.querySelector<HTMLDivElement>("#demoWsFilterGroup");
|
||||
const filterTextarea = document.querySelector<HTMLTextAreaElement>("#demoWsFilterTextarea");
|
||||
const configGroup = document.querySelector<HTMLDivElement>("#demoWsConfigGroup");
|
||||
const configTextarea = document.querySelector<HTMLTextAreaElement>("#demoWsConfigTextarea");
|
||||
const statusBadge = document.querySelector<HTMLSpanElement>("#demoWsStatusBadge");
|
||||
const stateText = document.querySelector<HTMLSpanElement>("#demoWsStateText");
|
||||
const endpointText = document.querySelector<HTMLSpanElement>("#demoWsEndpointText");
|
||||
const subscriptionText = document.querySelector<HTMLSpanElement>("#demoWsSubscriptionText");
|
||||
const requestText = document.querySelector<HTMLSpanElement>("#demoWsRequestText");
|
||||
const eventCountText = document.querySelector<HTMLSpanElement>("#demoWsEventCountText");
|
||||
const notificationCountText = document.querySelector<HTMLSpanElement>("#demoWsNotificationCountText");
|
||||
const uiLogCountText = document.querySelector<HTMLSpanElement>("#demoWsUiLogCountText");
|
||||
const suppressedLogCountText = document.querySelector<HTMLSpanElement>("#demoWsSuppressedLogCountText");
|
||||
const lastEventKindText = document.querySelector<HTMLSpanElement>("#demoWsLastEventKindText");
|
||||
const connectButton = document.querySelector<HTMLButtonElement>("#demoWsConnectButton");
|
||||
const disconnectButton = document.querySelector<HTMLButtonElement>("#demoWsDisconnectButton");
|
||||
const subscribeButton = document.querySelector<HTMLButtonElement>("#demoWsSubscribeButton");
|
||||
const unsubscribeButton = document.querySelector<HTMLButtonElement>("#demoWsUnsubscribeButton");
|
||||
const clearLogButton = document.querySelector<HTMLButtonElement>("#demoWsClearLogButton");
|
||||
const logTextarea = document.querySelector<HTMLTextAreaElement>("#demoWsLogTextarea");
|
||||
|
||||
if (
|
||||
!eventCountText || !notificationCountText || !uiLogCountText || !suppressedLogCountText || !lastEventKindText ||
|
||||
!endpointSelect || !methodSelect || !modeSelect || !targetGroup || !targetLabel || !targetInput ||
|
||||
!filterGroup || !filterTextarea || !configGroup || !configTextarea || !statusBadge ||
|
||||
!stateText || !endpointText || !subscriptionText || !requestText || !connectButton ||
|
||||
!disconnectButton || !subscribeButton || !unsubscribeButton || !clearLogButton || !logTextarea
|
||||
) {
|
||||
debug("demo_ws UI controls not found");
|
||||
return;
|
||||
}
|
||||
|
||||
let unlistenLogEvent: UnlistenFn | null = null;
|
||||
let unlistenStatusEvent: UnlistenFn | null = null;
|
||||
|
||||
try {
|
||||
unlistenLogEvent = await listen<string>("demo-ws-log", (event) => {
|
||||
appendLogLine(logTextarea, event.payload);
|
||||
});
|
||||
|
||||
unlistenStatusEvent = await listen<DemoWsStatusPayload>("demo-ws-status", (event) => {
|
||||
applyStatusToUi(
|
||||
event.payload,
|
||||
statusBadge,
|
||||
stateText,
|
||||
endpointText,
|
||||
subscriptionText,
|
||||
eventCountText,
|
||||
notificationCountText,
|
||||
uiLogCountText,
|
||||
suppressedLogCountText,
|
||||
lastEventKindText,
|
||||
connectButton,
|
||||
disconnectButton,
|
||||
subscribeButton,
|
||||
unsubscribeButton,
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] event listen error: ${String(error)}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const endpoints = await invoke<DemoWsEndpointSummary[]>("demo_ws_list_endpoints");
|
||||
|
||||
endpointSelect.innerHTML = "";
|
||||
for (const endpoint of endpoints) {
|
||||
const option = document.createElement("option");
|
||||
option.value = endpoint.name;
|
||||
option.textContent = `${endpoint.name} — ${endpoint.provider} — ${endpoint.resolvedUrl}`;
|
||||
option.disabled = !endpoint.enabled;
|
||||
endpointSelect.appendChild(option);
|
||||
}
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] endpoint list error: ${String(error)}`);
|
||||
}
|
||||
|
||||
updateFormVisibility(
|
||||
methodSelect,
|
||||
modeSelect,
|
||||
targetGroup,
|
||||
targetLabel,
|
||||
targetInput,
|
||||
filterGroup,
|
||||
filterTextarea,
|
||||
configGroup,
|
||||
configTextarea,
|
||||
);
|
||||
|
||||
methodSelect.addEventListener("change", () => {
|
||||
updateFormVisibility(
|
||||
methodSelect,
|
||||
modeSelect,
|
||||
targetGroup,
|
||||
targetLabel,
|
||||
targetInput,
|
||||
filterGroup,
|
||||
filterTextarea,
|
||||
configGroup,
|
||||
configTextarea,
|
||||
);
|
||||
});
|
||||
|
||||
modeSelect.addEventListener("change", () => {
|
||||
updateFormVisibility(
|
||||
methodSelect,
|
||||
modeSelect,
|
||||
targetGroup,
|
||||
targetLabel,
|
||||
targetInput,
|
||||
filterGroup,
|
||||
filterTextarea,
|
||||
configGroup,
|
||||
configTextarea,
|
||||
);
|
||||
});
|
||||
|
||||
try {
|
||||
const status = await invoke<DemoWsStatusPayload>("demo_ws_get_status");
|
||||
applyStatusToUi(
|
||||
status,
|
||||
statusBadge,
|
||||
stateText,
|
||||
endpointText,
|
||||
subscriptionText,
|
||||
eventCountText,
|
||||
notificationCountText,
|
||||
uiLogCountText,
|
||||
suppressedLogCountText,
|
||||
lastEventKindText,
|
||||
connectButton,
|
||||
disconnectButton,
|
||||
subscribeButton,
|
||||
unsubscribeButton,
|
||||
);
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] initial status error: ${String(error)}`);
|
||||
}
|
||||
|
||||
appendLogLine(logTextarea, "[ui] demo_ws window loaded");
|
||||
|
||||
connectButton.addEventListener("click", async () => {
|
||||
try {
|
||||
const status = await invoke<DemoWsStatusPayload>("demo_ws_connect", {
|
||||
endpointName: endpointSelect.value,
|
||||
});
|
||||
|
||||
applyStatusToUi(
|
||||
status,
|
||||
statusBadge,
|
||||
stateText,
|
||||
endpointText,
|
||||
subscriptionText,
|
||||
eventCountText,
|
||||
notificationCountText,
|
||||
uiLogCountText,
|
||||
suppressedLogCountText,
|
||||
lastEventKindText,
|
||||
connectButton,
|
||||
disconnectButton,
|
||||
subscribeButton,
|
||||
unsubscribeButton,
|
||||
);
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] connect error: ${String(error)}`);
|
||||
}
|
||||
});
|
||||
|
||||
disconnectButton.addEventListener("click", async () => {
|
||||
try {
|
||||
const status = await invoke<DemoWsStatusPayload>("demo_ws_disconnect");
|
||||
|
||||
applyStatusToUi(
|
||||
status,
|
||||
statusBadge,
|
||||
stateText,
|
||||
endpointText,
|
||||
subscriptionText,
|
||||
eventCountText,
|
||||
notificationCountText,
|
||||
uiLogCountText,
|
||||
suppressedLogCountText,
|
||||
lastEventKindText,
|
||||
connectButton,
|
||||
disconnectButton,
|
||||
subscribeButton,
|
||||
unsubscribeButton,
|
||||
);
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] disconnect error: ${String(error)}`);
|
||||
}
|
||||
});
|
||||
|
||||
subscribeButton.addEventListener("click", async () => {
|
||||
const request: DemoWsSubscribeRequest = {
|
||||
method: methodSelect.value,
|
||||
mode: modeSelect.value,
|
||||
target: targetInput.value.trim() === "" ? null : targetInput.value.trim(),
|
||||
filterJson: filterTextarea.value.trim() === "" ? null : filterTextarea.value.trim(),
|
||||
configJson: configTextarea.value.trim() === "" ? null : configTextarea.value.trim(),
|
||||
};
|
||||
|
||||
try {
|
||||
const requestId = await invoke<number>("demo_ws_subscribe", { request });
|
||||
requestText.textContent = `request_id=${requestId}`;
|
||||
appendLogLine(logTextarea, `[ui] subscribe request sent: request_id=${requestId}`);
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] subscribe error: ${String(error)}`);
|
||||
}
|
||||
});
|
||||
|
||||
unsubscribeButton.addEventListener("click", async () => {
|
||||
try {
|
||||
const requestId = await invoke<number>("demo_ws_unsubscribe_current");
|
||||
requestText.textContent = `unsubscribe_request_id=${requestId}`;
|
||||
appendLogLine(logTextarea, `[ui] unsubscribe request sent: request_id=${requestId}`);
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] unsubscribe error: ${String(error)}`);
|
||||
}
|
||||
});
|
||||
|
||||
clearLogButton.addEventListener("click", () => {
|
||||
logTextarea.value = "";
|
||||
});
|
||||
|
||||
window.addEventListener("beforeunload", () => {
|
||||
if (unlistenLogEvent) {
|
||||
unlistenLogEvent();
|
||||
}
|
||||
|
||||
if (unlistenStatusEvent) {
|
||||
unlistenStatusEvent();
|
||||
}
|
||||
});
|
||||
});
|
||||
224
kb_demo_app/frontend/ts/demo_ws_manager.ts
Normal file
224
kb_demo_app/frontend/ts/demo_ws_manager.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
// file: kb_demo_app/frontend/ts/demo_ws_manager.ts
|
||||
|
||||
import * as bootstrap from "bootstrap";
|
||||
import "simplebar";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { debug, takeoverConsole } from "@fltsci/tauri-plugin-tracing";
|
||||
import { DemoWsManagerSnapshotPayload } from './bindings/DemoWsManagerSnapshotPayload.ts';
|
||||
|
||||
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
|
||||
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
||||
|
||||
|
||||
const endpointCountText = document.querySelector<HTMLSpanElement>("#demoWsManagerEndpointCountText");
|
||||
const startedCountText = document.querySelector<HTMLSpanElement>("#demoWsManagerStartedCountText");
|
||||
const roleSelect = document.querySelector<HTMLSelectElement>("#demoWsManagerRoleSelect");
|
||||
const tableBody = document.querySelector<HTMLTableSectionElement>("#demoWsManagerTableBody");
|
||||
const logTextarea = document.querySelector<HTMLTextAreaElement>("#demoWsManagerLogTextarea");
|
||||
const startAllButton = document.querySelector<HTMLButtonElement>("#demoWsManagerStartAllButton");
|
||||
const stopAllButton = document.querySelector<HTMLButtonElement>("#demoWsManagerStopAllButton");
|
||||
const refreshButton = document.querySelector<HTMLButtonElement>("#demoWsManagerRefreshButton");
|
||||
const startRoleButton = document.querySelector<HTMLButtonElement>("#demoWsManagerStartRoleButton");
|
||||
const stopRoleButton = document.querySelector<HTMLButtonElement>("#demoWsManagerStopRoleButton");
|
||||
const clearLogButton = document.querySelector<HTMLButtonElement>("#demoWsManagerClearLogButton");
|
||||
|
||||
function appendLogLine(line: string): void {
|
||||
if (!logTextarea) {
|
||||
return;
|
||||
}
|
||||
const prefix = logTextarea.value.length > 0 ? "\n" : "";
|
||||
logTextarea.value += `${prefix}${line}`;
|
||||
logTextarea.scrollTop = logTextarea.scrollHeight;
|
||||
}
|
||||
|
||||
function renderSnapshot(snapshot: DemoWsManagerSnapshotPayload): void {
|
||||
if (endpointCountText) {
|
||||
endpointCountText.textContent = String(snapshot.endpointCount);
|
||||
}
|
||||
if (startedCountText) {
|
||||
startedCountText.textContent = String(snapshot.startedCount);
|
||||
}
|
||||
if (!tableBody) {
|
||||
return;
|
||||
}
|
||||
|
||||
tableBody.innerHTML = "";
|
||||
|
||||
for (const endpoint of snapshot.endpoints) {
|
||||
const row = document.createElement("tr");
|
||||
row.innerHTML = `
|
||||
<td>
|
||||
<div class="fw-semibold">${endpoint.name}</div>
|
||||
<div class="small text-body-secondary">${endpoint.resolvedUrl}</div>
|
||||
</td>
|
||||
<td>${endpoint.provider}</td>
|
||||
<td>${endpoint.roles.join(", ")}</td>
|
||||
<td>${endpoint.connectionState}</td>
|
||||
<td>${endpoint.activeSubscriptionCount}</td>
|
||||
`;
|
||||
tableBody.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshSnapshot(): Promise<void> {
|
||||
try {
|
||||
const snapshot = await invoke<DemoWsManagerSnapshotPayload>("demo_ws_manager_get_snapshot");
|
||||
renderSnapshot(snapshot);
|
||||
appendLogLine("[ui] refreshed manager snapshot");
|
||||
} catch (error) {
|
||||
appendLogLine(`[ui] snapshot error: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRoles(): Promise<void> {
|
||||
if (!roleSelect) {
|
||||
return;
|
||||
}
|
||||
|
||||
roleSelect.innerHTML = "";
|
||||
|
||||
try {
|
||||
const roles = await invoke<string[]>("demo_ws_manager_list_roles");
|
||||
for (const role of roles) {
|
||||
const option = document.createElement("option");
|
||||
option.value = role;
|
||||
option.textContent = role;
|
||||
roleSelect.appendChild(option);
|
||||
}
|
||||
} catch (error) {
|
||||
appendLogLine(`[ui] list roles error: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function startAll(): Promise<void> {
|
||||
try {
|
||||
const snapshot = await invoke<DemoWsManagerSnapshotPayload>("demo_ws_manager_start_all");
|
||||
renderSnapshot(snapshot);
|
||||
} catch (error) {
|
||||
appendLogLine(`[ui] start all error: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function stopAll(): Promise<void> {
|
||||
try {
|
||||
const snapshot = await invoke<DemoWsManagerSnapshotPayload>("demo_ws_manager_stop_all");
|
||||
renderSnapshot(snapshot);
|
||||
} catch (error) {
|
||||
appendLogLine(`[ui] stop all error: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function startRole(): Promise<void> {
|
||||
if (!roleSelect || roleSelect.value.trim().length === 0) {
|
||||
appendLogLine("[ui] no role selected");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const snapshot = await invoke<DemoWsManagerSnapshotPayload>("demo_ws_manager_start_role", {
|
||||
role: roleSelect.value,
|
||||
});
|
||||
renderSnapshot(snapshot);
|
||||
} catch (error) {
|
||||
appendLogLine(`[ui] start role error: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function stopRole(): Promise<void> {
|
||||
if (!roleSelect || roleSelect.value.trim().length === 0) {
|
||||
appendLogLine("[ui] no role selected");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const snapshot = await invoke<DemoWsManagerSnapshotPayload>("demo_ws_manager_stop_role", {
|
||||
role: roleSelect.value,
|
||||
});
|
||||
renderSnapshot(snapshot);
|
||||
} catch (error) {
|
||||
appendLogLine(`[ui] stop role error: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
void takeoverConsole();
|
||||
debug("demo_ws_manager window loaded");
|
||||
|
||||
const sidebarToggle = document.querySelector<HTMLButtonElement>('#sidebarToggle');
|
||||
if (sidebarToggle) {
|
||||
// restaurer l’état depuis localStorage
|
||||
if (localStorage.getItem('sidebar-toggle') === 'true') {
|
||||
document.body.classList.add('sidenav-toggled');
|
||||
}
|
||||
|
||||
sidebarToggle.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
document.body.classList.toggle('sidenav-toggled');
|
||||
localStorage.setItem('sidebar-toggle', document.body.classList.contains('sidenav-toggled') ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
|
||||
const toastElList = document.querySelectorAll('.toast');
|
||||
Array.from(toastElList).map(toastEl => new bootstrap.Toast(toastEl));
|
||||
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]');
|
||||
Array.from(popoverTriggerList).map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl));
|
||||
|
||||
const gobackto = location.pathname + location.search;
|
||||
|
||||
document.querySelectorAll<HTMLAnchorElement>('a[data-setlang]').forEach((a) => {
|
||||
const href = a.getAttribute("href");
|
||||
if (!href) return; // pas de href => on ignore
|
||||
|
||||
const url = new URL(href, location.origin);
|
||||
url.searchParams.set("gobackto", gobackto);
|
||||
|
||||
// conserve une URL relative (path + query)
|
||||
a.setAttribute("href", url.pathname + "?" + url.searchParams.toString());
|
||||
});
|
||||
|
||||
if (startAllButton) {
|
||||
startAllButton.addEventListener("click", () => {
|
||||
void startAll();
|
||||
});
|
||||
}
|
||||
if (stopAllButton) {
|
||||
stopAllButton.addEventListener("click", () => {
|
||||
void stopAll();
|
||||
});
|
||||
}
|
||||
if (refreshButton) {
|
||||
refreshButton.addEventListener("click", () => {
|
||||
void refreshSnapshot();
|
||||
});
|
||||
}
|
||||
if (startRoleButton) {
|
||||
startRoleButton.addEventListener("click", () => {
|
||||
void startRole();
|
||||
});
|
||||
}
|
||||
if (stopRoleButton) {
|
||||
stopRoleButton.addEventListener("click", () => {
|
||||
void stopRole();
|
||||
});
|
||||
}
|
||||
if (clearLogButton && logTextarea) {
|
||||
clearLogButton.addEventListener("click", () => {
|
||||
logTextarea.value = "";
|
||||
});
|
||||
}
|
||||
|
||||
await listen<string>("kb-demo-ws-manager-log", (event) => {
|
||||
appendLogLine(event.payload);
|
||||
});
|
||||
|
||||
await listen<DemoWsManagerSnapshotPayload>("kb-demo-ws-manager-snapshot", (event) => {
|
||||
renderSnapshot(event.payload);
|
||||
});
|
||||
|
||||
await loadRoles();
|
||||
await refreshSnapshot();
|
||||
});
|
||||
161
kb_demo_app/frontend/ts/main.ts
Normal file
161
kb_demo_app/frontend/ts/main.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
// file: kb_demo_app/frontend/ts/main.ts
|
||||
|
||||
import * as bootstrap from "bootstrap";
|
||||
import "simplebar";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { debug, takeoverConsole } from "@fltsci/tauri-plugin-tracing";
|
||||
|
||||
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
|
||||
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
||||
|
||||
async function openDemoWsWindow(): Promise<void> {
|
||||
try {
|
||||
await invoke("open_demo_ws_window");
|
||||
} catch (error) {
|
||||
console.error("open_demo_ws_window failed:", error);
|
||||
}
|
||||
}
|
||||
async function openDemoHttpWindow(): Promise<void> {
|
||||
try {
|
||||
await invoke("open_demo_http_window");
|
||||
} catch (error) {
|
||||
console.error("open_demo_http_window failed:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function openDemoWsManagerWindow(): Promise<void> {
|
||||
try {
|
||||
await invoke("open_demo_ws_manager_window");
|
||||
} catch (error) {
|
||||
console.error("open_demo_ws_manager_window failed:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function openDemoPipelineWindow(): Promise<void> {
|
||||
try {
|
||||
await invoke("open_demo_pipeline_window");
|
||||
} catch (error) {
|
||||
console.error("open_demo_pipeline_window failed:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function openDemoPipeline2Window(): Promise<void> {
|
||||
try {
|
||||
await invoke("open_demo_pipeline2_window");
|
||||
} catch (error) {
|
||||
console.error("open_demo_pipeline2_window2 failed:", error);
|
||||
}
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
void takeoverConsole();
|
||||
|
||||
debug("main window loaded");
|
||||
|
||||
const sidebarToggle = document.querySelector<HTMLButtonElement>('#sidebarToggle');
|
||||
if (sidebarToggle) {
|
||||
// restaurer l’état depuis localStorage
|
||||
if (localStorage.getItem('sidebar-toggle') === 'true') {
|
||||
document.body.classList.add('sidenav-toggled');
|
||||
}
|
||||
|
||||
sidebarToggle.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
document.body.classList.toggle('sidenav-toggled');
|
||||
localStorage.setItem('sidebar-toggle', document.body.classList.contains('sidenav-toggled') ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
|
||||
const toastElList = document.querySelectorAll('.toast');
|
||||
Array.from(toastElList).map(toastEl => new bootstrap.Toast(toastEl));
|
||||
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]');
|
||||
Array.from(popoverTriggerList).map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl));
|
||||
|
||||
const gobackto = location.pathname + location.search;
|
||||
|
||||
document.querySelectorAll<HTMLAnchorElement>('a[data-setlang]').forEach((a) => {
|
||||
const href = a.getAttribute("href");
|
||||
if (!href) return; // pas de href => on ignore
|
||||
|
||||
const url = new URL(href, location.origin);
|
||||
url.searchParams.set("gobackto", gobackto);
|
||||
|
||||
// conserve une URL relative (path + query)
|
||||
a.setAttribute("href", url.pathname + "?" + url.searchParams.toString());
|
||||
});
|
||||
|
||||
const openDemoWsButton = document.querySelector<HTMLButtonElement>("#openDemoWsButton");
|
||||
const openDemoWsButtonSecondary = document.querySelector<HTMLButtonElement>("#openDemoWsButtonSecondary");
|
||||
|
||||
const openDemoHttpButton = document.querySelector<HTMLButtonElement>("#openDemoHttpButton");
|
||||
const openDemoHttpButtonSecondary = document.querySelector<HTMLButtonElement>("#openDemoHttpButtonSecondary");
|
||||
const openDemoWsManagerButton = document.querySelector<HTMLButtonElement>("#openDemoWsManagerButton");
|
||||
const openDemoWsManagerButtonSecondary = document.querySelector<HTMLButtonElement>("#openDemoWsManagerButtonSecondary");
|
||||
const openDemoPipelineButton = document.querySelector<HTMLButtonElement>("#openDemoPipelineButton");
|
||||
const openDemoPipelineButtonSecondary = document.querySelector<HTMLButtonElement>("#openDemoPipelineButtonSecondary");
|
||||
const openDemoPipeline2Button = document.querySelector<HTMLButtonElement>("#openDemoPipeline2Button");
|
||||
const openDemoPipeline2ButtonSecondary = document.querySelector<HTMLButtonElement>("#openDemoPipeline2ButtonSecondary");
|
||||
|
||||
if (openDemoWsButton) {
|
||||
openDemoWsButton.addEventListener("click", () => {
|
||||
void openDemoWsWindow();
|
||||
});
|
||||
}
|
||||
|
||||
if (openDemoWsButtonSecondary) {
|
||||
openDemoWsButtonSecondary.addEventListener("click", () => {
|
||||
void openDemoWsWindow();
|
||||
});
|
||||
}
|
||||
|
||||
if (openDemoHttpButton) {
|
||||
openDemoHttpButton.addEventListener("click", () => {
|
||||
void openDemoHttpWindow();
|
||||
});
|
||||
}
|
||||
|
||||
if (openDemoHttpButtonSecondary) {
|
||||
openDemoHttpButtonSecondary.addEventListener("click", () => {
|
||||
void openDemoHttpWindow();
|
||||
});
|
||||
}
|
||||
|
||||
if (openDemoWsManagerButton) {
|
||||
openDemoWsManagerButton.addEventListener("click", () => {
|
||||
void openDemoWsManagerWindow();
|
||||
});
|
||||
}
|
||||
|
||||
if (openDemoWsManagerButtonSecondary) {
|
||||
openDemoWsManagerButtonSecondary.addEventListener("click", () => {
|
||||
void openDemoWsManagerWindow();
|
||||
});
|
||||
}
|
||||
|
||||
if (openDemoPipelineButton) {
|
||||
openDemoPipelineButton.addEventListener("click", () => {
|
||||
void openDemoPipelineWindow();
|
||||
});
|
||||
}
|
||||
|
||||
if (openDemoPipelineButtonSecondary) {
|
||||
openDemoPipelineButtonSecondary.addEventListener("click", () => {
|
||||
void openDemoPipelineWindow();
|
||||
});
|
||||
}
|
||||
|
||||
if (openDemoPipeline2Button) {
|
||||
openDemoPipeline2Button.addEventListener("click", () => {
|
||||
void openDemoPipeline2Window();
|
||||
});
|
||||
}
|
||||
|
||||
if (openDemoPipeline2ButtonSecondary) {
|
||||
openDemoPipeline2ButtonSecondary.addEventListener("click", () => {
|
||||
void openDemoPipeline2Window();
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
95
kb_demo_app/frontend/ts/splash.ts
Normal file
95
kb_demo_app/frontend/ts/splash.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
// file: kb_demo_app/frontend/ts/splash.ts
|
||||
|
||||
import { error, info, takeoverConsole } from "@fltsci/tauri-plugin-tracing";
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { SplashOrder } from './bindings/SplashOrder.ts';
|
||||
|
||||
// Fonction d'animation d'opacité
|
||||
async function animateOpacity(
|
||||
element: HTMLElement,
|
||||
fromOpacity: number,
|
||||
toOpacity: number,
|
||||
durationMs: number
|
||||
): Promise<void> {
|
||||
console.log(`Animating from ${fromOpacity} to ${toOpacity} over ${durationMs}ms`);
|
||||
//debug(`Animating from ${fromOpacity} to ${toOpacity} over ${durationMs}ms`);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const startTime = performance.now();
|
||||
const startOpacity = fromOpacity;
|
||||
const changeOpacity = toOpacity - fromOpacity;
|
||||
|
||||
function update(currentTime: number) {
|
||||
const elapsed = currentTime - startTime;
|
||||
if (elapsed >= durationMs) {
|
||||
element.style.opacity = toOpacity.toString();
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const progress = elapsed / durationMs;
|
||||
element.style.opacity = (startOpacity + changeOpacity * progress).toString();
|
||||
requestAnimationFrame(update);
|
||||
}
|
||||
|
||||
requestAnimationFrame(update);
|
||||
});
|
||||
}
|
||||
|
||||
// Journalisation
|
||||
function addLogMessage(message: string): void {
|
||||
console.log(`Splash: ${message}`);
|
||||
const debugInfo = document.getElementById('debug-info');
|
||||
if (debugInfo) {
|
||||
const time = new Date().toLocaleTimeString();
|
||||
let msg = `${time}: ${message}<br>`;
|
||||
debugInfo.innerHTML += msg;
|
||||
}
|
||||
}
|
||||
|
||||
// Pour ajouter des messages directement (sans événements)
|
||||
function addMessage(message: string, status: string): void {
|
||||
const messagesContainer = document.getElementById('messages-container');
|
||||
if (!messagesContainer) return;
|
||||
|
||||
const messageElement = document.createElement('div');
|
||||
messageElement.className = `splash-message ${status}`;
|
||||
messageElement.textContent = message;
|
||||
|
||||
messagesContainer.appendChild(messageElement);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
listen("splash", (event) => {
|
||||
const splashorder = event.payload as SplashOrder;
|
||||
if (splashorder.order == "fadein") {
|
||||
const container = document.getElementById('splash-container');
|
||||
if (container) {
|
||||
animateOpacity(container, 0, 1, 3000);
|
||||
} else {
|
||||
error("no container");
|
||||
}
|
||||
} else if (splashorder.order == "fadeout") {
|
||||
const container = document.getElementById('splash-container');
|
||||
if (container) {
|
||||
animateOpacity(container, 1, 0, 3000);
|
||||
} else {
|
||||
error("no container");
|
||||
}
|
||||
} else if (splashorder.order == "add_msg" && splashorder.msg && splashorder.status) {
|
||||
addMessage(splashorder.msg, splashorder.status);
|
||||
} else if (splashorder.order == "add_log" && splashorder.msg) {
|
||||
addLogMessage(splashorder.msg);
|
||||
} else {
|
||||
error("unknown order:" + splashorder.order);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Démarrer l'initialisation au chargement du DOM
|
||||
//window.addEventListener('DOMContentLoaded', initialize);
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
void takeoverConsole();
|
||||
info("window loaded");
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user