0.7.24-pre.0

This commit is contained in:
2026-05-02 11:27:10 +02:00
parent 60db521a88
commit aaff2dbd94
38 changed files with 3074 additions and 207 deletions

View File

@@ -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, };

View 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.
/**
* 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, };

View File

@@ -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, };

View 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>, };

View 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.
/**
* 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, };

View 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.
/**
* 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, };

View 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, };

View 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, };

View 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, };

View File

@@ -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, };

View 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.
/**
* 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, };

View File

@@ -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) {

View 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();
}
});

View File

@@ -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();
});
}
});