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

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