0.7.24-pre.0
This commit is contained in:
@@ -8,7 +8,8 @@
|
||||
"demo_ws",
|
||||
"demo_http",
|
||||
"demo_ws_manager",
|
||||
"demo_pipeline"
|
||||
"demo_pipeline",
|
||||
"demo_pipeline2"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
|
||||
@@ -71,19 +71,12 @@
|
||||
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"
|
||||
/>
|
||||
<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">
|
||||
@@ -94,13 +87,7 @@
|
||||
|
||||
<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"
|
||||
/>
|
||||
<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">
|
||||
@@ -108,56 +95,29 @@
|
||||
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"
|
||||
/>
|
||||
<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"
|
||||
/>
|
||||
<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"
|
||||
/>
|
||||
<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"
|
||||
/>
|
||||
<input id="demoPipelineBackfillPoolLimitInput" type="number" min="1" step="1" class="form-control" value="50" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -167,6 +127,22 @@
|
||||
</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">
|
||||
@@ -184,7 +160,7 @@
|
||||
<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>
|
||||
@@ -192,6 +168,36 @@
|
||||
</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">
|
||||
|
||||
199
kb_app/frontend/demo_pipeline2.html
Normal file
199
kb_app/frontend/demo_pipeline2.html
Normal file
@@ -0,0 +1,199 @@
|
||||
<!-- file: kb_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="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<h1 class="h4 mb-3">Catalogue local</h1>
|
||||
<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 class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 mb-3">Backfill ciblé</h2>
|
||||
|
||||
<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 class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 mb-3">Chargement candles</h2>
|
||||
|
||||
<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 class="col-12 col-xxl-8">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 mb-3">Backfill summary</h2>
|
||||
<textarea id="demoPipeline2BackfillSummaryTextarea" 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="demoPipeline2ChartMeta" class="small text-body-secondary">
|
||||
Aucun jeu de candles chargé.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="demoPipeline2Chart" class="w-100 border rounded bg-body" style="height: 560px;"></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">Log UI</h2>
|
||||
<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="12" 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 2
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="ts/demo_pipeline2.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -28,9 +28,12 @@
|
||||
<button id="openDemoWsManagerButton" type="button" class="btn btn-outline-primary">
|
||||
Demo Ws Manager
|
||||
</button>
|
||||
<button id="openDemoPipelineButton" type="button" class="btn btn-primary">
|
||||
<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>
|
||||
@@ -78,6 +81,9 @@
|
||||
<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 />
|
||||
|
||||
@@ -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 { KbDemoPipeline2CatalogPayload } from "./KbDemoPipeline2CatalogPayload";
|
||||
|
||||
/**
|
||||
* Shared backfill response payload.
|
||||
*/
|
||||
export type KbDemoPipeline2BackfillPayload = {
|
||||
/**
|
||||
* 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: KbDemoPipeline2CatalogPayload, };
|
||||
@@ -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 KbDemoPipeline2BackfillPoolRequest = {
|
||||
/**
|
||||
* 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 KbDemoPipeline2BackfillTokenRequest = {
|
||||
/**
|
||||
* 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, };
|
||||
25
kb_app/frontend/ts/bindings/KbDemoPipeline2CatalogPayload.ts
Normal file
25
kb_app/frontend/ts/bindings/KbDemoPipeline2CatalogPayload.ts
Normal file
@@ -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 { KbDemoPipeline2PairItem } from "./KbDemoPipeline2PairItem";
|
||||
import type { KbDemoPipeline2PoolItem } from "./KbDemoPipeline2PoolItem";
|
||||
import type { KbDemoPipeline2TokenItem } from "./KbDemoPipeline2TokenItem";
|
||||
|
||||
/**
|
||||
* Full local catalog payload.
|
||||
*/
|
||||
export type KbDemoPipeline2CatalogPayload = {
|
||||
/**
|
||||
* Open database URL.
|
||||
*/
|
||||
databaseUrl: string,
|
||||
/**
|
||||
* Observed token list.
|
||||
*/
|
||||
tokens: Array<KbDemoPipeline2TokenItem>,
|
||||
/**
|
||||
* Known pool list.
|
||||
*/
|
||||
pools: Array<KbDemoPipeline2PoolItem>,
|
||||
/**
|
||||
* Known pair list.
|
||||
*/
|
||||
pairs: Array<KbDemoPipeline2PairItem>, };
|
||||
@@ -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 KbDemoPipeline2PairCandlesPayload = {
|
||||
/**
|
||||
* 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 KbDemoPipeline2PairCandlesRequest = {
|
||||
/**
|
||||
* Pair id to load.
|
||||
*/
|
||||
pairId: number,
|
||||
/**
|
||||
* Timeframe in seconds.
|
||||
*/
|
||||
timeframeSeconds: number,
|
||||
/**
|
||||
* Whether materialized candles should be preferred when available.
|
||||
*/
|
||||
preferMaterialized: boolean, };
|
||||
30
kb_app/frontend/ts/bindings/KbDemoPipeline2PairItem.ts
Normal file
30
kb_app/frontend/ts/bindings/KbDemoPipeline2PairItem.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 KbDemoPipeline2PairItem = {
|
||||
/**
|
||||
* 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_app/frontend/ts/bindings/KbDemoPipeline2PoolItem.ts
Normal file
18
kb_app/frontend/ts/bindings/KbDemoPipeline2PoolItem.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 KbDemoPipeline2PoolItem = {
|
||||
/**
|
||||
* Pool address.
|
||||
*/
|
||||
poolAddress: string,
|
||||
/**
|
||||
* Optional internal pair id when known.
|
||||
*/
|
||||
pairId: number | null,
|
||||
/**
|
||||
* Optional DEX code.
|
||||
*/
|
||||
dexCode: string | null, };
|
||||
18
kb_app/frontend/ts/bindings/KbDemoPipeline2TokenItem.ts
Normal file
18
kb_app/frontend/ts/bindings/KbDemoPipeline2TokenItem.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 KbDemoPipeline2TokenItem = {
|
||||
/**
|
||||
* 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_app`.
|
||||
*/
|
||||
export type KbDemoPipelineBackfillPoolPayload = {
|
||||
/**
|
||||
* Backfilled pool address.
|
||||
*/
|
||||
poolAddress: string,
|
||||
/**
|
||||
* HTTP role used during backfill.
|
||||
*/
|
||||
httpRole: string,
|
||||
/**
|
||||
* Pretty JSON summary returned by `KbTokenBackfillService::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_app`.
|
||||
*/
|
||||
export type KbDemoPipelineBackfillPoolRequest = {
|
||||
/**
|
||||
* 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, };
|
||||
@@ -12,10 +12,391 @@ import { KbDemoPipelineInspectPairRequest } from './bindings/KbDemoPipelineInspe
|
||||
import { KbDemoPipelineInspectPoolRequest } from './bindings/KbDemoPipelineInspectPoolRequest.ts';
|
||||
import { KbDemoPipelineBackfillTokenRequest } from './bindings/KbDemoPipelineBackfillTokenRequest.ts';
|
||||
import { KbDemoPipelineBackfillTokenPayload } from './bindings/KbDemoPipelineBackfillTokenPayload.ts';
|
||||
import { KbDemoPipelineBackfillPoolRequest } from './bindings/KbDemoPipelineBackfillPoolRequest.ts';
|
||||
import { KbDemoPipelineBackfillPoolPayload } from './bindings/KbDemoPipelineBackfillPoolPayload.ts';
|
||||
import * as echarts from "echarts";
|
||||
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
|
||||
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
||||
|
||||
|
||||
interface DemoPipelinePairCandle {
|
||||
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 DemoPipelinePairCandleGroup {
|
||||
pairId: number;
|
||||
timeframeSeconds: number;
|
||||
candles: DemoPipelinePairCandle[];
|
||||
}
|
||||
|
||||
function parsePairCandleGroups(rawJson: string): DemoPipelinePairCandleGroup[] {
|
||||
if (rawJson.trim() === "") {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(rawJson) as unknown;
|
||||
if (!Array.isArray(parsed)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const groups: DemoPipelinePairCandleGroup[] = [];
|
||||
|
||||
for (const value of parsed) {
|
||||
if (typeof value !== "object" || value === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const maybeGroup = value as {
|
||||
pairId?: unknown;
|
||||
timeframeSeconds?: unknown;
|
||||
candles?: unknown;
|
||||
};
|
||||
|
||||
if (
|
||||
typeof maybeGroup.pairId !== "number" ||
|
||||
typeof maybeGroup.timeframeSeconds !== "number" ||
|
||||
!Array.isArray(maybeGroup.candles)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
groups.push({
|
||||
pairId: maybeGroup.pairId,
|
||||
timeframeSeconds: maybeGroup.timeframeSeconds,
|
||||
candles: maybeGroup.candles as DemoPipelinePairCandle[],
|
||||
});
|
||||
}
|
||||
|
||||
return groups;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function formatTimeframeLabel(timeframeSeconds: number): string {
|
||||
if (timeframeSeconds % 3600 === 0) {
|
||||
return `${timeframeSeconds / 3600}h`;
|
||||
}
|
||||
if (timeframeSeconds % 60 === 0) {
|
||||
return `${timeframeSeconds / 60}m`;
|
||||
}
|
||||
return `${timeframeSeconds}s`;
|
||||
}
|
||||
|
||||
function parseRawVolume(text: string | null, fallback: number): number {
|
||||
if (text === null || text.trim() === "") {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const parsed = Number.parseFloat(text);
|
||||
if (Number.isNaN(parsed)) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function setEmptyCandlesChart(
|
||||
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 refreshCandlesSelectors(
|
||||
groups: DemoPipelinePairCandleGroup[],
|
||||
pairSelect: HTMLSelectElement,
|
||||
timeframeSelect: HTMLSelectElement,
|
||||
): void {
|
||||
const currentPairValue = pairSelect.value;
|
||||
const currentTimeframeValue = timeframeSelect.value;
|
||||
|
||||
const uniquePairs = Array.from(new Set(groups.map((group) => group.pairId))).sort((left, right) => left - right);
|
||||
|
||||
pairSelect.innerHTML = "";
|
||||
if (uniquePairs.length === 0) {
|
||||
const option = document.createElement("option");
|
||||
option.value = "";
|
||||
option.textContent = "Aucune";
|
||||
pairSelect.appendChild(option);
|
||||
|
||||
timeframeSelect.innerHTML = "";
|
||||
const tfOption = document.createElement("option");
|
||||
tfOption.value = "";
|
||||
tfOption.textContent = "Aucun";
|
||||
timeframeSelect.appendChild(tfOption);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const pairId of uniquePairs) {
|
||||
const option = document.createElement("option");
|
||||
option.value = String(pairId);
|
||||
option.textContent = `Pair #${pairId}`;
|
||||
if (option.value === currentPairValue) {
|
||||
option.selected = true;
|
||||
}
|
||||
pairSelect.appendChild(option);
|
||||
}
|
||||
|
||||
if (pairSelect.value === "" && uniquePairs.length > 0) {
|
||||
pairSelect.value = String(uniquePairs[0]);
|
||||
}
|
||||
|
||||
const selectedPairId = Number.parseInt(pairSelect.value, 10);
|
||||
const pairGroups = groups
|
||||
.filter((group) => group.pairId === selectedPairId)
|
||||
.sort((left, right) => left.timeframeSeconds - right.timeframeSeconds);
|
||||
|
||||
timeframeSelect.innerHTML = "";
|
||||
|
||||
if (pairGroups.length === 0) {
|
||||
const option = document.createElement("option");
|
||||
option.value = "";
|
||||
option.textContent = "Aucun";
|
||||
timeframeSelect.appendChild(option);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const group of pairGroups) {
|
||||
const option = document.createElement("option");
|
||||
option.value = String(group.timeframeSeconds);
|
||||
option.textContent = formatTimeframeLabel(group.timeframeSeconds);
|
||||
if (option.value === currentTimeframeValue) {
|
||||
option.selected = true;
|
||||
}
|
||||
timeframeSelect.appendChild(option);
|
||||
}
|
||||
|
||||
if (timeframeSelect.value === "" && pairGroups.length > 0) {
|
||||
timeframeSelect.value = String(pairGroups[0].timeframeSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
function renderSelectedCandlesChart(
|
||||
chart: echarts.ECharts,
|
||||
chartMeta: HTMLElement,
|
||||
groups: DemoPipelinePairCandleGroup[],
|
||||
pairSelect: HTMLSelectElement,
|
||||
timeframeSelect: HTMLSelectElement,
|
||||
): void {
|
||||
if (groups.length === 0) {
|
||||
setEmptyCandlesChart(chart, chartMeta, "Aucune candle disponible.");
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedPairId = Number.parseInt(pairSelect.value, 10);
|
||||
const selectedTimeframe = Number.parseInt(timeframeSelect.value, 10);
|
||||
|
||||
if (Number.isNaN(selectedPairId) || Number.isNaN(selectedTimeframe)) {
|
||||
setEmptyCandlesChart(chart, chartMeta, "Sélection de paire/timeframe invalide.");
|
||||
return;
|
||||
}
|
||||
|
||||
const group = groups.find(
|
||||
(value) =>
|
||||
value.pairId === selectedPairId &&
|
||||
value.timeframeSeconds === selectedTimeframe,
|
||||
);
|
||||
|
||||
if (!group || group.candles.length === 0) {
|
||||
setEmptyCandlesChart(
|
||||
chart,
|
||||
chartMeta,
|
||||
`Aucune candle pour la pair #${selectedPairId} en ${formatTimeframeLabel(selectedTimeframe)}.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const candles = [...group.candles].sort(
|
||||
(left, right) => left.bucket_start_unix - right.bucket_start_unix,
|
||||
);
|
||||
|
||||
const categoryData = candles.map((candle) =>
|
||||
new Date(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 = candles.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 = candles.map((candle) =>
|
||||
parseRawVolume(candle.quote_volume_raw, candle.trade_count),
|
||||
);
|
||||
|
||||
chartMeta.textContent =
|
||||
`Pair #${selectedPairId} • ${formatTimeframeLabel(selectedTimeframe)} • ${candles.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,
|
||||
},
|
||||
{
|
||||
name: "Volume",
|
||||
type: "bar",
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 1,
|
||||
data: volumeData,
|
||||
},
|
||||
],
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
function applyInspectionPayload(
|
||||
payload: KbDemoPipelineInspectPayload,
|
||||
summaryTextarea: HTMLTextAreaElement,
|
||||
transactionTextarea: HTMLTextAreaElement,
|
||||
decodedEventsTextarea: HTMLTextAreaElement,
|
||||
poolsTextarea: HTMLTextAreaElement,
|
||||
pairsTextarea: HTMLTextAreaElement,
|
||||
launchAttributionsTextarea: HTMLTextAreaElement,
|
||||
poolOriginsTextarea: HTMLTextAreaElement,
|
||||
walletsTextarea: HTMLTextAreaElement,
|
||||
tradeEventsTextarea: HTMLTextAreaElement,
|
||||
pairMetricsTextarea: HTMLTextAreaElement,
|
||||
pairCandlesTextarea: HTMLTextAreaElement,
|
||||
pairAnalyticSignalsTextarea: HTMLTextAreaElement,
|
||||
chart: echarts.ECharts,
|
||||
chartMeta: HTMLElement,
|
||||
pairSelect: HTMLSelectElement,
|
||||
timeframeSelect: HTMLSelectElement,
|
||||
): void {
|
||||
summaryTextarea.value = payload.summaryJson;
|
||||
transactionTextarea.value = payload.transactionJson;
|
||||
decodedEventsTextarea.value = payload.decodedEventsJson;
|
||||
poolsTextarea.value = payload.poolsJson;
|
||||
pairsTextarea.value = payload.pairsJson;
|
||||
launchAttributionsTextarea.value = payload.launchAttributionsJson;
|
||||
poolOriginsTextarea.value = payload.poolOriginsJson;
|
||||
walletsTextarea.value = payload.walletsJson;
|
||||
tradeEventsTextarea.value = payload.tradeEventsJson;
|
||||
pairMetricsTextarea.value = payload.pairMetricsJson;
|
||||
pairCandlesTextarea.value = payload.pairCandlesJson;
|
||||
pairAnalyticSignalsTextarea.value = payload.pairAnalyticSignalsJson;
|
||||
|
||||
const groups = parsePairCandleGroups(payload.pairCandlesJson);
|
||||
refreshCandlesSelectors(groups, pairSelect, timeframeSelect);
|
||||
renderSelectedCandlesChart(chart, chartMeta, groups, pairSelect, timeframeSelect);
|
||||
}
|
||||
|
||||
function appendLogLine(textarea: HTMLTextAreaElement, line: string): void {
|
||||
const now = new Date();
|
||||
@@ -167,14 +548,30 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
const backfillTokenButton = document.querySelector<HTMLButtonElement>("#demoPipelineBackfillTokenButton");
|
||||
const backfillTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipelineBackfillTextarea");
|
||||
|
||||
const chartPairSelect = document.querySelector<HTMLSelectElement>("#demoPipelineChartPairSelect");
|
||||
const chartTimeframeSelect = document.querySelector<HTMLSelectElement>("#demoPipelineChartTimeframeSelect");
|
||||
const candlesChartElement = document.querySelector<HTMLDivElement>("#demoPipelineCandlesChart");
|
||||
const candlesChartMeta = document.querySelector<HTMLDivElement>("#demoPipelineCandlesChartMeta");
|
||||
|
||||
const backfillPoolAddressInput = document.querySelector<HTMLInputElement>("#demoPipelineBackfillPoolAddressInput");
|
||||
const backfillPoolOnlyLimitInput = document.querySelector<HTMLInputElement>("#demoPipelineBackfillPoolOnlyLimitInput");
|
||||
const backfillPoolButton = document.querySelector<HTMLButtonElement>("#demoPipelineBackfillPoolButton");
|
||||
|
||||
|
||||
if (
|
||||
!chartPairSelect ||
|
||||
!chartTimeframeSelect ||
|
||||
!candlesChartElement ||
|
||||
!candlesChartMeta ||
|
||||
!backfillTokenMintInput ||
|
||||
!backfillHttpRoleInput ||
|
||||
!backfillMintLimitInput ||
|
||||
!backfillPoolLimitInput ||
|
||||
!backfillTokenButton ||
|
||||
!backfillTextarea ||
|
||||
!backfillPoolAddressInput ||
|
||||
!backfillPoolOnlyLimitInput ||
|
||||
!backfillPoolButton ||
|
||||
!pairIdInput ||
|
||||
!inspectPairButton ||
|
||||
!poolAddressInput ||
|
||||
@@ -204,6 +601,36 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const candlesChart = echarts.init(candlesChartElement);
|
||||
setEmptyCandlesChart(candlesChart, candlesChartMeta, "Aucune candle disponible.");
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
candlesChart.resize();
|
||||
});
|
||||
|
||||
chartPairSelect.addEventListener("change", () => {
|
||||
const groups = parsePairCandleGroups(pairCandlesTextarea.value);
|
||||
refreshCandlesSelectors(groups, chartPairSelect, chartTimeframeSelect);
|
||||
renderSelectedCandlesChart(
|
||||
candlesChart,
|
||||
candlesChartMeta,
|
||||
groups,
|
||||
chartPairSelect,
|
||||
chartTimeframeSelect,
|
||||
);
|
||||
});
|
||||
|
||||
chartTimeframeSelect.addEventListener("change", () => {
|
||||
const groups = parsePairCandleGroups(pairCandlesTextarea.value);
|
||||
renderSelectedCandlesChart(
|
||||
candlesChart,
|
||||
candlesChartMeta,
|
||||
groups,
|
||||
chartPairSelect,
|
||||
chartTimeframeSelect,
|
||||
);
|
||||
});
|
||||
|
||||
clearButton.addEventListener("click", () => {
|
||||
clearInspection(
|
||||
backfillTextarea,
|
||||
@@ -229,9 +656,110 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
tokenMintInput.value = "";
|
||||
pairIdInput.value = "";
|
||||
poolAddressInput.value = "";
|
||||
backfillPoolAddressInput.value = "";
|
||||
backfillPoolOnlyLimitInput.value = "50";
|
||||
chartPairSelect.innerHTML = `<option value="">Aucune</option>`;
|
||||
chartTimeframeSelect.innerHTML = `<option value="">Aucun</option>`;
|
||||
setEmptyCandlesChart(candlesChart, candlesChartMeta, "Aucune candle disponible.");
|
||||
appendLogLine(logTextarea, "[ui] inspection state cleared");
|
||||
});
|
||||
|
||||
backfillPoolButton.addEventListener("click", async () => {
|
||||
const poolAddress = backfillPoolAddressInput.value.trim();
|
||||
if (poolAddress === "") {
|
||||
appendLogLine(logTextarea, "[ui] backfill pool address is required");
|
||||
return;
|
||||
}
|
||||
|
||||
const poolSignatureLimit = readPositiveIntegerInput(
|
||||
backfillPoolOnlyLimitInput,
|
||||
logTextarea,
|
||||
"poolSignatureLimit",
|
||||
);
|
||||
if (poolSignatureLimit === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const httpRoleText = backfillHttpRoleInput.value.trim();
|
||||
const httpRole = httpRoleText === "" ? null : httpRoleText;
|
||||
|
||||
appendLogLine(
|
||||
logTextarea,
|
||||
`[ui] launching pool backfill for '${poolAddress}' with role '${httpRole ?? "history_backfill"}' (pool=${poolSignatureLimit})`,
|
||||
);
|
||||
|
||||
const request: KbDemoPipelineBackfillPoolRequest = {
|
||||
poolAddress,
|
||||
httpRole,
|
||||
poolSignatureLimit,
|
||||
};
|
||||
|
||||
try {
|
||||
const payload = await invoke<KbDemoPipelineBackfillPoolPayload>(
|
||||
"demo_pipeline_backfill_pool_address",
|
||||
{ request },
|
||||
);
|
||||
|
||||
backfillTextarea.value = payload.backfillJson;
|
||||
appendLogLine(
|
||||
logTextarea,
|
||||
`[ui] pool backfill completed for '${payload.poolAddress}' with role '${payload.httpRole}'`,
|
||||
);
|
||||
|
||||
if (!payload.poolPersistedAfterBackfill) {
|
||||
appendLogLine(
|
||||
logTextarea,
|
||||
`[ui] backfill completed but pool '${payload.poolAddress}' is still absent from persisted pool objects; automatic pool inspection skipped`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const inspectRequest: KbDemoPipelineInspectPoolRequest = {
|
||||
poolAddress: payload.poolAddress,
|
||||
customTimeframeSeconds: null,
|
||||
};
|
||||
|
||||
try {
|
||||
const inspectPayload = await invoke<KbDemoPipelineInspectPayload>(
|
||||
"demo_pipeline_inspect_pool_address",
|
||||
{ request: inspectRequest },
|
||||
);
|
||||
|
||||
applyInspectionPayload(
|
||||
inspectPayload,
|
||||
summaryTextarea,
|
||||
transactionTextarea,
|
||||
decodedEventsTextarea,
|
||||
poolsTextarea,
|
||||
pairsTextarea,
|
||||
launchAttributionsTextarea,
|
||||
poolOriginsTextarea,
|
||||
walletsTextarea,
|
||||
tradeEventsTextarea,
|
||||
pairMetricsTextarea,
|
||||
pairCandlesTextarea,
|
||||
pairAnalyticSignalsTextarea,
|
||||
candlesChart,
|
||||
candlesChartMeta,
|
||||
chartPairSelect,
|
||||
chartTimeframeSelect,
|
||||
);
|
||||
|
||||
appendLogLine(
|
||||
logTextarea,
|
||||
`[ui] pool inspection refreshed after backfill for '${payload.poolAddress}'`,
|
||||
);
|
||||
} catch (error) {
|
||||
appendLogLine(
|
||||
logTextarea,
|
||||
`[ui] backfill completed but automatic pool inspection failed for '${payload.poolAddress}': ${String(error)}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
appendLogLine(logTextarea, `[ui] pool backfill error: ${String(error)}`);
|
||||
}
|
||||
});
|
||||
|
||||
clearLogButton.addEventListener("click", () => {
|
||||
logTextarea.value = "";
|
||||
});
|
||||
@@ -267,18 +795,25 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
try {
|
||||
const payload = await invoke<KbDemoPipelineInspectPayload>("demo_pipeline_inspect_signature", { request });
|
||||
|
||||
summaryTextarea.value = payload.summaryJson;
|
||||
transactionTextarea.value = payload.transactionJson;
|
||||
decodedEventsTextarea.value = payload.decodedEventsJson;
|
||||
poolsTextarea.value = payload.poolsJson;
|
||||
pairsTextarea.value = payload.pairsJson;
|
||||
launchAttributionsTextarea.value = payload.launchAttributionsJson;
|
||||
poolOriginsTextarea.value = payload.poolOriginsJson;
|
||||
walletsTextarea.value = payload.walletsJson;
|
||||
tradeEventsTextarea.value = payload.tradeEventsJson;
|
||||
pairMetricsTextarea.value = payload.pairMetricsJson;
|
||||
pairCandlesTextarea.value = payload.pairCandlesJson;
|
||||
pairAnalyticSignalsTextarea.value = payload.pairAnalyticSignalsJson;
|
||||
applyInspectionPayload(
|
||||
payload,
|
||||
summaryTextarea,
|
||||
transactionTextarea,
|
||||
decodedEventsTextarea,
|
||||
poolsTextarea,
|
||||
pairsTextarea,
|
||||
launchAttributionsTextarea,
|
||||
poolOriginsTextarea,
|
||||
walletsTextarea,
|
||||
tradeEventsTextarea,
|
||||
pairMetricsTextarea,
|
||||
pairCandlesTextarea,
|
||||
pairAnalyticSignalsTextarea,
|
||||
candlesChart,
|
||||
candlesChartMeta,
|
||||
chartPairSelect,
|
||||
chartTimeframeSelect,
|
||||
);
|
||||
|
||||
appendLogLine(logTextarea, `[ui] inspection completed for '${payload.signature}'`);
|
||||
} catch (error) {
|
||||
@@ -317,18 +852,25 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
try {
|
||||
const payload = await invoke<KbDemoPipelineInspectPayload>("demo_pipeline_inspect_token_mint", { request });
|
||||
|
||||
summaryTextarea.value = payload.summaryJson;
|
||||
transactionTextarea.value = payload.transactionJson;
|
||||
decodedEventsTextarea.value = payload.decodedEventsJson;
|
||||
poolsTextarea.value = payload.poolsJson;
|
||||
pairsTextarea.value = payload.pairsJson;
|
||||
launchAttributionsTextarea.value = payload.launchAttributionsJson;
|
||||
poolOriginsTextarea.value = payload.poolOriginsJson;
|
||||
walletsTextarea.value = payload.walletsJson;
|
||||
tradeEventsTextarea.value = payload.tradeEventsJson;
|
||||
pairMetricsTextarea.value = payload.pairMetricsJson;
|
||||
pairCandlesTextarea.value = payload.pairCandlesJson;
|
||||
pairAnalyticSignalsTextarea.value = payload.pairAnalyticSignalsJson;
|
||||
applyInspectionPayload(
|
||||
payload,
|
||||
summaryTextarea,
|
||||
transactionTextarea,
|
||||
decodedEventsTextarea,
|
||||
poolsTextarea,
|
||||
pairsTextarea,
|
||||
launchAttributionsTextarea,
|
||||
poolOriginsTextarea,
|
||||
walletsTextarea,
|
||||
tradeEventsTextarea,
|
||||
pairMetricsTextarea,
|
||||
pairCandlesTextarea,
|
||||
pairAnalyticSignalsTextarea,
|
||||
candlesChart,
|
||||
candlesChartMeta,
|
||||
chartPairSelect,
|
||||
chartTimeframeSelect,
|
||||
);
|
||||
|
||||
appendLogLine(logTextarea, `[ui] token inspection completed for '${payload.signature}'`);
|
||||
} catch (error) {
|
||||
@@ -367,18 +909,25 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
try {
|
||||
const payload = await invoke<KbDemoPipelineInspectPayload>("demo_pipeline_inspect_pair_id", { request });
|
||||
|
||||
summaryTextarea.value = payload.summaryJson;
|
||||
transactionTextarea.value = payload.transactionJson;
|
||||
decodedEventsTextarea.value = payload.decodedEventsJson;
|
||||
poolsTextarea.value = payload.poolsJson;
|
||||
pairsTextarea.value = payload.pairsJson;
|
||||
launchAttributionsTextarea.value = payload.launchAttributionsJson;
|
||||
poolOriginsTextarea.value = payload.poolOriginsJson;
|
||||
walletsTextarea.value = payload.walletsJson;
|
||||
tradeEventsTextarea.value = payload.tradeEventsJson;
|
||||
pairMetricsTextarea.value = payload.pairMetricsJson;
|
||||
pairCandlesTextarea.value = payload.pairCandlesJson;
|
||||
pairAnalyticSignalsTextarea.value = payload.pairAnalyticSignalsJson;
|
||||
applyInspectionPayload(
|
||||
payload,
|
||||
summaryTextarea,
|
||||
transactionTextarea,
|
||||
decodedEventsTextarea,
|
||||
poolsTextarea,
|
||||
pairsTextarea,
|
||||
launchAttributionsTextarea,
|
||||
poolOriginsTextarea,
|
||||
walletsTextarea,
|
||||
tradeEventsTextarea,
|
||||
pairMetricsTextarea,
|
||||
pairCandlesTextarea,
|
||||
pairAnalyticSignalsTextarea,
|
||||
candlesChart,
|
||||
candlesChartMeta,
|
||||
chartPairSelect,
|
||||
chartTimeframeSelect,
|
||||
);
|
||||
|
||||
appendLogLine(logTextarea, `[ui] pair inspection completed for '${payload.signature}'`);
|
||||
} catch (error) {
|
||||
@@ -411,18 +960,25 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
try {
|
||||
const payload = await invoke<KbDemoPipelineInspectPayload>("demo_pipeline_inspect_pool_address", { request });
|
||||
|
||||
summaryTextarea.value = payload.summaryJson;
|
||||
transactionTextarea.value = payload.transactionJson;
|
||||
decodedEventsTextarea.value = payload.decodedEventsJson;
|
||||
poolsTextarea.value = payload.poolsJson;
|
||||
pairsTextarea.value = payload.pairsJson;
|
||||
launchAttributionsTextarea.value = payload.launchAttributionsJson;
|
||||
poolOriginsTextarea.value = payload.poolOriginsJson;
|
||||
walletsTextarea.value = payload.walletsJson;
|
||||
tradeEventsTextarea.value = payload.tradeEventsJson;
|
||||
pairMetricsTextarea.value = payload.pairMetricsJson;
|
||||
pairCandlesTextarea.value = payload.pairCandlesJson;
|
||||
pairAnalyticSignalsTextarea.value = payload.pairAnalyticSignalsJson;
|
||||
applyInspectionPayload(
|
||||
payload,
|
||||
summaryTextarea,
|
||||
transactionTextarea,
|
||||
decodedEventsTextarea,
|
||||
poolsTextarea,
|
||||
pairsTextarea,
|
||||
launchAttributionsTextarea,
|
||||
poolOriginsTextarea,
|
||||
walletsTextarea,
|
||||
tradeEventsTextarea,
|
||||
pairMetricsTextarea,
|
||||
pairCandlesTextarea,
|
||||
pairAnalyticSignalsTextarea,
|
||||
candlesChart,
|
||||
candlesChartMeta,
|
||||
chartPairSelect,
|
||||
chartTimeframeSelect,
|
||||
);
|
||||
|
||||
appendLogLine(logTextarea, `[ui] pool inspection completed for '${payload.signature}'`);
|
||||
} catch (error) {
|
||||
|
||||
559
kb_app/frontend/ts/demo_pipeline2.ts
Normal file
559
kb_app/frontend/ts/demo_pipeline2.ts
Normal file
@@ -0,0 +1,559 @@
|
||||
// file: kb_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 { KbDemoPipeline2CatalogPayload } from "./bindings/KbDemoPipeline2CatalogPayload.ts";
|
||||
import type { KbDemoPipeline2BackfillTokenRequest } from "./bindings/KbDemoPipeline2BackfillTokenRequest.ts";
|
||||
import type { KbDemoPipeline2BackfillPoolRequest } from "./bindings/KbDemoPipeline2BackfillPoolRequest.ts";
|
||||
import type { KbDemoPipeline2BackfillPayload } from "./bindings/KbDemoPipeline2BackfillPayload.ts";
|
||||
import type { KbDemoPipeline2PairCandlesRequest } from "./bindings/KbDemoPipeline2PairCandlesRequest.ts";
|
||||
import type { KbDemoPipeline2PairCandlesPayload } from "./bindings/KbDemoPipeline2PairCandlesPayload.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;
|
||||
}
|
||||
|
||||
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 refreshPairSelect(
|
||||
catalog: KbDemoPipeline2CatalogPayload,
|
||||
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: KbDemoPipeline2CatalogPayload,
|
||||
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,
|
||||
},
|
||||
{
|
||||
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 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 clearLogButton = document.querySelector<HTMLButtonElement>("#demoPipeline2ClearLogButton");
|
||||
const logTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipeline2LogTextarea");
|
||||
|
||||
if (
|
||||
!refreshCatalogButton ||
|
||||
!tokensTextarea ||
|
||||
!poolsTextarea ||
|
||||
!pairsTextarea ||
|
||||
!httpRoleInput ||
|
||||
!mintInput ||
|
||||
!mintSignatureLimitInput ||
|
||||
!mintPoolLimitInput ||
|
||||
!backfillMintButton ||
|
||||
!poolInput ||
|
||||
!poolSignatureLimitInput ||
|
||||
!backfillPoolButton ||
|
||||
!pairSelect ||
|
||||
!timeframeSelect ||
|
||||
!customTimeframeInput ||
|
||||
!preferMaterializedInput ||
|
||||
!loadCandlesButton ||
|
||||
!backfillSummaryTextarea ||
|
||||
!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 chart = echarts.init(safeChartElement);
|
||||
setEmptyChart(chart, safeChartMeta, "Aucune candle disponible.");
|
||||
window.addEventListener("resize", () => chart.resize());
|
||||
|
||||
clearLogButton.addEventListener("click", () => {
|
||||
logTextarea.value = "";
|
||||
});
|
||||
|
||||
let currentCatalog: KbDemoPipeline2CatalogPayload | null = null;
|
||||
|
||||
async function refreshCatalog(): Promise<void> {
|
||||
appendLogLine(safeLogTextarea, "[ui] refreshing local catalog");
|
||||
|
||||
try {
|
||||
const catalog = await invoke<KbDemoPipeline2CatalogPayload>("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: KbDemoPipeline2BackfillTokenRequest = {
|
||||
tokenMint,
|
||||
httpRole,
|
||||
mintSignatureLimit,
|
||||
poolSignatureLimit,
|
||||
};
|
||||
|
||||
try {
|
||||
const payload = await invoke<KbDemoPipeline2BackfillPayload>(
|
||||
"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: KbDemoPipeline2BackfillPoolRequest = {
|
||||
poolAddress,
|
||||
httpRole,
|
||||
poolSignatureLimit,
|
||||
};
|
||||
|
||||
try {
|
||||
const payload = await invoke<KbDemoPipeline2BackfillPayload>(
|
||||
"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)}`);
|
||||
}
|
||||
});
|
||||
|
||||
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: KbDemoPipeline2PairCandlesRequest = {
|
||||
pairId: parsedPairId,
|
||||
timeframeSeconds,
|
||||
preferMaterialized: preferMaterializedInput.checked,
|
||||
};
|
||||
|
||||
try {
|
||||
const payload = await invoke<KbDemoPipeline2PairCandlesPayload>(
|
||||
"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();
|
||||
}
|
||||
});
|
||||
@@ -39,6 +39,14 @@ async function openDemoPipelineWindow(): Promise<void> {
|
||||
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();
|
||||
|
||||
@@ -87,6 +95,8 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
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", () => {
|
||||
@@ -136,4 +146,16 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
});
|
||||
}
|
||||
|
||||
if (openDemoPipeline2Button) {
|
||||
openDemoPipeline2Button.addEventListener("click", () => {
|
||||
void openDemoPipeline2Window();
|
||||
});
|
||||
}
|
||||
|
||||
if (openDemoPipeline2ButtonSecondary) {
|
||||
openDemoPipeline2ButtonSecondary.addEventListener("click", () => {
|
||||
void openDemoPipeline2Window();
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
@@ -1 +1 @@
|
||||
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main","splash","demo_ws","demo_http","demo_ws_manager","demo_pipeline"],"permissions":["core:default","tracing:default"]}}
|
||||
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main","splash","demo_ws","demo_http","demo_ws_manager","demo_pipeline","demo_pipeline2"],"permissions":["core:default","tracing:default"]}}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "kb-app",
|
||||
"private": true,
|
||||
"version": "0.7.23",
|
||||
"version": "0.7.24",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -6,7 +6,10 @@ use tauri::Manager;
|
||||
|
||||
/// Request payload for one pipeline inspection by signature.
|
||||
#[derive(Clone, Debug, serde::Deserialize, ts_rs::TS)]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/KbDemoPipelineInspectRequest.ts")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/KbDemoPipelineInspectRequest.ts"
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct KbDemoPipelineInspectRequest {
|
||||
/// Transaction signature to inspect.
|
||||
@@ -17,7 +20,10 @@ pub(crate) struct KbDemoPipelineInspectRequest {
|
||||
|
||||
/// Response payload for one pipeline inspection.
|
||||
#[derive(Clone, Debug, serde::Serialize, ts_rs::TS)]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/KbDemoPipelineInspectPayload.ts")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/KbDemoPipelineInspectPayload.ts"
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct KbDemoPipelineInspectPayload {
|
||||
/// Inspected signature.
|
||||
@@ -50,7 +56,10 @@ pub(crate) struct KbDemoPipelineInspectPayload {
|
||||
|
||||
/// Request payload for one pipeline inspection by token mint.
|
||||
#[derive(Clone, Debug, serde::Deserialize, ts_rs::TS)]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/KbDemoPipelineInspectTokenRequest.ts")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/KbDemoPipelineInspectTokenRequest.ts"
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct KbDemoPipelineInspectTokenRequest {
|
||||
/// Token mint to inspect.
|
||||
@@ -61,7 +70,10 @@ pub(crate) struct KbDemoPipelineInspectTokenRequest {
|
||||
|
||||
/// Request payload for one pipeline inspection by pair id.
|
||||
#[derive(Clone, Debug, serde::Deserialize, ts_rs::TS)]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/KbDemoPipelineInspectPairRequest.ts")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/KbDemoPipelineInspectPairRequest.ts"
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct KbDemoPipelineInspectPairRequest {
|
||||
/// Pair id to inspect.
|
||||
@@ -72,7 +84,10 @@ pub(crate) struct KbDemoPipelineInspectPairRequest {
|
||||
|
||||
/// Request payload for one pipeline inspection by pool address.
|
||||
#[derive(Clone, Debug, serde::Deserialize, ts_rs::TS)]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/KbDemoPipelineInspectPoolRequest.ts")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/KbDemoPipelineInspectPoolRequest.ts"
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct KbDemoPipelineInspectPoolRequest {
|
||||
/// Pool address to inspect.
|
||||
@@ -83,7 +98,10 @@ pub(crate) struct KbDemoPipelineInspectPoolRequest {
|
||||
|
||||
/// Request payload for one token backfill launched from `kb_app`.
|
||||
#[derive(Clone, Debug, serde::Deserialize, ts_rs::TS)]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/KbDemoPipelineBackfillTokenRequest.ts")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/KbDemoPipelineBackfillTokenRequest.ts"
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct KbDemoPipelineBackfillTokenRequest {
|
||||
/// Token mint to backfill.
|
||||
@@ -98,7 +116,10 @@ pub(crate) struct KbDemoPipelineBackfillTokenRequest {
|
||||
|
||||
/// Response payload for one token backfill launched from `kb_app`.
|
||||
#[derive(Clone, Debug, serde::Serialize, ts_rs::TS)]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/KbDemoPipelineBackfillTokenPayload.ts")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/KbDemoPipelineBackfillTokenPayload.ts"
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct KbDemoPipelineBackfillTokenPayload {
|
||||
/// Backfilled token mint.
|
||||
@@ -111,6 +132,109 @@ pub(crate) struct KbDemoPipelineBackfillTokenPayload {
|
||||
pub token_persisted_after_backfill: bool,
|
||||
}
|
||||
|
||||
/// Request payload for one pool backfill launched from `kb_app`.
|
||||
#[derive(Clone, Debug, serde::Deserialize, ts_rs::TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/KbDemoPipelineBackfillPoolRequest.ts"
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct KbDemoPipelineBackfillPoolRequest {
|
||||
/// Pool address to backfill.
|
||||
pub pool_address: std::string::String,
|
||||
/// HTTP role used to select one endpoint in the pool.
|
||||
pub http_role: std::option::Option<std::string::String>,
|
||||
/// Maximum number of signatures fetched from the pool address.
|
||||
pub pool_signature_limit: u32,
|
||||
}
|
||||
|
||||
/// Response payload for one pool backfill launched from `kb_app`.
|
||||
#[derive(Clone, Debug, serde::Serialize, ts_rs::TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/KbDemoPipelineBackfillPoolPayload.ts"
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct KbDemoPipelineBackfillPoolPayload {
|
||||
/// Backfilled pool address.
|
||||
pub pool_address: std::string::String,
|
||||
/// HTTP role used during backfill.
|
||||
pub http_role: std::string::String,
|
||||
/// Pretty JSON summary returned by `KbTokenBackfillService::backfill_pool_by_address`.
|
||||
pub backfill_json: std::string::String,
|
||||
/// Whether the pool exists in persisted pool objects after backfill.
|
||||
pub pool_persisted_after_backfill: bool,
|
||||
}
|
||||
|
||||
/// Launches one pool backfill through the persisted `kb_lib` services.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn demo_pipeline_backfill_pool_address(
|
||||
state: tauri::State<'_, crate::KbAppState>,
|
||||
request: KbDemoPipelineBackfillPoolRequest,
|
||||
) -> Result<KbDemoPipelineBackfillPoolPayload, std::string::String> {
|
||||
let pool_address = request.pool_address.trim().to_string();
|
||||
if pool_address.is_empty() {
|
||||
return Err("demo pipeline backfill pool address must not be empty".to_string());
|
||||
}
|
||||
let http_role = match request.http_role.clone() {
|
||||
Some(http_role) => {
|
||||
let trimmed = http_role.trim().to_string();
|
||||
if trimmed.is_empty() {
|
||||
"history_backfill".to_string()
|
||||
} else {
|
||||
trimmed
|
||||
}
|
||||
}
|
||||
None => "history_backfill".to_string(),
|
||||
};
|
||||
if request.pool_signature_limit == 0 {
|
||||
return Err("demo pipeline poolSignatureLimit must be > 0".to_string());
|
||||
}
|
||||
let database = state.database.clone();
|
||||
let http_pool = std::sync::Arc::new(state.http_pool.clone());
|
||||
let service =
|
||||
kb_lib::KbTokenBackfillService::new(http_pool, database.clone(), http_role.clone());
|
||||
let backfill_result = service
|
||||
.backfill_pool_by_address(pool_address.as_str(), request.pool_signature_limit as usize)
|
||||
.await;
|
||||
let backfill = match backfill_result {
|
||||
Ok(backfill) => backfill,
|
||||
Err(error) => {
|
||||
return Err(format!(
|
||||
"cannot backfill pool address '{}' with role '{}': {}",
|
||||
pool_address, http_role, error
|
||||
));
|
||||
}
|
||||
};
|
||||
let pool_result = kb_lib::get_pool_by_address(database.as_ref(), pool_address.as_str()).await;
|
||||
let pool_option = match pool_result {
|
||||
Ok(pool_option) => pool_option,
|
||||
Err(error) => {
|
||||
return Err(format!(
|
||||
"cannot verify persisted pool '{}' after backfill with role '{}': {}",
|
||||
pool_address, http_role, error
|
||||
));
|
||||
}
|
||||
};
|
||||
let pool_persisted_after_backfill = pool_option.is_some();
|
||||
let backfill_json_result = serde_json::to_string_pretty(&backfill);
|
||||
let backfill_json = match backfill_json_result {
|
||||
Ok(backfill_json) => backfill_json,
|
||||
Err(error) => {
|
||||
return Err(format!(
|
||||
"cannot serialize pool backfill result for '{}': {}",
|
||||
pool_address, error
|
||||
));
|
||||
}
|
||||
};
|
||||
Ok(KbDemoPipelineBackfillPoolPayload {
|
||||
pool_address,
|
||||
http_role,
|
||||
backfill_json,
|
||||
pool_persisted_after_backfill,
|
||||
})
|
||||
}
|
||||
|
||||
/// Launches one token backfill through the persisted `kb_lib` services.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn demo_pipeline_backfill_token_mint(
|
||||
|
||||
524
kb_app/src/demo_pipeline2.rs
Normal file
524
kb_app/src/demo_pipeline2.rs
Normal file
@@ -0,0 +1,524 @@
|
||||
// file: kb_app/src/demo_pipeline2.rs
|
||||
|
||||
//! Tauri commands for the focused pipeline demo window.
|
||||
//!
|
||||
//! This demo is intentionally narrower than `Demo Pipeline`:
|
||||
//! - read the local catalog of tokens / pools / pairs,
|
||||
//! - trigger targeted backfills from the chain,
|
||||
//! - load candles for one selected pair and timeframe.
|
||||
|
||||
use tauri::Manager;
|
||||
use ts_rs::TS;
|
||||
|
||||
/// One token item for the local catalog.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/KbDemoPipeline2TokenItem.ts"
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct KbDemoPipeline2TokenItem {
|
||||
/// Token mint.
|
||||
pub mint: std::string::String,
|
||||
/// Optional token symbol.
|
||||
pub symbol: std::option::Option<std::string::String>,
|
||||
/// Optional token name.
|
||||
pub name: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
/// One pool item for the local catalog.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/KbDemoPipeline2PoolItem.ts"
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct KbDemoPipeline2PoolItem {
|
||||
/// Pool address.
|
||||
pub pool_address: std::string::String,
|
||||
/// Optional internal pair id when known.
|
||||
#[ts(type = "number | null")]
|
||||
pub pair_id: std::option::Option<i64>,
|
||||
/// Optional DEX code.
|
||||
pub dex_code: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
/// One pair item for the local catalog.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/KbDemoPipeline2PairItem.ts"
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct KbDemoPipeline2PairItem {
|
||||
/// Internal pair id.
|
||||
#[ts(type = "number")]
|
||||
pub pair_id: i64,
|
||||
/// Related pool address.
|
||||
pub pool_address: std::string::String,
|
||||
/// Optional pair symbol.
|
||||
pub symbol: std::option::Option<std::string::String>,
|
||||
/// Optional DEX code.
|
||||
pub dex_code: std::option::Option<std::string::String>,
|
||||
/// Optional local trade count.
|
||||
#[ts(type = "number | null")]
|
||||
pub trade_count: std::option::Option<i64>,
|
||||
/// Optional local last price.
|
||||
#[ts(type = "number | null")]
|
||||
pub last_price_quote_per_base: std::option::Option<f64>,
|
||||
}
|
||||
|
||||
/// Full local catalog payload.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/KbDemoPipeline2CatalogPayload.ts"
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct KbDemoPipeline2CatalogPayload {
|
||||
/// Open database URL.
|
||||
pub database_url: std::string::String,
|
||||
/// Observed token list.
|
||||
pub tokens: std::vec::Vec<KbDemoPipeline2TokenItem>,
|
||||
/// Known pool list.
|
||||
pub pools: std::vec::Vec<KbDemoPipeline2PoolItem>,
|
||||
/// Known pair list.
|
||||
pub pairs: std::vec::Vec<KbDemoPipeline2PairItem>,
|
||||
}
|
||||
|
||||
/// Request payload for token backfill.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/KbDemoPipeline2BackfillTokenRequest.ts"
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct KbDemoPipeline2BackfillTokenRequest {
|
||||
/// Token mint to backfill.
|
||||
pub token_mint: std::string::String,
|
||||
/// Optional HTTP role.
|
||||
pub http_role: std::option::Option<std::string::String>,
|
||||
/// Limit for signatures fetched from the mint.
|
||||
pub mint_signature_limit: u32,
|
||||
/// Limit for signatures fetched from each discovered pool.
|
||||
pub pool_signature_limit: u32,
|
||||
}
|
||||
|
||||
/// Request payload for pool backfill.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/KbDemoPipeline2BackfillPoolRequest.ts"
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct KbDemoPipeline2BackfillPoolRequest {
|
||||
/// Pool address to backfill.
|
||||
pub pool_address: std::string::String,
|
||||
/// Optional HTTP role.
|
||||
pub http_role: std::option::Option<std::string::String>,
|
||||
/// Limit for signatures fetched from the pool.
|
||||
pub pool_signature_limit: u32,
|
||||
}
|
||||
|
||||
/// Shared backfill response payload.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/KbDemoPipeline2BackfillPayload.ts"
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct KbDemoPipeline2BackfillPayload {
|
||||
/// Object key used by the backfill.
|
||||
pub object_key: std::string::String,
|
||||
/// Mode: `tokenMint` or `poolAddress`.
|
||||
pub mode: std::string::String,
|
||||
/// HTTP role used.
|
||||
pub http_role: std::string::String,
|
||||
/// Pretty JSON summary.
|
||||
pub summary_json: std::string::String,
|
||||
/// Refreshed local catalog after backfill.
|
||||
pub catalog: KbDemoPipeline2CatalogPayload,
|
||||
}
|
||||
|
||||
/// Request payload for pair candles.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/KbDemoPipeline2PairCandlesRequest.ts"
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct KbDemoPipeline2PairCandlesRequest {
|
||||
/// Pair id to load.
|
||||
#[ts(type = "number")]
|
||||
pub pair_id: i64,
|
||||
/// Timeframe in seconds.
|
||||
#[ts(type = "number")]
|
||||
pub timeframe_seconds: i64,
|
||||
/// Whether materialized candles should be preferred when available.
|
||||
pub prefer_materialized: bool,
|
||||
}
|
||||
|
||||
/// Candle payload returned to the UI.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/KbDemoPipeline2PairCandlesPayload.ts"
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct KbDemoPipeline2PairCandlesPayload {
|
||||
/// Pair id.
|
||||
#[ts(type = "number")]
|
||||
pub pair_id: i64,
|
||||
/// Timeframe in seconds.
|
||||
#[ts(type = "number")]
|
||||
pub timeframe_seconds: i64,
|
||||
/// Pretty JSON array of candles.
|
||||
pub candles_json: std::string::String,
|
||||
}
|
||||
|
||||
/// Opens the `Demo Pipeline 2` window.
|
||||
#[tauri::command]
|
||||
pub(crate) fn open_demo_pipeline2_window(
|
||||
app_handle: tauri::AppHandle,
|
||||
) -> Result<(), std::string::String> {
|
||||
let existing_window_option = app_handle.get_webview_window("demo_pipeline2");
|
||||
|
||||
let demo_window = match existing_window_option {
|
||||
Some(demo_window) => demo_window,
|
||||
None => {
|
||||
let builder = tauri::WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
"demo_pipeline2",
|
||||
tauri::WebviewUrl::App("demo_pipeline2.html".into()),
|
||||
)
|
||||
.title("Demo Pipeline 2")
|
||||
.inner_size(1480.0, 920.0)
|
||||
.min_inner_size(1100.0, 720.0)
|
||||
.center()
|
||||
.visible(true)
|
||||
.transparent(false)
|
||||
.decorations(true);
|
||||
let build_result = builder.build();
|
||||
match build_result {
|
||||
Ok(window) => window,
|
||||
Err(error) => {
|
||||
return Err(format!("cannot create demo_pipeline2 window: {error:?}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let show_result = demo_window.show();
|
||||
if let Err(error) = show_result {
|
||||
return Err(format!("cannot show demo_pipeline2 window: {error:?}"));
|
||||
}
|
||||
let focus_result = demo_window.set_focus();
|
||||
if let Err(error) = focus_result {
|
||||
return Err(format!("cannot focus demo_pipeline2 window: {error:?}"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the local catalog of observed tokens, pools and pairs.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn demo_pipeline2_get_catalog(
|
||||
state: tauri::State<'_, crate::KbAppState>,
|
||||
) -> Result<KbDemoPipeline2CatalogPayload, std::string::String> {
|
||||
kb_demo_pipeline2_build_catalog(state.database.clone()).await
|
||||
}
|
||||
|
||||
/// Runs a targeted token backfill then returns the refreshed catalog.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn demo_pipeline2_backfill_token_mint(
|
||||
state: tauri::State<'_, crate::KbAppState>,
|
||||
request: KbDemoPipeline2BackfillTokenRequest,
|
||||
) -> Result<KbDemoPipeline2BackfillPayload, std::string::String> {
|
||||
let token_mint = request.token_mint.trim().to_string();
|
||||
if token_mint.is_empty() {
|
||||
return Err("token mint must not be empty".to_string());
|
||||
}
|
||||
if request.mint_signature_limit == 0 {
|
||||
return Err("mintSignatureLimit must be > 0".to_string());
|
||||
}
|
||||
if request.pool_signature_limit == 0 {
|
||||
return Err("poolSignatureLimit must be > 0".to_string());
|
||||
}
|
||||
let http_role = kb_demo_pipeline2_normalize_http_role(request.http_role);
|
||||
let database = state.database.clone();
|
||||
let http_pool = std::sync::Arc::new(state.http_pool.clone());
|
||||
let service =
|
||||
kb_lib::KbTokenBackfillService::new(http_pool, database.clone(), http_role.clone());
|
||||
let result = service
|
||||
.backfill_token_by_mint(
|
||||
token_mint.as_str(),
|
||||
request.mint_signature_limit as usize,
|
||||
request.pool_signature_limit as usize,
|
||||
)
|
||||
.await;
|
||||
let backfill = match result {
|
||||
Ok(backfill) => backfill,
|
||||
Err(error) => {
|
||||
return Err(format!(
|
||||
"cannot backfill token mint '{}' with role '{}': {}",
|
||||
token_mint, http_role, error
|
||||
));
|
||||
}
|
||||
};
|
||||
let summary_json_result = serde_json::to_string_pretty(&backfill);
|
||||
let summary_json = match summary_json_result {
|
||||
Ok(summary_json) => summary_json,
|
||||
Err(error) => {
|
||||
return Err(format!(
|
||||
"cannot serialize token backfill result for '{}': {}",
|
||||
token_mint, error
|
||||
));
|
||||
}
|
||||
};
|
||||
let catalog = kb_demo_pipeline2_build_catalog(database).await?;
|
||||
Ok(KbDemoPipeline2BackfillPayload {
|
||||
object_key: token_mint,
|
||||
mode: "tokenMint".to_string(),
|
||||
http_role,
|
||||
summary_json,
|
||||
catalog,
|
||||
})
|
||||
}
|
||||
|
||||
/// Runs a targeted pool backfill then returns the refreshed catalog.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn demo_pipeline2_backfill_pool_address(
|
||||
state: tauri::State<'_, crate::KbAppState>,
|
||||
request: KbDemoPipeline2BackfillPoolRequest,
|
||||
) -> Result<KbDemoPipeline2BackfillPayload, std::string::String> {
|
||||
let pool_address = request.pool_address.trim().to_string();
|
||||
if pool_address.is_empty() {
|
||||
return Err("pool address must not be empty".to_string());
|
||||
}
|
||||
if request.pool_signature_limit == 0 {
|
||||
return Err("poolSignatureLimit must be > 0".to_string());
|
||||
}
|
||||
let http_role = kb_demo_pipeline2_normalize_http_role(request.http_role);
|
||||
let database = state.database.clone();
|
||||
let http_pool = std::sync::Arc::new(state.http_pool.clone());
|
||||
let service =
|
||||
kb_lib::KbTokenBackfillService::new(http_pool, database.clone(), http_role.clone());
|
||||
let result = service
|
||||
.backfill_pool_by_address(pool_address.as_str(), request.pool_signature_limit as usize)
|
||||
.await;
|
||||
let backfill = match result {
|
||||
Ok(backfill) => backfill,
|
||||
Err(error) => {
|
||||
return Err(format!(
|
||||
"cannot backfill pool address '{}' with role '{}': {}",
|
||||
pool_address, http_role, error
|
||||
));
|
||||
}
|
||||
};
|
||||
let summary_json_result = serde_json::to_string_pretty(&backfill);
|
||||
let summary_json = match summary_json_result {
|
||||
Ok(summary_json) => summary_json,
|
||||
Err(error) => {
|
||||
return Err(format!(
|
||||
"cannot serialize pool backfill result for '{}': {}",
|
||||
pool_address, error
|
||||
));
|
||||
}
|
||||
};
|
||||
let catalog = kb_demo_pipeline2_build_catalog(database).await?;
|
||||
Ok(KbDemoPipeline2BackfillPayload {
|
||||
object_key: pool_address,
|
||||
mode: "poolAddress".to_string(),
|
||||
http_role,
|
||||
summary_json,
|
||||
catalog,
|
||||
})
|
||||
}
|
||||
|
||||
/// Loads candles for one pair and one timeframe.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn demo_pipeline2_get_pair_candles(
|
||||
state: tauri::State<'_, crate::KbAppState>,
|
||||
request: KbDemoPipeline2PairCandlesRequest,
|
||||
) -> Result<KbDemoPipeline2PairCandlesPayload, std::string::String> {
|
||||
if request.pair_id <= 0 {
|
||||
return Err("pairId must be > 0".to_string());
|
||||
}
|
||||
if request.timeframe_seconds <= 0 {
|
||||
return Err("timeframeSeconds must be > 0".to_string());
|
||||
}
|
||||
let query_service = kb_lib::KbPairCandleQueryService::new(state.database.clone());
|
||||
let candles_result = query_service
|
||||
.list_pair_candles(
|
||||
request.pair_id,
|
||||
request.timeframe_seconds,
|
||||
None,
|
||||
None,
|
||||
request.prefer_materialized,
|
||||
)
|
||||
.await;
|
||||
let candles = match candles_result {
|
||||
Ok(candles) => candles,
|
||||
Err(error) => {
|
||||
return Err(format!(
|
||||
"cannot load candles for pair '{}' timeframe '{}': {}",
|
||||
request.pair_id, request.timeframe_seconds, error
|
||||
));
|
||||
}
|
||||
};
|
||||
let candles_json_result = serde_json::to_string_pretty(&candles);
|
||||
let candles_json = match candles_json_result {
|
||||
Ok(candles_json) => candles_json,
|
||||
Err(error) => {
|
||||
return Err(format!(
|
||||
"cannot serialize candles for pair '{}' timeframe '{}': {}",
|
||||
request.pair_id, request.timeframe_seconds, error
|
||||
));
|
||||
}
|
||||
};
|
||||
Ok(KbDemoPipeline2PairCandlesPayload {
|
||||
pair_id: request.pair_id,
|
||||
timeframe_seconds: request.timeframe_seconds,
|
||||
candles_json,
|
||||
})
|
||||
}
|
||||
|
||||
async fn kb_demo_pipeline2_build_catalog(
|
||||
database: std::sync::Arc<kb_lib::KbDatabase>,
|
||||
) -> Result<KbDemoPipeline2CatalogPayload, std::string::String> {
|
||||
let dexes_result = kb_lib::list_dexes(database.as_ref()).await;
|
||||
let dexes = match dexes_result {
|
||||
Ok(dexes) => dexes,
|
||||
Err(error) => {
|
||||
return Err(format!("cannot list DEXes: {}", error));
|
||||
}
|
||||
};
|
||||
let mut dex_code_by_id = std::collections::BTreeMap::<i64, std::string::String>::new();
|
||||
for dex in dexes {
|
||||
if let Some(dex_id) = dex.id {
|
||||
dex_code_by_id.insert(dex_id, dex.code);
|
||||
}
|
||||
}
|
||||
let tokens_result = kb_lib::list_tokens(database.as_ref()).await;
|
||||
let db_tokens = match tokens_result {
|
||||
Ok(db_tokens) => db_tokens,
|
||||
Err(error) => {
|
||||
return Err(format!("cannot list tokens: {}", error));
|
||||
}
|
||||
};
|
||||
|
||||
let mut tokens = std::vec::Vec::<KbDemoPipeline2TokenItem>::new();
|
||||
for token in db_tokens {
|
||||
tokens.push(KbDemoPipeline2TokenItem {
|
||||
mint: token.mint,
|
||||
symbol: token.symbol,
|
||||
name: token.name,
|
||||
});
|
||||
}
|
||||
let pools_result = kb_lib::list_pools(database.as_ref()).await;
|
||||
let pools = match pools_result {
|
||||
Ok(pools) => pools,
|
||||
Err(error) => {
|
||||
return Err(format!("cannot list pools: {}", error));
|
||||
}
|
||||
};
|
||||
let pairs_result = kb_lib::list_pairs(database.as_ref()).await;
|
||||
let pairs = match pairs_result {
|
||||
Ok(pairs) => pairs,
|
||||
Err(error) => {
|
||||
return Err(format!("cannot list pairs: {}", error));
|
||||
}
|
||||
};
|
||||
let mut pair_by_pool_id = std::collections::BTreeMap::<i64, kb_lib::KbPairDto>::new();
|
||||
for pair in &pairs {
|
||||
pair_by_pool_id.insert(pair.pool_id, pair.clone());
|
||||
}
|
||||
let mut pair_items = std::vec::Vec::<KbDemoPipeline2PairItem>::new();
|
||||
for pair in pairs {
|
||||
let pair_id = match pair.id {
|
||||
Some(pair_id) => pair_id,
|
||||
None => continue,
|
||||
};
|
||||
let pool_result = kb_lib::get_pool_by_address(database.as_ref(), "").await;
|
||||
let _ = pool_result;
|
||||
let pool_address = {
|
||||
let all_pools_result = kb_lib::list_pools(database.as_ref()).await;
|
||||
let all_pools = match all_pools_result {
|
||||
Ok(all_pools) => all_pools,
|
||||
Err(error) => {
|
||||
return Err(format!("cannot reload pools for pair catalog: {}", error));
|
||||
}
|
||||
};
|
||||
let mut found_address = std::string::String::new();
|
||||
for pool in all_pools {
|
||||
let pool_id = match pool.id {
|
||||
Some(pool_id) => pool_id,
|
||||
None => continue,
|
||||
};
|
||||
if pool_id == pair.pool_id {
|
||||
found_address = pool.address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
found_address
|
||||
};
|
||||
let pair_metric_result =
|
||||
kb_lib::get_pair_metric_by_pair_id(database.as_ref(), pair_id).await;
|
||||
let pair_metric_option = match pair_metric_result {
|
||||
Ok(pair_metric_option) => pair_metric_option,
|
||||
Err(error) => {
|
||||
return Err(format!(
|
||||
"cannot fetch pair metric for pair '{}': {}",
|
||||
pair_id, error
|
||||
));
|
||||
}
|
||||
};
|
||||
let trade_count = pair_metric_option.as_ref().map(|metric| metric.trade_count);
|
||||
let last_price_quote_per_base =
|
||||
pair_metric_option.and_then(|metric| metric.last_price_quote_per_base);
|
||||
pair_items.push(KbDemoPipeline2PairItem {
|
||||
pair_id,
|
||||
pool_address,
|
||||
symbol: pair.symbol,
|
||||
dex_code: dex_code_by_id.get(&pair.dex_id).cloned(),
|
||||
trade_count,
|
||||
last_price_quote_per_base,
|
||||
});
|
||||
}
|
||||
let mut pool_items = std::vec::Vec::<KbDemoPipeline2PoolItem>::new();
|
||||
for pool in pools {
|
||||
let pool_id = match pool.id {
|
||||
Some(pool_id) => pool_id,
|
||||
None => continue,
|
||||
};
|
||||
let pair_id = pair_by_pool_id.get(&pool_id).and_then(|pair| pair.id);
|
||||
pool_items.push(KbDemoPipeline2PoolItem {
|
||||
pool_address: pool.address,
|
||||
pair_id,
|
||||
dex_code: dex_code_by_id.get(&pool.dex_id).cloned(),
|
||||
});
|
||||
}
|
||||
tokens.sort_by(|left, right| left.mint.cmp(&right.mint));
|
||||
pool_items.sort_by(|left, right| left.pool_address.cmp(&right.pool_address));
|
||||
pair_items.sort_by(|left, right| left.pair_id.cmp(&right.pair_id));
|
||||
Ok(KbDemoPipeline2CatalogPayload {
|
||||
database_url: database.database_url().to_string(),
|
||||
tokens,
|
||||
pools: pool_items,
|
||||
pairs: pair_items,
|
||||
})
|
||||
}
|
||||
|
||||
fn kb_demo_pipeline2_normalize_http_role(
|
||||
role: std::option::Option<std::string::String>,
|
||||
) -> std::string::String {
|
||||
match role {
|
||||
Some(role) => {
|
||||
let trimmed = role.trim().to_string();
|
||||
if trimmed.is_empty() {
|
||||
"history_backfill".to_string()
|
||||
} else {
|
||||
trimmed
|
||||
}
|
||||
}
|
||||
None => "history_backfill".to_string(),
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
mod demo_http;
|
||||
mod demo_pipeline;
|
||||
mod demo_pipeline2;
|
||||
mod demo_ws;
|
||||
mod demo_ws_manager;
|
||||
mod splash;
|
||||
@@ -143,6 +144,12 @@ pub async fn run() -> Result<(), kb_lib::KbError> {
|
||||
crate::demo_pipeline::demo_pipeline_inspect_pair_id,
|
||||
crate::demo_pipeline::demo_pipeline_inspect_pool_address,
|
||||
crate::demo_pipeline::demo_pipeline_backfill_token_mint,
|
||||
crate::demo_pipeline::demo_pipeline_backfill_pool_address,
|
||||
crate::demo_pipeline2::open_demo_pipeline2_window,
|
||||
crate::demo_pipeline2::demo_pipeline2_get_catalog,
|
||||
crate::demo_pipeline2::demo_pipeline2_backfill_token_mint,
|
||||
crate::demo_pipeline2::demo_pipeline2_backfill_pool_address,
|
||||
crate::demo_pipeline2::demo_pipeline2_get_pair_candles,
|
||||
]);
|
||||
tauri_builder = tauri_builder.plugin(tracing_builder.build::<tauri::Wry>());
|
||||
tauri_builder = tauri_builder.setup(|app| {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "kb-bapp",
|
||||
"version": "0.7.23",
|
||||
"version": "0.7.24",
|
||||
"identifier": "com.sasedev.kb-app",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
@@ -92,6 +92,20 @@
|
||||
"create": false,
|
||||
"transparent": false,
|
||||
"decorations": true
|
||||
},
|
||||
{
|
||||
"label": "demo_pipeline2",
|
||||
"url": "demo_pipeline2.html",
|
||||
"title": "Demo Pipeline2",
|
||||
"width": 1480,
|
||||
"height": 920,
|
||||
"minWidth": 1000,
|
||||
"minHeight": 700,
|
||||
"center": true,
|
||||
"visible": false,
|
||||
"create": false,
|
||||
"transparent": false,
|
||||
"decorations": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
|
||||
@@ -17,8 +17,7 @@
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": "./frontend"
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"frontend"
|
||||
|
||||
@@ -23,7 +23,11 @@ export default defineConfig(() => ({
|
||||
input: {
|
||||
"main": normalizePath(resolve(__dirname, 'frontend/main.html')),
|
||||
"splash": normalizePath(resolve(__dirname, 'frontend/splash.html')),
|
||||
"demo_ws": normalizePath(resolve(__dirname, 'frontend/demo_ws.html'))
|
||||
"demo_ws": normalizePath(resolve(__dirname, 'frontend/demo_ws.html')),
|
||||
"demo_http": normalizePath(resolve(__dirname, 'frontend/demo_http.html')),
|
||||
"demo_ws_manager": normalizePath(resolve(__dirname, 'frontend/demo_ws_manager.html')),
|
||||
"demo_pipeline": normalizePath(resolve(__dirname, 'frontend/demo_pipeline.html')),
|
||||
"demo_pipeline2": normalizePath(resolve(__dirname, 'frontend/demo_pipeline2.html'))
|
||||
},
|
||||
output: {
|
||||
entryFileNames: 'js/[name]-[hash].js',
|
||||
|
||||
Reference in New Issue
Block a user