This commit is contained in:
2026-05-01 00:29:32 +02:00
parent b3b0e882b2
commit c542aa9d32
17 changed files with 2347 additions and 49 deletions

1
.gitignore vendored
View File

@@ -38,6 +38,7 @@ config.json
# sqlite
data
dbdata
*.db
*.db-shm

View File

@@ -52,3 +52,4 @@
0.7.19 - Ajout dune première couche holdings observés avec agrégation par couple wallet/token et branchement automatique dans le pipeline de résolution transactionnelle
0.7.20 - Ajout dune première couche candles / OHLCV avec matérialisation en base des timeframes usuels et régénération à la demande pour un timeframe arbitraire depuis les trade events
0.7.21 - Ajout dune première couche de signaux analytiques enrichis par paire avec persistance dédiée et détection de first trade, trade burst, buy/sell imbalance, price jump et volume spike
0.7.22 - Ajout dune première fenêtre `Demo Pipeline` dans `kb_app` pour linspection en lecture seule du pipeline `0.7.x`, avec recherche par signature, token mint, pair id ou pool address, affichage structuré des transactions résolues, événements DEX décodés, pools, paires, listings, launch origins, pool origins, wallets et holdings observés, trade events, pair metrics, candles et signaux analytiques déjà persistés, ainsi que conservation dune instance partagée de la base SQLite pour éviter la réouverture et la réinitialisation du schéma à chaque commande UI

View File

@@ -8,7 +8,7 @@ members = [
]
[workspace.package]
version = "0.7.21"
version = "0.7.22"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-bobobot"

View File

@@ -622,7 +622,7 @@ Réalisé :
- ajout dune collecte de cibles `accountSubscribe` à partir des pools actifs connus,
- ajout dune couche dobservations techniques WS hybrides pour `logs / program / account`,
- ajout dune première déduplication en mémoire des notifications techniques reçues en parallèle,
- ajout dune façade runtime pour exposer ce comportement au futur branchement `ws_manager`.
- ajout dune façade runtime pour exposer ce comportement au branchement `ws_manager`.
### 6.050. Version `0.7.18` — Backfill historique ciblé par token
Réalisé :
@@ -663,17 +663,73 @@ Réalisé :
- branchement automatique dans le pipeline de résolution transactionnelle.
### 6.054. Version `0.7.22` — `kb_app` : inspection et tests du pipeline `0.7.x`
Objectif : permettre depuis lapplication desktop de tester, inspecter et valider tout le pipeline `0.7.x` sans recourir uniquement aux logs bruts ou à SQLite.
Réalisé :
- ajout dune fenêtre dédiée `Demo Pipeline` dans `kb_app`,
- inspection du pipeline persistant par `signature`,
- inspection du pipeline persistant par `token mint`,
- inspection du pipeline persistant par `pair id`,
- inspection du pipeline persistant par `pool address`,
- affichage structuré des transactions résolues, événements DEX décodés, pools, paires, listings, launch origins, pool origins, wallets observés, holdings observés, trade events, pair metrics, candles et signaux analytiques,
- possibilité dutiliser un timeframe custom pour régénérer à la demande les candles non matérialisées,
- conservation dune instance partagée de `KbDatabase` dans `kb_app` afin déviter la réouverture de la base et la réinitialisation du schéma à chaque commande UI,
- validation pratique de linspection du pipeline `0.7.x` sans dépendre uniquement des logs bruts ou de la consultation manuelle de SQLite.
### 6.055. Version `0.7.23` — `kb_app` : backfill token et inspection ciblée
Objectif : piloter le backfill historique depuis linterface desktop et afficher le résultat de façon exploitable.
À faire :
- ajouter une ou plusieurs vues `kb_app` dédiées à linspection des transactions résolues, événements DEX décodés, pools, paires, launch origins, pool origins, wallets observés, holdings observés et trade events,
- permettre la recherche par signature, pool, paire, token mint ou wallet,
- afficher les liens entre objets techniques et objets métier,
- permettre de lancer manuellement certains backfills ou inspections ciblées,
- fournir un socle UI pour tester en pratique tout ce qui a été construit dans la série `0.7.x`.
- ajouter une vue de saisie dun `token_mint`,
- permettre le déclenchement manuel du `KbTokenBackfillService`,
- afficher le résumé du backfill : signatures mint, pools retrouvés, signatures de pools, transactions résolues, decoded events, trade events, candles et analytic signals,
- permettre une navigation simple entre token, pools, paires et événements liés,
- préparer la réexécution ciblée de backfills sans casser lidempotence du modèle.
### 6.055. Version `0.7.x` — Couverture DEX v1
### 6.056. Version `0.7.24` — `kb_app` : visualisation candles / OHLCV
Objectif : fournir une vue graphique exploitable des candles via `echarts`.
À faire :
- ajouter une vue de sélection de paire,
- permettre le choix du timeframe,
- lire les candles matérialisées pour les timeframes usuels,
- permettre la régénération à la demande pour un timeframe arbitraire,
- afficher les chandeliers, les volumes et la navigation temporelle,
- préparer laffichage doverlays analytiques.
### 6.057. Version `0.7.25` — `kb_app` : overlays analytiques
Objectif : rendre visibles les signaux analytiques directement sur les graphes et vues de marché.
À faire :
- afficher les signaux analytiques par bucket au-dessus ou autour des candles,
- ajouter des marqueurs pour `first_trade_seen`, `trade_burst_60s`, `buy_sell_imbalance_60s`, `price_jump_up_60s`, `price_jump_down_60s` et `volume_spike_60s`,
- permettre le filtrage par type de signal et par sévérité,
- afficher un panneau latéral listant les signaux liés à une paire et à un timeframe.
### 6.058. Version `0.7.26` — `kb_app` : vues consolidées token / pair / pool
Objectif : fournir une lecture métier plus confortable du modèle `0.7.x`.
À faire :
- ajouter une fiche token,
- ajouter une fiche paire,
- ajouter une fiche pool,
- relier dans lUI les launch origins, pool origins, wallets observés, holdings observés, candles et analytic signals,
- préparer une navigation transversale entre objets techniques et objets métier.
### 6.059. Version `0.7.27` — Finition UI `0.7.x`
Objectif : stabiliser la couche desktop de validation avant louverture de `0.8.x`.
À faire :
- consolider les vues ajoutées dans `kb_app`,
- améliorer la navigation, les filtres et la pagination,
- ajouter les derniers raffinements de confort et de lisibilité,
- préparer une base UI suffisamment stable pour la future phase danalyse et filtrage `0.8.x`.
### 6.060. Version `0.7.x` — Couverture DEX v1
Objectif : structurer les connecteurs DEX autour dun pipeline complet de résolution, décodage et normalisation métier.
Protocoles cibles :
@@ -700,7 +756,7 @@ Résultat attendu :
- préparation dune détection temps réel hybride et dun backfill ciblé compatible avec les mêmes objets métier,
- préparation dagrégats DEX plus riches, de candles / OHLCV et dune UI dinspection du pipeline `0.7.x`.
### 6.056. Version `0.8.x` — Analyse et filtrage
### 6.061. Version `0.8.x` — Analyse et filtrage
Objectif : transformer les événements bruts en signaux exploitables.
À faire :
@@ -712,7 +768,7 @@ Objectif : transformer les événements bruts en signaux exploitables.
- premiers patterns,
- enrichissement des signaux analytiques préparés en fin de `0.7.x`.
### 6.057. Version `1.x.y` — Wallets et swap préparatoire
### 6.062. Version `1.x.y` — Wallets et swap préparatoire
Objectif : préparer la couche daction.
À faire :
@@ -723,7 +779,7 @@ Objectif : préparer la couche daction.
- préparation dordres et de swaps,
- simulation et garde-fous.
### 6.058. Version `2.x.y` — Trading semi-automatisé
### 6.063. Version `2.x.y` — Trading semi-automatisé
Objectif : brancher lanalyse à laction tout en gardant des garde-fous explicites.
À faire :
@@ -734,7 +790,7 @@ Objectif : brancher lanalyse à laction tout en gardant des garde-fous exp
- confirmations explicites ou semi-automatiques,
- journaux dexécution.
### 6.059. Version `3.x.y` — Yellowstone gRPC
### 6.064. Version `3.x.y` — Yellowstone gRPC
Objectif : ajouter le connecteur gRPC dédié.
À faire :
@@ -822,11 +878,10 @@ Le projet doit maintenir au minimum :
La priorité immédiate est désormais la suivante :
1. finaliser complètement la fin de série `0.7.x` avant louverture de `0.8.x`,
2. ajouter un renforcement temps réel hybride avec `logsSubscribe`, `programSubscribe` et `accountSubscribe` en parallèle des sources déjà exploitées,
3. conserver la résolution transactionnelle comme source de normalisation commune,
4. ajouter ensuite un mode de backfill historique ciblé par `token_mint` pour des tokens encore actifs donnés explicitement,
5. compléter la couche métier avec des `holdings observés`,
6. ajouter des `candles / OHLCV` et une première couche de signaux analytiques plus riches,
7. doter `kb_app` dune vraie UI dinspection et de test pour lensemble du pipeline `0.7.x`,
8. préparer enfin larrivée de Yellowstone gRPC comme extension de capacité, et non comme remplacement du socle existant.
1. poursuivre la fin de série `0.7.x` côté `kb_app` avant louverture de `0.8.x`,
2. ajouter un pilotage UI du backfill historique ciblé par `token_mint`,
3. ajouter une vue graphique des candles / OHLCV avec `echarts`,
4. ajouter les overlays des signaux analytiques sur les candles,
5. consolider les vues métier `token / pair / pool` dans `kb_app`,
6. stabiliser lergonomie, les filtres et la navigation de lUI dinspection,
7. préparer enfin larrivée de Yellowstone gRPC comme extension de capacité, et non comme remplacement du socle existant.

View File

@@ -7,7 +7,8 @@
"splash",
"demo_ws",
"demo_http",
"demo_ws_manager"
"demo_ws_manager",
"demo_pipeline"
],
"permissions": [
"core:default",

View File

@@ -0,0 +1,255 @@
<!-- file: kb_app/frontend/demo_pipeline.html -->
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Khadhroony-BoBoBot — Demo Pipeline</title>
<link rel="stylesheet" href="sass/main.scss" />
</head>
<body class="bg-body-tertiary">
<header class="app-header">
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
<div class="container my-0">
<a class="navbar-brand d-flex align-items-center" href="/">
<img alt="Logo" src="imgs/logo.png" class="app-logo" />
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Demo Pipeline</span>
</a>
</div>
</nav>
</header>
<main class="app-main">
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
<div class="container vcentered sketchy-translucid py-4">
<div class="row g-4">
<div class="col-12 col-xxl-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<h1 class="h4 mb-3">Inspection par signature</h1>
<p class="text-body-secondary mb-3">
Cette fenêtre inspecte la donnée déjà persistée dans <code>kb_lib</code> pour une
signature donnée et affiche létat du pipeline <code>0.7.x</code>.
</p>
<div class="mb-3">
<label for="demoPipelineSignatureInput" class="form-label">Signature</label>
<input id="demoPipelineSignatureInput" type="text" class="form-control font-monospace" spellcheck="false" placeholder="Signature Solana déjà présente dans la base locale" />
</div>
<div class="mb-3">
<label for="demoPipelineCustomTimeframeInput" class="form-label">
Timeframe custom optionnel (secondes)
</label>
<input id="demoPipelineCustomTimeframeInput" type="number" min="1" step="1" class="form-control" placeholder="Ex: 120" />
<div class="form-text">
Les timeframes matérialisés restent chargés depuis la base. Une valeur custom déclenche
une régénération à la demande.
</div>
</div>
<div class="d-flex flex-wrap gap-2 mb-4">
<button id="demoPipelineInspectButton" type="button" class="btn btn-primary">
Inspecter
</button>
<button id="demoPipelineClearButton" type="button" class="btn btn-outline-secondary">
Vider
</button>
</div>
<hr class="my-4" />
<div class="mb-3">
<label for="demoPipelineTokenMintInput" class="form-label">Token mint</label>
<input id="demoPipelineTokenMintInput" type="text" class="form-control font-monospace" spellcheck="false" placeholder="Mint SPL déjà présent dans la base locale" />
</div>
<div class="d-flex flex-wrap gap-2 mb-4">
<button id="demoPipelineInspectTokenButton" type="button" class="btn btn-outline-primary">
Inspecter token
</button>
</div>
<hr class="my-4" />
<div class="mb-3">
<label for="demoPipelinePairIdInput" class="form-label">Pair id</label>
<input
id="demoPipelinePairIdInput"
type="number"
min="1"
step="1"
class="form-control"
placeholder="Identifiant interne de la paire"
/>
</div>
<div class="d-flex flex-wrap gap-2 mb-3">
<button id="demoPipelineInspectPairButton" type="button" class="btn btn-outline-primary">
Inspecter pair
</button>
</div>
<div class="mb-3">
<label for="demoPipelinePoolAddressInput" class="form-label">Pool address</label>
<input
id="demoPipelinePoolAddressInput"
type="text"
class="form-control font-monospace"
spellcheck="false"
placeholder="Adresse du pool déjà présent dans la base locale"
/>
</div>
<div class="d-flex flex-wrap gap-2 mb-4">
<button id="demoPipelineInspectPoolButton" type="button" class="btn btn-outline-primary">
Inspecter pool
</button>
</div>
<hr class="my-4" />
<div class="small text-body-secondary">
<div><strong>But :</strong> vérifier rapidement la cohérence du pipeline <code>0.7.x</code>.</div>
<div><strong>Portée :</strong> lecture seule depuis SQLite via <code>kb_lib</code>.</div>
</div>
</div>
</div>
</div>
<div class="col-12 col-xxl-8">
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<h2 class="h5 mb-3">Résumé</h2>
<textarea id="demoPipelineSummaryTextarea" class="form-control font-monospace" rows="10" readonly spellcheck="false"></textarea>
</div>
</div>
<div class="accordion" id="demoPipelineAccordion">
<div class="accordion-item">
<h2 class="accordion-header" id="headingTransaction">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTransaction" aria-expanded="true" aria-controls="collapseTransaction">
Transaction résolue
</button>
</h2>
<div id="collapseTransaction" class="accordion-collapse collapse show" aria-labelledby="headingTransaction" data-bs-parent="#demoPipelineAccordion">
<div class="accordion-body">
<textarea id="demoPipelineTransactionTextarea" class="form-control font-monospace" rows="14" readonly spellcheck="false"></textarea>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingDecodedEvents">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseDecodedEvents" aria-expanded="false" aria-controls="collapseDecodedEvents">
Decoded events
</button>
</h2>
<div id="collapseDecodedEvents" class="accordion-collapse collapse" aria-labelledby="headingDecodedEvents" data-bs-parent="#demoPipelineAccordion">
<div class="accordion-body">
<textarea id="demoPipelineDecodedEventsTextarea" class="form-control font-monospace" rows="14" readonly spellcheck="false"></textarea>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingPools">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapsePools" aria-expanded="false" aria-controls="collapsePools">
Pools / pairs / origins
</button>
</h2>
<div id="collapsePools" class="accordion-collapse collapse" aria-labelledby="headingPools" data-bs-parent="#demoPipelineAccordion">
<div class="accordion-body">
<div class="mb-3">
<label class="form-label">Pools</label>
<textarea id="demoPipelinePoolsTextarea" class="form-control font-monospace" rows="10" readonly spellcheck="false"></textarea>
</div>
<div class="mb-3">
<label class="form-label">Pairs</label>
<textarea id="demoPipelinePairsTextarea" class="form-control font-monospace" rows="10" readonly spellcheck="false"></textarea>
</div>
<div class="mb-3">
<label class="form-label">Launch attributions</label>
<textarea id="demoPipelineLaunchAttributionsTextarea" class="form-control font-monospace" rows="10" readonly spellcheck="false"></textarea>
</div>
<div>
<label class="form-label">Pool origins</label>
<textarea id="demoPipelinePoolOriginsTextarea" class="form-control font-monospace" rows="10" readonly spellcheck="false"></textarea>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingWallets">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseWallets" aria-expanded="false" aria-controls="collapseWallets">
Wallets / participations / holdings
</button>
</h2>
<div id="collapseWallets" class="accordion-collapse collapse" aria-labelledby="headingWallets" data-bs-parent="#demoPipelineAccordion">
<div class="accordion-body">
<textarea id="demoPipelineWalletsTextarea" class="form-control font-monospace" rows="16" readonly spellcheck="false"></textarea>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingTrades">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTrades" aria-expanded="false" aria-controls="collapseTrades">
Trades / metrics / candles / analytic signals
</button>
</h2>
<div id="collapseTrades" class="accordion-collapse collapse" aria-labelledby="headingTrades" data-bs-parent="#demoPipelineAccordion">
<div class="accordion-body">
<div class="mb-3">
<label class="form-label">Trade events</label>
<textarea id="demoPipelineTradeEventsTextarea" class="form-control font-monospace" rows="10" readonly spellcheck="false"></textarea>
</div>
<div class="mb-3">
<label class="form-label">Pair metrics</label>
<textarea id="demoPipelinePairMetricsTextarea" class="form-control font-monospace" rows="10" readonly spellcheck="false"></textarea>
</div>
<div class="mb-3">
<label class="form-label">Pair candles</label>
<textarea id="demoPipelinePairCandlesTextarea" class="form-control font-monospace" rows="12" readonly spellcheck="false"></textarea>
</div>
<div>
<label class="form-label">Pair analytic signals</label>
<textarea id="demoPipelinePairAnalyticSignalsTextarea" class="form-control font-monospace" rows="12" readonly spellcheck="false"></textarea>
</div>
</div>
</div>
</div>
</div>
<div class="card shadow-sm border-0 mt-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="h5 mb-0">Log UI</h2>
<button id="demoPipelineClearLogButton" type="button" class="btn btn-outline-secondary btn-sm">Clear log</button>
</div>
<textarea id="demoPipelineLogTextarea" class="form-control font-monospace" rows="10" readonly spellcheck="false"></textarea>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<footer class="app-footer bg-dark text-light">
<div class="container h-100 d-flex align-items-center">
<div class="row flex-grow-1 align-items-center">
<div class="col-12 col-md-6 text-center text-small my-1 my-md-0">
&copy; 2026 SASEDEV — Demo Pipeline
</div>
</div>
</div>
</footer>
<script type="module" src="ts/demo_pipeline.ts" defer></script>
</body>
</html>

View File

@@ -28,6 +28,9 @@
<button id="openDemoWsManagerButton" type="button" class="btn btn-outline-primary">
Demo Ws Manager
</button>
<button id="openDemoPipelineButton" type="button" class="btn btn-primary">
Ouvrir Demo Pipeline
</button>
</div>
</div>
</nav>
@@ -72,6 +75,9 @@
<button id="openDemoWsManagerButtonSecondary" type="button" class="btn btn-primary">
Ouvrir Demo Ws Manager
</button>
<button id="openDemoPipelineButtonSecondary" type="button" class="btn btn-primary">
Ouvrir Demo Pipeline
</button>
</div>
<hr />

View File

@@ -0,0 +1,420 @@
// file: kb_app/frontend/ts/demo_pipeline.ts
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { debug, takeoverConsole } from "@fltsci/tauri-plugin-tracing";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
interface DemoPipelineInspectRequest {
signature: string;
customTimeframeSeconds: number | null;
}
interface DemoPipelineInspectPayload {
signature: string;
summaryJson: string;
transactionJson: string;
decodedEventsJson: string;
poolsJson: string;
pairsJson: string;
launchAttributionsJson: string;
poolOriginsJson: string;
walletsJson: string;
tradeEventsJson: string;
pairMetricsJson: string;
pairCandlesJson: string;
pairAnalyticSignalsJson: string;
}
interface DemoPipelineInspectTokenRequest {
tokenMint: string;
customTimeframeSeconds: number | null;
}
interface DemoPipelineInspectPairRequest {
pairId: number;
customTimeframeSeconds: number | null;
}
interface DemoPipelineInspectPoolRequest {
poolAddress: string;
customTimeframeSeconds: number | null;
}
function appendLogLine(textarea: HTMLTextAreaElement, line: string): void {
const now = new Date();
const timestamp = now.toLocaleTimeString("fr-CH", { hour12: false });
const lines = textarea.value === "" ? [] : textarea.value.split("\n");
lines.push(`[${timestamp}] ${line}`);
const maxLines = 300;
textarea.value = lines.slice(-maxLines).join("\n");
textarea.scrollTop = textarea.scrollHeight;
}
function clearInspection(
summaryTextarea: HTMLTextAreaElement,
transactionTextarea: HTMLTextAreaElement,
decodedEventsTextarea: HTMLTextAreaElement,
poolsTextarea: HTMLTextAreaElement,
pairsTextarea: HTMLTextAreaElement,
launchAttributionsTextarea: HTMLTextAreaElement,
poolOriginsTextarea: HTMLTextAreaElement,
walletsTextarea: HTMLTextAreaElement,
tradeEventsTextarea: HTMLTextAreaElement,
pairMetricsTextarea: HTMLTextAreaElement,
pairCandlesTextarea: HTMLTextAreaElement,
pairAnalyticSignalsTextarea: HTMLTextAreaElement,
): void {
summaryTextarea.value = "";
transactionTextarea.value = "";
decodedEventsTextarea.value = "";
poolsTextarea.value = "";
pairsTextarea.value = "";
launchAttributionsTextarea.value = "";
poolOriginsTextarea.value = "";
walletsTextarea.value = "";
tradeEventsTextarea.value = "";
pairMetricsTextarea.value = "";
pairCandlesTextarea.value = "";
pairAnalyticSignalsTextarea.value = "";
}
function readCustomTimeframeSeconds(
input: HTMLInputElement,
logTextarea: HTMLTextAreaElement,
): number | null | undefined {
const customTimeframeText = input.value.trim();
if (customTimeframeText === "") {
return null;
}
const parsed = Number.parseInt(customTimeframeText, 10);
if (Number.isNaN(parsed) || parsed <= 0) {
appendLogLine(logTextarea, `[ui] invalid custom timeframe '${customTimeframeText}'`);
return undefined;
}
return parsed;
}
document.addEventListener("DOMContentLoaded", async () => {
void takeoverConsole();
debug("demo_pipeline window loaded");
const sidebarToggle = document.querySelector<HTMLButtonElement>('#sidebarToggle');
if (sidebarToggle) {
// restaurer létat depuis localStorage
if (localStorage.getItem('sidebar-toggle') === 'true') {
document.body.classList.add('sidenav-toggled');
}
sidebarToggle.addEventListener('click', (event) => {
event.preventDefault();
document.body.classList.toggle('sidenav-toggled');
localStorage.setItem('sidebar-toggle', document.body.classList.contains('sidenav-toggled') ? 'true' : 'false');
});
}
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
const toastElList = document.querySelectorAll('.toast');
Array.from(toastElList).map(toastEl => new bootstrap.Toast(toastEl));
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]');
Array.from(popoverTriggerList).map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl));
const gobackto = location.pathname + location.search;
document.querySelectorAll<HTMLAnchorElement>('a[data-setlang]').forEach((a) => {
const href = a.getAttribute("href");
if (!href) return; // pas de href => on ignore
const url = new URL(href, location.origin);
url.searchParams.set("gobackto", gobackto);
// conserve une URL relative (path + query)
a.setAttribute("href", url.pathname + "?" + url.searchParams.toString());
});
const signatureInput = document.querySelector<HTMLInputElement>("#demoPipelineSignatureInput");
const customTimeframeInput = document.querySelector<HTMLInputElement>("#demoPipelineCustomTimeframeInput");
const inspectButton = document.querySelector<HTMLButtonElement>("#demoPipelineInspectButton");
const clearButton = document.querySelector<HTMLButtonElement>("#demoPipelineClearButton");
const clearLogButton = document.querySelector<HTMLButtonElement>("#demoPipelineClearLogButton");
const summaryTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipelineSummaryTextarea");
const transactionTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipelineTransactionTextarea");
const decodedEventsTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipelineDecodedEventsTextarea");
const poolsTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipelinePoolsTextarea");
const pairsTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipelinePairsTextarea");
const launchAttributionsTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipelineLaunchAttributionsTextarea");
const poolOriginsTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipelinePoolOriginsTextarea");
const walletsTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipelineWalletsTextarea");
const tradeEventsTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipelineTradeEventsTextarea");
const pairMetricsTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipelinePairMetricsTextarea");
const pairCandlesTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipelinePairCandlesTextarea");
const pairAnalyticSignalsTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipelinePairAnalyticSignalsTextarea");
const logTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipelineLogTextarea");
const tokenMintInput = document.querySelector<HTMLInputElement>("#demoPipelineTokenMintInput");
const inspectTokenButton = document.querySelector<HTMLButtonElement>("#demoPipelineInspectTokenButton");
const pairIdInput = document.querySelector<HTMLInputElement>("#demoPipelinePairIdInput");
const inspectPairButton = document.querySelector<HTMLButtonElement>("#demoPipelineInspectPairButton");
const poolAddressInput = document.querySelector<HTMLInputElement>("#demoPipelinePoolAddressInput");
const inspectPoolButton = document.querySelector<HTMLButtonElement>("#demoPipelineInspectPoolButton");
if (
!pairIdInput ||
!inspectPairButton ||
!poolAddressInput ||
!inspectPoolButton ||
!tokenMintInput ||
!inspectTokenButton ||
!signatureInput ||
!customTimeframeInput ||
!inspectButton ||
!clearButton ||
!clearLogButton ||
!summaryTextarea ||
!transactionTextarea ||
!decodedEventsTextarea ||
!poolsTextarea ||
!pairsTextarea ||
!launchAttributionsTextarea ||
!poolOriginsTextarea ||
!walletsTextarea ||
!tradeEventsTextarea ||
!pairMetricsTextarea ||
!pairCandlesTextarea ||
!pairAnalyticSignalsTextarea ||
!logTextarea
) {
console.error("demo_pipeline DOM is incomplete");
return;
}
clearButton.addEventListener("click", () => {
clearInspection(
summaryTextarea,
transactionTextarea,
decodedEventsTextarea,
poolsTextarea,
pairsTextarea,
launchAttributionsTextarea,
poolOriginsTextarea,
walletsTextarea,
tradeEventsTextarea,
pairMetricsTextarea,
pairCandlesTextarea,
pairAnalyticSignalsTextarea,
);
signatureInput.value = "";
customTimeframeInput.value = "";
tokenMintInput.value = "";
pairIdInput.value = "";
poolAddressInput.value = "";
appendLogLine(logTextarea, "[ui] inspection state cleared");
});
clearLogButton.addEventListener("click", () => {
logTextarea.value = "";
});
inspectButton.addEventListener("click", async () => {
const signature = signatureInput.value.trim();
if (signature === "") {
appendLogLine(logTextarea, "[ui] signature is required");
return;
}
let customTimeframeSeconds: number | null = null;
const customTimeframeText = customTimeframeInput.value.trim();
if (customTimeframeText !== "") {
const parsed = Number.parseInt(customTimeframeText, 10);
if (Number.isNaN(parsed) || parsed <= 0) {
appendLogLine(logTextarea, `[ui] invalid custom timeframe '${customTimeframeText}'`);
return;
}
customTimeframeSeconds = parsed;
}
appendLogLine(
logTextarea,
`[ui] inspecting signature '${signature}'${customTimeframeSeconds === null ? "" : ` with custom timeframe ${customTimeframeSeconds}s`}`,
);
const request: DemoPipelineInspectRequest = {
signature,
customTimeframeSeconds,
};
try {
const payload = await invoke<DemoPipelineInspectPayload>("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;
appendLogLine(logTextarea, `[ui] inspection completed for '${payload.signature}'`);
} catch (error) {
appendLogLine(logTextarea, `[ui] inspect error: ${String(error)}`);
}
});
inspectTokenButton.addEventListener("click", async () => {
const tokenMint = tokenMintInput.value.trim();
if (tokenMint === "") {
appendLogLine(logTextarea, "[ui] token mint is required");
return;
}
let customTimeframeSeconds: number | null = null;
const customTimeframeText = customTimeframeInput.value.trim();
if (customTimeframeText !== "") {
const parsed = Number.parseInt(customTimeframeText, 10);
if (Number.isNaN(parsed) || parsed <= 0) {
appendLogLine(logTextarea, `[ui] invalid custom timeframe '${customTimeframeText}'`);
return;
}
customTimeframeSeconds = parsed;
}
appendLogLine(
logTextarea,
`[ui] inspecting token mint '${tokenMint}'${customTimeframeSeconds === null ? "" : ` with custom timeframe ${customTimeframeSeconds}s`}`,
);
const request: DemoPipelineInspectTokenRequest = {
tokenMint,
customTimeframeSeconds,
};
try {
const payload = await invoke<DemoPipelineInspectPayload>("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;
appendLogLine(logTextarea, `[ui] token inspection completed for '${payload.signature}'`);
} catch (error) {
appendLogLine(logTextarea, `[ui] token inspect error: ${String(error)}`);
}
});
inspectPairButton.addEventListener("click", async () => {
const pairIdText = pairIdInput.value.trim();
if (pairIdText === "") {
appendLogLine(logTextarea, "[ui] pair id is required");
return;
}
const parsedPairId = Number.parseInt(pairIdText, 10);
if (Number.isNaN(parsedPairId) || parsedPairId <= 0) {
appendLogLine(logTextarea, `[ui] invalid pair id '${pairIdText}'`);
return;
}
const customTimeframeSeconds = readCustomTimeframeSeconds(customTimeframeInput, logTextarea);
if (customTimeframeSeconds === undefined) {
return;
}
appendLogLine(
logTextarea,
`[ui] inspecting pair id '${parsedPairId}'${customTimeframeSeconds === null ? "" : ` with custom timeframe ${customTimeframeSeconds}s`}`,
);
const request: DemoPipelineInspectPairRequest = {
pairId: parsedPairId,
customTimeframeSeconds,
};
try {
const payload = await invoke<DemoPipelineInspectPayload>("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;
appendLogLine(logTextarea, `[ui] pair inspection completed for '${payload.signature}'`);
} catch (error) {
appendLogLine(logTextarea, `[ui] pair inspect error: ${String(error)}`);
}
});
inspectPoolButton.addEventListener("click", async () => {
const poolAddress = poolAddressInput.value.trim();
if (poolAddress === "") {
appendLogLine(logTextarea, "[ui] pool address is required");
return;
}
const customTimeframeSeconds = readCustomTimeframeSeconds(customTimeframeInput, logTextarea);
if (customTimeframeSeconds === undefined) {
return;
}
appendLogLine(
logTextarea,
`[ui] inspecting pool '${poolAddress}'${customTimeframeSeconds === null ? "" : ` with custom timeframe ${customTimeframeSeconds}s`}`,
);
const request: DemoPipelineInspectPoolRequest = {
poolAddress,
customTimeframeSeconds,
};
try {
const payload = await invoke<DemoPipelineInspectPayload>("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;
appendLogLine(logTextarea, `[ui] pool inspection completed for '${payload.signature}'`);
} catch (error) {
appendLogLine(logTextarea, `[ui] pool inspect error: ${String(error)}`);
}
});
});

View File

@@ -31,8 +31,19 @@ async function openDemoWsManagerWindow(): Promise<void> {
console.error("open_demo_ws_manager_window failed:", error);
}
}
async function openDemoPipelineWindow(): Promise<void> {
try {
await invoke("open_demo_pipeline_window");
} catch (error) {
console.error("open_demo_pipeline_window failed:", error);
}
}
document.addEventListener("DOMContentLoaded", async () => {
void takeoverConsole();
debug("main window loaded");
const sidebarToggle = document.querySelector<HTMLButtonElement>('#sidebarToggle');
if (sidebarToggle) {
// restaurer létat depuis localStorage
@@ -74,6 +85,8 @@ document.addEventListener("DOMContentLoaded", async () => {
const openDemoHttpButtonSecondary = document.querySelector<HTMLButtonElement>("#openDemoHttpButtonSecondary");
const openDemoWsManagerButton = document.querySelector<HTMLButtonElement>("#openDemoWsManagerButton");
const openDemoWsManagerButtonSecondary = document.querySelector<HTMLButtonElement>("#openDemoWsManagerButtonSecondary");
const openDemoPipelineButton = document.querySelector<HTMLButtonElement>("#openDemoPipelineButton");
const openDemoPipelineButtonSecondary = document.querySelector<HTMLButtonElement>("#openDemoPipelineButtonSecondary");
if (openDemoWsButton) {
openDemoWsButton.addEventListener("click", () => {
@@ -111,6 +124,16 @@ document.addEventListener("DOMContentLoaded", async () => {
});
}
debug("window loaded");
if (openDemoPipelineButton) {
openDemoPipelineButton.addEventListener("click", () => {
void openDemoPipelineWindow();
});
}
if (openDemoPipelineButtonSecondary) {
openDemoPipelineButtonSecondary.addEventListener("click", () => {
void openDemoPipelineWindow();
});
}
});

View File

@@ -1 +1 @@
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main","splash","demo_ws","demo_http","demo_ws_manager"],"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"],"permissions":["core:default","tracing:default"]}}

View File

@@ -1,7 +1,7 @@
{
"name": "kb-app",
"private": true,
"version": "0.6.6",
"version": "0.7.22",
"type": "module",
"scripts": {
"dev": "vite",

1494
kb_app/src/demo_pipeline.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,7 @@
#![warn(missing_docs)]
mod demo_http;
mod demo_pipeline;
mod demo_ws;
mod demo_ws_manager;
mod splash;
@@ -36,16 +37,18 @@ impl KbWsRuntimeState {
/// Shared application state stored inside Tauri.
struct KbAppState {
config: kb_lib::KbConfig,
database: std::sync::Arc<kb_lib::KbDatabase>,
ws_runtime: tokio::sync::Mutex<KbWsRuntimeState>,
demo_ws_runtime: std::sync::Arc<tokio::sync::Mutex<crate::demo_ws::KbDemoWsRuntimeState>>,
demo_ws_manager_runtime: std::sync::Arc<tokio::sync::Mutex<crate::demo_ws_manager::KbDemoWsManagerRuntimeState>>,
demo_ws_manager_runtime:
std::sync::Arc<tokio::sync::Mutex<crate::demo_ws_manager::KbDemoWsManagerRuntimeState>>,
ws_manager: std::sync::Arc<kb_lib::WsManager>,
http_pool: kb_lib::HttpEndpointPool,
}
/// Runs the desktop application.
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
pub async fn run() -> Result<(), kb_lib::KbError> {
let config_path = kb_lib::KbConfig::default_path();
let config_result = kb_lib::KbConfig::load_from_path(&config_path);
let config = match config_result {
@@ -56,20 +59,20 @@ pub fn run() {
config_path.display(),
error
);
return;
return Err(error);
}
};
let prepare_result = config.prepare_filesystem();
if let Err(error) = prepare_result {
eprintln!("kb_app filesystem preparation error: {error}");
return;
return Err(error);
}
let tracing_guard_result = kb_lib::init_tracing(&config.logging);
let _tracing_guard = match tracing_guard_result {
Ok(guard) => guard,
Err(error) => {
eprintln!("kb_app tracing initialization error: {error}");
return;
return Err(error);
}
};
tracing::info!(
@@ -77,6 +80,11 @@ pub fn run() {
environment = %config.app.environment,
"starting desktop application"
);
let database_result = kb_lib::KbDatabase::connect_and_initialize(&config.database).await;
let database = match database_result {
Ok(database) => database,
Err(error) => return Err(error),
};
let http_pool_result = kb_lib::HttpEndpointPool::from_config(&config);
let http_pool = match http_pool_result {
Ok(http_pool) => http_pool,
@@ -95,6 +103,7 @@ pub fn run() {
};
let app_state = KbAppState {
config: config.clone(),
database: std::sync::Arc::new(database),
ws_runtime: tokio::sync::Mutex::new(KbWsRuntimeState::new()),
demo_ws_runtime: std::sync::Arc::new(tokio::sync::Mutex::new(
crate::demo_ws::KbDemoWsRuntimeState::new(),
@@ -128,6 +137,11 @@ pub fn run() {
crate::demo_ws_manager::demo_ws_manager_stop_all,
crate::demo_ws_manager::demo_ws_manager_start_role,
crate::demo_ws_manager::demo_ws_manager_stop_role,
crate::demo_pipeline::open_demo_pipeline_window,
crate::demo_pipeline::demo_pipeline_inspect_signature,
crate::demo_pipeline::demo_pipeline_inspect_token_mint,
crate::demo_pipeline::demo_pipeline_inspect_pair_id,
crate::demo_pipeline::demo_pipeline_inspect_pool_address,
]);
tauri_builder = tauri_builder.plugin(tracing_builder.build::<tauri::Wry>());
tauri_builder = tauri_builder.setup(|app| {
@@ -202,7 +216,11 @@ pub fn run() {
let run_result = tauri_builder.run(tauri::generate_context!());
if let Err(error) = run_result {
tracing::error!("error while running tauri application: {error:?}");
return Err(kb_lib::KbError::InvalidState(format!(
"error while running tauri application: {error:?}"
)));
}
Ok(())
}
fn emit_splash_order(

View File

@@ -14,9 +14,7 @@ use fs2::FileExt;
/// Entrypoint of the kb app binary.
#[tokio::main]
async
fn main() -> std::process::ExitCode
{
async fn main() -> std::process::ExitCode {
let mut lock_path = std::env::temp_dir();
lock_path.push("com_khadhroony_solana_rust.lock");
let lock_file = match std::fs::File::create(lock_path) {
@@ -24,7 +22,7 @@ fn main() -> std::process::ExitCode
Err(_err) => {
eprintln!("Cannot create lock!");
std::process::exit(1);
},
}
};
// trying to aquire an exclusive lock
if lock_file.try_lock_exclusive().is_err() {
@@ -34,13 +32,17 @@ fn main() -> std::process::ExitCode
if rustls::crypto::CryptoProvider::get_default().is_none() {
let provider_result = rustls::crypto::aws_lc_rs::default_provider().install_default();
match provider_result {
Ok(()) => {},
Ok(()) => {}
Err(error) => {
eprintln!("kb_app rustls provider init error: {:?}", error);
return std::process::ExitCode::FAILURE;
},
}
}
kb_app_lib::run();
}
let run_result = kb_app_lib::run().await;
if let Err(error) = run_result {
eprintln!("application error: {}", error);
std::process::exit(1);
}
std::process::ExitCode::SUCCESS
}

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "kb-bapp",
"version": "0.6.6",
"version": "0.7.22",
"identifier": "com.sasedev.kb-app",
"build": {
"beforeDevCommand": "npm run dev",
@@ -78,6 +78,20 @@
"create": false,
"transparent": false,
"decorations": true
},
{
"label": "demo_pipeline",
"url": "demo_pipeline.html",
"title": "Demo Pipeline",
"width": 1480,
"height": 920,
"minWidth": 1000,
"minHeight": 700,
"center": true,
"visible": false,
"create": false,
"transparent": false,
"decorations": true
}
],
"security": {

View File

@@ -152,7 +152,7 @@ impl KbConfig {
wallets_directory.display()
)));
}
let sqlite_path = self.data.sqlite_path_buf();
let sqlite_path = self.database.sqlite.path_buf();
let sqlite_parent_option = sqlite_path.parent();
if let Some(sqlite_parent) = sqlite_parent_option {
if !sqlite_parent.as_os_str().is_empty() {
@@ -509,6 +509,13 @@ pub struct KbSqliteDatabaseConfig {
pub use_wal: bool,
}
impl KbSqliteDatabaseConfig {
/// Returns the resolved SQLite database path.
pub fn path_buf(&self) -> std::path::PathBuf {
kb_resolve_workspace_relative_path(&self.path)
}
}
/// Database configuration.
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]

View File

@@ -12,7 +12,8 @@ pub(crate) fn sqlite_database_url_from_config(
"database.sqlite.path must not be empty".to_string(),
));
}
Ok(format!("sqlite://{}", path))
let database_path = config.sqlite.path_buf();
Ok(format!("sqlite://{}", database_path.display()))
}
/// Opens a SQLite pool according to configuration.
@@ -30,7 +31,7 @@ pub(crate) async fn connect_sqlite(
"database.sqlite.max_connections must be > 0".to_string(),
));
}
let database_path = std::path::Path::new(path);
let database_path = config.sqlite.path_buf();
let parent_option = database_path.parent();
if let Some(parent) = parent_option {
if !parent.as_os_str().is_empty() {
@@ -45,7 +46,7 @@ pub(crate) async fn connect_sqlite(
}
}
let mut connect_options = sqlx::sqlite::SqliteConnectOptions::new()
.filename(database_path)
.filename(&database_path)
.create_if_missing(config.sqlite.create_if_missing)
.foreign_keys(true)
.busy_timeout(std::time::Duration::from_millis(
@@ -61,7 +62,7 @@ pub(crate) async fn connect_sqlite(
Ok(pool) => Ok(pool),
Err(error) => Err(crate::KbError::Db(format!(
"cannot open sqlite database '{}': {}",
path, error
database_path.display(), error
))),
}
}